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 { 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,
|
||||
|
||||
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 {
|
||||
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<GraphPerfReport>();
|
||||
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<string, ProjectGraphDependency[]>
|
||||
) {
|
||||
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(<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 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<string, ProjectGraphDependency[]>
|
||||
) {
|
||||
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: <CytoscapeDagreConfig>{
|
||||
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<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(
|
||||
selectedProjects: ProjectGraphNode[],
|
||||
projects: ProjectGraphNode[],
|
||||
groupByFolder: boolean,
|
||||
workspaceLayout: {
|
||||
appsDir: string;
|
||||
libsDir: string;
|
||||
},
|
||||
focusedProject: string,
|
||||
affectedProjects: string[],
|
||||
dependencies: Record<string, ProjectGraphDependency[]>
|
||||
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||
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()
|
||||
|
||||
@ -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,
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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,
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -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<DepGraphContext>
|
||||
>;
|
||||
|
||||
|
||||
@ -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<string, ProjectGraphDependency[]> = {
|
||||
},
|
||||
],
|
||||
'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', () => {
|
||||
|
||||
@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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<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
|
||||
export interface DepGraphContext {
|
||||
projects: ProjectGraphNode[];
|
||||
@ -61,16 +121,19 @@ export interface DepGraphContext {
|
||||
libsDir: string;
|
||||
appsDir: string;
|
||||
};
|
||||
graph: ActorRef<GraphRenderEvents>;
|
||||
}
|
||||
|
||||
export type DepGraphStateNodeConfig = StateNodeConfig<
|
||||
DepGraphContext,
|
||||
{},
|
||||
DepGraphEvents,
|
||||
ActionObject<DepGraphContext, DepGraphEvents>
|
||||
DepGraphUIEvents,
|
||||
ActionObject<DepGraphContext, DepGraphUIEvents>
|
||||
>;
|
||||
|
||||
export type DepGraphSend = (event: DepGraphEvents | DepGraphEvents[]) => void;
|
||||
export type DepGraphSend = (
|
||||
event: DepGraphUIEvents | DepGraphUIEvents[]
|
||||
) => void;
|
||||
export type DepGraphStateObservable = Observable<{
|
||||
value: StateValue;
|
||||
context: DepGraphContext;
|
||||
|
||||
@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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,
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -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"]'
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
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-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",
|
||||
|
||||
@ -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==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user