diff --git a/graph/client/src/app/external-api.ts b/graph/client/src/app/external-api.ts index c6fa8f7f51451..94ae4c13afe7c 100644 --- a/graph/client/src/app/external-api.ts +++ b/graph/client/src/app/external-api.ts @@ -1,6 +1,7 @@ import { getRouter } from './get-router'; import { getProjectGraphService } from './machines/get-services'; import { ProjectGraphMachineEvents } from './feature-projects/machines/interfaces'; +import { getGraphService } from './machines/graph.service'; export class ExternalApi { _projectGraphService = getProjectGraphService(); @@ -13,6 +14,7 @@ export class ExternalApi { }); router = getRouter(); + graphService = getGraphService(); projectGraphService = { send: (event: ProjectGraphMachineEvents) => { @@ -20,10 +22,21 @@ export class ExternalApi { }, }; + private fileClickCallbackListeners: ((url: string) => void)[] = []; + get depGraphService() { return this.projectGraphService; } + constructor() { + this.graphService.listen((event) => { + if (event.type === 'FileLinkClick') { + const url = `${event.sourceRoot}/${event.file}`; + this.fileClickCallbackListeners.forEach((cb) => cb(url)); + } + }); + } + focusProject(projectName: string) { this.router.navigate(`/projects/${encodeURIComponent(projectName)}`); } @@ -42,6 +55,10 @@ export class ExternalApi { window.appConfig.showExperimentalFeatures = false; } + registerFileClickCallback(callback: (url: string) => void) { + this.fileClickCallbackListeners.push(callback); + } + private handleLegacyProjectGraphEvent(event: ProjectGraphMachineEvents) { switch (event.type) { case 'focusProject': diff --git a/graph/client/src/app/feature-projects/machines/project-graph.spec.ts b/graph/client/src/app/feature-projects/machines/project-graph.spec.ts index 69986d8e04ed2..d71cfe7eca2e2 100644 --- a/graph/client/src/app/feature-projects/machines/project-graph.spec.ts +++ b/graph/client/src/app/feature-projects/machines/project-graph.spec.ts @@ -7,6 +7,7 @@ import type { /* eslint-enable @nx/enforce-module-boundaries */ import { interpret } from 'xstate'; import { projectGraphMachine } from './project-graph.machine'; +import { AppConfig } from '../../interfaces'; export const mockProjects: ProjectGraphProjectNode[] = [ { @@ -96,7 +97,24 @@ export const mockDependencies: Record = { 'auth-lib': [], }; +const mockAppConfig: AppConfig = { + showDebugger: false, + showExperimentalFeatures: false, + workspaces: [ + { + id: 'local', + label: 'local', + projectGraphUrl: 'assets/project-graphs/e2e.json', + taskGraphUrl: 'assets/task-graphs/e2e.json', + }, + ], + defaultWorkspaceId: 'local', +}; + describe('dep-graph machine', () => { + beforeEach(() => { + window.appConfig = mockAppConfig; + }); describe('initGraph', () => { it('should set projects, dependencies, and workspaceLayout', () => { const result = projectGraphMachine.transition( diff --git a/graph/client/src/app/hooks/get-project-graph-data-service.ts b/graph/client/src/app/hooks/get-project-graph-data-service.ts index 1f5e129478c5d..5d8f1213c5f60 100644 --- a/graph/client/src/app/hooks/get-project-graph-data-service.ts +++ b/graph/client/src/app/hooks/get-project-graph-data-service.ts @@ -7,11 +7,14 @@ let projectGraphService: ProjectGraphService; export function getProjectGraphDataService() { if (projectGraphService === undefined) { - if (window.environment === 'dev' || window.environment === 'nx-console') { + if (window.environment === 'dev') { projectGraphService = new FetchProjectGraphService(); } else if (window.environment === 'watch') { projectGraphService = new MockProjectGraphService(); - } else if (window.environment === 'release') { + } else if ( + window.environment === 'release' || + window.environment === 'nx-console' + ) { if (window.localMode === 'build') { projectGraphService = new LocalProjectGraphService(); } else { diff --git a/graph/client/src/app/machines/graph.service.ts b/graph/client/src/app/machines/graph.service.ts index e44fad5a765b5..fb0562cbba224 100644 --- a/graph/client/src/app/machines/graph.service.ts +++ b/graph/client/src/app/machines/graph.service.ts @@ -1,14 +1,16 @@ import { GraphService } from '@nx/graph/ui-graph'; import { selectValueByThemeStatic } from '../theme-resolver'; +import { getEnvironmentConfig } from '../hooks/use-environment-config'; let graphService: GraphService; export function getGraphService(): GraphService { + const environment = getEnvironmentConfig(); if (!graphService) { - const darkModeEnabled = selectValueByThemeStatic(true, false); graphService = new GraphService( 'cytoscape-graph', - selectValueByThemeStatic('dark', 'light') + selectValueByThemeStatic('dark', 'light'), + environment.environment === 'nx-console' ? 'nx-console' : undefined ); } diff --git a/graph/ui-graph/src/lib/graph-interaction-events.ts b/graph/ui-graph/src/lib/graph-interaction-events.ts index 77a0b92d3854c..297dde2630b4e 100644 --- a/graph/ui-graph/src/lib/graph-interaction-events.ts +++ b/graph/ui-graph/src/lib/graph-interaction-events.ts @@ -32,9 +32,16 @@ interface BackgroundClickEvent { type: 'BackgroundClick'; } +interface FileLinkClickEvent { + type: 'FileLinkClick'; + sourceRoot: string; + file: string; +} + export type GraphInteractionEvents = | ProjectNodeClickEvent | EdgeClickEvent | GraphRegeneratedEvent | TaskNodeClickEvent - | BackgroundClickEvent; + | BackgroundClickEvent + | FileLinkClickEvent; diff --git a/graph/ui-graph/src/lib/graph.ts b/graph/ui-graph/src/lib/graph.ts index e51bec11710b4..ef779e2d75e82 100644 --- a/graph/ui-graph/src/lib/graph.ts +++ b/graph/ui-graph/src/lib/graph.ts @@ -31,7 +31,7 @@ export class GraphService { constructor( container: string | HTMLElement, theme: 'light' | 'dark', - renderMode?: 'nx-console' | 'nx-docs', + public renderMode?: 'nx-console' | 'nx-docs', rankDir: 'TB' | 'LR' = 'TB' ) { use(cytoscapeDagre); diff --git a/graph/ui-graph/src/lib/tooltip-service.ts b/graph/ui-graph/src/lib/tooltip-service.ts index d820786f7f820..430dcf89eeb07 100644 --- a/graph/ui-graph/src/lib/tooltip-service.ts +++ b/graph/ui-graph/src/lib/tooltip-service.ts @@ -33,11 +33,21 @@ export class GraphTooltipService { }); break; case 'EdgeClick': + const callback = + graph.renderMode === 'nx-console' + ? (url) => + graph.broadcast({ + type: 'FileLinkClick', + sourceRoot: event.data.sourceRoot, + file: url, + }) + : undefined; this.openEdgeToolTip(event.ref, { type: event.data.type, target: event.data.target, source: event.data.source, fileDependencies: event.data.fileDependencies, + fileClickCallback: callback, }); break; } @@ -57,7 +67,11 @@ export class GraphTooltipService { } openEdgeToolTip(ref: VirtualElement, props: ProjectEdgeNodeTooltipProps) { - this.currentTooltip = { type: 'projectEdge', ref, props }; + this.currentTooltip = { + type: 'projectEdge', + ref, + props, + }; this.broadcastChange(); } diff --git a/graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts b/graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts index 9c389ee30ec90..acacf2b31fa40 100644 --- a/graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts +++ b/graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts @@ -310,7 +310,6 @@ export class ProjectTraversalGraph { projectNode.affected = affectedProjectIds.includes(project.name); projectNodes.push(projectNode); - dependencies[project.name].forEach((dep) => { if (filteredProjectNames.includes(dep.target)) { const edge = new ProjectEdge(dep); diff --git a/graph/ui-graph/src/lib/util-cytoscape/render-graph.ts b/graph/ui-graph/src/lib/util-cytoscape/render-graph.ts index 4f98e38ed3e6e..69a32a8f391f1 100644 --- a/graph/ui-graph/src/lib/util-cytoscape/render-graph.ts +++ b/graph/ui-graph/src/lib/util-cytoscape/render-graph.ts @@ -276,6 +276,7 @@ export class RenderGraph { type: edge.data('type'), source: edge.source().id(), target: edge.target().id(), + sourceRoot: edge.source().data('root'), fileDependencies: edge .source() diff --git a/graph/ui-tooltips/src/lib/project-edge-tooltip.tsx b/graph/ui-tooltips/src/lib/project-edge-tooltip.tsx index 4607b8af9e518..f5a9fc89c006d 100644 --- a/graph/ui-tooltips/src/lib/project-edge-tooltip.tsx +++ b/graph/ui-tooltips/src/lib/project-edge-tooltip.tsx @@ -6,6 +6,7 @@ export interface ProjectEdgeNodeTooltipProps { target: string; fileDependencies: Array<{ fileName: string }>; description?: string; + fileClickCallback: (fileName: string) => void; } export function ProjectEdgeNodeTooltip({ @@ -14,6 +15,7 @@ export function ProjectEdgeNodeTooltip({ target, fileDependencies, description, + fileClickCallback, }: ProjectEdgeNodeTooltipProps) { return (
@@ -33,7 +35,16 @@ export function ProjectEdgeNodeTooltip({ {fileDependencies.map((fileDep) => (
  • fileClickCallback(fileDep.fileName) + : () => {} + } > {fileDep.fileName}