chore(dep-graph): move project focus selection to cytoscape (#7780)
This commit is contained in:
parent
764d69bdc4
commit
56aaeb7931
@ -3,17 +3,15 @@ import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/de
|
|||||||
import { fromEvent } from 'rxjs';
|
import { fromEvent } from 'rxjs';
|
||||||
import { startWith } from 'rxjs/operators';
|
import { startWith } from 'rxjs/operators';
|
||||||
import { DebuggerPanel } from './debugger-panel';
|
import { DebuggerPanel } from './debugger-panel';
|
||||||
import { GraphComponent } from './graph';
|
import { useGraphService } from './graph.service';
|
||||||
import { useDepGraphService } from './machines/dep-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 { AppConfig, DEFAULT_CONFIG, ProjectGraphService } from './models';
|
||||||
import { GraphTooltipService } from './tooltip-service';
|
|
||||||
import { SidebarComponent } from './ui-sidebar/sidebar';
|
import { SidebarComponent } from './ui-sidebar/sidebar';
|
||||||
|
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
private sidebar = new SidebarComponent();
|
private sidebar = new SidebarComponent();
|
||||||
private tooltipService = new GraphTooltipService();
|
private graph = useGraphService();
|
||||||
private graph = new GraphComponent(this.tooltipService);
|
|
||||||
private debuggerPanel: DebuggerPanel;
|
private debuggerPanel: DebuggerPanel;
|
||||||
|
|
||||||
private windowResize$ = fromEvent(window, 'resize').pipe(startWith({}));
|
private windowResize$ = fromEvent(window, 'resize').pipe(startWith({}));
|
||||||
@ -24,7 +22,16 @@ export class AppComponent {
|
|||||||
private config: AppConfig = DEFAULT_CONFIG,
|
private config: AppConfig = DEFAULT_CONFIG,
|
||||||
private projectGraphService: ProjectGraphService
|
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.send = send;
|
||||||
|
|
||||||
this.loadProjectGraph(config.defaultProjectGraph);
|
this.loadProjectGraph(config.defaultProjectGraph);
|
||||||
@ -47,7 +54,6 @@ export class AppComponent {
|
|||||||
await this.projectGraphService.getProjectGraph(projectInfo.url);
|
await this.projectGraphService.getProjectGraph(projectInfo.url);
|
||||||
|
|
||||||
const workspaceLayout = project?.layout;
|
const workspaceLayout = project?.layout;
|
||||||
|
|
||||||
this.send({
|
this.send({
|
||||||
type: 'initGraph',
|
type: 'initGraph',
|
||||||
projects: project.projects,
|
projects: project.projects,
|
||||||
|
|||||||
15
dep-graph/dep-graph/src/app/graph.service.ts
Normal file
15
dep-graph/dep-graph/src/app/graph.service.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -1,15 +1,11 @@
|
|||||||
import type {
|
import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
||||||
ProjectGraph,
|
|
||||||
ProjectGraphDependency,
|
|
||||||
ProjectGraphNode,
|
|
||||||
} from '@nrwl/devkit';
|
|
||||||
import type { VirtualElement } from '@popperjs/core';
|
import type { VirtualElement } from '@popperjs/core';
|
||||||
import * as cy from 'cytoscape';
|
import * as cy from 'cytoscape';
|
||||||
import cytoscapeDagre from 'cytoscape-dagre';
|
import * as cytoscapeDagre from 'cytoscape-dagre';
|
||||||
import popper from 'cytoscape-popper';
|
import * as popper from 'cytoscape-popper';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import type { Instance } from 'tippy.js';
|
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 { ProjectNodeToolTip } from './project-node-tooltip';
|
||||||
import { edgeStyles, nodeStyles } from './styles-graph';
|
import { edgeStyles, nodeStyles } from './styles-graph';
|
||||||
import { GraphTooltipService } from './tooltip-service';
|
import { GraphTooltipService } from './tooltip-service';
|
||||||
@ -25,129 +21,350 @@ export interface GraphPerfReport {
|
|||||||
numNodes: number;
|
numNodes: number;
|
||||||
numEdges: number;
|
numEdges: number;
|
||||||
}
|
}
|
||||||
export class GraphComponent {
|
export class GraphService {
|
||||||
private graph: cy.Core;
|
private traversalGraph: cy.Core;
|
||||||
|
private renderGraph: cy.Core;
|
||||||
|
|
||||||
private openTooltip: Instance = null;
|
private openTooltip: Instance = null;
|
||||||
|
|
||||||
private renderTimesSubject = new Subject<GraphPerfReport>();
|
private renderTimesSubject = new Subject<GraphPerfReport>();
|
||||||
renderTimes$ = this.renderTimesSubject.asObservable();
|
renderTimes$ = this.renderTimesSubject.asObservable();
|
||||||
|
|
||||||
private send;
|
constructor(
|
||||||
constructor(private tooltipService: GraphTooltipService) {
|
private tooltipService: GraphTooltipService,
|
||||||
|
private containerId: string
|
||||||
|
) {
|
||||||
cy.use(cytoscapeDagre);
|
cy.use(cytoscapeDagre);
|
||||||
cy.use(popper);
|
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(
|
handleEvent(event: GraphRenderEvents): string[] {
|
||||||
selectedProjects: ProjectGraphNode[],
|
|
||||||
groupByFolder: boolean,
|
|
||||||
workspaceLayout,
|
|
||||||
focusedProject: string,
|
|
||||||
affectedProjects: string[],
|
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>
|
|
||||||
) {
|
|
||||||
const time = Date.now();
|
const time = Date.now();
|
||||||
|
|
||||||
if (selectedProjects.length === 0) {
|
if (
|
||||||
document.getElementById('no-projects-chosen').style.display = 'flex';
|
this.renderGraph &&
|
||||||
} else {
|
event.type !== 'notifyGraphFocusProject' &&
|
||||||
document.getElementById('no-projects-chosen').style.display = 'none';
|
event.type !== 'notifyGraphUpdateGraph'
|
||||||
|
) {
|
||||||
|
this.renderGraph.nodes('.focused').removeClass('focused');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tooltipService.hideAll();
|
switch (event.type) {
|
||||||
this.generateCytoscapeLayout(
|
case 'notifyGraphInitGraph':
|
||||||
selectedProjects,
|
this.initGraph(
|
||||||
groupByFolder,
|
event.projects,
|
||||||
workspaceLayout,
|
event.groupByFolder,
|
||||||
focusedProject,
|
event.workspaceLayout,
|
||||||
affectedProjects,
|
event.dependencies,
|
||||||
dependencies
|
event.affectedProjects
|
||||||
);
|
);
|
||||||
this.listenForProjectNodeClicks();
|
break;
|
||||||
this.listenForProjectNodeHovers();
|
|
||||||
|
case 'notifyGraphUpdateGraph':
|
||||||
|
this.initGraph(
|
||||||
|
event.projects,
|
||||||
|
event.groupByFolder,
|
||||||
|
event.workspaceLayout,
|
||||||
|
event.dependencies,
|
||||||
|
event.affectedProjects
|
||||||
|
);
|
||||||
|
this.setShownProjects(event.selectedProjects);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'notifyGraphFocusProject':
|
||||||
|
this.focusProject(event.projectName, event.searchDepth);
|
||||||
|
break;
|
||||||
|
|
||||||
|
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(<CytoscapeDagreConfig>{
|
||||||
|
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 renderTime = Date.now() - time;
|
||||||
|
|
||||||
const report: GraphPerfReport = {
|
const report: GraphPerfReport = {
|
||||||
renderTime,
|
renderTime,
|
||||||
numNodes: this.graph.nodes().length,
|
numNodes: this.renderGraph.nodes().length,
|
||||||
numEdges: this.graph.edges().length,
|
numEdges: this.renderGraph.edges().length,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.renderTimesSubject.next(report);
|
this.renderTimesSubject.next(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateCytoscapeLayout(
|
return visibleProjects;
|
||||||
selectedProjects: ProjectGraphNode[],
|
}
|
||||||
groupByFolder: boolean,
|
|
||||||
workspaceLayout,
|
setShownProjects(selectedProjectNames: string[]) {
|
||||||
focusedProject: string,
|
let nodesToAdd = this.traversalGraph.collection();
|
||||||
affectedProjects: string[],
|
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>
|
selectedProjectNames.forEach((name) => {
|
||||||
) {
|
nodesToAdd = nodesToAdd.union(this.traversalGraph.$id(name));
|
||||||
const elements = this.createElements(
|
});
|
||||||
selectedProjects,
|
|
||||||
groupByFolder,
|
const ancestorsToAdd = nodesToAdd.ancestors();
|
||||||
workspaceLayout,
|
|
||||||
|
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,
|
focusedProject,
|
||||||
affectedProjects,
|
searchDepth
|
||||||
dependencies
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.graph = cy({
|
const includedNodes = focusedProject.union(includedProjects);
|
||||||
container: document.getElementById('graph-container'),
|
|
||||||
elements: [...elements],
|
const includedAncestors = includedNodes.ancestors();
|
||||||
layout: <CytoscapeDagreConfig>{
|
|
||||||
name: 'dagre',
|
const nodesToRender = includedNodes.union(includedAncestors);
|
||||||
nodeDimensionsIncludeLabels: true,
|
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
||||||
rankSep: 75,
|
|
||||||
edgeSep: 50,
|
this.transferToRenderGraph(nodesToRender.union(edgesToRender));
|
||||||
ranker: 'network-simplex',
|
|
||||||
},
|
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,
|
boxSelectionEnabled: false,
|
||||||
style: [...nodeStyles, ...edgeStyles],
|
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) {
|
if (this.openTooltip) {
|
||||||
this.openTooltip.hide();
|
this.openTooltip.hide();
|
||||||
this.openTooltip = null;
|
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<string, ProjectGraphDependency[]>,
|
||||||
|
affectedProjectIds: string[]
|
||||||
|
) {
|
||||||
|
this.tooltipService.hideAll();
|
||||||
|
|
||||||
|
this.generateCytoscapeLayout(
|
||||||
|
allProjects,
|
||||||
|
groupByFolder,
|
||||||
|
workspaceLayout,
|
||||||
|
dependencies,
|
||||||
|
affectedProjectIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCytoscapeLayout(
|
||||||
|
allProjects: ProjectGraphNode[],
|
||||||
|
groupByFolder: boolean,
|
||||||
|
workspaceLayout,
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||||
|
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(
|
private createElements(
|
||||||
selectedProjects: ProjectGraphNode[],
|
projects: ProjectGraphNode[],
|
||||||
groupByFolder: boolean,
|
groupByFolder: boolean,
|
||||||
workspaceLayout: {
|
workspaceLayout: {
|
||||||
appsDir: string;
|
appsDir: string;
|
||||||
libsDir: string;
|
libsDir: string;
|
||||||
},
|
},
|
||||||
focusedProject: string,
|
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||||
affectedProjects: string[],
|
affectedProjectIds: string[]
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>
|
|
||||||
) {
|
) {
|
||||||
let elements: cy.ElementDefinition[] = [];
|
let elements: cy.ElementDefinition[] = [];
|
||||||
const filteredProjectNames = selectedProjects.map(
|
const filteredProjectNames = projects.map((project) => project.name);
|
||||||
(project) => project.name
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectNodes: ProjectNode[] = [];
|
const projectNodes: ProjectNode[] = [];
|
||||||
const edgeNodes: ProjectEdge[] = [];
|
const edgeNodes: ProjectEdge[] = [];
|
||||||
@ -156,24 +373,21 @@ export class GraphComponent {
|
|||||||
{ id: string; parentId: string; label: string }
|
{ id: string; parentId: string; label: string }
|
||||||
> = {};
|
> = {};
|
||||||
|
|
||||||
selectedProjects.forEach((project) => {
|
projects.forEach((project) => {
|
||||||
const workspaceRoot =
|
const workspaceRoot =
|
||||||
project.type === 'app' || project.type === 'e2e'
|
project.type === 'app' || project.type === 'e2e'
|
||||||
? workspaceLayout.appsDir
|
? workspaceLayout.appsDir
|
||||||
: workspaceLayout.libsDir;
|
: workspaceLayout.libsDir;
|
||||||
|
|
||||||
const projectNode = new ProjectNode(project, workspaceRoot);
|
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);
|
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);
|
||||||
edge.affected =
|
|
||||||
affectedProjects.includes(dep.source) &&
|
|
||||||
affectedProjects.includes(dep.target);
|
|
||||||
edgeNodes.push(edge);
|
edgeNodes.push(edge);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -205,7 +419,7 @@ export class GraphComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listenForProjectNodeClicks() {
|
listenForProjectNodeClicks() {
|
||||||
this.graph.$('node:childless').on('click', (event) => {
|
this.renderGraph.$('node:childless').on('click', (event) => {
|
||||||
const node = event.target;
|
const node = event.target;
|
||||||
|
|
||||||
let ref: VirtualElement = node.popperRef(); // used only for positioning
|
let ref: VirtualElement = node.popperRef(); // used only for positioning
|
||||||
@ -217,11 +431,11 @@ export class GraphComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listenForProjectNodeHovers(): void {
|
listenForProjectNodeHovers(): void {
|
||||||
this.graph.on('mouseover', (event) => {
|
this.renderGraph.on('mouseover', (event) => {
|
||||||
const node = event.target;
|
const node = event.target;
|
||||||
if (!node.isNode || !node.isNode() || node.isParent()) return;
|
if (!node.isNode || !node.isNode() || node.isParent()) return;
|
||||||
|
|
||||||
this.graph
|
this.renderGraph
|
||||||
.elements()
|
.elements()
|
||||||
.difference(node.outgoers().union(node.incomers()))
|
.difference(node.outgoers().union(node.incomers()))
|
||||||
.not(node)
|
.not(node)
|
||||||
@ -232,11 +446,12 @@ export class GraphComponent {
|
|||||||
.union(node.incomers())
|
.union(node.incomers())
|
||||||
.addClass('highlight');
|
.addClass('highlight');
|
||||||
});
|
});
|
||||||
this.graph.on('mouseout', (event) => {
|
|
||||||
|
this.renderGraph.on('mouseout', (event) => {
|
||||||
const node = event.target;
|
const node = event.target;
|
||||||
if (!node.isNode || !node.isNode() || node.isParent()) return;
|
if (!node.isNode || !node.isNode() || node.isParent()) return;
|
||||||
|
|
||||||
this.graph.elements().removeClass('transparent');
|
this.renderGraph.elements().removeClass('transparent');
|
||||||
node
|
node
|
||||||
.removeClass('highlight')
|
.removeClass('highlight')
|
||||||
.outgoers()
|
.outgoers()
|
||||||
|
|||||||
@ -1,23 +1,40 @@
|
|||||||
import { assign } from '@xstate/immer';
|
import { assign } from '@xstate/immer';
|
||||||
|
import { send } from 'xstate';
|
||||||
import { DepGraphStateNodeConfig } from './interfaces';
|
import { DepGraphStateNodeConfig } from './interfaces';
|
||||||
|
|
||||||
export const customSelectedStateConfig: DepGraphStateNodeConfig = {
|
export const customSelectedStateConfig: DepGraphStateNodeConfig = {
|
||||||
on: {
|
on: {
|
||||||
updateGraph: {
|
updateGraph: {
|
||||||
|
target: 'customSelected',
|
||||||
actions: [
|
actions: [
|
||||||
assign((ctx, event) => {
|
assign((ctx, event) => {
|
||||||
const existingProjectNames = ctx.projects.map(
|
const existingProjectNames = ctx.projects.map(
|
||||||
(project) => project.name
|
(project) => project.name
|
||||||
);
|
);
|
||||||
const newProjectNames = event.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)
|
(projectName) => !existingProjectNames.includes(projectName)
|
||||||
);
|
);
|
||||||
|
ctx.selectedProjects = [
|
||||||
ctx.projects = event.projects;
|
...ctx.selectedProjects,
|
||||||
ctx.dependencies = event.dependencies;
|
...newSelectedProjects,
|
||||||
ctx.selectedProjects = [...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,
|
||||||
|
}
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
import { assign } from '@xstate/immer';
|
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 { customSelectedStateConfig } from './custom-selected.state';
|
||||||
import { focusedStateConfig } from './focused.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 { textFilteredStateConfig } from './text-filtered.state';
|
||||||
import { unselectedStateConfig } from './unselected.state';
|
import { unselectedStateConfig } from './unselected.state';
|
||||||
|
|
||||||
@ -21,12 +26,25 @@ export const initialContext: DepGraphContext = {
|
|||||||
libsDir: '',
|
libsDir: '',
|
||||||
appsDir: '',
|
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<
|
export const depGraphMachine = Machine<
|
||||||
DepGraphContext,
|
DepGraphContext,
|
||||||
DepGraphSchema,
|
DepGraphSchema,
|
||||||
DepGraphEvents
|
DepGraphUIEvents
|
||||||
>(
|
>(
|
||||||
{
|
{
|
||||||
id: 'DepGraph',
|
id: 'DepGraph',
|
||||||
@ -42,37 +60,39 @@ export const depGraphMachine = Machine<
|
|||||||
on: {
|
on: {
|
||||||
initGraph: {
|
initGraph: {
|
||||||
target: 'unselected',
|
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) => {
|
actions: assign((ctx, event) => {
|
||||||
ctx.projects = event.projects;
|
ctx.selectedProjects = event.selectedProjectNames;
|
||||||
ctx.affectedProjects = event.affectedProjects;
|
|
||||||
ctx.dependencies = event.dependencies;
|
|
||||||
ctx.workspaceLayout = event.workspaceLayout;
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
selectProject: {
|
selectProject: {
|
||||||
target: 'customSelected',
|
target: 'customSelected',
|
||||||
actions: [
|
actions: ['notifyGraphShowProject'],
|
||||||
assign((ctx, event) => {
|
|
||||||
ctx.selectedProjects.push(event.projectName);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
selectAll: {
|
selectAll: {
|
||||||
target: 'customSelected',
|
target: 'customSelected',
|
||||||
actions: [
|
actions: ['notifyGraphShowAllProjects'],
|
||||||
assign((ctx) => {
|
|
||||||
ctx.selectedProjects = ctx.projects.map((project) => project.name);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
selectAffected: {
|
selectAffected: {
|
||||||
target: 'customSelected',
|
target: 'customSelected',
|
||||||
actions: [
|
actions: ['notifyGraphShowAffectedProjects'],
|
||||||
assign((ctx) => {
|
|
||||||
ctx.selectedProjects = ctx.affectedProjects;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
deselectProject: [
|
deselectProject: [
|
||||||
{
|
{
|
||||||
@ -81,15 +101,7 @@ export const depGraphMachine = Machine<
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 'customSelected',
|
target: 'customSelected',
|
||||||
actions: [
|
actions: ['notifyGraphHideProject'],
|
||||||
assign((ctx, event) => {
|
|
||||||
const index = ctx.selectedProjects.findIndex(
|
|
||||||
(project) => project === event.projectName
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.selectedProjects.splice(index, 1);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
deselectAll: {
|
deselectAll: {
|
||||||
@ -100,9 +112,21 @@ export const depGraphMachine = Machine<
|
|||||||
},
|
},
|
||||||
setGroupByFolder: {
|
setGroupByFolder: {
|
||||||
actions: [
|
actions: [
|
||||||
assign((ctx, event: any) => {
|
'setGroupByFolder',
|
||||||
ctx.groupByFolder = event.groupByFolder;
|
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: {
|
setIncludeProjectsByPath: {
|
||||||
@ -113,25 +137,13 @@ export const depGraphMachine = Machine<
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
incrementSearchDepth: {
|
incrementSearchDepth: {
|
||||||
actions: [
|
actions: ['incrementSearchDepth'],
|
||||||
assign((ctx) => {
|
|
||||||
ctx.searchDepth = ctx.searchDepth + 1;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
decrementSearchDepth: {
|
decrementSearchDepth: {
|
||||||
actions: [
|
actions: ['decrementSearchDepth'],
|
||||||
assign((ctx) => {
|
|
||||||
ctx.searchDepth = ctx.searchDepth > 1 ? ctx.searchDepth - 1 : 1;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
setSearchDepthEnabled: {
|
setSearchDepthEnabled: {
|
||||||
actions: [
|
actions: ['setSearchDepthEnabled'],
|
||||||
assign((ctx, event) => {
|
|
||||||
ctx.searchDepthEnabled = event.searchDepthEnabled;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
filterByText: {
|
filterByText: {
|
||||||
target: 'textFiltered',
|
target: 'textFiltered',
|
||||||
@ -144,5 +156,111 @@ export const depGraphMachine = Machine<
|
|||||||
return ctx.selectedProjects.length <= 1;
|
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,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,15 +4,16 @@ import { interpret, Interpreter, Typestate } from 'xstate';
|
|||||||
import { depGraphMachine } from './dep-graph.machine';
|
import { depGraphMachine } from './dep-graph.machine';
|
||||||
import {
|
import {
|
||||||
DepGraphContext,
|
DepGraphContext,
|
||||||
DepGraphEvents,
|
DepGraphUIEvents,
|
||||||
DepGraphSend,
|
DepGraphSend,
|
||||||
DepGraphStateObservable,
|
DepGraphStateObservable,
|
||||||
|
DepGraphSchema,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
|
|
||||||
let depGraphService: Interpreter<
|
let depGraphService: Interpreter<
|
||||||
DepGraphContext,
|
DepGraphContext,
|
||||||
any,
|
DepGraphSchema,
|
||||||
DepGraphEvents,
|
DepGraphUIEvents,
|
||||||
Typestate<DepGraphContext>
|
Typestate<DepGraphContext>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
||||||
import { depGraphMachine } from './dep-graph.machine';
|
import { depGraphMachine } from './dep-graph.machine';
|
||||||
|
import { interpret } from 'xstate';
|
||||||
|
|
||||||
export const mockProjects: ProjectGraphNode[] = [
|
export const mockProjects: ProjectGraphNode[] = [
|
||||||
{
|
{
|
||||||
@ -74,6 +75,7 @@ export const mockDependencies: Record<string, ProjectGraphDependency[]> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
'ui-lib': [],
|
'ui-lib': [],
|
||||||
|
'auth-lib': [],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('dep-graph machine', () => {
|
describe('dep-graph machine', () => {
|
||||||
@ -109,8 +111,20 @@ describe('dep-graph machine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('selecting projects', () => {
|
describe('selecting projects', () => {
|
||||||
it('should select projects', () => {
|
it('should select projects', (done) => {
|
||||||
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
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',
|
type: 'initGraph',
|
||||||
projects: mockProjects,
|
projects: mockProjects,
|
||||||
dependencies: mockDependencies,
|
dependencies: mockDependencies,
|
||||||
@ -118,26 +132,33 @@ describe('dep-graph machine', () => {
|
|||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
service.send({
|
||||||
type: 'selectProject',
|
type: 'selectProject',
|
||||||
projectName: 'app1',
|
projectName: 'app1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.value).toEqual('customSelected');
|
service.send({
|
||||||
expect(result.context.selectedProjects).toEqual(['app1']);
|
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
|
||||||
type: 'selectProject',
|
type: 'selectProject',
|
||||||
projectName: 'app2',
|
projectName: 'app2',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.selectedProjects).toEqual(['app1', 'app2']);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deselecting projects', () => {
|
describe('deselecting projects', () => {
|
||||||
it('should deselect projects', () => {
|
it('should deselect projects', (done) => {
|
||||||
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
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',
|
type: 'initGraph',
|
||||||
projects: mockProjects,
|
projects: mockProjects,
|
||||||
dependencies: mockDependencies,
|
dependencies: mockDependencies,
|
||||||
@ -145,23 +166,20 @@ describe('dep-graph machine', () => {
|
|||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
service.send({
|
||||||
type: 'selectProject',
|
type: 'selectProject',
|
||||||
projectName: 'app1',
|
projectName: 'app1',
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
service.send({
|
||||||
type: 'selectProject',
|
type: 'selectProject',
|
||||||
projectName: 'app2',
|
projectName: 'app2',
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
service.send({
|
||||||
type: 'deselectProject',
|
type: 'deselectProject',
|
||||||
projectName: 'app1',
|
projectName: 'app1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.value).toEqual('customSelected');
|
|
||||||
expect(result.context.selectedProjects).toEqual(['app2']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should go to unselected when last project is deselected', () => {
|
it('should go to unselected when last project is deselected', () => {
|
||||||
@ -217,8 +235,22 @@ describe('dep-graph machine', () => {
|
|||||||
expect(result.context.focusedProject).toEqual('app1');
|
expect(result.context.focusedProject).toEqual('app1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select the projects by the focused project', () => {
|
it('should select the projects by the focused project', (done) => {
|
||||||
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
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',
|
type: 'initGraph',
|
||||||
projects: mockProjects,
|
projects: mockProjects,
|
||||||
dependencies: mockDependencies,
|
dependencies: mockDependencies,
|
||||||
@ -226,17 +258,7 @@ describe('dep-graph machine', () => {
|
|||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
service.send({ type: 'focusProject', projectName: 'app1' });
|
||||||
type: 'focusProject',
|
|
||||||
projectName: 'app1',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.context.selectedProjects).toEqual([
|
|
||||||
'app1',
|
|
||||||
'ui-lib',
|
|
||||||
'feature-lib1',
|
|
||||||
'auth-lib',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select no projects on unfocus', () => {
|
it('should select no projects on unfocus', () => {
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
import { assign } from '@xstate/immer';
|
import { assign } from '@xstate/immer';
|
||||||
|
import { send } from 'xstate';
|
||||||
import { selectProjectsForFocusedProject } from '../util';
|
import { selectProjectsForFocusedProject } from '../util';
|
||||||
import { DepGraphStateNodeConfig } from './interfaces';
|
import { DepGraphStateNodeConfig } from './interfaces';
|
||||||
|
|
||||||
export const focusedStateConfig: DepGraphStateNodeConfig = {
|
export const focusedStateConfig: DepGraphStateNodeConfig = {
|
||||||
entry: [
|
entry: [
|
||||||
assign((ctx, event: any) => {
|
assign((ctx, event) => {
|
||||||
ctx.selectedProjects = selectProjectsForFocusedProject(
|
if (event.type !== 'focusProject') return;
|
||||||
ctx.projects,
|
|
||||||
ctx.dependencies,
|
|
||||||
event.projectName,
|
|
||||||
ctx.searchDepthEnabled ? ctx.searchDepth : -1
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.focusedProject = event.projectName;
|
ctx.focusedProject = event.projectName;
|
||||||
}),
|
}),
|
||||||
|
'notifyGraphFocusProject',
|
||||||
],
|
],
|
||||||
exit: [
|
exit: [
|
||||||
assign((ctx) => {
|
assign((ctx) => {
|
||||||
@ -22,69 +19,35 @@ export const focusedStateConfig: DepGraphStateNodeConfig = {
|
|||||||
],
|
],
|
||||||
on: {
|
on: {
|
||||||
incrementSearchDepth: {
|
incrementSearchDepth: {
|
||||||
actions: [
|
actions: ['incrementSearchDepth', 'notifyGraphFocusProject'],
|
||||||
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;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
decrementSearchDepth: {
|
decrementSearchDepth: {
|
||||||
actions: [
|
actions: ['decrementSearchDepth', 'notifyGraphFocusProject'],
|
||||||
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;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
setSearchDepthEnabled: {
|
setSearchDepthEnabled: {
|
||||||
actions: [
|
actions: ['setSearchDepthEnabled', 'notifyGraphFocusProject'],
|
||||||
assign((ctx, event) => {
|
|
||||||
const selectedProjects = selectProjectsForFocusedProject(
|
|
||||||
ctx.projects,
|
|
||||||
ctx.dependencies,
|
|
||||||
ctx.focusedProject,
|
|
||||||
event.searchDepthEnabled ? ctx.searchDepth : -1
|
|
||||||
);
|
|
||||||
|
|
||||||
(ctx.searchDepthEnabled = event.searchDepthEnabled),
|
|
||||||
(ctx.selectedProjects = selectedProjects);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
unfocusProject: {
|
unfocusProject: {
|
||||||
target: 'unselected',
|
target: 'unselected',
|
||||||
},
|
},
|
||||||
updateGraph: {
|
updateGraph: {
|
||||||
actions: [
|
actions: [
|
||||||
assign((ctx, event) => {
|
'setGraph',
|
||||||
const selectedProjects = selectProjectsForFocusedProject(
|
send(
|
||||||
event.projects,
|
(ctx, event) => ({
|
||||||
event.dependencies,
|
type: 'notifyGraphUpdateGraph',
|
||||||
ctx.focusedProject,
|
projects: ctx.projects,
|
||||||
ctx.searchDepthEnabled ? ctx.searchDepth : -1
|
dependencies: ctx.dependencies,
|
||||||
);
|
affectedProjects: ctx.affectedProjects,
|
||||||
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
ctx.projects = event.projects;
|
groupByFolder: ctx.groupByFolder,
|
||||||
ctx.dependencies = event.dependencies;
|
selectedProjects: ctx.selectedProjects,
|
||||||
ctx.selectedProjects = selectedProjects;
|
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
to: (context) => context.graph,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'notifyGraphFocusProject',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
||||||
import { Observable } from 'rxjs';
|
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
|
// The hierarchical (recursive) schema for the states
|
||||||
export interface DepGraphSchema {
|
export interface DepGraphSchema {
|
||||||
@ -14,7 +15,9 @@ export interface DepGraphSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The events that the machine handles
|
// The events that the machine handles
|
||||||
export type DepGraphEvents =
|
|
||||||
|
export type DepGraphUIEvents =
|
||||||
|
| { type: 'setSelectedProjectsFromGraph'; selectedProjectNames: string[] }
|
||||||
| { type: 'selectProject'; projectName: string }
|
| { type: 'selectProject'; projectName: string }
|
||||||
| { type: 'deselectProject'; projectName: string }
|
| { type: 'deselectProject'; projectName: string }
|
||||||
| { type: 'selectAll' }
|
| { type: 'selectAll' }
|
||||||
@ -45,6 +48,63 @@ export type DepGraphEvents =
|
|||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The events that the graph actor handles
|
||||||
|
|
||||||
|
export type GraphRenderEvents =
|
||||||
|
| {
|
||||||
|
type: 'notifyGraphInitGraph';
|
||||||
|
projects: ProjectGraphNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
groupByFolder: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyGraphUpdateGraph';
|
||||||
|
projects: ProjectGraphNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
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
|
// The context (extended state) of the machine
|
||||||
export interface DepGraphContext {
|
export interface DepGraphContext {
|
||||||
projects: ProjectGraphNode[];
|
projects: ProjectGraphNode[];
|
||||||
@ -61,16 +121,19 @@ export interface DepGraphContext {
|
|||||||
libsDir: string;
|
libsDir: string;
|
||||||
appsDir: string;
|
appsDir: string;
|
||||||
};
|
};
|
||||||
|
graph: ActorRef<GraphRenderEvents>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DepGraphStateNodeConfig = StateNodeConfig<
|
export type DepGraphStateNodeConfig = StateNodeConfig<
|
||||||
DepGraphContext,
|
DepGraphContext,
|
||||||
{},
|
{},
|
||||||
DepGraphEvents,
|
DepGraphUIEvents,
|
||||||
ActionObject<DepGraphContext, DepGraphEvents>
|
ActionObject<DepGraphContext, DepGraphUIEvents>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type DepGraphSend = (event: DepGraphEvents | DepGraphEvents[]) => void;
|
export type DepGraphSend = (
|
||||||
|
event: DepGraphUIEvents | DepGraphUIEvents[]
|
||||||
|
) => void;
|
||||||
export type DepGraphStateObservable = Observable<{
|
export type DepGraphStateObservable = Observable<{
|
||||||
value: StateValue;
|
value: StateValue;
|
||||||
context: DepGraphContext;
|
context: DepGraphContext;
|
||||||
|
|||||||
@ -1,19 +1,15 @@
|
|||||||
import { assign } from '@xstate/immer';
|
import { assign } from '@xstate/immer';
|
||||||
import { filterProjectsByText } from '../util';
|
import { send } from 'xstate';
|
||||||
import { DepGraphStateNodeConfig } from './interfaces';
|
import { DepGraphStateNodeConfig } from './interfaces';
|
||||||
|
|
||||||
export const textFilteredStateConfig: DepGraphStateNodeConfig = {
|
export const textFilteredStateConfig: DepGraphStateNodeConfig = {
|
||||||
entry: [
|
entry: [
|
||||||
assign((ctx, event: any) => {
|
assign((ctx, event) => {
|
||||||
|
if (event.type !== 'filterByText') return;
|
||||||
|
|
||||||
ctx.textFilter = event.search;
|
ctx.textFilter = event.search;
|
||||||
ctx.selectedProjects = filterProjectsByText(
|
|
||||||
event.search,
|
|
||||||
ctx.includePath,
|
|
||||||
ctx.searchDepthEnabled ? ctx.searchDepth : -1,
|
|
||||||
ctx.projects,
|
|
||||||
ctx.dependencies
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
|
'notifyGraphFilterProjectsByText',
|
||||||
],
|
],
|
||||||
on: {
|
on: {
|
||||||
clearTextFilter: {
|
clearTextFilter: {
|
||||||
@ -24,79 +20,35 @@ export const textFilteredStateConfig: DepGraphStateNodeConfig = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
setIncludeProjectsByPath: {
|
setIncludeProjectsByPath: {
|
||||||
actions: [
|
actions: ['setIncludeProjectsByPath', 'notifyGraphFilterProjectsByText'],
|
||||||
assign((ctx, event) => {
|
|
||||||
ctx.includePath = event.includeProjectsByPath;
|
|
||||||
ctx.selectedProjects = filterProjectsByText(
|
|
||||||
ctx.textFilter,
|
|
||||||
event.includeProjectsByPath,
|
|
||||||
ctx.searchDepthEnabled ? ctx.searchDepth : -1,
|
|
||||||
ctx.projects,
|
|
||||||
ctx.dependencies
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
incrementSearchDepth: {
|
incrementSearchDepth: {
|
||||||
actions: [
|
actions: ['incrementSearchDepth', 'notifyGraphFilterProjectsByText'],
|
||||||
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;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
decrementSearchDepth: {
|
decrementSearchDepth: {
|
||||||
actions: [
|
actions: ['decrementSearchDepth', 'notifyGraphFilterProjectsByText'],
|
||||||
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;
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
setSearchDepthEnabled: {
|
setSearchDepthEnabled: {
|
||||||
actions: [
|
actions: ['setSearchDepthEnabled', 'notifyGraphFilterProjectsByText'],
|
||||||
assign((ctx, event) => {
|
|
||||||
ctx.searchDepthEnabled = event.searchDepthEnabled;
|
|
||||||
ctx.selectedProjects = filterProjectsByText(
|
|
||||||
ctx.textFilter,
|
|
||||||
ctx.includePath,
|
|
||||||
event.searchDepthEnabled ? ctx.searchDepth : -1,
|
|
||||||
ctx.projects,
|
|
||||||
ctx.dependencies
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
updateGraph: {
|
updateGraph: {
|
||||||
actions: [
|
actions: [
|
||||||
assign((ctx, event) => {
|
'setGraph',
|
||||||
ctx.selectedProjects = filterProjectsByText(
|
send(
|
||||||
ctx.textFilter,
|
(ctx, event) => ({
|
||||||
ctx.includePath,
|
type: 'notifyGraphUpdateGraph',
|
||||||
ctx.searchDepthEnabled ? ctx.searchDepth : -1,
|
projects: ctx.projects,
|
||||||
event.projects,
|
dependencies: ctx.dependencies,
|
||||||
event.dependencies
|
affectedProjects: ctx.affectedProjects,
|
||||||
);
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
|
groupByFolder: ctx.groupByFolder,
|
||||||
ctx.projects = event.projects;
|
selectedProjects: ctx.selectedProjects,
|
||||||
ctx.dependencies = event.dependencies;
|
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
to: (context) => context.graph,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
'notifyGraphFilterProjectsByText',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,28 +1,43 @@
|
|||||||
import { assign } from '@xstate/immer';
|
import { assign } from '@xstate/immer';
|
||||||
|
import { send } from 'xstate';
|
||||||
|
import { useGraphService } from '../graph.service';
|
||||||
import { DepGraphStateNodeConfig } from './interfaces';
|
import { DepGraphStateNodeConfig } from './interfaces';
|
||||||
|
|
||||||
export const unselectedStateConfig: DepGraphStateNodeConfig = {
|
export const unselectedStateConfig: DepGraphStateNodeConfig = {
|
||||||
entry: [
|
entry: ['notifyGraphHideAllProjects'],
|
||||||
assign((ctx) => {
|
|
||||||
ctx.selectedProjects = [];
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
on: {
|
on: {
|
||||||
updateGraph: {
|
updateGraph: {
|
||||||
|
target: 'customSelected',
|
||||||
actions: [
|
actions: [
|
||||||
assign((ctx, event) => {
|
assign((ctx, event) => {
|
||||||
const existingProjectNames = ctx.projects.map(
|
const existingProjectNames = ctx.projects.map(
|
||||||
(project) => project.name
|
(project) => project.name
|
||||||
);
|
);
|
||||||
const newProjectNames = event.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)
|
(projectName) => !existingProjectNames.includes(projectName)
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.projects = event.projects;
|
ctx.selectedProjects = [
|
||||||
ctx.dependencies = event.dependencies;
|
...ctx.selectedProjects,
|
||||||
ctx.selectedProjects = [...ctx.selectedProjects, ...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,
|
||||||
|
}
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -15,11 +15,16 @@ export class DisplayOptionsPanel {
|
|||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
state$.subscribe((state) => {
|
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.classList.remove('hidden');
|
||||||
this.affectedButtonElement.addEventListener('click', () =>
|
} else if (
|
||||||
this.send({ type: 'selectAffected' })
|
state.context.affectedProjects.length === 0 &&
|
||||||
);
|
!this.affectedButtonElement.classList.contains('hidden')
|
||||||
|
) {
|
||||||
|
this.affectedButtonElement.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searchDepthDisplay.innerText = state.context.searchDepth.toString();
|
this.searchDepthDisplay.innerText = state.context.searchDepth.toString();
|
||||||
@ -111,6 +116,10 @@ export class DisplayOptionsPanel {
|
|||||||
'[data-cy="affectedButton"]'
|
'[data-cy="affectedButton"]'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.affectedButtonElement.addEventListener('click', () =>
|
||||||
|
this.send({ type: 'selectAffected' })
|
||||||
|
);
|
||||||
|
|
||||||
const selectAllButtonElement: HTMLElement = element.querySelector(
|
const selectAllButtonElement: HTMLElement = element.querySelector(
|
||||||
'[data-cy="selectAllButton"]'
|
'[data-cy="selectAllButton"]'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export class ParentNode {
|
|||||||
id: this.config.id,
|
id: this.config.id,
|
||||||
parent: this.config.parentId,
|
parent: this.config.parentId,
|
||||||
label: this.config.label,
|
label: this.config.label,
|
||||||
|
type: 'dir',
|
||||||
},
|
},
|
||||||
selectable: false,
|
selectable: false,
|
||||||
grabbable: false,
|
grabbable: false,
|
||||||
|
|||||||
@ -48,10 +48,6 @@ export class ProjectNode {
|
|||||||
private getClasses(): string {
|
private getClasses(): string {
|
||||||
let classes = this.project.type ?? '';
|
let classes = this.project.type ?? '';
|
||||||
|
|
||||||
if (this.focused) {
|
|
||||||
classes += ' focused';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.affected) {
|
if (this.affected) {
|
||||||
classes += ' affected';
|
classes += ' affected';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,16 @@ window.appConfig = {
|
|||||||
label: 'Storybook',
|
label: 'Storybook',
|
||||||
url: 'assets/graphs/storybook.json',
|
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',
|
defaultProjectGraph: 'nx',
|
||||||
};
|
};
|
||||||
|
|||||||
119
dep-graph/dep-graph/src/assets/graphs/affected.json
Normal file
119
dep-graph/dep-graph/src/assets/graphs/affected.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
119
dep-graph/dep-graph/src/assets/graphs/focus-testing.json
Normal file
119
dep-graph/dep-graph/src/assets/graphs/focus-testing.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -95,8 +95,8 @@
|
|||||||
"@testing-library/react": "11.2.6",
|
"@testing-library/react": "11.2.6",
|
||||||
"@testing-library/react-hooks": "7.0.1",
|
"@testing-library/react-hooks": "7.0.1",
|
||||||
"@types/css-minimizer-webpack-plugin": "^3.0.2",
|
"@types/css-minimizer-webpack-plugin": "^3.0.2",
|
||||||
"@types/cytoscape": "^3.14.12",
|
|
||||||
"@types/eslint": "^8.2.0",
|
"@types/eslint": "^8.2.0",
|
||||||
|
"@types/cytoscape": "^3.18.2",
|
||||||
"@types/express": "4.17.0",
|
"@types/express": "4.17.0",
|
||||||
"@types/find-parent-dir": "^0.3.0",
|
"@types/find-parent-dir": "^0.3.0",
|
||||||
"@types/flat": "^5.0.1",
|
"@types/flat": "^5.0.1",
|
||||||
|
|||||||
@ -4569,7 +4569,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "5 - 7"
|
postcss "5 - 7"
|
||||||
|
|
||||||
"@types/cytoscape@^3.14.12":
|
"@types/cytoscape@^3.18.2":
|
||||||
version "3.19.0"
|
version "3.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.19.0.tgz#1acd8df260af0eb088191f84df8ca055353f6196"
|
resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.19.0.tgz#1acd8df260af0eb088191f84df8ca055353f6196"
|
||||||
integrity sha512-EbUrbDqequXtSkpPvtpX1Xf7nDFh+eB/2h/Sv1SiQ9IjCJENSwi9Wfv/vRkH9YTcgNoCot20eyIyMbxHfh0YDg==
|
integrity sha512-EbUrbDqequXtSkpPvtpX1Xf7nDFh+eB/2h/Sv1SiQ9IjCJENSwi9Wfv/vRkH9YTcgNoCot20eyIyMbxHfh0YDg==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user