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 3f7dcbc489..e73bfab9a6 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 @@ -25,15 +25,65 @@ export const compositeGraphStateConfig: ProjectGraphStateNodeConfig = { ), ], exit: [ - send(() => ({ type: 'notifyGraphDisableCompositeGraph' }), { - to: (ctx) => ctx.graphActor, - }), assign((ctx) => { ctx.compositeGraph.enabled = false; ctx.compositeGraph.context = undefined; }), + send( + (ctx) => ({ + type: 'notifyGraphUpdateGraph', + projects: ctx.projects, + dependencies: ctx.dependencies, + fileMap: ctx.fileMap, + affectedProjects: ctx.affectedProjects, + workspaceLayout: ctx.workspaceLayout, + groupByFolder: ctx.groupByFolder, + selectedProjects: ctx.selectedProjects, + composite: ctx.compositeGraph, + }), + { + to: (ctx) => ctx.graphActor, + } + ), ], 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 +162,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/composite-graph-panel.tsx b/graph/client/src/app/feature-projects/panels/composite-graph-panel.tsx index 0b1a147de8..3969b660bb 100644 --- a/graph/client/src/app/feature-projects/panels/composite-graph-panel.tsx +++ b/graph/client/src/app/feature-projects/panels/composite-graph-panel.tsx @@ -8,7 +8,7 @@ export interface CompositeGraphPanelProps { export const CompositeGraphPanel = memo( ({ compositeEnabled, compositeEnabledChanged }: CompositeGraphPanelProps) => { return ( -
+
compositeEnabledChanged(event.target.checked) } 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 34e5ef3fc9..f17c5ff5fd 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 33af5316eb..2ba6ef72d8 100644 --- a/graph/client/src/app/feature-projects/project-list.tsx +++ b/graph/client/src/app/feature-projects/project-list.tsx @@ -27,6 +27,7 @@ import { getProjectGraphService } from '../machines/get-services'; import { Link, useNavigate, useNavigation } from 'react-router-dom'; import { useRouteConstructor } from '@nx/graph/shared'; import { CompositeNode } from '../interfaces'; +import { useMemo } from 'react'; interface SidebarProject { projectGraphNode: ProjectGraphProjectNode; @@ -249,6 +250,10 @@ function CompositeNodeListItem({ const routeConstructor = useRouteConstructor(); const navigate = useNavigate(); + const label = compositeNode.parent + ? `${compositeNode.parent}/${compositeNode.label}` + : compositeNode.label; + function toggleProject() { if (compositeNode.state !== 'hidden') { projectGraphService.send({ @@ -283,13 +288,8 @@ function CompositeNodeListItem({
- {compositeNode.label} + {label}
@@ -339,8 +339,6 @@ function CompositeNodeList({ }: { compositeNodes: CompositeNode[]; }) { - const projectGraphService = getProjectGraphService(); - if (compositeNodes.length === 0) { return

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 676f318bdb..95380ecefd 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,9 +42,13 @@ 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'; +import { getGraphService } from '../machines/graph.service'; export function ProjectsSidebar(): JSX.Element { const environmentConfig = useEnvironmentConfig(); + const graphService = getGraphService(); const projectGraphService = getProjectGraphService(); const focusedProject = useProjectGraphSelector(focusedProjectNameSelector); const searchDepthInfo = useProjectGraphSelector(searchDepthSelector); @@ -53,6 +59,11 @@ 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 +86,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 +168,19 @@ export function ProjectsSidebar(): JSX.Element { }); } + function compositeEnabledChanged(checked: boolean) { + navigate( + routeConstructor('/projects', (searchParams) => { + if (checked) { + searchParams.set('composite', 'true'); + } else { + searchParams.delete('composite'); + } + return searchParams; + }) + ); + } + function incrementDepthFilter() { const newSearchDepth = searchDepthInfo.searchDepth + 1; setSearchParams((currentSearchParams) => { @@ -182,6 +237,19 @@ export function ProjectsSidebar(): JSX.Element { }); } + useEffect(() => { + return graphService.listen((event) => { + if (event.type === 'CompositeNodeDblClick') { + projectGraphService.send({ + type: event.data.expanded + ? 'collapseCompositeNode' + : 'expandCompositeNode', + id: event.id, + }); + } + }); + }, []); + useEffect(() => { projectGraphService.send({ type: 'setProjects', @@ -224,7 +292,7 @@ export function ProjectsSidebar(): JSX.Element { projectName: routeParams.endTrace, }); } - }, [routeParams]); + }, [routeParams, compositeEnabled]); useEffect(() => { if (searchParams.has('groupByFolder') && groupByFolder === false) { @@ -251,6 +319,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 +408,13 @@ export function ProjectsSidebar(): JSX.Element { <> + {compositeEnabled && compositeContext ? ( + + ) : null} + {focusedProject ? ( + + -
+

Experimental Features

diff --git a/graph/client/src/app/interfaces.ts b/graph/client/src/app/interfaces.ts index 3fc4f5b46a..58d4bafaca 100644 --- a/graph/client/src/app/interfaces.ts +++ b/graph/client/src/app/interfaces.ts @@ -36,4 +36,5 @@ export interface CompositeNode { id: string; label: string; state: 'expanded' | 'collapsed' | 'hidden'; + parent?: string; } diff --git a/graph/client/src/app/shell.tsx b/graph/client/src/app/shell.tsx index 6e424ee0f8..3bb86c48df 100644 --- a/graph/client/src/app/shell.tsx +++ b/graph/client/src/app/shell.tsx @@ -23,7 +23,7 @@ import { Dropdown, Spinner } from '@nx/graph/ui-components'; import { getSystemTheme, Theme, ThemePanel } from '@nx/graph-internal/ui-theme'; import { Tooltip } from '@nx/graph/ui-tooltips'; import classNames from 'classnames'; -import { useLayoutEffect, useState } from 'react'; +import { useEffect, useLayoutEffect, useState } from 'react'; import { Outlet, useNavigate, @@ -43,14 +43,18 @@ import { TooltipDisplay } from './ui-tooltips/graph-tooltip-display'; export function Shell(): JSX.Element { const projectGraphService = getProjectGraphService(); const projectGraphDataService = getProjectGraphDataService(); - const graphService = getGraphService(); - const lastPerfReport = useSyncExternalStore( - (callback) => graphService.listen(callback), - () => graphService.lastPerformanceReport + const [lastPerfReport, setLastPerfReport] = useState( + graphService.lastPerformanceReport ); + useEffect(() => { + graphService.listen(() => { + setLastPerfReport(graphService.lastPerformanceReport); + }); + }, []); + const nodesVisible = lastPerfReport.numNodes !== 0; const environment = useEnvironmentConfig(); diff --git a/graph/client/src/app/ui-components/checkbox-panel.tsx b/graph/client/src/app/ui-components/checkbox-panel.tsx index 9fea965cac..90ac08e1ae 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 9bcf1c0f2e..bcc70677e4 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': @@ -81,7 +80,12 @@ export function TooltipDisplay() { navigate( routeConstructor( `/projects/trace/${encodeURIComponent(start)}/${action.id}`, - true + (searchParams) => { + if (searchParams.has('composite')) { + searchParams.delete('composite'); + } + return searchParams; + } ) ); break; diff --git a/package.json b/package.json index e6d947b802..d81c5de797 100644 --- a/package.json +++ b/package.json @@ -327,7 +327,7 @@ "@markdoc/markdoc": "0.2.2", "@monaco-editor/react": "^4.4.6", "@napi-rs/canvas": "^0.1.52", - "@nx/graph": "0.0.1-alpha.16", + "@nx/graph": "0.0.1-alpha.23", "@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 8d9c2ec841..7bf4d455e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,8 +31,8 @@ importers: specifier: ^0.1.52 version: 0.1.55 '@nx/graph': - specifier: 0.0.1-alpha.16 - version: 0.0.1-alpha.16(@nx/devkit@19.8.0-beta.2(nx@19.8.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))))(nx@19.8.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + specifier: 0.0.1-alpha.23 + version: 0.0.1-alpha.23(@nx/devkit@19.8.0-beta.2(nx@19.8.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))))(nx@19.8.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(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.4(@react-three/fiber@8.17.7(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) @@ -4575,8 +4575,8 @@ packages: '@zkochan/js-yaml': optional: true - '@nx/graph@0.0.1-alpha.16': - resolution: {integrity: sha512-pJVFuTunlRfa8BuO2Vy6wxRtfC2kDT3TKDR6y1KXmMl90xTVT30zU/K9ITXPZzLfo8nLpewIfbv41gGSCY9+Dg==} + '@nx/graph@0.0.1-alpha.23': + resolution: {integrity: sha512-vgP9CxcCLa551AWZkwfN6qkwLGdZazU+GL7KZusIH8L3enRo2GHqyPQaF93fnTenokYM0SphR85/X4QibF3mlA==} peerDependencies: '@nx/devkit': '>= 19 < 20' nx: '>= 19 < 20' @@ -21629,7 +21629,7 @@ snapshots: - supports-color - verdaccio - '@nx/graph@0.0.1-alpha.16(@nx/devkit@19.8.0-beta.2(nx@19.8.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))))(nx@19.8.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@nx/graph@0.0.1-alpha.23(@nx/devkit@19.8.0-beta.2(nx@19.8.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11))))(nx@19.8.0-beta.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.5.4))(@swc/core@1.5.7(@swc/helpers@0.5.11)))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.26.2(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)