diff --git a/dep-graph/dep-graph/src/app/app.ts b/dep-graph/dep-graph/src/app/app.ts index db243e3d97..9f6b1e9915 100644 --- a/dep-graph/dep-graph/src/app/app.ts +++ b/dep-graph/dep-graph/src/app/app.ts @@ -3,17 +3,15 @@ import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/de import { fromEvent } from 'rxjs'; import { startWith } from 'rxjs/operators'; import { DebuggerPanel } from './debugger-panel'; -import { GraphComponent } from './graph'; +import { useGraphService } from './graph.service'; import { useDepGraphService } from './machines/dep-graph.service'; -import { DepGraphSend } from './machines/interfaces'; +import { DepGraphUIEvents, DepGraphSend } from './machines/interfaces'; import { AppConfig, DEFAULT_CONFIG, ProjectGraphService } from './models'; -import { GraphTooltipService } from './tooltip-service'; import { SidebarComponent } from './ui-sidebar/sidebar'; export class AppComponent { private sidebar = new SidebarComponent(); - private tooltipService = new GraphTooltipService(); - private graph = new GraphComponent(this.tooltipService); + private graph = useGraphService(); private debuggerPanel: DebuggerPanel; private windowResize$ = fromEvent(window, 'resize').pipe(startWith({})); @@ -24,7 +22,16 @@ export class AppComponent { private config: AppConfig = DEFAULT_CONFIG, private projectGraphService: ProjectGraphService ) { - const [_, send] = useDepGraphService(); + const [state$, send] = useDepGraphService(); + + state$.subscribe((state) => { + if (state.context.selectedProjects.length !== 0) { + document.getElementById('no-projects-chosen').style.display = 'none'; + } else { + document.getElementById('no-projects-chosen').style.display = 'flex'; + } + }); + this.send = send; this.loadProjectGraph(config.defaultProjectGraph); @@ -47,7 +54,6 @@ export class AppComponent { await this.projectGraphService.getProjectGraph(projectInfo.url); const workspaceLayout = project?.layout; - this.send({ type: 'initGraph', projects: project.projects, diff --git a/dep-graph/dep-graph/src/app/graph.service.ts b/dep-graph/dep-graph/src/app/graph.service.ts new file mode 100644 index 0000000000..eced47c014 --- /dev/null +++ b/dep-graph/dep-graph/src/app/graph.service.ts @@ -0,0 +1,15 @@ +import { GraphService } from './graph'; +import { GraphTooltipService } from './tooltip-service'; + +let graphService: GraphService; + +export function useGraphService(): GraphService { + if (!graphService) { + graphService = new GraphService( + new GraphTooltipService(), + 'graph-container' + ); + } + + return graphService; +} diff --git a/dep-graph/dep-graph/src/app/graph.ts b/dep-graph/dep-graph/src/app/graph.ts index 47a0d60cc5..dfa2f9d31c 100644 --- a/dep-graph/dep-graph/src/app/graph.ts +++ b/dep-graph/dep-graph/src/app/graph.ts @@ -1,15 +1,11 @@ -import type { - ProjectGraph, - ProjectGraphDependency, - ProjectGraphNode, -} from '@nrwl/devkit'; +import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; import type { VirtualElement } from '@popperjs/core'; import * as cy from 'cytoscape'; -import cytoscapeDagre from 'cytoscape-dagre'; -import popper from 'cytoscape-popper'; +import * as cytoscapeDagre from 'cytoscape-dagre'; +import * as popper from 'cytoscape-popper'; import { Subject } from 'rxjs'; import type { Instance } from 'tippy.js'; -import { useDepGraphService } from './machines/dep-graph.service'; +import { GraphRenderEvents } from './machines/interfaces'; import { ProjectNodeToolTip } from './project-node-tooltip'; import { edgeStyles, nodeStyles } from './styles-graph'; import { GraphTooltipService } from './tooltip-service'; @@ -25,129 +21,350 @@ export interface GraphPerfReport { numNodes: number; numEdges: number; } -export class GraphComponent { - private graph: cy.Core; +export class GraphService { + private traversalGraph: cy.Core; + private renderGraph: cy.Core; + private openTooltip: Instance = null; private renderTimesSubject = new Subject(); renderTimes$ = this.renderTimesSubject.asObservable(); - private send; - constructor(private tooltipService: GraphTooltipService) { + constructor( + private tooltipService: GraphTooltipService, + private containerId: string + ) { cy.use(cytoscapeDagre); cy.use(popper); - - const [state$, send] = useDepGraphService(); - this.send = send; - - state$.subscribe((state) => { - const projects = state.context.selectedProjects.map((projectName) => - state.context.projects.find((project) => project.name === projectName) - ); - this.render( - projects, - state.context.groupByFolder, - state.context.workspaceLayout, - state.context.focusedProject, - state.context.affectedProjects, - state.context.dependencies - ); - }); } - render( - selectedProjects: ProjectGraphNode[], - groupByFolder: boolean, - workspaceLayout, - focusedProject: string, - affectedProjects: string[], - dependencies: Record - ) { + handleEvent(event: GraphRenderEvents): string[] { const time = Date.now(); - if (selectedProjects.length === 0) { - document.getElementById('no-projects-chosen').style.display = 'flex'; - } else { - document.getElementById('no-projects-chosen').style.display = 'none'; + if ( + this.renderGraph && + event.type !== 'notifyGraphFocusProject' && + event.type !== 'notifyGraphUpdateGraph' + ) { + this.renderGraph.nodes('.focused').removeClass('focused'); } - this.tooltipService.hideAll(); - this.generateCytoscapeLayout( - selectedProjects, - groupByFolder, - workspaceLayout, - focusedProject, - affectedProjects, - dependencies - ); - this.listenForProjectNodeClicks(); - this.listenForProjectNodeHovers(); + switch (event.type) { + case 'notifyGraphInitGraph': + this.initGraph( + event.projects, + event.groupByFolder, + event.workspaceLayout, + event.dependencies, + event.affectedProjects + ); + break; - const renderTime = Date.now() - time; + case 'notifyGraphUpdateGraph': + this.initGraph( + event.projects, + event.groupByFolder, + event.workspaceLayout, + event.dependencies, + event.affectedProjects + ); + this.setShownProjects(event.selectedProjects); + break; - const report: GraphPerfReport = { - renderTime, - numNodes: this.graph.nodes().length, - numEdges: this.graph.edges().length, - }; + case 'notifyGraphFocusProject': + this.focusProject(event.projectName, event.searchDepth); + break; - this.renderTimesSubject.next(report); + case 'notifyGraphFilterProjectsByText': + this.filterProjectsByText( + event.search, + event.includeProjectsByPath, + event.searchDepth + ); + break; + + case 'notifyGraphShowProject': + this.showProjects([event.projectName]); + break; + + case 'notifyGraphHideProject': + this.hideProject(event.projectName); + break; + + case 'notifyGraphShowAllProjects': + this.showAllProjects(); + break; + + case 'notifyGraphHideAllProjects': + this.hideAllProjects(); + break; + + case 'notifyGraphShowAffectedProjects': + this.showAffectedProjects(); + break; + } + + let visibleProjects: string[] = []; + + if (this.renderGraph) { + this.renderGraph + .elements() + .sort((a, b) => a.id().localeCompare(b.id())) + .layout({ + name: 'dagre', + nodeDimensionsIncludeLabels: true, + rankSep: 75, + rankDir: 'TB', + edgeSep: 50, + ranker: 'network-simplex', + }) + .run(); + + this.renderGraph.fit().center().resize(); + + visibleProjects = this.renderGraph + .nodes('[type!="dir"]') + .map((node) => node.id()); + + const renderTime = Date.now() - time; + + const report: GraphPerfReport = { + renderTime, + numNodes: this.renderGraph.nodes().length, + numEdges: this.renderGraph.edges().length, + }; + + this.renderTimesSubject.next(report); + } + + return visibleProjects; } - private generateCytoscapeLayout( - selectedProjects: ProjectGraphNode[], - groupByFolder: boolean, - workspaceLayout, - focusedProject: string, - affectedProjects: string[], - dependencies: Record - ) { - const elements = this.createElements( - selectedProjects, - groupByFolder, - workspaceLayout, + setShownProjects(selectedProjectNames: string[]) { + let nodesToAdd = this.traversalGraph.collection(); + + selectedProjectNames.forEach((name) => { + nodesToAdd = nodesToAdd.union(this.traversalGraph.$id(name)); + }); + + const ancestorsToAdd = nodesToAdd.ancestors(); + + const nodesToRender = nodesToAdd.union(ancestorsToAdd); + const edgesToRender = nodesToRender.edgesTo(nodesToRender); + + this.transferToRenderGraph(nodesToRender.union(edgesToRender)); + } + + showProjects(selectedProjectNames: string[]) { + const currentNodes = + this.renderGraph?.nodes() ?? this.traversalGraph.collection(); + + let nodesToAdd = this.traversalGraph.collection(); + + selectedProjectNames.forEach((name) => { + nodesToAdd = nodesToAdd.union(this.traversalGraph.$id(name)); + }); + + const ancestorsToAdd = nodesToAdd.ancestors(); + + const nodesToRender = currentNodes.union(nodesToAdd).union(ancestorsToAdd); + const edgesToRender = nodesToRender.edgesTo(nodesToRender); + + this.transferToRenderGraph(nodesToRender.union(edgesToRender)); + } + + hideProject(projectName: string) { + const currentNodes = + this.renderGraph?.nodes() ?? this.traversalGraph.collection(); + const nodeToHide = this.renderGraph.$id(projectName); + + const nodesToAdd = currentNodes.difference(nodeToHide); + const ancestorsToAdd = nodesToAdd.ancestors(); + const nodesToRender = nodesToAdd.union(ancestorsToAdd); + const edgesToRender = nodesToRender.edgesTo(nodesToRender); + + this.transferToRenderGraph(nodesToRender.union(edgesToRender)); + } + + showAffectedProjects() { + const affectedProjects = this.traversalGraph.nodes('.affected'); + const affectedAncestors = affectedProjects.ancestors(); + + const affectedNodes = affectedProjects.union(affectedAncestors); + const affectedEdges = affectedNodes.edgesTo(affectedNodes); + + this.transferToRenderGraph(affectedNodes.union(affectedEdges)); + } + + focusProject(focusedProjectName: string, searchDepth: number = 1) { + const focusedProject = this.traversalGraph.$id(focusedProjectName); + + const includedProjects = this.includeProjectsByDepth( focusedProject, - affectedProjects, - dependencies + searchDepth ); - this.graph = cy({ - container: document.getElementById('graph-container'), - elements: [...elements], - layout: { - name: 'dagre', - nodeDimensionsIncludeLabels: true, - rankSep: 75, - edgeSep: 50, - ranker: 'network-simplex', - }, + const includedNodes = focusedProject.union(includedProjects); + + const includedAncestors = includedNodes.ancestors(); + + const nodesToRender = includedNodes.union(includedAncestors); + const edgesToRender = nodesToRender.edgesTo(nodesToRender); + + this.transferToRenderGraph(nodesToRender.union(edgesToRender)); + + this.renderGraph.$id(focusedProjectName).addClass('focused'); + } + + showAllProjects() { + this.transferToRenderGraph(this.traversalGraph.elements()); + } + + hideAllProjects() { + this.transferToRenderGraph(this.traversalGraph.collection()); + } + + filterProjectsByText( + search: string, + includePath: boolean, + searchDepth: number = -1 + ) { + const split = search.split(','); + + let filteredProjects = this.traversalGraph.nodes().filter((node) => { + return split.findIndex((splitItem) => node.id().includes(splitItem)) > -1; + }); + + if (includePath) { + filteredProjects = filteredProjects.union( + this.includeProjectsByDepth(filteredProjects, searchDepth) + ); + } + + filteredProjects = filteredProjects.union(filteredProjects.ancestors()); + const edgesToRender = filteredProjects.edgesTo(filteredProjects); + + this.transferToRenderGraph(filteredProjects.union(edgesToRender)); + } + + private transferToRenderGraph(elements: cy.Collection) { + let currentFocusedProjectName; + if (this.renderGraph) { + currentFocusedProjectName = this.renderGraph + .nodes('.focused') + .first() + .id(); + this.renderGraph.destroy(); + delete this.renderGraph; + } + const container = document.getElementById(this.containerId); + + this.renderGraph = cy({ + container: container, + headless: !container, boxSelectionEnabled: false, style: [...nodeStyles, ...edgeStyles], }); - this.graph.on('zoom', () => { + this.renderGraph.add(elements); + + if (!!currentFocusedProjectName) { + this.renderGraph.$id(currentFocusedProjectName).addClass('focused'); + } + this.renderGraph.on('zoom', () => { if (this.openTooltip) { this.openTooltip.hide(); this.openTooltip = null; } }); + this.listenForProjectNodeClicks(); + this.listenForProjectNodeHovers(); + } + + private includeProjectsByDepth( + projects: cy.NodeCollection | cy.NodeSingular, + depth: number = -1 + ) { + let predecessors = this.traversalGraph.collection(); + + if (depth === -1) { + predecessors = projects.predecessors(); + } else { + predecessors = projects.incomers(); + + for (let i = 1; i < depth; i++) { + predecessors = predecessors.union(predecessors.incomers()); + } + } + + let successors = this.traversalGraph.collection(); + + if (depth === -1) { + successors = projects.successors(); + } else { + successors = projects.outgoers(); + + for (let i = 1; i < depth; i++) { + successors = successors.union(successors.outgoers()); + } + } + + return projects.union(predecessors).union(successors); + } + + initGraph( + allProjects: ProjectGraphNode[], + groupByFolder: boolean, + workspaceLayout, + dependencies: Record, + affectedProjectIds: string[] + ) { + this.tooltipService.hideAll(); + + this.generateCytoscapeLayout( + allProjects, + groupByFolder, + workspaceLayout, + dependencies, + affectedProjectIds + ); + } + + private generateCytoscapeLayout( + allProjects: ProjectGraphNode[], + groupByFolder: boolean, + workspaceLayout, + dependencies: Record, + affectedProjectIds: string[] + ) { + const elements = this.createElements( + allProjects, + groupByFolder, + workspaceLayout, + dependencies, + affectedProjectIds + ); + + this.traversalGraph = cy({ + headless: true, + elements: [...elements], + boxSelectionEnabled: false, + style: [...nodeStyles, ...edgeStyles], + }); } private createElements( - selectedProjects: ProjectGraphNode[], + projects: ProjectGraphNode[], groupByFolder: boolean, workspaceLayout: { appsDir: string; libsDir: string; }, - focusedProject: string, - affectedProjects: string[], - dependencies: Record + dependencies: Record, + affectedProjectIds: string[] ) { let elements: cy.ElementDefinition[] = []; - const filteredProjectNames = selectedProjects.map( - (project) => project.name - ); + const filteredProjectNames = projects.map((project) => project.name); const projectNodes: ProjectNode[] = []; const edgeNodes: ProjectEdge[] = []; @@ -156,24 +373,21 @@ export class GraphComponent { { id: string; parentId: string; label: string } > = {}; - selectedProjects.forEach((project) => { + projects.forEach((project) => { const workspaceRoot = project.type === 'app' || project.type === 'e2e' ? workspaceLayout.appsDir : workspaceLayout.libsDir; const projectNode = new ProjectNode(project, workspaceRoot); - projectNode.focused = project.name === focusedProject; - projectNode.affected = affectedProjects.includes(project.name); + + projectNode.affected = affectedProjectIds.includes(project.name); projectNodes.push(projectNode); dependencies[project.name].forEach((dep) => { if (filteredProjectNames.includes(dep.target)) { const edge = new ProjectEdge(dep); - edge.affected = - affectedProjects.includes(dep.source) && - affectedProjects.includes(dep.target); edgeNodes.push(edge); } }); @@ -205,7 +419,7 @@ export class GraphComponent { } listenForProjectNodeClicks() { - this.graph.$('node:childless').on('click', (event) => { + this.renderGraph.$('node:childless').on('click', (event) => { const node = event.target; let ref: VirtualElement = node.popperRef(); // used only for positioning @@ -217,11 +431,11 @@ export class GraphComponent { } listenForProjectNodeHovers(): void { - this.graph.on('mouseover', (event) => { + this.renderGraph.on('mouseover', (event) => { const node = event.target; if (!node.isNode || !node.isNode() || node.isParent()) return; - this.graph + this.renderGraph .elements() .difference(node.outgoers().union(node.incomers())) .not(node) @@ -232,11 +446,12 @@ export class GraphComponent { .union(node.incomers()) .addClass('highlight'); }); - this.graph.on('mouseout', (event) => { + + this.renderGraph.on('mouseout', (event) => { const node = event.target; if (!node.isNode || !node.isNode() || node.isParent()) return; - this.graph.elements().removeClass('transparent'); + this.renderGraph.elements().removeClass('transparent'); node .removeClass('highlight') .outgoers() diff --git a/dep-graph/dep-graph/src/app/machines/custom-selected.state.ts b/dep-graph/dep-graph/src/app/machines/custom-selected.state.ts index c54ae951af..6566160e2e 100644 --- a/dep-graph/dep-graph/src/app/machines/custom-selected.state.ts +++ b/dep-graph/dep-graph/src/app/machines/custom-selected.state.ts @@ -1,23 +1,40 @@ import { assign } from '@xstate/immer'; +import { send } from 'xstate'; import { DepGraphStateNodeConfig } from './interfaces'; export const customSelectedStateConfig: DepGraphStateNodeConfig = { on: { updateGraph: { + target: 'customSelected', actions: [ assign((ctx, event) => { const existingProjectNames = ctx.projects.map( (project) => project.name ); const newProjectNames = event.projects.map((project) => project.name); - const selectedProjects = newProjectNames.filter( + const newSelectedProjects = newProjectNames.filter( (projectName) => !existingProjectNames.includes(projectName) ); - - ctx.projects = event.projects; - ctx.dependencies = event.dependencies; - ctx.selectedProjects = [...ctx.selectedProjects, ...selectedProjects]; + ctx.selectedProjects = [ + ...ctx.selectedProjects, + ...newSelectedProjects, + ]; }), + 'setGraph', + send( + (ctx, event) => ({ + type: 'notifyGraphUpdateGraph', + projects: ctx.projects, + dependencies: ctx.dependencies, + affectedProjects: ctx.affectedProjects, + workspaceLayout: ctx.workspaceLayout, + groupByFolder: ctx.groupByFolder, + selectedProjects: ctx.selectedProjects, + }), + { + to: (context) => context.graph, + } + ), ], }, }, diff --git a/dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts b/dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts index d43f2a59f4..885c92cbf0 100644 --- a/dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts +++ b/dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts @@ -1,8 +1,13 @@ import { assign } from '@xstate/immer'; -import { Machine } from 'xstate'; +import { Machine, send, spawn } from 'xstate'; +import { useGraphService } from '../graph.service'; import { customSelectedStateConfig } from './custom-selected.state'; import { focusedStateConfig } from './focused.state'; -import { DepGraphContext, DepGraphEvents, DepGraphSchema } from './interfaces'; +import { + DepGraphContext, + DepGraphUIEvents, + DepGraphSchema, +} from './interfaces'; import { textFilteredStateConfig } from './text-filtered.state'; import { unselectedStateConfig } from './unselected.state'; @@ -21,12 +26,25 @@ export const initialContext: DepGraphContext = { libsDir: '', appsDir: '', }, + graph: null, +}; + +const graphActor = (callback, receive) => { + const graphService = useGraphService(); + + receive((e) => { + const selectedProjectNames = graphService.handleEvent(e); + callback({ + type: 'setSelectedProjectsFromGraph', + selectedProjectNames, + }); + }); }; export const depGraphMachine = Machine< DepGraphContext, DepGraphSchema, - DepGraphEvents + DepGraphUIEvents >( { id: 'DepGraph', @@ -42,37 +60,39 @@ export const depGraphMachine = Machine< on: { initGraph: { target: 'unselected', + actions: [ + 'setGraph', + send( + (ctx, event) => ({ + type: 'notifyGraphInitGraph', + projects: ctx.projects, + dependencies: ctx.dependencies, + affectedProjects: ctx.affectedProjects, + workspaceLayout: ctx.workspaceLayout, + groupByFolder: ctx.groupByFolder, + }), + { + to: (context) => context.graph, + } + ), + ], + }, + setSelectedProjectsFromGraph: { actions: assign((ctx, event) => { - ctx.projects = event.projects; - ctx.affectedProjects = event.affectedProjects; - ctx.dependencies = event.dependencies; - ctx.workspaceLayout = event.workspaceLayout; + ctx.selectedProjects = event.selectedProjectNames; }), }, - selectProject: { target: 'customSelected', - actions: [ - assign((ctx, event) => { - ctx.selectedProjects.push(event.projectName); - }), - ], + actions: ['notifyGraphShowProject'], }, selectAll: { target: 'customSelected', - actions: [ - assign((ctx) => { - ctx.selectedProjects = ctx.projects.map((project) => project.name); - }), - ], + actions: ['notifyGraphShowAllProjects'], }, selectAffected: { target: 'customSelected', - actions: [ - assign((ctx) => { - ctx.selectedProjects = ctx.affectedProjects; - }), - ], + actions: ['notifyGraphShowAffectedProjects'], }, deselectProject: [ { @@ -81,15 +101,7 @@ export const depGraphMachine = Machine< }, { target: 'customSelected', - actions: [ - assign((ctx, event) => { - const index = ctx.selectedProjects.findIndex( - (project) => project === event.projectName - ); - - ctx.selectedProjects.splice(index, 1); - }), - ], + actions: ['notifyGraphHideProject'], }, ], deselectAll: { @@ -100,9 +112,21 @@ export const depGraphMachine = Machine< }, setGroupByFolder: { actions: [ - assign((ctx, event: any) => { - ctx.groupByFolder = event.groupByFolder; - }), + 'setGroupByFolder', + send( + (ctx, event) => ({ + type: 'notifyGraphUpdateGraph', + projects: ctx.projects, + dependencies: ctx.dependencies, + affectedProjects: ctx.affectedProjects, + workspaceLayout: ctx.workspaceLayout, + groupByFolder: ctx.groupByFolder, + selectedProjects: ctx.selectedProjects, + }), + { + to: (context) => context.graph, + } + ), ], }, setIncludeProjectsByPath: { @@ -113,25 +137,13 @@ export const depGraphMachine = Machine< ], }, incrementSearchDepth: { - actions: [ - assign((ctx) => { - ctx.searchDepth = ctx.searchDepth + 1; - }), - ], + actions: ['incrementSearchDepth'], }, decrementSearchDepth: { - actions: [ - assign((ctx) => { - ctx.searchDepth = ctx.searchDepth > 1 ? ctx.searchDepth - 1 : 1; - }), - ], + actions: ['decrementSearchDepth'], }, setSearchDepthEnabled: { - actions: [ - assign((ctx, event) => { - ctx.searchDepthEnabled = event.searchDepthEnabled; - }), - ], + actions: ['setSearchDepthEnabled'], }, filterByText: { target: 'textFiltered', @@ -144,5 +156,111 @@ export const depGraphMachine = Machine< return ctx.selectedProjects.length <= 1; }, }, + actions: { + setGroupByFolder: assign((ctx, event) => { + if (event.type !== 'setGroupByFolder') return; + + ctx.groupByFolder = event.groupByFolder; + }), + incrementSearchDepth: assign((ctx) => { + ctx.searchDepth = ctx.searchDepth + 1; + }), + decrementSearchDepth: assign((ctx) => { + ctx.searchDepth = ctx.searchDepth > 1 ? ctx.searchDepth - 1 : 1; + }), + setSearchDepthEnabled: assign((ctx, event) => { + if (event.type !== 'setSearchDepthEnabled') return; + + ctx.searchDepthEnabled = event.searchDepthEnabled; + }), + setIncludeProjectsByPath: assign((ctx, event) => { + if (event.type !== 'setIncludeProjectsByPath') return; + + ctx.includePath = event.includeProjectsByPath; + }), + setGraph: assign((ctx, event) => { + if (event.type !== 'initGraph' && event.type !== 'updateGraph') return; + + ctx.projects = event.projects; + ctx.dependencies = event.dependencies; + ctx.graph = spawn(graphActor, 'graphActor'); + + if (event.type === 'initGraph') { + ctx.workspaceLayout = event.workspaceLayout; + ctx.affectedProjects = event.affectedProjects; + } + }), + notifyGraphShowProject: send( + (context, event) => { + if (event.type !== 'selectProject') return; + + return { + type: 'notifyGraphShowProject', + projectName: event.projectName, + }; + }, + { + to: (context) => context.graph, + } + ), + notifyGraphHideProject: send( + (context, event) => { + if (event.type !== 'deselectProject') return; + + return { + type: 'notifyGraphHideProject', + projectName: event.projectName, + }; + }, + { + to: (context) => context.graph, + } + ), + notifyGraphShowAllProjects: send( + (context, event) => ({ + type: 'notifyGraphShowAllProjects', + }), + { + to: (context) => context.graph, + } + ), + notifyGraphHideAllProjects: send( + (context, event) => ({ + type: 'notifyGraphHideAllProjects', + }), + { + to: (context) => context.graph, + } + ), + notifyGraphShowAffectedProjects: send( + { + type: 'notifyGraphShowAffectedProjects', + }, + { + to: (ctx) => ctx.graph, + } + ), + notifyGraphFocusProject: send( + (context, event) => ({ + type: 'notifyGraphFocusProject', + projectName: context.focusedProject, + searchDepth: context.searchDepthEnabled ? context.searchDepth : -1, + }), + { + to: (context) => context.graph, + } + ), + notifyGraphFilterProjectsByText: send( + (context, event) => ({ + type: 'notifyGraphFilterProjectsByText', + search: context.textFilter, + includeProjectsByPath: context.includePath, + searchDepth: context.searchDepthEnabled ? context.searchDepth : -1, + }), + { + to: (context) => context.graph, + } + ), + }, } ); diff --git a/dep-graph/dep-graph/src/app/machines/dep-graph.service.ts b/dep-graph/dep-graph/src/app/machines/dep-graph.service.ts index 01713406de..1c242f6906 100644 --- a/dep-graph/dep-graph/src/app/machines/dep-graph.service.ts +++ b/dep-graph/dep-graph/src/app/machines/dep-graph.service.ts @@ -4,15 +4,16 @@ import { interpret, Interpreter, Typestate } from 'xstate'; import { depGraphMachine } from './dep-graph.machine'; import { DepGraphContext, - DepGraphEvents, + DepGraphUIEvents, DepGraphSend, DepGraphStateObservable, + DepGraphSchema, } from './interfaces'; let depGraphService: Interpreter< DepGraphContext, - any, - DepGraphEvents, + DepGraphSchema, + DepGraphUIEvents, Typestate >; diff --git a/dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts b/dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts index 8a18c136b4..5f92c5c973 100644 --- a/dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts +++ b/dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts @@ -1,5 +1,6 @@ import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; import { depGraphMachine } from './dep-graph.machine'; +import { interpret } from 'xstate'; export const mockProjects: ProjectGraphNode[] = [ { @@ -74,6 +75,7 @@ export const mockDependencies: Record = { }, ], 'ui-lib': [], + 'auth-lib': [], }; describe('dep-graph machine', () => { @@ -109,8 +111,20 @@ describe('dep-graph machine', () => { }); describe('selecting projects', () => { - it('should select projects', () => { - let result = depGraphMachine.transition(depGraphMachine.initialState, { + it('should select projects', (done) => { + let service = interpret(depGraphMachine).onTransition((state) => { + if ( + state.matches('customSelected') && + state.context.selectedProjects.includes('app1') && + state.context.selectedProjects.includes('app2') + ) { + done(); + } + }); + + service.start(); + + service.send({ type: 'initGraph', projects: mockProjects, dependencies: mockDependencies, @@ -118,26 +132,33 @@ describe('dep-graph machine', () => { workspaceLayout: { appsDir: 'apps', libsDir: 'libs' }, }); - result = depGraphMachine.transition(result, { + service.send({ type: 'selectProject', projectName: 'app1', }); - expect(result.value).toEqual('customSelected'); - expect(result.context.selectedProjects).toEqual(['app1']); - - result = depGraphMachine.transition(result, { + service.send({ type: 'selectProject', projectName: 'app2', }); - - expect(result.context.selectedProjects).toEqual(['app1', 'app2']); }); }); describe('deselecting projects', () => { - it('should deselect projects', () => { - let result = depGraphMachine.transition(depGraphMachine.initialState, { + it('should deselect projects', (done) => { + let service = interpret(depGraphMachine).onTransition((state) => { + if ( + state.matches('customSelected') && + !state.context.selectedProjects.includes('app1') && + state.context.selectedProjects.includes('app2') + ) { + done(); + } + }); + + service.start(); + + service.send({ type: 'initGraph', projects: mockProjects, dependencies: mockDependencies, @@ -145,23 +166,20 @@ describe('dep-graph machine', () => { workspaceLayout: { appsDir: 'apps', libsDir: 'libs' }, }); - result = depGraphMachine.transition(result, { + service.send({ type: 'selectProject', projectName: 'app1', }); - result = depGraphMachine.transition(result, { + service.send({ type: 'selectProject', projectName: 'app2', }); - result = depGraphMachine.transition(result, { + service.send({ type: 'deselectProject', projectName: 'app1', }); - - expect(result.value).toEqual('customSelected'); - expect(result.context.selectedProjects).toEqual(['app2']); }); it('should go to unselected when last project is deselected', () => { @@ -217,8 +235,22 @@ describe('dep-graph machine', () => { expect(result.context.focusedProject).toEqual('app1'); }); - it('should select the projects by the focused project', () => { - let result = depGraphMachine.transition(depGraphMachine.initialState, { + it('should select the projects by the focused project', (done) => { + let service = interpret(depGraphMachine).onTransition((state) => { + if ( + state.matches('focused') && + state.context.selectedProjects.includes('app1') && + state.context.selectedProjects.includes('ui-lib') && + state.context.selectedProjects.includes('feature-lib1') && + state.context.selectedProjects.includes('auth-lib') + ) { + done(); + } + }); + + service.start(); + + service.send({ type: 'initGraph', projects: mockProjects, dependencies: mockDependencies, @@ -226,17 +258,7 @@ describe('dep-graph machine', () => { workspaceLayout: { appsDir: 'apps', libsDir: 'libs' }, }); - result = depGraphMachine.transition(result, { - type: 'focusProject', - projectName: 'app1', - }); - - expect(result.context.selectedProjects).toEqual([ - 'app1', - 'ui-lib', - 'feature-lib1', - 'auth-lib', - ]); + service.send({ type: 'focusProject', projectName: 'app1' }); }); it('should select no projects on unfocus', () => { diff --git a/dep-graph/dep-graph/src/app/machines/focused.state.ts b/dep-graph/dep-graph/src/app/machines/focused.state.ts index d395b141eb..b4ebb518c2 100644 --- a/dep-graph/dep-graph/src/app/machines/focused.state.ts +++ b/dep-graph/dep-graph/src/app/machines/focused.state.ts @@ -1,19 +1,16 @@ import { assign } from '@xstate/immer'; +import { send } from 'xstate'; import { selectProjectsForFocusedProject } from '../util'; import { DepGraphStateNodeConfig } from './interfaces'; export const focusedStateConfig: DepGraphStateNodeConfig = { entry: [ - assign((ctx, event: any) => { - ctx.selectedProjects = selectProjectsForFocusedProject( - ctx.projects, - ctx.dependencies, - event.projectName, - ctx.searchDepthEnabled ? ctx.searchDepth : -1 - ); + assign((ctx, event) => { + if (event.type !== 'focusProject') return; ctx.focusedProject = event.projectName; }), + 'notifyGraphFocusProject', ], exit: [ assign((ctx) => { @@ -22,69 +19,35 @@ export const focusedStateConfig: DepGraphStateNodeConfig = { ], on: { incrementSearchDepth: { - actions: [ - assign((ctx) => { - const searchDepth = ctx.searchDepth + 1; - const selectedProjects = selectProjectsForFocusedProject( - ctx.projects, - ctx.dependencies, - ctx.focusedProject, - ctx.searchDepthEnabled ? searchDepth : -1 - ); - - ctx.selectedProjects = selectedProjects; - ctx.searchDepth = searchDepth; - }), - ], + actions: ['incrementSearchDepth', 'notifyGraphFocusProject'], }, decrementSearchDepth: { - actions: [ - assign((ctx) => { - const searchDepth = ctx.searchDepth > 1 ? ctx.searchDepth - 1 : 1; - const selectedProjects = selectProjectsForFocusedProject( - ctx.projects, - ctx.dependencies, - ctx.focusedProject, - ctx.searchDepthEnabled ? searchDepth : -1 - ); - - ctx.selectedProjects = selectedProjects; - ctx.searchDepth = searchDepth; - }), - ], + actions: ['decrementSearchDepth', 'notifyGraphFocusProject'], }, setSearchDepthEnabled: { - actions: [ - assign((ctx, event) => { - const selectedProjects = selectProjectsForFocusedProject( - ctx.projects, - ctx.dependencies, - ctx.focusedProject, - event.searchDepthEnabled ? ctx.searchDepth : -1 - ); - - (ctx.searchDepthEnabled = event.searchDepthEnabled), - (ctx.selectedProjects = selectedProjects); - }), - ], + actions: ['setSearchDepthEnabled', 'notifyGraphFocusProject'], }, unfocusProject: { target: 'unselected', }, updateGraph: { actions: [ - assign((ctx, event) => { - const selectedProjects = selectProjectsForFocusedProject( - event.projects, - event.dependencies, - ctx.focusedProject, - ctx.searchDepthEnabled ? ctx.searchDepth : -1 - ); - - ctx.projects = event.projects; - ctx.dependencies = event.dependencies; - ctx.selectedProjects = selectedProjects; - }), + 'setGraph', + send( + (ctx, event) => ({ + type: 'notifyGraphUpdateGraph', + projects: ctx.projects, + dependencies: ctx.dependencies, + affectedProjects: ctx.affectedProjects, + workspaceLayout: ctx.workspaceLayout, + groupByFolder: ctx.groupByFolder, + selectedProjects: ctx.selectedProjects, + }), + { + to: (context) => context.graph, + } + ), + 'notifyGraphFocusProject', ], }, }, diff --git a/dep-graph/dep-graph/src/app/machines/interfaces.ts b/dep-graph/dep-graph/src/app/machines/interfaces.ts index 6747eaa703..a9ffa5c35f 100644 --- a/dep-graph/dep-graph/src/app/machines/interfaces.ts +++ b/dep-graph/dep-graph/src/app/machines/interfaces.ts @@ -1,6 +1,7 @@ import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit'; import { Observable } from 'rxjs'; -import { ActionObject, StateNodeConfig, StateValue } from 'xstate'; +import { ActionObject, ActorRef, StateNodeConfig, StateValue } from 'xstate'; +import { GraphService } from '../graph'; // The hierarchical (recursive) schema for the states export interface DepGraphSchema { @@ -14,7 +15,9 @@ export interface DepGraphSchema { } // The events that the machine handles -export type DepGraphEvents = + +export type DepGraphUIEvents = + | { type: 'setSelectedProjectsFromGraph'; selectedProjectNames: string[] } | { type: 'selectProject'; projectName: string } | { type: 'deselectProject'; projectName: string } | { type: 'selectAll' } @@ -45,6 +48,63 @@ export type DepGraphEvents = dependencies: Record; }; +// The events that the graph actor handles + +export type GraphRenderEvents = + | { + type: 'notifyGraphInitGraph'; + projects: ProjectGraphNode[]; + dependencies: Record; + affectedProjects: string[]; + workspaceLayout: { + libsDir: string; + appsDir: string; + }; + groupByFolder: boolean; + } + | { + type: 'notifyGraphUpdateGraph'; + projects: ProjectGraphNode[]; + dependencies: Record; + affectedProjects: string[]; + workspaceLayout: { + libsDir: string; + appsDir: string; + }; + groupByFolder: boolean; + selectedProjects: string[]; + } + | { + type: 'notifyGraphFocusProject'; + projectName: string; + searchDepth: number; + } + | { + type: 'notifyGraphShowProject'; + projectName: string; + } + | { + type: 'notifyGraphHideProject'; + projectName: string; + } + | { + type: 'notifyGraphShowAllProjects'; + } + | { + type: 'notifyGraphHideAllProjects'; + } + | { + type: 'notifyGraphShowAffectedProjects'; + } + | { + type: 'notifyGraphFilterProjectsByText'; + search: string; + includeProjectsByPath: boolean; + searchDepth: number; + }; + +export type AllEvents = DepGraphUIEvents | GraphRenderEvents; + // The context (extended state) of the machine export interface DepGraphContext { projects: ProjectGraphNode[]; @@ -61,16 +121,19 @@ export interface DepGraphContext { libsDir: string; appsDir: string; }; + graph: ActorRef; } export type DepGraphStateNodeConfig = StateNodeConfig< DepGraphContext, {}, - DepGraphEvents, - ActionObject + DepGraphUIEvents, + ActionObject >; -export type DepGraphSend = (event: DepGraphEvents | DepGraphEvents[]) => void; +export type DepGraphSend = ( + event: DepGraphUIEvents | DepGraphUIEvents[] +) => void; export type DepGraphStateObservable = Observable<{ value: StateValue; context: DepGraphContext; diff --git a/dep-graph/dep-graph/src/app/machines/text-filtered.state.ts b/dep-graph/dep-graph/src/app/machines/text-filtered.state.ts index 1416a7f4be..554e5dd385 100644 --- a/dep-graph/dep-graph/src/app/machines/text-filtered.state.ts +++ b/dep-graph/dep-graph/src/app/machines/text-filtered.state.ts @@ -1,19 +1,15 @@ import { assign } from '@xstate/immer'; -import { filterProjectsByText } from '../util'; +import { send } from 'xstate'; import { DepGraphStateNodeConfig } from './interfaces'; export const textFilteredStateConfig: DepGraphStateNodeConfig = { entry: [ - assign((ctx, event: any) => { + assign((ctx, event) => { + if (event.type !== 'filterByText') return; + ctx.textFilter = event.search; - ctx.selectedProjects = filterProjectsByText( - event.search, - ctx.includePath, - ctx.searchDepthEnabled ? ctx.searchDepth : -1, - ctx.projects, - ctx.dependencies - ); }), + 'notifyGraphFilterProjectsByText', ], on: { clearTextFilter: { @@ -24,79 +20,35 @@ export const textFilteredStateConfig: DepGraphStateNodeConfig = { }), }, setIncludeProjectsByPath: { - actions: [ - assign((ctx, event) => { - ctx.includePath = event.includeProjectsByPath; - ctx.selectedProjects = filterProjectsByText( - ctx.textFilter, - event.includeProjectsByPath, - ctx.searchDepthEnabled ? ctx.searchDepth : -1, - ctx.projects, - ctx.dependencies - ); - }), - ], + actions: ['setIncludeProjectsByPath', 'notifyGraphFilterProjectsByText'], }, incrementSearchDepth: { - actions: [ - assign((ctx) => { - const searchDepth = ctx.searchDepth + 1; - ctx.selectedProjects = filterProjectsByText( - ctx.textFilter, - ctx.includePath, - ctx.searchDepthEnabled ? searchDepth : -1, - ctx.projects, - ctx.dependencies - ); - - ctx.searchDepth = searchDepth; - }), - ], + actions: ['incrementSearchDepth', 'notifyGraphFilterProjectsByText'], }, decrementSearchDepth: { - actions: [ - assign((ctx) => { - const searchDepth = ctx.searchDepth > 1 ? ctx.searchDepth - 1 : 1; - ctx.selectedProjects = filterProjectsByText( - ctx.textFilter, - ctx.includePath, - ctx.searchDepthEnabled ? searchDepth : -1, - ctx.projects, - ctx.dependencies - ); - - ctx.searchDepth = searchDepth; - }), - ], + actions: ['decrementSearchDepth', 'notifyGraphFilterProjectsByText'], }, setSearchDepthEnabled: { - actions: [ - assign((ctx, event) => { - ctx.searchDepthEnabled = event.searchDepthEnabled; - ctx.selectedProjects = filterProjectsByText( - ctx.textFilter, - ctx.includePath, - event.searchDepthEnabled ? ctx.searchDepth : -1, - ctx.projects, - ctx.dependencies - ); - }), - ], + actions: ['setSearchDepthEnabled', 'notifyGraphFilterProjectsByText'], }, updateGraph: { actions: [ - assign((ctx, event) => { - ctx.selectedProjects = filterProjectsByText( - ctx.textFilter, - ctx.includePath, - ctx.searchDepthEnabled ? ctx.searchDepth : -1, - event.projects, - event.dependencies - ); - - ctx.projects = event.projects; - ctx.dependencies = event.dependencies; - }), + 'setGraph', + send( + (ctx, event) => ({ + type: 'notifyGraphUpdateGraph', + projects: ctx.projects, + dependencies: ctx.dependencies, + affectedProjects: ctx.affectedProjects, + workspaceLayout: ctx.workspaceLayout, + groupByFolder: ctx.groupByFolder, + selectedProjects: ctx.selectedProjects, + }), + { + to: (context) => context.graph, + } + ), + 'notifyGraphFilterProjectsByText', ], }, }, diff --git a/dep-graph/dep-graph/src/app/machines/unselected.state.ts b/dep-graph/dep-graph/src/app/machines/unselected.state.ts index 285be7215a..2df2f04c02 100644 --- a/dep-graph/dep-graph/src/app/machines/unselected.state.ts +++ b/dep-graph/dep-graph/src/app/machines/unselected.state.ts @@ -1,28 +1,43 @@ import { assign } from '@xstate/immer'; +import { send } from 'xstate'; +import { useGraphService } from '../graph.service'; import { DepGraphStateNodeConfig } from './interfaces'; export const unselectedStateConfig: DepGraphStateNodeConfig = { - entry: [ - assign((ctx) => { - ctx.selectedProjects = []; - }), - ], + entry: ['notifyGraphHideAllProjects'], on: { updateGraph: { + target: 'customSelected', actions: [ assign((ctx, event) => { const existingProjectNames = ctx.projects.map( (project) => project.name ); const newProjectNames = event.projects.map((project) => project.name); - const selectedProjects = newProjectNames.filter( + const newSelectedProjects = newProjectNames.filter( (projectName) => !existingProjectNames.includes(projectName) ); - ctx.projects = event.projects; - ctx.dependencies = event.dependencies; - ctx.selectedProjects = [...ctx.selectedProjects, ...selectedProjects]; + ctx.selectedProjects = [ + ...ctx.selectedProjects, + ...newSelectedProjects, + ]; }), + 'setGraph', + send( + (ctx, event) => ({ + type: 'notifyGraphUpdateGraph', + projects: ctx.projects, + dependencies: ctx.dependencies, + affectedProjects: ctx.affectedProjects, + workspaceLayout: ctx.workspaceLayout, + groupByFolder: ctx.groupByFolder, + selectedProjects: ctx.selectedProjects, + }), + { + to: (context) => context.graph, + } + ), ], }, }, diff --git a/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts b/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts index 69a652a758..d1c5508844 100644 --- a/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts +++ b/dep-graph/dep-graph/src/app/ui-sidebar/display-options-panel.ts @@ -15,11 +15,16 @@ export class DisplayOptionsPanel { this.render(); state$.subscribe((state) => { - if (state.context.affectedProjects.length > 0) { + if ( + state.context.affectedProjects.length > 0 && + this.affectedButtonElement.classList.contains('hidden') + ) { this.affectedButtonElement.classList.remove('hidden'); - this.affectedButtonElement.addEventListener('click', () => - this.send({ type: 'selectAffected' }) - ); + } else if ( + state.context.affectedProjects.length === 0 && + !this.affectedButtonElement.classList.contains('hidden') + ) { + this.affectedButtonElement.classList.add('hidden'); } this.searchDepthDisplay.innerText = state.context.searchDepth.toString(); @@ -111,6 +116,10 @@ export class DisplayOptionsPanel { '[data-cy="affectedButton"]' ); + this.affectedButtonElement.addEventListener('click', () => + this.send({ type: 'selectAffected' }) + ); + const selectAllButtonElement: HTMLElement = element.querySelector( '[data-cy="selectAllButton"]' ); diff --git a/dep-graph/dep-graph/src/app/util-cytoscape/parent-node.ts b/dep-graph/dep-graph/src/app/util-cytoscape/parent-node.ts index 0a11c00c48..fed0fab636 100644 --- a/dep-graph/dep-graph/src/app/util-cytoscape/parent-node.ts +++ b/dep-graph/dep-graph/src/app/util-cytoscape/parent-node.ts @@ -12,6 +12,7 @@ export class ParentNode { id: this.config.id, parent: this.config.parentId, label: this.config.label, + type: 'dir', }, selectable: false, grabbable: false, diff --git a/dep-graph/dep-graph/src/app/util-cytoscape/project-node.ts b/dep-graph/dep-graph/src/app/util-cytoscape/project-node.ts index 3ca6aca2bb..d5de5d45d2 100644 --- a/dep-graph/dep-graph/src/app/util-cytoscape/project-node.ts +++ b/dep-graph/dep-graph/src/app/util-cytoscape/project-node.ts @@ -48,10 +48,6 @@ export class ProjectNode { private getClasses(): string { let classes = this.project.type ?? ''; - if (this.focused) { - classes += ' focused'; - } - if (this.affected) { classes += ' affected'; } diff --git a/dep-graph/dep-graph/src/assets/environment.dev.js b/dep-graph/dep-graph/src/assets/environment.dev.js index bc1411acbe..309f314ef4 100644 --- a/dep-graph/dep-graph/src/assets/environment.dev.js +++ b/dep-graph/dep-graph/src/assets/environment.dev.js @@ -33,6 +33,16 @@ window.appConfig = { label: 'Storybook', url: 'assets/graphs/storybook.json', }, + { + id: 'focus-testing', + label: 'Focus', + url: 'assets/graphs/focus-testing.json', + }, + { + id: 'affected', + label: 'Affected', + url: 'assets/graphs/affected.json', + }, ], defaultProjectGraph: 'nx', }; diff --git a/dep-graph/dep-graph/src/assets/graphs/affected.json b/dep-graph/dep-graph/src/assets/graphs/affected.json new file mode 100644 index 0000000000..30140be791 --- /dev/null +++ b/dep-graph/dep-graph/src/assets/graphs/affected.json @@ -0,0 +1,119 @@ +{ + "hash": "1c2b69586aa096dc5e42eb252d0b5bfb94f20dc969a1e7b6f381a3b13add6928", + "layout": { + "appsDir": "apps", + "libsDir": "libs" + }, + "projects": [ + { + "name": "app1", + "type": "app", + "data": { + "tags": [], + "root": "apps/app1" + } + }, + { + "name": "app2", + "type": "app", + "data": { + "tags": [], + "root": "apps/app2" + } + }, + { + "name": "lib1", + "type": "lib", + "data": { + "tags": [], + "root": "libs/lib1" + } + }, + { + "name": "lib2", + "type": "lib", + "data": { + "tags": [], + "root": "libs/lib2" + } + }, + { + "name": "lib3", + "type": "lib", + "data": { + "tags": [], + "root": "libs/lib3" + } + }, + { + "name": "lib4", + "type": "lib", + "data": { + "tags": [], + "root": "libs/lib4" + } + }, + { + "name": "lib5", + "type": "lib", + "data": { + "tags": [], + "root": "libs/lib5" + } + } + ], + "dependencies": { + "app1": [ + { + "type": "static", + "source": "app1", + "target": "lib1" + } + ], + "app2": [ + { + "type": "static", + "source": "app2", + "target": "lib2" + }, + { + "type": "static", + "source": "app2", + "target": "lib5" + } + ], + "lib1": [ + { + "type": "static", + "source": "lib1", + "target": "lib3" + } + ], + "lib2": [ + { + "type": "static", + "source": "lib2", + "target": "lib3" + } + ], + "lib3": [ + { + "type": "static", + "source": "lib3", + "target": "lib4" + } + ], + "lib4": [], + "lib5": [ + { + "type": "static", + "source": "lib5", + "target": "lib4" + } + ] + }, + "affected": ["lib3", "lib1", "lib2", "app1", "app2"], + "changes": { + "added": [] + } +} diff --git a/dep-graph/dep-graph/src/assets/graphs/focus-testing.json b/dep-graph/dep-graph/src/assets/graphs/focus-testing.json new file mode 100644 index 0000000000..e90b0b9e2a --- /dev/null +++ b/dep-graph/dep-graph/src/assets/graphs/focus-testing.json @@ -0,0 +1,119 @@ +{ + "hash": "1c2b69586aa096dc5e42eb252d0b5bfb94f20dc969a1e7b6f381a3b13add6928", + "layout": { + "appsDir": "apps", + "libsDir": "libs" + }, + "projects": [ + { + "name": "app1", + "type": "app", + "data": { + "tags": [], + "root": "apps/app1" + } + }, + { + "name": "app2", + "type": "app", + "data": { + "tags": [], + "root": "apps/app2" + } + }, + { + "name": "lib1", + "type": "app", + "data": { + "tags": [], + "root": "libs/lib1" + } + }, + { + "name": "lib2", + "type": "app", + "data": { + "tags": [], + "root": "libs/lib2" + } + }, + { + "name": "lib3", + "type": "app", + "data": { + "tags": [], + "root": "libs/lib3" + } + }, + { + "name": "lib4", + "type": "app", + "data": { + "tags": [], + "root": "libs/lib4" + } + }, + { + "name": "lib5", + "type": "app", + "data": { + "tags": [], + "root": "libs/lib5" + } + } + ], + "dependencies": { + "app1": [ + { + "type": "static", + "source": "app1", + "target": "lib1" + } + ], + "app2": [ + { + "type": "static", + "source": "app2", + "target": "lib2" + }, + { + "type": "static", + "source": "app2", + "target": "lib5" + } + ], + "lib1": [ + { + "type": "static", + "source": "lib1", + "target": "lib3" + } + ], + "lib2": [ + { + "type": "static", + "source": "lib2", + "target": "lib3" + } + ], + "lib3": [ + { + "type": "static", + "source": "lib3", + "target": "lib4" + } + ], + "lib4": [], + "lib5": [ + { + "type": "static", + "source": "lib5", + "target": "lib4" + } + ] + }, + "affected": [], + "changes": { + "added": [] + } +} diff --git a/dep-graph/dep-graph/src/graphs/index.ts b/dep-graph/dep-graph/src/graphs/index.ts deleted file mode 100644 index 3782035371..0000000000 --- a/dep-graph/dep-graph/src/graphs/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ProjectGraphList } from '../app/models'; - -export const projectGraphs: ProjectGraphList[] = [ - { - id: 'nx', - label: 'Nx', - url: 'assets/graphs/nx.json', - }, - { - id: 'ocean', - label: 'Ocean', - url: 'assets/graphs/ocean.json', - }, - { - id: 'nx-examples', - label: 'Nx Examples', - url: 'assets/graphs/nx-examples.json', - }, - { - id: 'sub-apps', - label: 'Sub Apps', - url: 'assets/graphs/sub-apps.json', - }, - { - id: 'storybook', - label: 'Storybook', - url: 'assets/graphs/storybook.json', - }, -]; diff --git a/package.json b/package.json index 45adcdcf0d..1247530e7f 100644 --- a/package.json +++ b/package.json @@ -95,8 +95,8 @@ "@testing-library/react": "11.2.6", "@testing-library/react-hooks": "7.0.1", "@types/css-minimizer-webpack-plugin": "^3.0.2", - "@types/cytoscape": "^3.14.12", "@types/eslint": "^8.2.0", + "@types/cytoscape": "^3.18.2", "@types/express": "4.17.0", "@types/find-parent-dir": "^0.3.0", "@types/flat": "^5.0.1", diff --git a/yarn.lock b/yarn.lock index 7af134a0ad..d1b9f0d0db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4569,7 +4569,7 @@ dependencies: postcss "5 - 7" -"@types/cytoscape@^3.14.12": +"@types/cytoscape@^3.18.2": version "3.19.0" resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.19.0.tgz#1acd8df260af0eb088191f84df8ca055353f6196" integrity sha512-EbUrbDqequXtSkpPvtpX1Xf7nDFh+eB/2h/Sv1SiQ9IjCJENSwi9Wfv/vRkH9YTcgNoCot20eyIyMbxHfh0YDg==