feat(graph): add clickable project edge file links in nx console (#18113)
This commit is contained in:
parent
7aee21afd1
commit
f8068b7cf6
@ -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':
|
||||
|
||||
@ -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<string, ProjectGraphDependency[]> = {
|
||||
'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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 (
|
||||
<div className="text-sm text-slate-700 dark:text-slate-400">
|
||||
@ -33,7 +35,16 @@ export function ProjectEdgeNodeTooltip({
|
||||
{fileDependencies.map((fileDep) => (
|
||||
<li
|
||||
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">
|
||||
{fileDep.fileName}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user