Skip to content

Commit

Permalink
feat: option to highlight "Deep dependencies" when clicking on source…
Browse files Browse the repository at this point in the history
… files (#180)

* chore: add deep dependency node and edge color

* feat: add "Deep dependencies" button and state

* feat: select deep dependencies on click

* chore: add changeset message

* fix: never disable "Deep dependencies"

* fix: only render deep dependencies when it's toggle is active

* fix: only toggle one of "Circular dependencies" or "Deep dependencies"

* chore: move ui mutation to reducer and dispatch "select_node" action

* chore: state injectable select_node dispatcher

* test: add toggle_deep test case

* fix: prefer to mutate ui in toggleDependencies reducer

* test: add select_node action spec
  • Loading branch information
MengLinMaker authored Nov 22, 2024
1 parent 9646be0 commit 93bc556
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/clean-turkeys-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"skott-webapp": minor
---

Add option to highlight "Deep dependencies" when clicking on source files
8 changes: 8 additions & 0 deletions apps/web/src/core/boostrap-app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,17 @@ describe("Initialization of the application", () => {
ui: {
filters: storeDefaultValue.ui.filters,
network: {
selectedNodeId: '',
dependencies: {
builtin: {
active: false,
},
circular: {
active: false,
},
deep: {
active: false,
},
thirdparty: {
active: false,
},
Expand Down Expand Up @@ -171,13 +175,17 @@ describe("Initialization of the application", () => {
ui: {
filters: storeDefaultValue.ui.filters,
network: {
selectedNodeId: '',
dependencies: {
builtin: {
active: false,
},
circular: {
active: false,
},
deep: {
active: false,
},
thirdparty: {
active: false,
},
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/core/network/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NetworkLayout } from "@/store/state";

export type NetworkActions =
| { action: "select_node"; payload: { nodeId: string, oldNodeId: string } }
| { action: "toggle_deep"; payload: { enabled: boolean } }
| { action: "toggle_circular"; payload: { enabled: boolean } }
| { action: "toggle_builtin"; payload: { enabled: boolean } }
| { action: "toggle_thirdparty"; payload: { enabled: boolean } }
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/core/network/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ import * as Option from "@effect/data/Option";

function toggleDependencies(): AppReducer {
return function (event, state) {
if (event.action === "select_node") {
return Option.some({
data: state.data,
ui: {
...state.ui,
network: {
...state.ui.network,
selectedNodeId: event.payload.nodeId
},
},
});
}

if (
event.action === "toggle_deep" ||
event.action === "toggle_circular" ||
event.action === "toggle_builtin" ||
event.action === "toggle_thirdparty"
Expand Down
52 changes: 52 additions & 0 deletions apps/web/src/core/network/select-node.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { AppState, storeDefaultValue } from "@/store/state";
import { AppStore } from "@/store/store";
import { BehaviorSubject } from "rxjs";
import { describe, expect, test } from "vitest";
import { toPromise } from "../utils";
import { networkReducers } from "./reducers";
import { selectNode } from "./select-node";

describe("When clicking on a node", () => {
test(`Should update selected nodeId`, async () => {
const appStore = new AppStore(
new BehaviorSubject<AppState>(storeDefaultValue),
networkReducers
);

const emittedEvents: string[] = [];
const subscription = appStore.events$.subscribe((events) => {
emittedEvents.push(events.action);
});

const dispatchAction = selectNode(appStore);

dispatchAction(['file-1.ts']);

const { ui: uiState1 } = await toPromise(appStore.store$);

expect(uiState1).toEqual({
...storeDefaultValue.ui,
network: {
...storeDefaultValue.ui.network,
selectedNodeId: 'file-1.ts'
},
});

dispatchAction(['file-2.ts']);

const { ui: uiState2 } = await toPromise(appStore.store$);

expect(uiState2).toEqual({
...storeDefaultValue.ui,
network: {
...storeDefaultValue.ui.network,
selectedNodeId: 'file-2.ts'
},
});

const appEvent = "select_node";
expect(emittedEvents).toEqual([appEvent, appEvent]);

subscription.unsubscribe();
});
});
16 changes: 16 additions & 0 deletions apps/web/src/core/network/select-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AppStore } from "@/store/store";

export function selectNode(appStore: AppStore) {
return function (nodes: string[]) {
const { ui } = appStore.getState();
appStore.dispatch({
action: "select_node",
payload: {
nodeId: nodes.length > 0 ? nodes[0] : "",
oldNodeId: ui.network.selectedNodeId
},
}, {
notify: true
});
};
}
1 change: 1 addition & 0 deletions apps/web/src/core/network/toggle-dependencies.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { networkReducers } from "./reducers";

describe("When interacting with network dependencies", () => {
describe.each([
{ target: "deep" },
{ target: "circular" },
{ target: "builtin" },
{ target: "thirdparty" },
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/core/network/toggle-dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AppStore } from "@/store/store";

export function toggleDependencies(appStore: AppStore) {
return function (params: { target: "circular" | "builtin" | "thirdparty" }) {
return function (params: { target: "deep" | "circular" | "builtin" | "thirdparty" }) {
const networkDependency =
appStore.getState().ui.network.dependencies[params.target];

Expand Down
66 changes: 65 additions & 1 deletion apps/web/src/network/Network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ import {
circularNodeOptions,
computeBuiltinDependencies,
computeThirdPartyDependencies,
deepDependencyEdgeOptions,
deepDependencyNodeOptions,
} from "./dependencies";
import { ProgressLoader } from "@/network/ProgressLoader";
import { AppEffects, callUseCase, notify } from "@/store/store";
import { updateConfiguration } from "@/core/network/update-configuration";
import { storeDefaultValue } from "@/store/state";
import { selectNode } from "@/core/network/select-node";

export default function GraphNetwork() {
const appStore = useAppStore();
Expand All @@ -58,6 +61,47 @@ export default function GraphNetwork() {
});
}

function highlightDeepDependencies(
data: SkottStructureWithCycles,
nodeId: string,
highlighted: boolean
) {
const nodeOptions = highlighted ? deepDependencyNodeOptions : defaultNodeOptions;
const edgeOptions = highlighted ? deepDependencyEdgeOptions : defaultEdgeOptions;

const traversedNodeId = new Set<string>()

function dfs(nodeId: string) {
if (traversedNodeId.has(nodeId)) return
traversedNodeId.add(nodeId)

if (!data.graph[nodeId]) return
nodesDataset.update({
id: nodeId,
...nodeOptions,
})
for (const childNodeId of data.graph[nodeId].adjacentTo) {
nodesDataset.update({
id: childNodeId,
...nodeOptions,
});
edgesDataset.update({
id: createEdgeId(nodeId, childNodeId),
...edgeOptions,
from: nodeId,
to: childNodeId,
});
dfs(childNodeId)
}

}
dfs(nodeId)

if (!highlighted && data.entrypoint !== "none") {
highlightEntrypoint(data.entrypoint);
}
}

function highlightEntrypoint(nodeId: string) {
nodesDataset.update({
id: nodeId,
Expand Down Expand Up @@ -150,14 +194,32 @@ export default function GraphNetwork() {
dataStore: AppState["data"],
appEvents: AppActions | AppEvents
) {
const { ui, data } = appStore.getState();
switch (appEvents.action) {
case "select_node": {
if (ui.network.dependencies.deep.active) {
if (appEvents.payload.nodeId !== appEvents.payload.oldNodeId) {
highlightDeepDependencies(data, appEvents.payload.oldNodeId, false);
if (appEvents.payload.nodeId !== '') {
highlightDeepDependencies(data, appEvents.payload.nodeId, true);
}
}
}
break;
}
case "focus_on_node": {
focusOnNetworkNode(appEvents.payload.nodeId);
break;
}
case "toggle_deep": {
if (ui.network.dependencies.deep.active === false) {
highlightDeepDependencies(data, ui.network.selectedNodeId, false);
ui.network.selectedNodeId = ''
}
break;
}
case "toggle_circular": {
highlightCircularDependencies(dataStore, appEvents.payload.enabled);

break;
}
case "toggle_builtin": {
Expand Down Expand Up @@ -226,6 +288,8 @@ export default function GraphNetwork() {
_network.stopSimulation();
});


_network.on('click', (params) => selectNode(appStore)(params.nodes))
setNetwork(_network);
reconciliateNetwork(_network);
}
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/network/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ export const circularEdgeOptions = {
inherit: false,
};

export const deepDependencyNodeOptions = {
color: {
border: "#000000",
background: "#73e6ac",
highlight: {
border: "#000000",
background: "#73e6ac",
},
},
};

export const deepDependencyEdgeOptions = {
color: "#73e6ac",
highlight: "#DF0000",
hover: "#DF0000",
inherit: false,
};

export const builtinNodeOptions = {
color: {
border: "#000000",
Expand Down
16 changes: 14 additions & 2 deletions apps/web/src/sidebar/dependencies/Dependencies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function Dependencies() {
};

function toggleDepsVisualizationOption(
option: "circular" | "thirdparty" | "builtin"
option: "deep" | "circular" | "thirdparty" | "builtin"
) {
const invokeUseCase = callUseCase(toggleDependencies);
invokeUseCase({ target: option });
Expand All @@ -35,10 +35,22 @@ export function Dependencies() {
<Navbar.Section>
<Box p="md">Dependencies visualization</Box>

<Box p="md">
<Checkbox
label="Deep dependencies"
radius="md"
color="cyan"
disabled={network?.dependencies.circular.active}
checked={network?.dependencies.deep.active ?? false}
onChange={() => {
toggleDepsVisualizationOption("deep");
}}
/>
</Box>
<Box p="md">
<Checkbox
label="Circular dependencies"
disabled={state.data.cycles.length === 0}
disabled={state.data.cycles.length === 0 || network?.dependencies.deep.active}
radius="md"
color="red"
checked={network?.dependencies.circular.active ?? false}
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/store/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export interface UiState {
glob: string;
};
network: {
selectedNodeId: string,
dependencies: {
deep: {
active: boolean;
};
circular: {
active: boolean;
};
Expand Down Expand Up @@ -69,7 +73,11 @@ export const storeDefaultValue = {
glob: "",
},
network: {
selectedNodeId: '',
dependencies: {
deep: {
active: false
},
circular: {
active: false,
},
Expand Down

0 comments on commit 93bc556

Please # to comment.