From ea495bb9110367db3925e6e7bf7d1ea831539b20 Mon Sep 17 00:00:00 2001 From: nartc Date: Tue, 27 Aug 2024 10:42:01 -0500 Subject: [PATCH] feat(graph): enable composite graph functionality This PR enables composite graph functionality: - Experimental feature to enable Composite Graph - In Composite Graph mode: - Nodes are shown by default. - Show/Hide All Projects function similarly to regular mode - Focus a Composite Node renders the inner nodes with up-to 3 additional containers: Green area contains external nodes that depend on the inner nodes; Orange area contains external nodes that the inner nodes depend depend on; Purple area contains external nodes with circular dependencies with the inner nodes. - Focused node can be unfocus/reset. - Only one node can be focused at one given time. - Show All projects while having a focused node will unfocus the node. - Expand a Composite Node renders the inner nodes of the composite node in-place (i.e: still keep the context of the current graph). Expanded node can be collapsed to go back. --- .../machines/composite-graph.state.ts | 38 +++++++++ .../panels/group-by-folder-panel.tsx | 6 ++ .../src/app/feature-projects/project-list.tsx | 11 +-- .../app/feature-projects/projects-sidebar.tsx | 84 +++++++++++++++++-- .../src/app/ui-components/checkbox-panel.tsx | 27 +++++- .../app/ui-tooltips/graph-tooltip-display.tsx | 21 +++-- package.json | 2 +- pnpm-lock.yaml | 10 +-- 8 files changed, 165 insertions(+), 34 deletions(-) diff --git a/graph/client/src/app/feature-projects/machines/composite-graph.state.ts b/graph/client/src/app/feature-projects/machines/composite-graph.state.ts index 3f7dcbc4891be..d34d6806ce83d 100644 --- a/graph/client/src/app/feature-projects/machines/composite-graph.state.ts +++ b/graph/client/src/app/feature-projects/machines/composite-graph.state.ts @@ -34,6 +34,43 @@ export const compositeGraphStateConfig: ProjectGraphStateNodeConfig = { }), ], on: { + selectAll: { + actions: [ + assign((ctx, event) => { + if (event.type !== 'selectAll') return; + ctx.compositeGraph.enabled = true; + ctx.compositeGraph.context = null; + }), + send((ctx) => ({ + type: 'enableCompositeGraph', + context: ctx.compositeGraph.context, + })), + ], + }, + deselectAll: { + actions: [ + assign((ctx, event) => { + if (event.type !== 'deselectAll') return; + ctx.compositeGraph.enabled = true; + }), + send( + () => ({ + type: 'notifyGraphHideAllProjects', + }), + { to: (context) => context.graphActor } + ), + ], + }, + selectAffected: { + actions: [ + send( + () => ({ + type: 'notifyGraphShowAffectedProjects', + }), + { to: (context) => context.graphActor } + ), + ], + }, focusProject: { actions: [ assign((ctx, event) => { @@ -112,6 +149,7 @@ export const compositeGraphStateConfig: ProjectGraphStateNodeConfig = { if (event.type !== 'enableCompositeGraph') return; ctx.compositeGraph.enabled = true; ctx.compositeGraph.context = event.context || undefined; + ctx.focusedProject = null; }), send( (ctx, event) => ({ diff --git a/graph/client/src/app/feature-projects/panels/group-by-folder-panel.tsx b/graph/client/src/app/feature-projects/panels/group-by-folder-panel.tsx index 34e5ef3fc964d..f17c5ff5fd7c4 100644 --- a/graph/client/src/app/feature-projects/panels/group-by-folder-panel.tsx +++ b/graph/client/src/app/feature-projects/panels/group-by-folder-panel.tsx @@ -3,11 +3,15 @@ import { CheckboxPanel } from '../../ui-components/checkbox-panel'; export interface DisplayOptionsPanelProps { groupByFolder: boolean; groupByFolderChanged: (checked: boolean) => void; + disabled?: boolean; + disabledDescription?: string; } export const GroupByFolderPanel = ({ groupByFolder, groupByFolderChanged, + disabled, + disabledDescription, }: DisplayOptionsPanelProps) => { return ( ); }; diff --git a/graph/client/src/app/feature-projects/project-list.tsx b/graph/client/src/app/feature-projects/project-list.tsx index 33af5316eb9c8..ef9a906934575 100644 --- a/graph/client/src/app/feature-projects/project-list.tsx +++ b/graph/client/src/app/feature-projects/project-list.tsx @@ -283,13 +283,8 @@ function CompositeNodeListItem({
No composite nodes

; } diff --git a/graph/client/src/app/feature-projects/projects-sidebar.tsx b/graph/client/src/app/feature-projects/projects-sidebar.tsx index 676f318bdb084..52c05eb1b6a1d 100644 --- a/graph/client/src/app/feature-projects/projects-sidebar.tsx +++ b/graph/client/src/app/feature-projects/projects-sidebar.tsx @@ -7,6 +7,8 @@ import { useProjectGraphSelector } from './hooks/use-project-graph-selector'; import { TracingAlgorithmType } from './machines/interfaces'; import { collapseEdgesSelector, + compositeContextSelector, + compositeGraphEnabledSelector, focusedProjectNameSelector, getTracingInfo, groupByFolderSelector, @@ -40,6 +42,8 @@ import { } from 'react-router-dom'; import { useCurrentPath } from '../hooks/use-current-path'; import { ProjectDetailsModal } from '../ui-components/project-details-modal'; +import { CompositeGraphPanel } from './panels/composite-graph-panel'; +import { CompositeContextPanel } from '../ui-components/composite-context-panel'; export function ProjectsSidebar(): JSX.Element { const environmentConfig = useEnvironmentConfig(); @@ -53,6 +57,10 @@ export function ProjectsSidebar(): JSX.Element { ); const groupByFolder = useProjectGraphSelector(groupByFolderSelector); const collapseEdges = useProjectGraphSelector(collapseEdgesSelector); + const compositeEnabled = useProjectGraphSelector( + compositeGraphEnabledSelector + ); + const compositeContext = useProjectGraphSelector(compositeContextSelector); const isTracing = projectGraphService.getSnapshot().matches('tracing'); const tracingInfo = useProjectGraphSelector(getTracingInfo); @@ -75,17 +83,48 @@ export function ProjectsSidebar(): JSX.Element { navigate(routeConstructor('/projects', true)); } + function resetCompositeContext() { + projectGraphService.send({ type: 'enableCompositeGraph', context: null }); + navigate( + routeConstructor( + { pathname: '/projects', search: '?composite=true' }, + true + ) + ); + } + function showAllProjects() { - navigate(routeConstructor('/projects/all', true)); + navigate( + routeConstructor('/projects/all', (searchParams) => { + if (searchParams.has('composite')) { + searchParams.set('composite', 'true'); + } + return searchParams; + }) + ); } function hideAllProjects() { projectGraphService.send({ type: 'deselectAll' }); - navigate(routeConstructor('/projects', true)); + navigate( + routeConstructor('/projects', (searchParams) => { + if (searchParams.has('composite')) { + searchParams.set('composite', 'true'); + } + return searchParams; + }) + ); } function showAffectedProjects() { - navigate(routeConstructor('/projects/affected', true)); + navigate( + routeConstructor('/projects/affected', (searchParams) => { + if (searchParams.has('composite')) { + searchParams.set('composite', 'true'); + } + return searchParams; + }) + ); } function searchDepthFilterEnabledChange(checked: boolean) { @@ -126,6 +165,17 @@ export function ProjectsSidebar(): JSX.Element { }); } + function compositeEnabledChanged(checked: boolean) { + setSearchParams((currentSearchParams) => { + if (checked) { + currentSearchParams.set('composite', 'true'); + } else { + currentSearchParams.delete('composite'); + } + return currentSearchParams; + }); + } + function incrementDepthFilter() { const newSearchDepth = searchDepthInfo.searchDepth + 1; setSearchParams((currentSearchParams) => { @@ -224,7 +274,7 @@ export function ProjectsSidebar(): JSX.Element { projectName: routeParams.endTrace, }); } - }, [routeParams]); + }, [routeParams, compositeEnabled]); useEffect(() => { if (searchParams.has('groupByFolder') && groupByFolder === false) { @@ -251,6 +301,17 @@ export function ProjectsSidebar(): JSX.Element { }); } + if (searchParams.has('composite')) { + const compositeParam = searchParams.get('composite'); + projectGraphService.send({ + type: 'enableCompositeGraph', + context: compositeParam === 'true' ? null : compositeParam, + }); + } else if (!searchParams.has('composite')) { + projectGraphService.send({ type: 'disableCompositeGraph' }); + navigate(routeConstructor('/projects', true)); + } + if (searchParams.has('searchDepth')) { const parsedValue = parseInt(searchParams.get('searchDepth'), 10); @@ -329,6 +390,13 @@ export function ProjectsSidebar(): JSX.Element { <> + {compositeEnabled && compositeContext ? ( + + ) : null} + {focusedProject ? ( -
+

Experimental Features

@@ -386,6 +456,10 @@ export function ProjectsSidebar(): JSX.Element { collapseEdges={collapseEdges} collapseEdgesChanged={collapseEdgesChanged} > +
diff --git a/graph/client/src/app/ui-components/checkbox-panel.tsx b/graph/client/src/app/ui-components/checkbox-panel.tsx index 9fea965cac1d8..90ac08e1ae3ff 100644 --- a/graph/client/src/app/ui-components/checkbox-panel.tsx +++ b/graph/client/src/app/ui-components/checkbox-panel.tsx @@ -1,4 +1,5 @@ import { memo } from 'react'; +import classNames from 'classnames'; export interface CheckboxPanelProps { checked: boolean; @@ -6,12 +7,28 @@ export interface CheckboxPanelProps { name: string; label: string; description: string; + disabled?: boolean; + disabledDescription?: string; } export const CheckboxPanel = memo( - ({ checked, checkChanged, label, description, name }: CheckboxPanelProps) => { + ({ + checked, + checkChanged, + label, + description, + name, + disabled, + disabledDescription, + }: CheckboxPanelProps) => { return ( -
+
checkChanged(event.target.checked)} checked={checked} + disabled={disabled} />
diff --git a/graph/client/src/app/ui-tooltips/graph-tooltip-display.tsx b/graph/client/src/app/ui-tooltips/graph-tooltip-display.tsx index 9bcf1c0f2e5d5..7ac1caa3c9dd8 100644 --- a/graph/client/src/app/ui-tooltips/graph-tooltip-display.tsx +++ b/graph/client/src/app/ui-tooltips/graph-tooltip-display.tsx @@ -41,17 +41,16 @@ export function TooltipDisplay() { }); break; case 'focus-node': { - const to = - action.tooltipNodeType === 'compositeNode' - ? routeConstructor( - { - pathname: `/projects`, - search: `?composite=true&compositeContext=${action.id}`, - }, - false - ) - : routeConstructor(`/projects/${action.id}`, true); - navigate(to); + if (action.tooltipNodeType === 'compositeNode') { + navigate( + routeConstructor( + { pathname: `/projects`, search: `?composite=${action.id}` }, + true + ) + ); + } else { + navigate(routeConstructor(`/projects/${action.id}`, true)); + } break; } case 'collapse-node': diff --git a/package.json b/package.json index a2105a7786e12..c84bcfd5078cf 100644 --- a/package.json +++ b/package.json @@ -319,7 +319,7 @@ "@markdoc/markdoc": "0.2.2", "@monaco-editor/react": "^4.4.6", "@napi-rs/canvas": "^0.1.52", - "@nx/graph": "0.0.1-alpha.15", + "@nx/graph": "0.0.1-alpha.17", "@react-spring/three": "^9.7.3", "@react-three/drei": "^9.108.3", "@react-three/fiber": "^8.16.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb638601f93ab..aa6a54d78d231 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,8 +31,8 @@ importers: specifier: ^0.1.52 version: 0.1.52 '@nx/graph': - specifier: 0.0.1-alpha.15 - version: 0.0.1-alpha.15(@nx/devkit@19.7.0-beta.6(nx@19.7.0-beta.6(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.5.3))(@swc/core@1.5.7(@swc/helpers@0.5.11))))(nx@19.7.0-beta.6(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.5.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + specifier: 0.0.1-alpha.17 + version: 0.0.1-alpha.17(@nx/devkit@19.7.0-beta.6(nx@19.7.0-beta.6(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.5.3))(@swc/core@1.5.7(@swc/helpers@0.5.11))))(nx@19.7.0-beta.6(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.5.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@react-spring/three': specifier: ^9.7.3 version: 9.7.3(@react-three/fiber@8.16.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(three@0.166.1))(react@18.3.1)(three@0.166.1) @@ -5776,8 +5776,8 @@ packages: '@zkochan/js-yaml': optional: true - '@nx/graph@0.0.1-alpha.15': - resolution: {integrity: sha512-wwotjQcUCz46NknyvZ99Pi0qpvVul6Maa1Y+bvcd6SUgrpmbfW/zyMQejlyq+iKufZCTJv/iiDUIKZEKvvVRjw==} + '@nx/graph@0.0.1-alpha.17': + resolution: {integrity: sha512-s1pe+om4dk0vGojjv1yHQIRNbeIajHs66glS/lcZP2YovCLWXy+3OBmM1jzzWrKIW1fdSwamO5S9WBdWnAPKFQ==} peerDependencies: '@nx/devkit': '>= 19 < 20' nx: '>= 19 < 20' @@ -25471,7 +25471,7 @@ snapshots: - supports-color - verdaccio - '@nx/graph@0.0.1-alpha.15(@nx/devkit@19.7.0-beta.6(nx@19.7.0-beta.6(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.5.3))(@swc/core@1.5.7(@swc/helpers@0.5.11))))(nx@19.7.0-beta.6(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.5.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@nx/graph@0.0.1-alpha.17(@nx/devkit@19.7.0-beta.6(nx@19.7.0-beta.6(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.5.3))(@swc/core@1.5.7(@swc/helpers@0.5.11))))(nx@19.7.0-beta.6(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.5.3))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.23.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react': 0.26.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)