- {type ?? 'unknown'}
+ {type ?? 'unknown'}
{source} → {target}
{type !== 'implicit' ? (
diff --git a/graph/client/src/app/ui-tooltips/project-node-tooltip.stories.tsx b/graph/client/src/app/ui-tooltips/project-node-tooltip.stories.tsx
index 0d1415d3a98f9..f6c9357cee6a9 100644
--- a/graph/client/src/app/ui-tooltips/project-node-tooltip.stories.tsx
+++ b/graph/client/src/app/ui-tooltips/project-node-tooltip.stories.tsx
@@ -1,4 +1,4 @@
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
import {
ProjectNodeToolTip,
ProjectNodeToolTipProps,
diff --git a/graph/client/src/app/ui-tooltips/project-node-tooltip.tsx b/graph/client/src/app/ui-tooltips/project-node-tooltip.tsx
index f380331415247..979ae85332a36 100644
--- a/graph/client/src/app/ui-tooltips/project-node-tooltip.tsx
+++ b/graph/client/src/app/ui-tooltips/project-node-tooltip.tsx
@@ -1,9 +1,5 @@
import { getProjectGraphService } from '../machines/get-services';
-import {
- DocumentMagnifyingGlassIcon,
- FlagIcon,
- MapPinIcon,
-} from '@heroicons/react/24/solid';
+import { FlagIcon, MapPinIcon } from '@heroicons/react/24/solid';
import Tag from '../ui-components/tag';
export interface ProjectNodeToolTipProps {
@@ -17,31 +13,31 @@ export function ProjectNodeToolTip({
id,
tags,
}: ProjectNodeToolTipProps) {
- const depGraphService = getProjectGraphService();
+ const projectGraphService = getProjectGraphService();
function onFocus() {
- depGraphService.send({
+ projectGraphService.send({
type: 'focusProject',
projectName: id,
});
}
function onExclude() {
- depGraphService.send({
+ projectGraphService.send({
type: 'deselectProject',
projectName: id,
});
}
function onStartTrace() {
- depGraphService.send({
+ projectGraphService.send({
type: 'setTracingStart',
projectName: id,
});
}
function onEndTrace() {
- depGraphService.send({
+ projectGraphService.send({
type: 'setTracingEnd',
projectName: id,
});
@@ -50,7 +46,7 @@ export function ProjectNodeToolTip({
return (
- {type}
+ {type}
{id}
{tags.length > 0 ? (
diff --git a/graph/client/src/app/ui-tooltips/tooltip-service.ts b/graph/client/src/app/ui-tooltips/tooltip-service.ts
index 451a26787a815..d441f665d5f6d 100644
--- a/graph/client/src/app/ui-tooltips/tooltip-service.ts
+++ b/graph/client/src/app/ui-tooltips/tooltip-service.ts
@@ -3,7 +3,7 @@ import { getGraphService } from '../machines/graph.service';
import { VirtualElement } from '@popperjs/core';
import { ProjectNodeToolTipProps } from './project-node-tooltip';
import { EdgeNodeTooltipProps } from './edge-tooltip';
-import { GraphInteractionEvents, GraphService } from '@nrwl/graph/ui-graph';
+import { GraphService } from '@nrwl/graph/ui-graph';
export class GraphTooltipService {
private subscribers: Set
= new Set();
diff --git a/graph/client/src/globals.d.ts b/graph/client/src/globals.d.ts
index 33ee6b0b2828f..4c28fa9235613 100644
--- a/graph/client/src/globals.d.ts
+++ b/graph/client/src/globals.d.ts
@@ -1,17 +1,17 @@
// nx-ignore-next-line
import type {
- DepGraphClientResponse,
+ ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/dep-graph';
import { AppConfig } from './app/interfaces';
-import { ExternalApi } from './app/machines/externalApi';
+import { ExternalApi } from './app/external-api';
export declare global {
export interface Window {
exclude: string[];
watch: boolean;
localMode: 'serve' | 'build';
- projectGraphResponse?: DepGraphClientResponse;
+ projectGraphResponse?: ProjectGraphClientResponse;
taskGraphResponse?: TaskGraphClientResponse;
environment: 'dev' | 'watch' | 'release' | 'nx-console';
appConfig: AppConfig;
diff --git a/graph/client/src/main.tsx b/graph/client/src/main.tsx
index c7759dc21ca2a..3105c512e9ab5 100644
--- a/graph/client/src/main.tsx
+++ b/graph/client/src/main.tsx
@@ -2,7 +2,7 @@ import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom';
import { inspect } from '@xstate/inspect';
import App from './app/app';
-import { ExternalApi } from './app/machines/externalApi';
+import { ExternalApi } from './app/external-api';
if (window.useXstateInspect === true) {
inspect({
diff --git a/graph/client/src/styles.scss b/graph/client/src/styles.scss
index 370893e67f82c..31f6ecf89d2bb 100644
--- a/graph/client/src/styles.scss
+++ b/graph/client/src/styles.scss
@@ -25,14 +25,6 @@ html {
display: flex;
}
-#no-projects-chosen {
- display: flex;
- width: 100%;
- height: 100%;
- align-items: center;
- justify-content: center;
-}
-
#graph-container,
#cytoscape-graph {
width: 100%;
diff --git a/graph/ui-graph/src/index.ts b/graph/ui-graph/src/index.ts
index 40eb064a181ae..5d6a1042c76e7 100644
--- a/graph/ui-graph/src/index.ts
+++ b/graph/ui-graph/src/index.ts
@@ -1,3 +1,3 @@
-export * from './lib/nx-graph-viz';
+export * from './lib/nx-project-graph-viz';
export * from './lib/graph';
export * from './lib/graph-interaction-events';
diff --git a/graph/ui-graph/src/lib/graph-interaction-events.ts b/graph/ui-graph/src/lib/graph-interaction-events.ts
index 7f95b65fbda55..2e5382d50bcfa 100644
--- a/graph/ui-graph/src/lib/graph-interaction-events.ts
+++ b/graph/ui-graph/src/lib/graph-interaction-events.ts
@@ -1,6 +1,6 @@
import { VirtualElement } from '@popperjs/core';
import { NodeDataDefinition } from './util-cytoscape/project-node';
-import { EdgeDataDefinition } from './util-cytoscape/edge';
+import { EdgeDataDefinition } from './util-cytoscape/project-edge';
interface NodeClickEvent {
type: 'NodeClick';
diff --git a/graph/ui-graph/src/lib/graph.ts b/graph/ui-graph/src/lib/graph.ts
index b77cf8f32a71c..3375c799c9114 100644
--- a/graph/ui-graph/src/lib/graph.ts
+++ b/graph/ui-graph/src/lib/graph.ts
@@ -2,13 +2,19 @@
import { CollectionReturnValue, use } from 'cytoscape';
import cytoscapeDagre from 'cytoscape-dagre';
import popper from 'cytoscape-popper';
-import { GraphPerfReport, GraphRenderEvents } from './interfaces';
+import {
+ GraphPerfReport,
+ ProjectGraphRenderEvents,
+ TaskGraphRenderEvents,
+} from './interfaces';
import { GraphInteractionEvents } from './graph-interaction-events';
import { RenderGraph } from './util-cytoscape/render-graph';
import { ProjectTraversalGraph } from './util-cytoscape/project-traversal-graph';
+import { TaskTraversalGraph } from './util-cytoscape/task-traversal.graph';
export class GraphService {
- private traversalGraph: ProjectTraversalGraph;
+ private projectTraversalGraph: ProjectTraversalGraph;
+ private taskTraversalGraph: TaskTraversalGraph;
private renderGraph: RenderGraph;
private listeners = new Map<
@@ -28,7 +34,8 @@ export class GraphService {
this.renderGraph = new RenderGraph(container, theme, renderMode, rankDir);
this.renderGraph.listen((event) => this.broadcast(event));
- this.traversalGraph = new ProjectTraversalGraph();
+ this.projectTraversalGraph = new ProjectTraversalGraph();
+ this.taskTraversalGraph = new TaskTraversalGraph();
}
set theme(theme: 'light' | 'dark') {
@@ -52,7 +59,7 @@ export class GraphService {
this.listeners.forEach((callback) => callback(event));
}
- handleEvent(event: GraphRenderEvents): {
+ handleProjectEvent(event: ProjectGraphRenderEvents): {
selectedProjectNames: string[];
perfReport: GraphPerfReport;
} {
@@ -70,7 +77,7 @@ export class GraphService {
case 'notifyGraphInitGraph':
this.renderGraph.collapseEdges = event.collapseEdges;
this.broadcast({ type: 'GraphRegenerated' });
- this.traversalGraph.initGraph(
+ this.projectTraversalGraph.initGraph(
event.projects,
event.groupByFolder,
event.workspaceLayout,
@@ -83,7 +90,7 @@ export class GraphService {
case 'notifyGraphUpdateGraph':
this.renderGraph.collapseEdges = event.collapseEdges;
this.broadcast({ type: 'GraphRegenerated' });
- this.traversalGraph.initGraph(
+ this.projectTraversalGraph.initGraph(
event.projects,
event.groupByFolder,
event.workspaceLayout,
@@ -91,7 +98,7 @@ export class GraphService {
event.affectedProjects,
event.collapseEdges
);
- elementsToSendToRender = this.traversalGraph.setShownProjects(
+ elementsToSendToRender = this.projectTraversalGraph.setShownProjects(
event.selectedProjects.length > 0
? event.selectedProjects
: this.renderGraph.getCurrentlyShownProjectIds()
@@ -99,7 +106,7 @@ export class GraphService {
break;
case 'notifyGraphFocusProject':
- elementsToSendToRender = this.traversalGraph.focusProject(
+ elementsToSendToRender = this.projectTraversalGraph.focusProject(
event.projectName,
event.searchDepth
);
@@ -107,51 +114,54 @@ export class GraphService {
break;
case 'notifyGraphFilterProjectsByText':
- elementsToSendToRender = this.traversalGraph.filterProjectsByText(
- event.search,
- event.includeProjectsByPath,
- event.searchDepth
- );
+ elementsToSendToRender =
+ this.projectTraversalGraph.filterProjectsByText(
+ event.search,
+ event.includeProjectsByPath,
+ event.searchDepth
+ );
break;
case 'notifyGraphShowProjects':
- elementsToSendToRender = this.traversalGraph.showProjects(
+ elementsToSendToRender = this.projectTraversalGraph.showProjects(
event.projectNames,
this.renderGraph.getCurrentlyShownProjectIds()
);
break;
case 'notifyGraphHideProjects':
- elementsToSendToRender = this.traversalGraph.hideProjects(
+ elementsToSendToRender = this.projectTraversalGraph.hideProjects(
event.projectNames,
this.renderGraph.getCurrentlyShownProjectIds()
);
break;
case 'notifyGraphShowAllProjects':
- elementsToSendToRender = this.traversalGraph.showAllProjects();
+ elementsToSendToRender = this.projectTraversalGraph.showAllProjects();
break;
case 'notifyGraphHideAllProjects':
- elementsToSendToRender = this.traversalGraph.hideAllProjects();
+ elementsToSendToRender = this.projectTraversalGraph.hideAllProjects();
break;
case 'notifyGraphShowAffectedProjects':
- elementsToSendToRender = this.traversalGraph.showAffectedProjects();
+ elementsToSendToRender =
+ this.projectTraversalGraph.showAffectedProjects();
break;
case 'notifyGraphTracing':
if (event.start && event.end) {
if (event.algorithm === 'shortest') {
- elementsToSendToRender = this.traversalGraph.traceProjects(
+ elementsToSendToRender = this.projectTraversalGraph.traceProjects(
event.start,
event.end
);
} else {
- elementsToSendToRender = this.traversalGraph.traceAllProjects(
- event.start,
- event.end
- );
+ elementsToSendToRender =
+ this.projectTraversalGraph.traceAllProjects(
+ event.start,
+ event.end
+ );
}
}
break;
@@ -189,6 +199,51 @@ export class GraphService {
return { selectedProjectNames, perfReport };
}
+ handleTaskEvent(event: TaskGraphRenderEvents) {
+ const time = Date.now();
+
+ this.broadcast({ type: 'GraphRegenerated' });
+
+ let elementsToSendToRender: CollectionReturnValue;
+ switch (event.type) {
+ case 'notifyTaskGraphSetProjects':
+ this.taskTraversalGraph.setProjects(event.projects, event.taskGraphs);
+ break;
+ case 'notifyTaskGraphTaskSelected':
+ elementsToSendToRender = this.taskTraversalGraph.selectTask(
+ event.taskId
+ );
+ break;
+ }
+
+ let selectedProjectNames: string[] = [];
+ let perfReport: GraphPerfReport = {
+ numEdges: 0,
+ numNodes: 0,
+ renderTime: 0,
+ };
+
+ if (this.renderGraph && elementsToSendToRender) {
+ this.renderGraph.setElements(elementsToSendToRender);
+
+ const { numEdges, numNodes } = this.renderGraph.render();
+
+ selectedProjectNames = (
+ elementsToSendToRender.nodes('[type!="dir"]') ?? []
+ ).map((node) => node.id());
+
+ const renderTime = Date.now() - time;
+
+ perfReport = {
+ renderTime,
+ numNodes,
+ numEdges,
+ };
+ }
+
+ return { selectedProjectNames, perfReport };
+ }
+
getImage() {
return this.renderGraph.getImage();
}
diff --git a/graph/ui-graph/src/lib/interfaces.ts b/graph/ui-graph/src/lib/interfaces.ts
index d44c325f588d4..53f805e4fd6d3 100644
--- a/graph/ui-graph/src/lib/interfaces.ts
+++ b/graph/ui-graph/src/lib/interfaces.ts
@@ -2,20 +2,8 @@
import type {
ProjectGraphDependency,
ProjectGraphProjectNode,
+ TaskGraph,
} from '@nrwl/devkit';
-import { ActionObject, ActorRef, State, StateNodeConfig } from 'xstate';
-
-// The hierarchical (recursive) schema for the states
-export interface DepGraphSchema {
- states: {
- idle: {};
- unselected: {};
- focused: {};
- textFiltered: {};
- customSelected: {};
- tracing: {};
- };
-}
export interface GraphPerfReport {
renderTime: number;
@@ -24,56 +12,10 @@ export interface GraphPerfReport {
}
export type TracingAlgorithmType = 'shortest' | 'all';
-// The events that the machine handles
-
-export type DepGraphUIEvents =
- | {
- type: 'setSelectedProjectsFromGraph';
- selectedProjectNames: string[];
- perfReport: GraphPerfReport;
- }
- | { type: 'selectProject'; projectName: string }
- | { type: 'deselectProject'; projectName: string }
- | { type: 'selectProjects'; projectNames: string[] }
- | { type: 'deselectProjects'; projectNames: string[] }
- | { type: 'selectAll' }
- | { type: 'deselectAll' }
- | { type: 'selectAffected' }
- | { type: 'setGroupByFolder'; groupByFolder: boolean }
- | { type: 'setTracingStart'; projectName: string }
- | { type: 'setTracingEnd'; projectName: string }
- | { type: 'clearTraceStart' }
- | { type: 'clearTraceEnd' }
- | { type: 'setTracingAlgorithm'; algorithm: TracingAlgorithmType }
- | { type: 'setCollapseEdges'; collapseEdges: boolean }
- | { type: 'setIncludeProjectsByPath'; includeProjectsByPath: boolean }
- | { type: 'incrementSearchDepth' }
- | { type: 'decrementSearchDepth' }
- | { type: 'setSearchDepthEnabled'; searchDepthEnabled: boolean }
- | { type: 'setSearchDepth'; searchDepth: number }
- | { type: 'focusProject'; projectName: string }
- | { type: 'unfocusProject' }
- | { type: 'filterByText'; search: string }
- | { type: 'clearTextFilter' }
- | {
- type: 'initGraph';
- projects: ProjectGraphProjectNode[];
- dependencies: Record;
- affectedProjects: string[];
- workspaceLayout: {
- libsDir: string;
- appsDir: string;
- };
- }
- | {
- type: 'updateGraph';
- projects: ProjectGraphProjectNode[];
- dependencies: Record;
- };
// The events that the graph actor handles
-export type GraphRenderEvents =
+export type ProjectGraphRenderEvents =
| {
type: 'notifyGraphInitGraph';
projects: ProjectGraphProjectNode[];
@@ -169,53 +111,17 @@ export type RouteEvents =
algorithm: TracingAlgorithmType;
};
-export type AllEvents = DepGraphUIEvents | GraphRenderEvents | RouteEvents;
-
-// The context (extended state) of the machine
-export interface DepGraphContext {
- projects: ProjectGraphProjectNode[];
- dependencies: Record;
- affectedProjects: string[];
- selectedProjects: string[];
- focusedProject: string | null;
- textFilter: string;
- includePath: boolean;
- searchDepth: number;
- searchDepthEnabled: boolean;
- groupByFolder: boolean;
- collapseEdges: boolean;
- workspaceLayout: {
- libsDir: string;
- appsDir: string;
- };
- graphActor: ActorRef;
- routeSetterActor: ActorRef;
- routeListenerActor: ActorRef;
- lastPerfReport: GraphPerfReport;
- tracing: {
- start: string;
- end: string;
- algorithm: TracingAlgorithmType;
- };
-}
-
-export type DepGraphStateNodeConfig = StateNodeConfig<
- DepGraphContext,
- {},
- DepGraphUIEvents,
- ActionObject
->;
-
-export type DepGraphSend = (
- event: DepGraphUIEvents | DepGraphUIEvents[]
-) => void;
-
-export type DepGraphState = State<
- DepGraphContext,
- DepGraphUIEvents,
- any,
- {
- value: any;
- context: DepGraphContext;
- }
->;
+export type TaskGraphRecord = Record;
+export type TaskGraphRenderEvents =
+ | {
+ type: 'notifyTaskGraphSetProjects';
+ projects: ProjectGraphProjectNode[];
+ taskGraphs: TaskGraphRecord;
+ }
+ | {
+ type: 'notifyTaskGraphTaskSelected';
+ taskId: string;
+ }
+ | {
+ type: 'notifyTaskGraphDeselectTask';
+ };
diff --git a/graph/ui-graph/src/lib/nx-graph-viz.stories.tsx b/graph/ui-graph/src/lib/nx-graph-viz.stories.tsx
index 9ff0c4f3e805f..23f23ced383f6 100644
--- a/graph/ui-graph/src/lib/nx-graph-viz.stories.tsx
+++ b/graph/ui-graph/src/lib/nx-graph-viz.stories.tsx
@@ -1,14 +1,14 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
-import { NxGraphViz } from './nx-graph-viz';
+import { NxProjectGraphViz } from './nx-project-graph-viz';
-const Story: ComponentMeta = {
- component: NxGraphViz,
+const Story: ComponentMeta = {
+ component: NxProjectGraphViz,
title: 'NxGraphViz',
};
export default Story;
-const Template: ComponentStory = (args) => (
-
+const Template: ComponentStory = (args) => (
+
);
export const Primary = Template.bind({});
diff --git a/graph/ui-graph/src/lib/nx-graph-viz.tsx b/graph/ui-graph/src/lib/nx-project-graph-viz.tsx
similarity index 91%
rename from graph/ui-graph/src/lib/nx-graph-viz.tsx
rename to graph/ui-graph/src/lib/nx-project-graph-viz.tsx
index 3efe55b83934e..e916e851454fd 100644
--- a/graph/ui-graph/src/lib/nx-graph-viz.tsx
+++ b/graph/ui-graph/src/lib/nx-project-graph-viz.tsx
@@ -25,7 +25,7 @@ function resolveTheme(theme: Theme): 'dark' | 'light' {
return darkMedia.matches ? 'dark' : 'light';
}
}
-export function NxGraphViz({
+export function NxProjectGraphViz({
projects,
groupByFolder,
workspaceLayout,
@@ -58,7 +58,7 @@ export function NxGraphViz({
'nx-docs',
'TB'
);
- graph.handleEvent({
+ graph.handleProjectEvent({
type: 'notifyGraphInitGraph',
projects,
groupByFolder,
@@ -67,7 +67,7 @@ export function NxGraphViz({
affectedProjects: affectedProjectIds,
collapseEdges: false,
});
- graph.handleEvent({ type: 'notifyGraphShowAllProjects' });
+ graph.handleProjectEvent({ type: 'notifyGraphShowAllProjects' });
setGraph(graph);
});
}
@@ -82,4 +82,4 @@ export function NxGraphViz({
);
}
-export default NxGraphViz;
+export default NxProjectGraphViz;
diff --git a/graph/ui-graph/src/lib/nx-task-graph-viz.stories.tsx b/graph/ui-graph/src/lib/nx-task-graph-viz.stories.tsx
new file mode 100644
index 0000000000000..d70d33cc06d95
--- /dev/null
+++ b/graph/ui-graph/src/lib/nx-task-graph-viz.stories.tsx
@@ -0,0 +1,95 @@
+import type { ComponentStory, ComponentMeta } from '@storybook/react';
+import { NxTaskGraphViz } from './nx-task-graph-viz';
+
+const Story: ComponentMeta = {
+ component: NxTaskGraphViz,
+ title: 'NxTaskGraphViz',
+};
+export default Story;
+
+const Template: ComponentStory = (args) => (
+
+);
+
+export const Primary = Template.bind({});
+Primary.args = {
+ projects: [
+ {
+ type: 'app',
+ name: 'app',
+ data: {
+ tags: ['scope:cart'],
+ },
+ },
+ {
+ type: 'lib',
+ name: 'lib',
+ data: {
+ tags: ['scope:cart'],
+ },
+ },
+ {
+ type: 'lib',
+ name: 'lib2',
+ data: {
+ root: 'libs/nested-scope/lib2',
+ tags: ['scope:cart'],
+ },
+ },
+ {
+ type: 'lib',
+ name: 'lib3',
+ data: {
+ root: 'libs/nested-scope/lib3',
+ tags: ['scope:cart'],
+ },
+ },
+ ],
+ taskGraphs: {
+ app: {
+ build: {
+ production: {
+ roots: ['app:build:production'],
+ tasks: {
+ 'app:build:production': {
+ id: 'app:build:production',
+ target: {
+ project: 'app',
+ target: 'build',
+ },
+ },
+ 'lib1:build': {
+ id: 'lib1:build',
+ target: {
+ project: 'lib1',
+ target: 'build',
+ },
+ },
+ 'lib2:build': {
+ id: 'lib2:build',
+ target: {
+ project: 'lib2',
+ target: 'build',
+ },
+ },
+ 'lib3:build': {
+ id: 'lib3:build',
+ target: {
+ project: 'lib3',
+ target: 'build',
+ },
+ },
+ },
+ dependencies: {
+ 'app:build:production': ['lib1:build', 'lib2:build', 'lib3:build'],
+ 'lib1:build': [],
+ 'lib2:build': [],
+ 'lib3:build': [],
+ },
+ },
+ },
+ },
+ },
+ taskId: 'app:build:production',
+ height: '450px',
+};
diff --git a/graph/ui-graph/src/lib/nx-task-graph-viz.tsx b/graph/ui-graph/src/lib/nx-task-graph-viz.tsx
new file mode 100644
index 0000000000000..be54fff870ad0
--- /dev/null
+++ b/graph/ui-graph/src/lib/nx-task-graph-viz.tsx
@@ -0,0 +1,80 @@
+/* nx-ignore-next-line */
+import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
+import { useEffect, useRef, useState } from 'react';
+import { GraphService } from './graph';
+import { TaskGraphRecord } from './interfaces';
+
+type Theme = 'light' | 'dark' | 'system';
+
+export interface TaskGraphUiGraphProps {
+ projects: ProjectGraphProjectNode[];
+ taskGraphs: TaskGraphRecord;
+ taskId: string;
+ theme: Theme;
+ height: string;
+}
+
+function resolveTheme(theme: Theme): 'dark' | 'light' {
+ if (theme !== 'system') {
+ return theme;
+ } else {
+ const darkMedia = window.matchMedia('(prefers-color-scheme: dark)');
+ return darkMedia.matches ? 'dark' : 'light';
+ }
+}
+
+export function NxTaskGraphViz({
+ projects,
+ taskId,
+ taskGraphs,
+ theme,
+ height,
+}: TaskGraphUiGraphProps) {
+ const containerRef = useRef(null);
+ const [graph, setGraph] = useState(null);
+ const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>();
+
+ const newlyResolvedTheme = resolveTheme(theme);
+
+ if (newlyResolvedTheme !== resolvedTheme) {
+ setResolvedTheme(newlyResolvedTheme);
+
+ if (graph) {
+ graph.theme = newlyResolvedTheme;
+ }
+ }
+ useEffect(() => {
+ if (containerRef.current !== null) {
+ import('./graph')
+ .then((module) => module.GraphService)
+ .then((GraphService) => {
+ const graph = new GraphService(
+ containerRef.current,
+ resolvedTheme,
+ 'nx-docs',
+ 'TB'
+ );
+ graph.handleTaskEvent({
+ type: 'notifiyTaskGraphSetProjects',
+ projects,
+ taskGraphs,
+ });
+ graph.handleTaskEvent({
+ type: 'notifyTaskGraphTaskSelected',
+ taskId,
+ });
+ setGraph(graph);
+ });
+ }
+ }, []);
+
+ return (
+
+ );
+}
+
+export default NxTaskGraphViz;
diff --git a/graph/ui-graph/src/lib/util-cytoscape/index.ts b/graph/ui-graph/src/lib/util-cytoscape/index.ts
index f10dc4ac0b409..06dade8987cfb 100644
--- a/graph/ui-graph/src/lib/util-cytoscape/index.ts
+++ b/graph/ui-graph/src/lib/util-cytoscape/index.ts
@@ -1,4 +1,4 @@
export * from './cytoscape.models';
-export * from './edge';
+export * from './project-edge';
export * from './parent-node';
export * from './project-node';
diff --git a/graph/ui-graph/src/lib/util-cytoscape/edge.ts b/graph/ui-graph/src/lib/util-cytoscape/project-edge.ts
similarity index 100%
rename from graph/ui-graph/src/lib/util-cytoscape/edge.ts
rename to graph/ui-graph/src/lib/util-cytoscape/project-edge.ts
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 b53f10a0ff876..2870ffe961253 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
@@ -13,7 +13,7 @@ import {
} from 'nx/src/config/project-graph';
import { edgeStyles, nodeStyles } from '../styles-graph';
import { ProjectNode } from './project-node';
-import { ProjectEdge } from './edge';
+import { ProjectEdge } from './project-edge';
import { ParentNode } from './parent-node';
export class ProjectTraversalGraph {
@@ -269,7 +269,6 @@ export class ProjectTraversalGraph {
headless: true,
elements: [...elements],
boxSelectionEnabled: false,
- style: [...nodeStyles, ...edgeStyles],
});
}
diff --git a/graph/ui-graph/src/lib/util-cytoscape/task-edge.ts b/graph/ui-graph/src/lib/util-cytoscape/task-edge.ts
new file mode 100644
index 0000000000000..125899b36866f
--- /dev/null
+++ b/graph/ui-graph/src/lib/util-cytoscape/task-edge.ts
@@ -0,0 +1,27 @@
+// nx-ignore-next-line
+import type { ProjectGraphDependency } from '@nrwl/devkit';
+import * as cy from 'cytoscape';
+
+export interface TaskEdgeDataDefinition extends cy.NodeDataDefinition {
+ id: string;
+ source: string;
+ target: string;
+}
+
+export class TaskEdge {
+ constructor(private source: string, private target: string) {}
+
+ getCytoscapeNodeDef(): cy.NodeDefinition {
+ let edge: cy.EdgeDefinition;
+ edge = {
+ group: 'edges',
+ data: {
+ id: `${this.source}|${this.target}`,
+ source: this.source,
+ target: this.target,
+ },
+ };
+
+ return edge;
+ }
+}
diff --git a/graph/ui-graph/src/lib/util-cytoscape/task-node.ts b/graph/ui-graph/src/lib/util-cytoscape/task-node.ts
new file mode 100644
index 0000000000000..d53a69c9bd099
--- /dev/null
+++ b/graph/ui-graph/src/lib/util-cytoscape/task-node.ts
@@ -0,0 +1,29 @@
+// nx-ignore-next-line
+import type { ProjectGraphProjectNode, Task } from '@nrwl/devkit';
+import * as cy from 'cytoscape';
+
+export interface TaskNodeDataDefinition extends cy.NodeDataDefinition {
+ id: string;
+ executor: string;
+}
+
+export class TaskNode {
+ constructor(private task: Task, private project: ProjectGraphProjectNode) {}
+
+ getCytoscapeNodeDef(): cy.NodeDefinition {
+ return {
+ group: 'nodes',
+ data: this.getData(),
+ selectable: false,
+ grabbable: false,
+ pannable: true,
+ };
+ }
+
+ private getData(): TaskNodeDataDefinition {
+ return {
+ id: this.task.id,
+ executor: 'placeholder',
+ };
+ }
+}
diff --git a/graph/ui-graph/src/lib/util-cytoscape/task-traversal.graph.ts b/graph/ui-graph/src/lib/util-cytoscape/task-traversal.graph.ts
new file mode 100644
index 0000000000000..2980a678dd0c1
--- /dev/null
+++ b/graph/ui-graph/src/lib/util-cytoscape/task-traversal.graph.ts
@@ -0,0 +1,67 @@
+// nx-ignore-next-line
+import type { ProjectGraphProjectNode } from '@nrwl/devkit';
+import { TaskGraphRecord } from '../interfaces';
+import { TaskNode } from './task-node';
+import { TaskEdge } from './task-edge';
+import cytoscape, { Core } from 'cytoscape';
+
+export class TaskTraversalGraph {
+ private projects: ProjectGraphProjectNode[] = [];
+ private taskGraphs: TaskGraphRecord = {};
+ private cy: Core;
+
+ setProjects(
+ projects: ProjectGraphProjectNode[],
+ taskGraphs: TaskGraphRecord
+ ) {
+ this.projects = projects;
+ this.taskGraphs = taskGraphs;
+ }
+
+ selectTask(taskId: string) {
+ this.createElements(taskId);
+
+ return this.cy.elements();
+ }
+
+ private createElements(taskId: string) {
+ const [projectName, target, configuration] = taskId.split(':');
+ const taskGraph = this.taskGraphs[taskId];
+
+ if (taskGraph === undefined) {
+ console.log(this.taskGraphs);
+ throw new Error(`Could not find task graph for ${taskId}`);
+ }
+
+ const project = this.projects.find(
+ (project) => project.name === projectName
+ );
+
+ if (project === undefined) {
+ throw new Error(`Could not find project ${projectName}`);
+ }
+
+ const taskElements = [];
+
+ for (let taskName in taskGraph.tasks) {
+ taskElements.push(
+ new TaskNode(
+ taskGraph.tasks[taskName],
+ this.projects[taskGraph.tasks[taskName].target.project]
+ )
+ );
+ }
+
+ for (let topDep in taskGraph.dependencies) {
+ taskGraph.dependencies[topDep].forEach((childDep) =>
+ taskElements.push(new TaskEdge(topDep, childDep))
+ );
+ }
+
+ this.cy = cytoscape({
+ headless: true,
+ elements: taskElements.map((element) => element.getCytoscapeNodeDef()),
+ boxSelectionEnabled: false,
+ });
+ }
+}
diff --git a/nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx b/nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx
index 6d64783958dac..ebe2297ff7070 100644
--- a/nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx
+++ b/nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx
@@ -7,7 +7,8 @@ import { ReactElement } from 'react';
* in the top level of the module for preloading to work, similar to React.lazy.
*/
const NxGraphViz = dynamic(
- () => import('@nrwl/graph/ui-graph').then((module) => module.NxGraphViz),
+ () =>
+ import('@nrwl/graph/ui-graph').then((module) => module.NxProjectGraphViz),
{
ssr: false,
loading: () => (
diff --git a/packages/nx/src/command-line/dep-graph.ts b/packages/nx/src/command-line/dep-graph.ts
index 4f9072b59a6c5..907081ee90425 100644
--- a/packages/nx/src/command-line/dep-graph.ts
+++ b/packages/nx/src/command-line/dep-graph.ts
@@ -25,7 +25,7 @@ import { createTaskGraph } from 'nx/src/tasks-runner/create-task-graph';
import { TargetDefaults, TargetDependencies } from 'nx/src/config/nx-json';
import { TaskGraph } from 'nx/src/config/task-graph';
-export interface DepGraphClientResponse {
+export interface ProjectGraphClientResponse {
hash: string;
projects: ProjectGraphProjectNode[];
dependencies: Record;
@@ -36,12 +36,8 @@ export interface DepGraphClientResponse {
exclude: string[];
}
-export type TaskGraphDependencies = Record<
- string,
- Record>
->;
export interface TaskGraphClientResponse {
- dependencies: TaskGraphDependencies;
+ taskGraphs: Record;
}
// maps file extention to MIME types
@@ -66,7 +62,7 @@ function buildEnvironmentJs(
exclude: string[],
watchMode: boolean,
localMode: 'build' | 'serve',
- depGraphClientResponse?: DepGraphClientResponse,
+ depGraphClientResponse?: ProjectGraphClientResponse,
taskGraphClientResponse?: TaskGraphClientResponse
) {
let environmentJs = `window.exclude = ${JSON.stringify(exclude)};
@@ -416,7 +412,7 @@ async function startServer(
}
}
-let currentDepGraphClientResponse: DepGraphClientResponse = {
+let currentDepGraphClientResponse: ProjectGraphClientResponse = {
hash: null,
projects: [],
dependencies: {},
@@ -495,7 +491,7 @@ function createFileWatcher(root: string, changeHandler: () => Promise) {
async function createDepGraphClientResponse(
affected: string[] = []
-): Promise {
+): Promise {
performance.mark('project graph watch calculation:start');
await defaultFileHasher.init();
@@ -558,7 +554,7 @@ async function createTaskGraphClientResponse(): Promise
performance.mark('task graph generation:start');
- const tasks = getAllTaskGraphsForWorkspace(graph);
+ const taskGraphs = getAllTaskGraphsForWorkspace(graph);
performance.mark('task graph generation:end');
@@ -569,53 +565,51 @@ async function createTaskGraphClientResponse(): Promise
);
return {
- dependencies: tasks,
+ taskGraphs,
};
}
function getAllTaskGraphsForWorkspace(
projectGraph: ProjectGraph
-): TaskGraphDependencies {
+): Record {
const nxJson = readNxJson();
const defaultDependencyConfigs = mapTargetDefaultsToDependencies(
nxJson.targetDefaults
);
- const taskGraphs: TaskGraphDependencies = {};
+ const taskGraphs: Record = {};
for (const projectName in projectGraph.nodes) {
const project = projectGraph.nodes[projectName];
const targets = Object.keys(project.data.targets);
- taskGraphs[projectName] = {};
-
targets.forEach((target) => {
- taskGraphs[projectName][target] = {};
+ taskGraphs[createTaskId(projectName, target)] = createTaskGraph(
+ projectGraph,
+ defaultDependencyConfigs,
+ [projectName],
+ [target],
+ undefined,
+ {}
+ );
const configurations = Object.keys(
project.data.targets[target]?.configurations || {}
);
+
if (configurations.length > 0) {
configurations.forEach((configuration) => {
- taskGraphs[projectName][target][configuration] = createTaskGraph(
- projectGraph,
- defaultDependencyConfigs,
- [projectName],
- [target],
- configuration,
- {}
- );
+ taskGraphs[createTaskId(projectName, target, configuration)] =
+ createTaskGraph(
+ projectGraph,
+ defaultDependencyConfigs,
+ [projectName],
+ [target],
+ configuration,
+ {}
+ );
});
- } else {
- taskGraphs[projectName][target]['no-configurations'] = createTaskGraph(
- projectGraph,
- defaultDependencyConfigs,
- [projectName],
- [target],
- undefined,
- {}
- );
}
});
}
@@ -633,3 +627,15 @@ function mapTargetDefaultsToDependencies(
return res;
}
+
+function createTaskId(
+ projectId: string,
+ targetId: string,
+ configurationId?: string
+) {
+ if (configurationId) {
+ return `${projectId}:${targetId}:${configurationId}`;
+ } else {
+ return `${projectId}:${targetId}`;
+ }
+}