feat(graph): add clickable project edge file links in nx console (#18113)

This commit is contained in:
MaxKless 2023-07-25 11:10:25 +02:00 committed by GitHub
parent 7aee21afd1
commit f8068b7cf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 81 additions and 9 deletions

View File

@ -1,6 +1,7 @@
import { getRouter } from './get-router'; import { getRouter } from './get-router';
import { getProjectGraphService } from './machines/get-services'; import { getProjectGraphService } from './machines/get-services';
import { ProjectGraphMachineEvents } from './feature-projects/machines/interfaces'; import { ProjectGraphMachineEvents } from './feature-projects/machines/interfaces';
import { getGraphService } from './machines/graph.service';
export class ExternalApi { export class ExternalApi {
_projectGraphService = getProjectGraphService(); _projectGraphService = getProjectGraphService();
@ -13,6 +14,7 @@ export class ExternalApi {
}); });
router = getRouter(); router = getRouter();
graphService = getGraphService();
projectGraphService = { projectGraphService = {
send: (event: ProjectGraphMachineEvents) => { send: (event: ProjectGraphMachineEvents) => {
@ -20,10 +22,21 @@ export class ExternalApi {
}, },
}; };
private fileClickCallbackListeners: ((url: string) => void)[] = [];
get depGraphService() { get depGraphService() {
return this.projectGraphService; 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) { focusProject(projectName: string) {
this.router.navigate(`/projects/${encodeURIComponent(projectName)}`); this.router.navigate(`/projects/${encodeURIComponent(projectName)}`);
} }
@ -42,6 +55,10 @@ export class ExternalApi {
window.appConfig.showExperimentalFeatures = false; window.appConfig.showExperimentalFeatures = false;
} }
registerFileClickCallback(callback: (url: string) => void) {
this.fileClickCallbackListeners.push(callback);
}
private handleLegacyProjectGraphEvent(event: ProjectGraphMachineEvents) { private handleLegacyProjectGraphEvent(event: ProjectGraphMachineEvents) {
switch (event.type) { switch (event.type) {
case 'focusProject': case 'focusProject':

View File

@ -7,6 +7,7 @@ import type {
/* eslint-enable @nx/enforce-module-boundaries */ /* eslint-enable @nx/enforce-module-boundaries */
import { interpret } from 'xstate'; import { interpret } from 'xstate';
import { projectGraphMachine } from './project-graph.machine'; import { projectGraphMachine } from './project-graph.machine';
import { AppConfig } from '../../interfaces';
export const mockProjects: ProjectGraphProjectNode[] = [ export const mockProjects: ProjectGraphProjectNode[] = [
{ {
@ -96,7 +97,24 @@ export const mockDependencies: Record<string, ProjectGraphDependency[]> = {
'auth-lib': [], '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', () => { describe('dep-graph machine', () => {
beforeEach(() => {
window.appConfig = mockAppConfig;
});
describe('initGraph', () => { describe('initGraph', () => {
it('should set projects, dependencies, and workspaceLayout', () => { it('should set projects, dependencies, and workspaceLayout', () => {
const result = projectGraphMachine.transition( const result = projectGraphMachine.transition(

View File

@ -7,11 +7,14 @@ let projectGraphService: ProjectGraphService;
export function getProjectGraphDataService() { export function getProjectGraphDataService() {
if (projectGraphService === undefined) { if (projectGraphService === undefined) {
if (window.environment === 'dev' || window.environment === 'nx-console') { if (window.environment === 'dev') {
projectGraphService = new FetchProjectGraphService(); projectGraphService = new FetchProjectGraphService();
} else if (window.environment === 'watch') { } else if (window.environment === 'watch') {
projectGraphService = new MockProjectGraphService(); projectGraphService = new MockProjectGraphService();
} else if (window.environment === 'release') { } else if (
window.environment === 'release' ||
window.environment === 'nx-console'
) {
if (window.localMode === 'build') { if (window.localMode === 'build') {
projectGraphService = new LocalProjectGraphService(); projectGraphService = new LocalProjectGraphService();
} else { } else {

View File

@ -1,14 +1,16 @@
import { GraphService } from '@nx/graph/ui-graph'; import { GraphService } from '@nx/graph/ui-graph';
import { selectValueByThemeStatic } from '../theme-resolver'; import { selectValueByThemeStatic } from '../theme-resolver';
import { getEnvironmentConfig } from '../hooks/use-environment-config';
let graphService: GraphService; let graphService: GraphService;
export function getGraphService(): GraphService { export function getGraphService(): GraphService {
const environment = getEnvironmentConfig();
if (!graphService) { if (!graphService) {
const darkModeEnabled = selectValueByThemeStatic(true, false);
graphService = new GraphService( graphService = new GraphService(
'cytoscape-graph', 'cytoscape-graph',
selectValueByThemeStatic('dark', 'light') selectValueByThemeStatic('dark', 'light'),
environment.environment === 'nx-console' ? 'nx-console' : undefined
); );
} }

View File

@ -32,9 +32,16 @@ interface BackgroundClickEvent {
type: 'BackgroundClick'; type: 'BackgroundClick';
} }
interface FileLinkClickEvent {
type: 'FileLinkClick';
sourceRoot: string;
file: string;
}
export type GraphInteractionEvents = export type GraphInteractionEvents =
| ProjectNodeClickEvent | ProjectNodeClickEvent
| EdgeClickEvent | EdgeClickEvent
| GraphRegeneratedEvent | GraphRegeneratedEvent
| TaskNodeClickEvent | TaskNodeClickEvent
| BackgroundClickEvent; | BackgroundClickEvent
| FileLinkClickEvent;

View File

@ -31,7 +31,7 @@ export class GraphService {
constructor( constructor(
container: string | HTMLElement, container: string | HTMLElement,
theme: 'light' | 'dark', theme: 'light' | 'dark',
renderMode?: 'nx-console' | 'nx-docs', public renderMode?: 'nx-console' | 'nx-docs',
rankDir: 'TB' | 'LR' = 'TB' rankDir: 'TB' | 'LR' = 'TB'
) { ) {
use(cytoscapeDagre); use(cytoscapeDagre);

View File

@ -33,11 +33,21 @@ export class GraphTooltipService {
}); });
break; break;
case 'EdgeClick': case 'EdgeClick':
const callback =
graph.renderMode === 'nx-console'
? (url) =>
graph.broadcast({
type: 'FileLinkClick',
sourceRoot: event.data.sourceRoot,
file: url,
})
: undefined;
this.openEdgeToolTip(event.ref, { this.openEdgeToolTip(event.ref, {
type: event.data.type, type: event.data.type,
target: event.data.target, target: event.data.target,
source: event.data.source, source: event.data.source,
fileDependencies: event.data.fileDependencies, fileDependencies: event.data.fileDependencies,
fileClickCallback: callback,
}); });
break; break;
} }
@ -57,7 +67,11 @@ export class GraphTooltipService {
} }
openEdgeToolTip(ref: VirtualElement, props: ProjectEdgeNodeTooltipProps) { openEdgeToolTip(ref: VirtualElement, props: ProjectEdgeNodeTooltipProps) {
this.currentTooltip = { type: 'projectEdge', ref, props }; this.currentTooltip = {
type: 'projectEdge',
ref,
props,
};
this.broadcastChange(); this.broadcastChange();
} }

View File

@ -310,7 +310,6 @@ export class ProjectTraversalGraph {
projectNode.affected = affectedProjectIds.includes(project.name); projectNode.affected = affectedProjectIds.includes(project.name);
projectNodes.push(projectNode); projectNodes.push(projectNode);
dependencies[project.name].forEach((dep) => { dependencies[project.name].forEach((dep) => {
if (filteredProjectNames.includes(dep.target)) { if (filteredProjectNames.includes(dep.target)) {
const edge = new ProjectEdge(dep); const edge = new ProjectEdge(dep);

View File

@ -276,6 +276,7 @@ export class RenderGraph {
type: edge.data('type'), type: edge.data('type'),
source: edge.source().id(), source: edge.source().id(),
target: edge.target().id(), target: edge.target().id(),
sourceRoot: edge.source().data('root'),
fileDependencies: fileDependencies:
edge edge
.source() .source()

View File

@ -6,6 +6,7 @@ export interface ProjectEdgeNodeTooltipProps {
target: string; target: string;
fileDependencies: Array<{ fileName: string }>; fileDependencies: Array<{ fileName: string }>;
description?: string; description?: string;
fileClickCallback: (fileName: string) => void;
} }
export function ProjectEdgeNodeTooltip({ export function ProjectEdgeNodeTooltip({
@ -14,6 +15,7 @@ export function ProjectEdgeNodeTooltip({
target, target,
fileDependencies, fileDependencies,
description, description,
fileClickCallback,
}: ProjectEdgeNodeTooltipProps) { }: ProjectEdgeNodeTooltipProps) {
return ( return (
<div className="text-sm text-slate-700 dark:text-slate-400"> <div className="text-sm text-slate-700 dark:text-slate-400">
@ -33,7 +35,16 @@ export function ProjectEdgeNodeTooltip({
{fileDependencies.map((fileDep) => ( {fileDependencies.map((fileDep) => (
<li <li
key={fileDep.fileName} key={fileDep.fileName}
className="whitespace-nowrap px-4 py-2 text-sm font-medium text-slate-800 dark:text-slate-300" className={`whitespace-nowrap px-4 py-2 text-sm font-medium text-slate-800 dark:text-slate-300 ${
fileClickCallback !== undefined
? 'hover:underline hover:cursor-pointer'
: ''
}`}
onClick={
fileClickCallback !== undefined
? () => fileClickCallback(fileDep.fileName)
: () => {}
}
> >
<span className="block truncate font-normal"> <span className="block truncate font-normal">
{fileDep.fileName} {fileDep.fileName}