feat(dep-graph): use xstate for state management (#7634)
This commit is contained in:
parent
07c256b4dc
commit
5f9279a3ff
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,3 +11,4 @@ tmp
|
|||||||
jest.debug.config.js
|
jest.debug.config.js
|
||||||
.tool-versions
|
.tool-versions
|
||||||
/.verdaccio/build/local-registry
|
/.verdaccio/build/local-registry
|
||||||
|
dep-graph/dep-graph/src/assets/environment.js
|
||||||
@ -8,7 +8,8 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"cypressConfig": "dep-graph/dep-graph-e2e/cypress.json",
|
"cypressConfig": "dep-graph/dep-graph-e2e/cypress.json",
|
||||||
"tsConfig": "dep-graph/dep-graph-e2e/tsconfig.e2e.json",
|
"tsConfig": "dep-graph/dep-graph-e2e/tsconfig.e2e.json",
|
||||||
"devServerTarget": "dep-graph-dep-graph:serve"
|
"devServerTarget": "dep-graph-dep-graph:serve-for-e2e",
|
||||||
|
"baseUrl": "http://localhost:4200"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"e2e-watch-disabled": {
|
"e2e-watch-disabled": {
|
||||||
@ -16,7 +17,8 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"cypressConfig": "dep-graph/dep-graph-e2e/cypress-watch-mode.json",
|
"cypressConfig": "dep-graph/dep-graph-e2e/cypress-watch-mode.json",
|
||||||
"tsConfig": "dep-graph/dep-graph-e2e/tsconfig.e2e.json",
|
"tsConfig": "dep-graph/dep-graph-e2e/tsconfig.e2e.json",
|
||||||
"devServerTarget": "dep-graph-dep-graph:serve:watch"
|
"devServerTarget": "dep-graph-dep-graph:serve-for-e2e:watch",
|
||||||
|
"baseUrl": "http://localhost:4200"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
|
|||||||
@ -12,7 +12,12 @@ import {
|
|||||||
|
|
||||||
describe('dep-graph-client', () => {
|
describe('dep-graph-client', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
cy.intercept('/assets/graphs/*').as('getGraph');
|
||||||
|
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
|
||||||
|
// wait for first graph to finish loading
|
||||||
|
cy.wait('@getGraph');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display message to select projects', () => {
|
it('should display message to select projects', () => {
|
||||||
@ -51,12 +56,12 @@ describe('dep-graph-client', () => {
|
|||||||
|
|
||||||
describe('selecting projects', () => {
|
describe('selecting projects', () => {
|
||||||
it('should select a project by clicking on the project name', () => {
|
it('should select a project by clicking on the project name', () => {
|
||||||
// cy.get('[data-project="nx-dev"]').should('have.data', 'active', false);
|
cy.get('[data-project="nx-dev"]').should('have.data', 'active', false);
|
||||||
cy.get('[data-project="nx-dev"]')
|
cy.get('[data-project="nx-dev"]').click({
|
||||||
.click({
|
force: true,
|
||||||
force: true,
|
});
|
||||||
})
|
|
||||||
.should('have.data', 'active', true);
|
cy.get('[data-project="nx-dev"]').should('have.data', 'active', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should deselect a project by clicking on the project name again', () => {
|
it('should deselect a project by clicking on the project name again', () => {
|
||||||
|
|||||||
@ -32,39 +32,16 @@
|
|||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"dev": {
|
"dev": {
|
||||||
"fileReplacements": [
|
"fileReplacements": [],
|
||||||
{
|
|
||||||
"replace": "dep-graph/dep-graph/src/environments/environment.ts",
|
|
||||||
"with": "dep-graph/dep-graph/src/environments/environment.dev.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"assets": [
|
"assets": [
|
||||||
"dep-graph/dep-graph/src/favicon.ico",
|
"dep-graph/dep-graph/src/favicon.ico",
|
||||||
"dep-graph/dep-graph/src/assets"
|
"dep-graph/dep-graph/src/assets/graphs/",
|
||||||
],
|
|
||||||
"optimization": false,
|
|
||||||
"outputHashing": "none",
|
|
||||||
"sourceMap": true,
|
|
||||||
"extractCss": true,
|
|
||||||
"namedChunks": false,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"vendorChunk": true,
|
|
||||||
"budgets": [
|
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"input": "dep-graph/dep-graph/src/assets",
|
||||||
"maximumWarning": "2mb",
|
"output": "/",
|
||||||
"maximumError": "5mb"
|
"glob": "environment.js"
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"watch": {
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "dep-graph/dep-graph/src/environments/environment.ts",
|
|
||||||
"with": "dep-graph/dep-graph/src/environments/environment.watch.ts"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"assets": [],
|
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"outputHashing": "none",
|
"outputHashing": "none",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
@ -83,15 +60,10 @@
|
|||||||
},
|
},
|
||||||
"outputs": ["{options.outputPath}"]
|
"outputs": ["{options.outputPath}"]
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve-base": {
|
||||||
"executor": "@nrwl/web:dev-server",
|
"executor": "@nrwl/web:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "dep-graph-dep-graph:build-base:dev"
|
"buildTarget": "dep-graph-dep-graph:build-base:dev"
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"watch": {
|
|
||||||
"buildTarget": "dep-graph-dep-graph:build-base:watch"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
@ -112,6 +84,44 @@
|
|||||||
"jestConfig": "dep-graph/dep-graph/jest.config.js",
|
"jestConfig": "dep-graph/dep-graph/jest.config.js",
|
||||||
"passWithNoTests": true
|
"passWithNoTests": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"executor": "@nrwl/workspace:run-commands",
|
||||||
|
"outputs": [],
|
||||||
|
"options": {
|
||||||
|
"commands": [
|
||||||
|
"npx ts-node -P ./scripts/tsconfig.scripts.json ./scripts/copy-dep-graph-environment.ts dev",
|
||||||
|
"nx serve-base dep-graph-dep-graph"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"watch": {
|
||||||
|
"commands": [
|
||||||
|
"npx ts-node -P ./scripts/tsconfig.scripts.json ./scripts/copy-dep-graph-environment.ts watch",
|
||||||
|
"nx serve-base dep-graph-dep-graph"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-for-e2e": {
|
||||||
|
"executor": "@nrwl/workspace:run-commands",
|
||||||
|
"outputs": [],
|
||||||
|
"options": {
|
||||||
|
"commands": [
|
||||||
|
"npx ts-node -P ./scripts/tsconfig.scripts.json ./scripts/copy-dep-graph-environment.ts dev",
|
||||||
|
"nx serve-base dep-graph-dep-graph"
|
||||||
|
],
|
||||||
|
"readyWhen": "No issues found."
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"watch": {
|
||||||
|
"commands": [
|
||||||
|
"npx ts-node -P ./scripts/tsconfig.scripts.json ./scripts/copy-dep-graph-environment.ts watch",
|
||||||
|
"nx serve-base dep-graph-dep-graph"
|
||||||
|
],
|
||||||
|
"readyWhen": "No issues found."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": ["core"]
|
"tags": ["core"]
|
||||||
|
|||||||
@ -1,31 +1,38 @@
|
|||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph';
|
import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph';
|
||||||
import { ProjectGraph } from '@nrwl/devkit';
|
import { fromEvent } from 'rxjs';
|
||||||
import { combineLatest, fromEvent, Subject } from 'rxjs';
|
import { startWith } from 'rxjs/operators';
|
||||||
import { startWith, takeUntil } from 'rxjs/operators';
|
|
||||||
import { DebuggerPanel } from './debugger-panel';
|
import { DebuggerPanel } from './debugger-panel';
|
||||||
import { GraphComponent } from './graph';
|
import { GraphComponent } from './graph';
|
||||||
import { AppConfig, DEFAULT_CONFIG } from './models';
|
import { useDepGraphService } from './machines/dep-graph.service';
|
||||||
|
import { DepGraphSend } from './machines/interfaces';
|
||||||
|
import { AppConfig, DEFAULT_CONFIG, ProjectGraphService } from './models';
|
||||||
import { GraphTooltipService } from './tooltip-service';
|
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: SidebarComponent;
|
private sidebar = new SidebarComponent();
|
||||||
private tooltipService = new GraphTooltipService();
|
private tooltipService = new GraphTooltipService();
|
||||||
private graph = new GraphComponent(this.tooltipService);
|
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({}));
|
||||||
private render$ = new Subject<{ newProjects: string[] }>();
|
|
||||||
|
|
||||||
constructor(private config: AppConfig = DEFAULT_CONFIG) {
|
private send: DepGraphSend;
|
||||||
this.render$.subscribe((nextRenderConfig) => this.render(nextRenderConfig));
|
|
||||||
|
constructor(
|
||||||
|
private config: AppConfig = DEFAULT_CONFIG,
|
||||||
|
private projectGraphService: ProjectGraphService
|
||||||
|
) {
|
||||||
|
const [_, send] = useDepGraphService();
|
||||||
|
this.send = send;
|
||||||
|
|
||||||
this.loadProjectGraph(config.defaultProjectGraph);
|
this.loadProjectGraph(config.defaultProjectGraph);
|
||||||
|
this.render();
|
||||||
|
|
||||||
if (window.watch === true) {
|
if (window.watch === true) {
|
||||||
setInterval(
|
setInterval(
|
||||||
() => this.loadProjectGraph(config.defaultProjectGraph),
|
() => this.updateProjectGraph(config.defaultProjectGraph),
|
||||||
5000
|
5000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -37,39 +44,49 @@ export class AppComponent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const project: DepGraphClientResponse =
|
const project: DepGraphClientResponse =
|
||||||
await this.config.projectGraphService.getProjectGraph(projectInfo.url);
|
await this.projectGraphService.getProjectGraph(projectInfo.url);
|
||||||
|
|
||||||
const workspaceLayout = project?.layout;
|
const workspaceLayout = project?.layout;
|
||||||
|
|
||||||
const nodes = Object.values(project.projects).reduce((acc, cur: any) => {
|
this.send({
|
||||||
acc[cur.name] = cur;
|
type: 'initGraph',
|
||||||
return acc;
|
projects: project.projects,
|
||||||
}, {});
|
|
||||||
|
|
||||||
const newProjects = !!window.graph
|
|
||||||
? project.changes.added.filter(
|
|
||||||
(addedProject) => !window.graph.nodes[addedProject]
|
|
||||||
)
|
|
||||||
: project.changes.added;
|
|
||||||
|
|
||||||
window.projects = project.projects;
|
|
||||||
window.graph = <ProjectGraph>{
|
|
||||||
dependencies: project.dependencies,
|
dependencies: project.dependencies,
|
||||||
nodes: nodes,
|
affectedProjects: project.affected,
|
||||||
};
|
workspaceLayout: workspaceLayout,
|
||||||
window.focusedProject = null;
|
});
|
||||||
window.projectGraphList = this.config.projectGraphs;
|
|
||||||
window.selectedProjectGraph = projectGraphId;
|
|
||||||
window.workspaceLayout = workspaceLayout;
|
|
||||||
|
|
||||||
if (this.sidebar) {
|
if (!!window.focusedProject) {
|
||||||
this.render$.next({ newProjects });
|
this.send({
|
||||||
} else {
|
type: 'focusProject',
|
||||||
this.render$.next();
|
projectName: window.focusedProject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.groupByFolder) {
|
||||||
|
this.send({
|
||||||
|
type: 'setGroupByFolder',
|
||||||
|
groupByFolder: window.groupByFolder,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private render(renderConfig: { newProjects: string[] } | undefined) {
|
private async updateProjectGraph(projectGraphId: string) {
|
||||||
|
const projectInfo = this.config.projectGraphs.find(
|
||||||
|
(graph) => graph.id === projectGraphId
|
||||||
|
);
|
||||||
|
|
||||||
|
const project: DepGraphClientResponse =
|
||||||
|
await this.projectGraphService.getProjectGraph(projectInfo.url);
|
||||||
|
|
||||||
|
this.send({
|
||||||
|
type: 'updateGraph',
|
||||||
|
projects: project.projects,
|
||||||
|
dependencies: project.dependencies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private render() {
|
||||||
const debuggerPanelContainer = document.getElementById('debugger-panel');
|
const debuggerPanelContainer = document.getElementById('debugger-panel');
|
||||||
|
|
||||||
if (this.config.showDebugger) {
|
if (this.config.showDebugger) {
|
||||||
@ -78,59 +95,17 @@ export class AppComponent {
|
|||||||
|
|
||||||
this.debuggerPanel = new DebuggerPanel(
|
this.debuggerPanel = new DebuggerPanel(
|
||||||
debuggerPanelContainer,
|
debuggerPanelContainer,
|
||||||
window.projectGraphList
|
this.config.projectGraphs,
|
||||||
|
this.config.defaultProjectGraph
|
||||||
);
|
);
|
||||||
|
|
||||||
this.debuggerPanel.selectProject$.subscribe((id) => {
|
this.debuggerPanel.selectProject$.subscribe((id) => {
|
||||||
this.loadProjectGraph(id);
|
this.loadProjectGraph(id);
|
||||||
this.sidebar.resetSidebarVisibility();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.graph.projectGraph = window.graph;
|
|
||||||
const affectedProjects = window.affected;
|
|
||||||
|
|
||||||
this.graph.affectedProjects = affectedProjects;
|
|
||||||
|
|
||||||
if (!this.sidebar) {
|
|
||||||
this.sidebar = new SidebarComponent(affectedProjects);
|
|
||||||
} else {
|
|
||||||
this.sidebar.projects = window.projects;
|
|
||||||
|
|
||||||
if (renderConfig?.newProjects.length > 0) {
|
|
||||||
this.sidebar.selectProjects(renderConfig.newProjects);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
combineLatest([
|
|
||||||
this.sidebar.selectedProjectsChanged$,
|
|
||||||
this.sidebar.groupByFolderChanged$,
|
|
||||||
this.windowResize$,
|
|
||||||
])
|
|
||||||
.pipe(takeUntil(this.render$))
|
|
||||||
.subscribe(([selectedProjectNames, groupByFolder]) => {
|
|
||||||
const selectedProjects = [];
|
|
||||||
|
|
||||||
selectedProjectNames.forEach((projectName) => {
|
|
||||||
if (window.graph.nodes[projectName]) {
|
|
||||||
selectedProjects.push(window.graph.nodes[projectName]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedProjects.length === 0) {
|
|
||||||
document.getElementById('no-projects-chosen').style.display = 'flex';
|
|
||||||
} else {
|
|
||||||
document.getElementById('no-projects-chosen').style.display = 'none';
|
|
||||||
this.graph.render(selectedProjects, groupByFolder);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.debuggerPanel) {
|
this.graph.renderTimes$.subscribe(
|
||||||
this.graph.renderTimes$
|
(renderTime) => (this.debuggerPanel.renderTime = renderTime)
|
||||||
.pipe(takeUntil(this.render$))
|
);
|
||||||
.subscribe(
|
|
||||||
(renderTime) => (this.debuggerPanel.renderTime = renderTime)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,8 @@ export class DebuggerPanel {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private container: HTMLElement,
|
private container: HTMLElement,
|
||||||
private projectGraphs: ProjectGraphList[]
|
private projectGraphs: ProjectGraphList[],
|
||||||
|
private initialSelectedGraph: string
|
||||||
) {
|
) {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
@ -40,7 +41,7 @@ export class DebuggerPanel {
|
|||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
select.value = window.selectedProjectGraph;
|
select.value = this.initialSelectedGraph;
|
||||||
select.dataset['cy'] = 'project-select';
|
select.dataset['cy'] = 'project-select';
|
||||||
|
|
||||||
select.onchange = (event) =>
|
select.onchange = (event) =>
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
import type { ProjectGraph, ProjectGraphNode } from '@nrwl/devkit';
|
import type {
|
||||||
|
ProjectGraph,
|
||||||
|
ProjectGraphDependency,
|
||||||
|
ProjectGraphNode,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import type { VirtualElement } from '@popperjs/core';
|
||||||
import * as cy from 'cytoscape';
|
import * as cy from 'cytoscape';
|
||||||
import cytoscapeDagre from 'cytoscape-dagre';
|
import cytoscapeDagre from 'cytoscape-dagre';
|
||||||
import popper from 'cytoscape-popper';
|
import 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 { 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';
|
||||||
@ -13,7 +19,6 @@ import {
|
|||||||
ProjectEdge,
|
ProjectEdge,
|
||||||
ProjectNode,
|
ProjectNode,
|
||||||
} from './util-cytoscape';
|
} from './util-cytoscape';
|
||||||
import type { VirtualElement } from '@popperjs/core';
|
|
||||||
|
|
||||||
export interface GraphPerfReport {
|
export interface GraphPerfReport {
|
||||||
renderTime: number;
|
renderTime: number;
|
||||||
@ -24,22 +29,57 @@ export class GraphComponent {
|
|||||||
private graph: cy.Core;
|
private graph: cy.Core;
|
||||||
private openTooltip: Instance = null;
|
private openTooltip: Instance = null;
|
||||||
|
|
||||||
affectedProjects: string[];
|
|
||||||
projectGraph: ProjectGraph;
|
|
||||||
|
|
||||||
private renderTimesSubject = new Subject<GraphPerfReport>();
|
private renderTimesSubject = new Subject<GraphPerfReport>();
|
||||||
renderTimes$ = this.renderTimesSubject.asObservable();
|
renderTimes$ = this.renderTimesSubject.asObservable();
|
||||||
|
|
||||||
|
private send;
|
||||||
constructor(private tooltipService: GraphTooltipService) {
|
constructor(private tooltipService: GraphTooltipService) {
|
||||||
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(selectedProjects: ProjectGraphNode[], groupByFolder: boolean) {
|
render(
|
||||||
|
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) {
|
||||||
|
document.getElementById('no-projects-chosen').style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
document.getElementById('no-projects-chosen').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
this.tooltipService.hideAll();
|
this.tooltipService.hideAll();
|
||||||
this.generateCytoscapeLayout(selectedProjects, groupByFolder);
|
this.generateCytoscapeLayout(
|
||||||
|
selectedProjects,
|
||||||
|
groupByFolder,
|
||||||
|
workspaceLayout,
|
||||||
|
focusedProject,
|
||||||
|
affectedProjects,
|
||||||
|
dependencies
|
||||||
|
);
|
||||||
this.listenForProjectNodeClicks();
|
this.listenForProjectNodeClicks();
|
||||||
this.listenForProjectNodeHovers();
|
this.listenForProjectNodeHovers();
|
||||||
|
|
||||||
@ -56,9 +96,20 @@ export class GraphComponent {
|
|||||||
|
|
||||||
private generateCytoscapeLayout(
|
private generateCytoscapeLayout(
|
||||||
selectedProjects: ProjectGraphNode[],
|
selectedProjects: ProjectGraphNode[],
|
||||||
groupByFolder: boolean
|
groupByFolder: boolean,
|
||||||
|
workspaceLayout,
|
||||||
|
focusedProject: string,
|
||||||
|
affectedProjects: string[],
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>
|
||||||
) {
|
) {
|
||||||
const elements = this.createElements(selectedProjects, groupByFolder);
|
const elements = this.createElements(
|
||||||
|
selectedProjects,
|
||||||
|
groupByFolder,
|
||||||
|
workspaceLayout,
|
||||||
|
focusedProject,
|
||||||
|
affectedProjects,
|
||||||
|
dependencies
|
||||||
|
);
|
||||||
|
|
||||||
this.graph = cy({
|
this.graph = cy({
|
||||||
container: document.getElementById('graph-container'),
|
container: document.getElementById('graph-container'),
|
||||||
@ -84,7 +135,14 @@ export class GraphComponent {
|
|||||||
|
|
||||||
private createElements(
|
private createElements(
|
||||||
selectedProjects: ProjectGraphNode[],
|
selectedProjects: ProjectGraphNode[],
|
||||||
groupByFolder: boolean
|
groupByFolder: boolean,
|
||||||
|
workspaceLayout: {
|
||||||
|
appsDir: string;
|
||||||
|
libsDir: string;
|
||||||
|
},
|
||||||
|
focusedProject: string,
|
||||||
|
affectedProjects: string[],
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>
|
||||||
) {
|
) {
|
||||||
let elements: cy.ElementDefinition[] = [];
|
let elements: cy.ElementDefinition[] = [];
|
||||||
const filteredProjectNames = selectedProjects.map(
|
const filteredProjectNames = selectedProjects.map(
|
||||||
@ -101,21 +159,21 @@ export class GraphComponent {
|
|||||||
selectedProjects.forEach((project) => {
|
selectedProjects.forEach((project) => {
|
||||||
const workspaceRoot =
|
const workspaceRoot =
|
||||||
project.type === 'app' || project.type === 'e2e'
|
project.type === 'app' || project.type === 'e2e'
|
||||||
? window.workspaceLayout.appsDir
|
? workspaceLayout.appsDir
|
||||||
: window.workspaceLayout.libsDir;
|
: workspaceLayout.libsDir;
|
||||||
|
|
||||||
const projectNode = new ProjectNode(project, workspaceRoot);
|
const projectNode = new ProjectNode(project, workspaceRoot);
|
||||||
projectNode.focused = project.name === window.focusedProject;
|
projectNode.focused = project.name === focusedProject;
|
||||||
projectNode.affected = this.affectedProjects.includes(project.name);
|
projectNode.affected = affectedProjects.includes(project.name);
|
||||||
|
|
||||||
projectNodes.push(projectNode);
|
projectNodes.push(projectNode);
|
||||||
|
|
||||||
this.projectGraph.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 =
|
edge.affected =
|
||||||
this.affectedProjects.includes(dep.source) &&
|
affectedProjects.includes(dep.source) &&
|
||||||
this.affectedProjects.includes(dep.target);
|
affectedProjects.includes(dep.target);
|
||||||
edgeNodes.push(edge);
|
edgeNodes.push(edge);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
import { DepGraphStateNodeConfig } from './interfaces';
|
||||||
|
|
||||||
|
export const customSelectedStateConfig: DepGraphStateNodeConfig = {
|
||||||
|
on: {
|
||||||
|
updateGraph: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
const existingProjectNames = ctx.projects.map(
|
||||||
|
(project) => project.name
|
||||||
|
);
|
||||||
|
const newProjectNames = event.projects.map((project) => project.name);
|
||||||
|
const selectedProjects = newProjectNames.filter(
|
||||||
|
(projectName) => !existingProjectNames.includes(projectName)
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.projects = event.projects;
|
||||||
|
ctx.dependencies = event.dependencies;
|
||||||
|
ctx.selectedProjects = [...ctx.selectedProjects, ...selectedProjects];
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
154
dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts
Normal file
154
dep-graph/dep-graph/src/app/machines/dep-graph.machine.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
import { Machine } from 'xstate';
|
||||||
|
import { customSelectedStateConfig } from './custom-selected.state';
|
||||||
|
import { focusedStateConfig } from './focused.state';
|
||||||
|
import { DepGraphContext, DepGraphEvents, DepGraphSchema } from './interfaces';
|
||||||
|
import { textFilteredStateConfig } from './text-filtered.state';
|
||||||
|
import { unselectedStateConfig } from './unselected.state';
|
||||||
|
|
||||||
|
export const initialContext: DepGraphContext = {
|
||||||
|
projects: [],
|
||||||
|
dependencies: {},
|
||||||
|
affectedProjects: [],
|
||||||
|
selectedProjects: [],
|
||||||
|
focusedProject: null,
|
||||||
|
textFilter: '',
|
||||||
|
includePath: false,
|
||||||
|
searchDepth: 1,
|
||||||
|
searchDepthEnabled: false,
|
||||||
|
groupByFolder: false,
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: '',
|
||||||
|
appsDir: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const depGraphMachine = Machine<
|
||||||
|
DepGraphContext,
|
||||||
|
DepGraphSchema,
|
||||||
|
DepGraphEvents
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
id: 'DepGraph',
|
||||||
|
initial: 'idle',
|
||||||
|
context: initialContext,
|
||||||
|
states: {
|
||||||
|
idle: {},
|
||||||
|
unselected: unselectedStateConfig,
|
||||||
|
customSelected: customSelectedStateConfig,
|
||||||
|
focused: focusedStateConfig,
|
||||||
|
textFiltered: textFilteredStateConfig,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
initGraph: {
|
||||||
|
target: 'unselected',
|
||||||
|
actions: assign((ctx, event) => {
|
||||||
|
ctx.projects = event.projects;
|
||||||
|
ctx.affectedProjects = event.affectedProjects;
|
||||||
|
ctx.dependencies = event.dependencies;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
selectProject: {
|
||||||
|
target: 'customSelected',
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.selectedProjects.push(event.projectName);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
selectAll: {
|
||||||
|
target: 'customSelected',
|
||||||
|
actions: [
|
||||||
|
assign((ctx) => {
|
||||||
|
ctx.selectedProjects = ctx.projects.map((project) => project.name);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
selectAffected: {
|
||||||
|
target: 'customSelected',
|
||||||
|
actions: [
|
||||||
|
assign((ctx) => {
|
||||||
|
ctx.selectedProjects = ctx.affectedProjects;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
deselectProject: [
|
||||||
|
{
|
||||||
|
target: 'unselected',
|
||||||
|
cond: 'deselectLastProject',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'customSelected',
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
const index = ctx.selectedProjects.findIndex(
|
||||||
|
(project) => project === event.projectName
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.selectedProjects.splice(index, 1);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
deselectAll: {
|
||||||
|
target: 'unselected',
|
||||||
|
},
|
||||||
|
focusProject: {
|
||||||
|
target: 'focused',
|
||||||
|
},
|
||||||
|
setGroupByFolder: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event: any) => {
|
||||||
|
ctx.groupByFolder = event.groupByFolder;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
setIncludeProjectsByPath: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.includePath = event.includeProjectsByPath;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
setSearchDepth: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.searchDepth = event.searchDepth;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
incrementSearchDepth: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx) => {
|
||||||
|
ctx.searchDepth = ctx.searchDepth + 1;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
decrementSearchDepth: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx) => {
|
||||||
|
ctx.searchDepth = ctx.searchDepth - 1;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
setSearchDepthEnabled: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.searchDepthEnabled = event.searchDepthEnabled;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
filterByText: {
|
||||||
|
target: 'textFiltered',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
guards: {
|
||||||
|
deselectLastProject: (ctx) => {
|
||||||
|
return ctx.selectedProjects.length <= 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
38
dep-graph/dep-graph/src/app/machines/dep-graph.service.ts
Normal file
38
dep-graph/dep-graph/src/app/machines/dep-graph.service.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { from } from 'rxjs';
|
||||||
|
import { map, shareReplay } from 'rxjs/operators';
|
||||||
|
import { interpret, Interpreter, Typestate } from 'xstate';
|
||||||
|
import { depGraphMachine } from './dep-graph.machine';
|
||||||
|
import {
|
||||||
|
DepGraphContext,
|
||||||
|
DepGraphEvents,
|
||||||
|
DepGraphSend,
|
||||||
|
DepGraphStateObservable,
|
||||||
|
} from './interfaces';
|
||||||
|
|
||||||
|
let depGraphService: Interpreter<
|
||||||
|
DepGraphContext,
|
||||||
|
any,
|
||||||
|
DepGraphEvents,
|
||||||
|
Typestate<DepGraphContext>
|
||||||
|
>;
|
||||||
|
|
||||||
|
let depGraphState$: DepGraphStateObservable;
|
||||||
|
|
||||||
|
export function useDepGraphService(): [DepGraphStateObservable, DepGraphSend] {
|
||||||
|
if (!depGraphService) {
|
||||||
|
depGraphService = interpret(depGraphMachine, {
|
||||||
|
devTools: !!window.useXstateInspect,
|
||||||
|
});
|
||||||
|
depGraphService.start();
|
||||||
|
|
||||||
|
depGraphState$ = from(depGraphService).pipe(
|
||||||
|
map((state) => ({
|
||||||
|
value: state.value,
|
||||||
|
context: state.context,
|
||||||
|
})),
|
||||||
|
shareReplay(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [depGraphState$, depGraphService.send];
|
||||||
|
}
|
||||||
260
dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts
Normal file
260
dep-graph/dep-graph/src/app/machines/dep-graph.spec.ts
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
||||||
|
import { depGraphMachine } from './dep-graph.machine';
|
||||||
|
|
||||||
|
export const mockProjects: ProjectGraphNode[] = [
|
||||||
|
{
|
||||||
|
name: 'app1',
|
||||||
|
type: 'app',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'app2',
|
||||||
|
type: 'app',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ui-lib',
|
||||||
|
type: 'lib',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feature-lib1',
|
||||||
|
type: 'lib',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feature-lib2',
|
||||||
|
type: 'lib',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'auth-lib',
|
||||||
|
type: 'lib',
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockDependencies: Record<string, ProjectGraphDependency[]> = {
|
||||||
|
app1: [
|
||||||
|
{
|
||||||
|
type: 'static',
|
||||||
|
source: 'app1',
|
||||||
|
target: 'auth-lib',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'static',
|
||||||
|
source: 'app1',
|
||||||
|
target: 'feature-lib1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
app2: [
|
||||||
|
{
|
||||||
|
type: 'static',
|
||||||
|
source: 'app2',
|
||||||
|
target: 'auth-lib',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'static',
|
||||||
|
source: 'app2',
|
||||||
|
target: 'feature-lib2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'feature-lib1': [
|
||||||
|
{
|
||||||
|
type: 'static',
|
||||||
|
source: 'feature-lib1',
|
||||||
|
target: 'ui-lib',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'feature-lib2': [
|
||||||
|
{
|
||||||
|
type: 'static',
|
||||||
|
source: 'feature-lib2',
|
||||||
|
target: 'ui-lib',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'ui-lib': [],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('dep-graph machine', () => {
|
||||||
|
describe('initGraph', () => {
|
||||||
|
it('should set projects and dependencies', () => {
|
||||||
|
const result = depGraphMachine.transition(depGraphMachine.initialState, {
|
||||||
|
type: 'initGraph',
|
||||||
|
projects: mockProjects,
|
||||||
|
dependencies: mockDependencies,
|
||||||
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
});
|
||||||
|
expect(result.context.projects).toEqual(mockProjects);
|
||||||
|
expect(result.context.dependencies).toEqual(mockDependencies);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with no projects selected', () => {
|
||||||
|
const result = depGraphMachine.transition(depGraphMachine.initialState, {
|
||||||
|
type: 'initGraph',
|
||||||
|
projects: mockProjects,
|
||||||
|
dependencies: mockDependencies,
|
||||||
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.value).toEqual('unselected');
|
||||||
|
expect(result.context.selectedProjects).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('selecting projects', () => {
|
||||||
|
it('should select projects', () => {
|
||||||
|
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
||||||
|
type: 'initGraph',
|
||||||
|
projects: mockProjects,
|
||||||
|
dependencies: mockDependencies,
|
||||||
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'selectProject',
|
||||||
|
projectName: 'app1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.value).toEqual('customSelected');
|
||||||
|
expect(result.context.selectedProjects).toEqual(['app1']);
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'selectProject',
|
||||||
|
projectName: 'app2',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.context.selectedProjects).toEqual(['app1', 'app2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deselecting projects', () => {
|
||||||
|
it('should deselect projects', () => {
|
||||||
|
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
||||||
|
type: 'initGraph',
|
||||||
|
projects: mockProjects,
|
||||||
|
dependencies: mockDependencies,
|
||||||
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'selectProject',
|
||||||
|
projectName: 'app1',
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'selectProject',
|
||||||
|
projectName: 'app2',
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
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', () => {
|
||||||
|
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
||||||
|
type: 'initGraph',
|
||||||
|
projects: mockProjects,
|
||||||
|
dependencies: mockDependencies,
|
||||||
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'selectProject',
|
||||||
|
projectName: 'app1',
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'selectProject',
|
||||||
|
projectName: 'app2',
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'deselectProject',
|
||||||
|
projectName: 'app1',
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'deselectProject',
|
||||||
|
projectName: 'app2',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.value).toEqual('unselected');
|
||||||
|
expect(result.context.selectedProjects).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('focusing projects', () => {
|
||||||
|
it('should set the focused project', () => {
|
||||||
|
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
||||||
|
type: 'initGraph',
|
||||||
|
projects: mockProjects,
|
||||||
|
dependencies: mockDependencies,
|
||||||
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'focusProject',
|
||||||
|
projectName: 'app1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.value).toEqual('focused');
|
||||||
|
expect(result.context.focusedProject).toEqual('app1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select the projects by the focused project', () => {
|
||||||
|
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
||||||
|
type: 'initGraph',
|
||||||
|
projects: mockProjects,
|
||||||
|
dependencies: mockDependencies,
|
||||||
|
affectedProjects: [],
|
||||||
|
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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select no projects on unfocus', () => {
|
||||||
|
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
||||||
|
type: 'initGraph',
|
||||||
|
projects: mockProjects,
|
||||||
|
dependencies: mockDependencies,
|
||||||
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'focusProject',
|
||||||
|
projectName: 'app1',
|
||||||
|
});
|
||||||
|
|
||||||
|
result = depGraphMachine.transition(result, {
|
||||||
|
type: 'unfocusProject',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.value).toEqual('unselected');
|
||||||
|
expect(result.context.selectedProjects).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
91
dep-graph/dep-graph/src/app/machines/focused.state.ts
Normal file
91
dep-graph/dep-graph/src/app/machines/focused.state.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.focusedProject = event.projectName;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
exit: [
|
||||||
|
assign((ctx) => {
|
||||||
|
ctx.focusedProject = null;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
decrementSearchDepth: {
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
78
dep-graph/dep-graph/src/app/machines/interfaces.ts
Normal file
78
dep-graph/dep-graph/src/app/machines/interfaces.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ActionObject, StateNodeConfig, StateValue } from 'xstate';
|
||||||
|
|
||||||
|
// The hierarchical (recursive) schema for the states
|
||||||
|
export interface DepGraphSchema {
|
||||||
|
states: {
|
||||||
|
idle: {};
|
||||||
|
unselected: {};
|
||||||
|
focused: {};
|
||||||
|
textFiltered: {};
|
||||||
|
customSelected: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The events that the machine handles
|
||||||
|
export type DepGraphEvents =
|
||||||
|
| { type: 'selectProject'; projectName: string }
|
||||||
|
| { type: 'deselectProject'; projectName: string }
|
||||||
|
| { type: 'selectAll' }
|
||||||
|
| { type: 'deselectAll' }
|
||||||
|
| { type: 'selectAffected' }
|
||||||
|
| { type: 'setGroupByFolder'; groupByFolder: boolean }
|
||||||
|
| { type: 'setIncludeProjectsByPath'; includeProjectsByPath: boolean }
|
||||||
|
| { type: 'setSearchDepth'; searchDepth: number }
|
||||||
|
| { type: 'incrementSearchDepth' }
|
||||||
|
| { type: 'decrementSearchDepth' }
|
||||||
|
| { type: 'setSearchDepthEnabled'; searchDepthEnabled: boolean }
|
||||||
|
| { type: 'focusProject'; projectName: string }
|
||||||
|
| { type: 'unfocusProject' }
|
||||||
|
| { type: 'filterByText'; search: string }
|
||||||
|
| { type: 'clearTextFilter' }
|
||||||
|
| {
|
||||||
|
type: 'initGraph';
|
||||||
|
projects: ProjectGraphNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'updateGraph';
|
||||||
|
projects: ProjectGraphNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The context (extended state) of the machine
|
||||||
|
export interface DepGraphContext {
|
||||||
|
projects: ProjectGraphNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
selectedProjects: string[];
|
||||||
|
focusedProject: string | null;
|
||||||
|
textFilter: string;
|
||||||
|
includePath: boolean;
|
||||||
|
searchDepth: number;
|
||||||
|
searchDepthEnabled: boolean;
|
||||||
|
groupByFolder: boolean;
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DepGraphStateNodeConfig = StateNodeConfig<
|
||||||
|
DepGraphContext,
|
||||||
|
{},
|
||||||
|
DepGraphEvents,
|
||||||
|
ActionObject<DepGraphContext, DepGraphEvents>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DepGraphSend = (event: DepGraphEvents | DepGraphEvents[]) => void;
|
||||||
|
export type DepGraphStateObservable = Observable<{
|
||||||
|
value: StateValue;
|
||||||
|
context: DepGraphContext;
|
||||||
|
}>;
|
||||||
85
dep-graph/dep-graph/src/app/machines/text-filtered.state.ts
Normal file
85
dep-graph/dep-graph/src/app/machines/text-filtered.state.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
import { filterProjectsByText } from '../util';
|
||||||
|
import { DepGraphStateNodeConfig } from './interfaces';
|
||||||
|
|
||||||
|
export const textFilteredStateConfig: DepGraphStateNodeConfig = {
|
||||||
|
entry: [
|
||||||
|
assign((ctx, event: any) => {
|
||||||
|
ctx.textFilter = event.search;
|
||||||
|
ctx.selectedProjects = filterProjectsByText(
|
||||||
|
event.search,
|
||||||
|
ctx.includePath,
|
||||||
|
ctx.searchDepthEnabled ? ctx.searchDepth : -1,
|
||||||
|
ctx.projects,
|
||||||
|
ctx.dependencies
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
on: {
|
||||||
|
clearTextFilter: {
|
||||||
|
target: 'unselected',
|
||||||
|
actions: assign((ctx) => {
|
||||||
|
ctx.includePath = false;
|
||||||
|
ctx.textFilter = '';
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
setSearchDepth: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
ctx.searchDepth = event.searchDepth;
|
||||||
|
ctx.selectedProjects = filterProjectsByText(
|
||||||
|
ctx.textFilter,
|
||||||
|
ctx.includePath,
|
||||||
|
ctx.searchDepthEnabled ? event.searchDepth : -1,
|
||||||
|
ctx.projects,
|
||||||
|
ctx.dependencies
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
29
dep-graph/dep-graph/src/app/machines/unselected.state.ts
Normal file
29
dep-graph/dep-graph/src/app/machines/unselected.state.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
import { DepGraphStateNodeConfig } from './interfaces';
|
||||||
|
|
||||||
|
export const unselectedStateConfig: DepGraphStateNodeConfig = {
|
||||||
|
entry: [
|
||||||
|
assign((ctx) => {
|
||||||
|
ctx.selectedProjects = [];
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
on: {
|
||||||
|
updateGraph: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
const existingProjectNames = ctx.projects.map(
|
||||||
|
(project) => project.name
|
||||||
|
);
|
||||||
|
const newProjectNames = event.projects.map((project) => project.name);
|
||||||
|
const selectedProjects = newProjectNames.filter(
|
||||||
|
(projectName) => !existingProjectNames.includes(projectName)
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.projects = event.projects;
|
||||||
|
ctx.dependencies = event.dependencies;
|
||||||
|
ctx.selectedProjects = [...ctx.selectedProjects, ...selectedProjects];
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -38,9 +38,6 @@ export class MockProjectGraphService implements ProjectGraphService {
|
|||||||
],
|
],
|
||||||
'existing-lib-1': [],
|
'existing-lib-1': [],
|
||||||
},
|
},
|
||||||
changes: {
|
|
||||||
added: [],
|
|
||||||
},
|
|
||||||
affected: [],
|
affected: [],
|
||||||
focus: null,
|
focus: null,
|
||||||
exclude: [],
|
exclude: [],
|
||||||
@ -88,8 +85,14 @@ export class MockProjectGraphService implements ProjectGraphService {
|
|||||||
type: 'static',
|
type: 'static',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.response.projects.push(newProject);
|
|
||||||
this.response.dependencies[newProject.name] = newDependency;
|
this.response = {
|
||||||
this.response.changes.added.push(newProject.name);
|
...this.response,
|
||||||
|
projects: [...this.response.projects, newProject],
|
||||||
|
dependencies: {
|
||||||
|
...this.response.dependencies,
|
||||||
|
[newProject.name]: newDependency,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,20 +17,17 @@ export interface ProjectGraphService {
|
|||||||
getProjectGraph: (url: string) => Promise<DepGraphClientResponse>;
|
getProjectGraph: (url: string) => Promise<DepGraphClientResponse>;
|
||||||
}
|
}
|
||||||
export interface Environment {
|
export interface Environment {
|
||||||
environment: 'dev' | 'dev-watch' | 'release';
|
environment: 'dev' | 'watch' | 'release';
|
||||||
appConfig: AppConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
showDebugger: boolean;
|
showDebugger: boolean;
|
||||||
projectGraphs: ProjectGraphList[];
|
projectGraphs: ProjectGraphList[];
|
||||||
defaultProjectGraph: string;
|
defaultProjectGraph: string;
|
||||||
projectGraphService: ProjectGraphService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: AppConfig = {
|
export const DEFAULT_CONFIG: AppConfig = {
|
||||||
showDebugger: false,
|
showDebugger: false,
|
||||||
projectGraphs: [],
|
projectGraphs: [],
|
||||||
defaultProjectGraph: null,
|
defaultProjectGraph: null,
|
||||||
projectGraphService: null,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import * as cy from 'cytoscape';
|
import * as cy from 'cytoscape';
|
||||||
|
import { useDepGraphService } from './machines/dep-graph.service';
|
||||||
|
|
||||||
export class ProjectNodeToolTip {
|
export class ProjectNodeToolTip {
|
||||||
constructor(private node: cy.NodeSingular) {}
|
constructor(private node: cy.NodeSingular) {}
|
||||||
@ -53,13 +54,15 @@ export class ProjectNodeToolTip {
|
|||||||
|
|
||||||
wrapper.classList.add('flex');
|
wrapper.classList.add('flex');
|
||||||
|
|
||||||
|
const [_, send] = useDepGraphService();
|
||||||
|
|
||||||
focusButton.addEventListener('click', () =>
|
focusButton.addEventListener('click', () =>
|
||||||
window.focusProject(this.node.attr('id'))
|
send({ type: 'focusProject', projectName: this.node.attr('id') })
|
||||||
);
|
);
|
||||||
focusButton.innerText = 'Focus';
|
focusButton.innerText = 'Focus';
|
||||||
|
|
||||||
excludeButton.addEventListener('click', () => {
|
excludeButton.addEventListener('click', () => {
|
||||||
window.excludeProject(this.node.attr('id'));
|
send({ type: 'deselectProject', projectName: this.node.attr('id') });
|
||||||
});
|
});
|
||||||
|
|
||||||
excludeButton.innerText = 'Exclude';
|
excludeButton.innerText = 'Exclude';
|
||||||
|
|||||||
@ -1,51 +1,34 @@
|
|||||||
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
|
import { useDepGraphService } from '../machines/dep-graph.service';
|
||||||
import { distinctUntilChanged, map, withLatestFrom } from 'rxjs/operators';
|
import { DepGraphSend } from '../machines/interfaces';
|
||||||
import { removeChildrenFromContainer } from '../util';
|
import { removeChildrenFromContainer } from '../util';
|
||||||
|
|
||||||
export class DisplayOptionsPanel {
|
export class DisplayOptionsPanel {
|
||||||
private showAffected = false;
|
|
||||||
private groupByFolder = false;
|
|
||||||
private selectAffectedSubject = new Subject<void>();
|
|
||||||
private selectAllSubject = new Subject<void>();
|
|
||||||
private deselectAllSubject = new Subject<void>();
|
|
||||||
private groupByFolderSubject = new Subject<boolean>();
|
|
||||||
private searchByDepthSubject = new BehaviorSubject<number>(1);
|
|
||||||
private searchByDepthEnabledSubject = new BehaviorSubject<boolean>(false);
|
|
||||||
private searchDepthChangesSubject = new Subject<'increment' | 'decrement'>();
|
|
||||||
|
|
||||||
selectAffected$ = this.selectAffectedSubject.asObservable();
|
|
||||||
selectAll$ = this.selectAllSubject.asObservable();
|
|
||||||
deselectAll$ = this.deselectAllSubject.asObservable();
|
|
||||||
groupByFolder$ = this.groupByFolderSubject.asObservable();
|
|
||||||
searchDepth$ = combineLatest([
|
|
||||||
this.searchByDepthSubject,
|
|
||||||
this.searchByDepthEnabledSubject,
|
|
||||||
]).pipe(
|
|
||||||
map(([searchDepth, enabled]) => {
|
|
||||||
return enabled ? searchDepth : -1;
|
|
||||||
}),
|
|
||||||
distinctUntilChanged()
|
|
||||||
);
|
|
||||||
|
|
||||||
searchDepthDisplay: HTMLSpanElement;
|
searchDepthDisplay: HTMLSpanElement;
|
||||||
|
affectedButtonElement: HTMLElement;
|
||||||
|
groupByFolderCheckboxElement: HTMLInputElement;
|
||||||
|
|
||||||
constructor(showAffected = false, groupByFolder = false) {
|
send: DepGraphSend;
|
||||||
this.showAffected = showAffected;
|
|
||||||
this.groupByFolder = groupByFolder;
|
|
||||||
|
|
||||||
this.searchDepthChangesSubject
|
constructor(private container: HTMLElement) {
|
||||||
.pipe(withLatestFrom(this.searchByDepthSubject))
|
const [state$, send] = useDepGraphService();
|
||||||
.subscribe(([action, current]) => {
|
this.send = send;
|
||||||
if (action === 'decrement' && current > 1) {
|
this.render();
|
||||||
this.searchByDepthSubject.next(current - 1);
|
|
||||||
} else if (action === 'increment') {
|
|
||||||
this.searchByDepthSubject.next(current + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.searchByDepthSubject.subscribe((current) => {
|
state$.subscribe((state) => {
|
||||||
if (this.searchDepthDisplay) {
|
if (state.context.affectedProjects.length > 0) {
|
||||||
this.searchDepthDisplay.innerText = current.toString();
|
this.affectedButtonElement.classList.remove('hidden');
|
||||||
|
this.affectedButtonElement.addEventListener('click', () =>
|
||||||
|
this.send({ type: 'selectAffected' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchDepthDisplay.innerText = state.context.searchDepth.toString();
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.groupByFolderCheckboxElement.checked !==
|
||||||
|
state.context.groupByFolder
|
||||||
|
) {
|
||||||
|
this.groupByFolderCheckboxElement.checked = state.context.groupByFolder;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -119,44 +102,39 @@ export class DisplayOptionsPanel {
|
|||||||
return render.content.firstChild as HTMLElement;
|
return render.content.firstChild as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(container: HTMLElement) {
|
private render() {
|
||||||
removeChildrenFromContainer(container);
|
removeChildrenFromContainer(this.container);
|
||||||
|
|
||||||
const element = DisplayOptionsPanel.renderHtmlTemplate();
|
const element = DisplayOptionsPanel.renderHtmlTemplate();
|
||||||
|
|
||||||
const affectedButtonElement: HTMLElement = element.querySelector(
|
this.affectedButtonElement = element.querySelector(
|
||||||
'[data-cy="affectedButton"]'
|
'[data-cy="affectedButton"]'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.showAffected) {
|
|
||||||
affectedButtonElement.classList.remove('hidden');
|
|
||||||
affectedButtonElement.addEventListener('click', () =>
|
|
||||||
this.selectAffectedSubject.next()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectAllButtonElement: HTMLElement = element.querySelector(
|
const selectAllButtonElement: HTMLElement = element.querySelector(
|
||||||
'[data-cy="selectAllButton"]'
|
'[data-cy="selectAllButton"]'
|
||||||
);
|
);
|
||||||
selectAllButtonElement.addEventListener('click', () => {
|
selectAllButtonElement.addEventListener('click', () => {
|
||||||
this.selectAllSubject.next();
|
this.send({ type: 'selectAll' });
|
||||||
});
|
});
|
||||||
|
|
||||||
const deselectAllButtonElement: HTMLElement = element.querySelector(
|
const deselectAllButtonElement: HTMLElement = element.querySelector(
|
||||||
'[data-cy="deselectAllButton"]'
|
'[data-cy="deselectAllButton"]'
|
||||||
);
|
);
|
||||||
deselectAllButtonElement.addEventListener('click', () => {
|
deselectAllButtonElement.addEventListener('click', () => {
|
||||||
this.deselectAllSubject.next();
|
this.send({ type: 'deselectAll' });
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupByFolderCheckboxElement: HTMLInputElement =
|
this.groupByFolderCheckboxElement =
|
||||||
element.querySelector('#displayOptions');
|
element.querySelector('#displayOptions');
|
||||||
groupByFolderCheckboxElement.checked = this.groupByFolder;
|
|
||||||
|
|
||||||
groupByFolderCheckboxElement.addEventListener(
|
this.groupByFolderCheckboxElement.addEventListener(
|
||||||
'change',
|
'change',
|
||||||
(event: InputEvent) =>
|
(event: InputEvent) =>
|
||||||
this.groupByFolderSubject.next((<HTMLInputElement>event.target).checked)
|
this.send({
|
||||||
|
type: 'setGroupByFolder',
|
||||||
|
groupByFolder: (event.target as HTMLInputElement).checked,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.searchDepthDisplay = element.querySelector('#depthFilterValue');
|
this.searchDepthDisplay = element.querySelector('#depthFilterValue');
|
||||||
@ -170,18 +148,19 @@ export class DisplayOptionsPanel {
|
|||||||
element.querySelector('#depthFilter');
|
element.querySelector('#depthFilter');
|
||||||
|
|
||||||
incrementButtonElement.addEventListener('click', () => {
|
incrementButtonElement.addEventListener('click', () => {
|
||||||
this.searchDepthChangesSubject.next('increment');
|
this.send({ type: 'incrementSearchDepth' });
|
||||||
});
|
});
|
||||||
decrementButtonElement.addEventListener('click', () => {
|
decrementButtonElement.addEventListener('click', () => {
|
||||||
this.searchDepthChangesSubject.next('decrement');
|
this.send({ type: 'decrementSearchDepth' });
|
||||||
});
|
});
|
||||||
|
|
||||||
searchDepthEnabledElement.addEventListener('change', (event: InputEvent) =>
|
searchDepthEnabledElement.addEventListener('change', (event: InputEvent) =>
|
||||||
this.searchByDepthEnabledSubject.next(
|
this.send({
|
||||||
(<HTMLInputElement>event.target).checked
|
type: 'setSearchDepthEnabled',
|
||||||
)
|
searchDepthEnabled: (<HTMLInputElement>event.target).checked,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
container.appendChild(element);
|
this.container.appendChild(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
import { Subject } from 'rxjs';
|
import { map } from 'rxjs/operators';
|
||||||
|
import { useDepGraphService } from '../machines/dep-graph.service';
|
||||||
|
import { DepGraphSend } from '../machines/interfaces';
|
||||||
import { removeChildrenFromContainer } from '../util';
|
import { removeChildrenFromContainer } from '../util';
|
||||||
|
|
||||||
export class FocusedProjectPanel {
|
export class FocusedProjectPanel {
|
||||||
private unfocusSubject = new Subject<void>();
|
private send: DepGraphSend;
|
||||||
|
|
||||||
set projectName(projectName: string) {
|
|
||||||
this.render(projectName);
|
|
||||||
}
|
|
||||||
|
|
||||||
unfocus$ = this.unfocusSubject.asObservable();
|
|
||||||
|
|
||||||
constructor(private container: HTMLElement) {
|
constructor(private container: HTMLElement) {
|
||||||
this.render();
|
const [state$, send] = useDepGraphService();
|
||||||
|
this.send = send;
|
||||||
|
|
||||||
|
state$
|
||||||
|
.pipe(map(({ context }) => context.focusedProject))
|
||||||
|
.subscribe((focusedProject) => this.render(focusedProject));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static renderHtmlTemplate(): HTMLElement {
|
private static renderHtmlTemplate(): HTMLElement {
|
||||||
@ -39,10 +40,6 @@ export class FocusedProjectPanel {
|
|||||||
return render.content.firstChild as HTMLElement;
|
return render.content.firstChild as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
unfocusProject() {
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
private render(projectName?: string) {
|
private render(projectName?: string) {
|
||||||
removeChildrenFromContainer(this.container);
|
removeChildrenFromContainer(this.container);
|
||||||
|
|
||||||
@ -62,7 +59,7 @@ export class FocusedProjectPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unfocusButtonElement.addEventListener('click', () =>
|
unfocusButtonElement.addEventListener('click', () =>
|
||||||
this.unfocusSubject.next()
|
this.send({ type: 'unfocusProject' })
|
||||||
);
|
);
|
||||||
|
|
||||||
this.container.appendChild(element);
|
this.container.appendChild(element);
|
||||||
|
|||||||
@ -1,35 +1,24 @@
|
|||||||
import type { ProjectGraphNode } from '@nrwl/devkit';
|
import type { ProjectGraphNode } from '@nrwl/devkit';
|
||||||
import { Subject } from 'rxjs';
|
import { useDepGraphService } from '../machines/dep-graph.service';
|
||||||
|
import { DepGraphSend } from '../machines/interfaces';
|
||||||
import {
|
import {
|
||||||
parseParentDirectoriesFromPilePath,
|
parseParentDirectoriesFromPilePath,
|
||||||
removeChildrenFromContainer,
|
removeChildrenFromContainer,
|
||||||
} from '../util';
|
} from '../util';
|
||||||
|
|
||||||
export class ProjectList {
|
export class ProjectList {
|
||||||
private focusProjectSubject = new Subject<string>();
|
private projectItems: Record<string, HTMLElement> = {};
|
||||||
private checkedProjectsChangeSubject = new Subject<string[]>();
|
|
||||||
private selectedItems: Record<string, HTMLElement> = {};
|
|
||||||
checkedProjectsChange$ = this.checkedProjectsChangeSubject.asObservable();
|
|
||||||
focusProject$ = this.focusProjectSubject.asObservable();
|
|
||||||
|
|
||||||
private _projects: ProjectGraphNode[] = [];
|
private send: DepGraphSend;
|
||||||
|
|
||||||
set projects(projects: ProjectGraphNode[]) {
|
|
||||||
this._projects = projects;
|
|
||||||
|
|
||||||
const previouslyCheckedProjects = Object.values(this.selectedItems)
|
|
||||||
.filter((checkbox) => checkbox.dataset['active'] === 'true')
|
|
||||||
.map((checkbox) => checkbox.dataset['project']);
|
|
||||||
this.render();
|
|
||||||
this.selectProjects(previouslyCheckedProjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
get projects(): ProjectGraphNode[] {
|
|
||||||
return this._projects;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private container: HTMLElement) {
|
constructor(private container: HTMLElement) {
|
||||||
this.render();
|
const [state$, send] = useDepGraphService();
|
||||||
|
this.send = send;
|
||||||
|
|
||||||
|
state$.subscribe((state) => {
|
||||||
|
this.render(state.context.projects, state.context.workspaceLayout);
|
||||||
|
this.setSelectedProjects(state.context.selectedProjects);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static renderHtmlItemTemplate(): HTMLElement {
|
private static renderHtmlItemTemplate(): HTMLElement {
|
||||||
@ -60,63 +49,49 @@ export class ProjectList {
|
|||||||
return render.content.firstChild as HTMLElement;
|
return render.content.firstChild as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectProjects(projects: string[]) {
|
setSelectedProjects(selectedProjects: string[]) {
|
||||||
projects.forEach((projectName) => {
|
Object.keys(this.projectItems).forEach((projectName) => {
|
||||||
if (!!this.selectedItems[projectName]) {
|
this.projectItems[projectName].dataset['active'] = selectedProjects
|
||||||
this.selectedItems[projectName].dataset['active'] = 'true';
|
|
||||||
this.selectedItems[projectName].dispatchEvent(
|
|
||||||
new CustomEvent('change')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.emitChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCheckedProjects(selectedProjects: string[]) {
|
|
||||||
Object.keys(this.selectedItems).forEach((projectName) => {
|
|
||||||
this.selectedItems[projectName].dataset['active'] = selectedProjects
|
|
||||||
.includes(projectName)
|
.includes(projectName)
|
||||||
.toString();
|
.toString();
|
||||||
this.selectedItems[projectName].dispatchEvent(new CustomEvent('change'));
|
this.projectItems[projectName].dispatchEvent(new CustomEvent('change'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAllProjects() {
|
checkAllProjects() {
|
||||||
Object.values(this.selectedItems).forEach((item) => {
|
this.send({ type: 'selectAll' });
|
||||||
item.dataset['active'] = 'true';
|
|
||||||
item.dispatchEvent(new CustomEvent('change'));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uncheckAllProjects() {
|
uncheckAllProjects() {
|
||||||
Object.values(this.selectedItems).forEach((item) => {
|
this.send({ type: 'deselectAll' });
|
||||||
item.dataset['active'] = 'false';
|
|
||||||
item.dispatchEvent(new CustomEvent('change'));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uncheckProject(projectName: string) {
|
uncheckProject(projectName: string) {
|
||||||
this.selectedItems[projectName].dataset['active'] = 'false';
|
this.send({ type: 'deselectProject', projectName });
|
||||||
this.selectedItems[projectName].dispatchEvent(new CustomEvent('change'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitChanges() {
|
private render(
|
||||||
const changes = Object.values(this.selectedItems)
|
projects: ProjectGraphNode[],
|
||||||
.filter((item) => item.dataset['active'] === 'true')
|
workspaceLayout: { appsDir: string; libsDir: string }
|
||||||
.map((item) => item.dataset['project']);
|
) {
|
||||||
this.checkedProjectsChangeSubject.next(changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private render() {
|
|
||||||
removeChildrenFromContainer(this.container);
|
removeChildrenFromContainer(this.container);
|
||||||
|
|
||||||
const appProjects = this.getProjectsByType('app');
|
const appProjects = this.getProjectsByType('app', projects);
|
||||||
const libProjects = this.getProjectsByType('lib');
|
const libProjects = this.getProjectsByType('lib', projects);
|
||||||
const e2eProjects = this.getProjectsByType('e2e');
|
const e2eProjects = this.getProjectsByType('e2e', projects);
|
||||||
|
|
||||||
const appDirectoryGroups = this.groupProjectsByDirectory(appProjects);
|
const appDirectoryGroups = this.groupProjectsByDirectory(
|
||||||
const libDirectoryGroups = this.groupProjectsByDirectory(libProjects);
|
appProjects,
|
||||||
const e2eDirectoryGroups = this.groupProjectsByDirectory(e2eProjects);
|
workspaceLayout
|
||||||
|
);
|
||||||
|
const libDirectoryGroups = this.groupProjectsByDirectory(
|
||||||
|
libProjects,
|
||||||
|
workspaceLayout
|
||||||
|
);
|
||||||
|
const e2eDirectoryGroups = this.groupProjectsByDirectory(
|
||||||
|
e2eProjects,
|
||||||
|
workspaceLayout
|
||||||
|
);
|
||||||
|
|
||||||
const sortedAppDirectories = Object.keys(appDirectoryGroups).sort();
|
const sortedAppDirectories = Object.keys(appDirectoryGroups).sort();
|
||||||
const sortedLibDirectories = Object.keys(libDirectoryGroups).sort();
|
const sortedLibDirectories = Object.keys(libDirectoryGroups).sort();
|
||||||
@ -153,20 +128,23 @@ export class ProjectList {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProjectsByType(type) {
|
private getProjectsByType(type: string, projects: ProjectGraphNode[]) {
|
||||||
return this.projects
|
return projects
|
||||||
.filter((project) => project.type === type)
|
.filter((project) => project.type === type)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
private groupProjectsByDirectory(projects: ProjectGraphNode[]) {
|
private groupProjectsByDirectory(
|
||||||
|
projects: ProjectGraphNode[],
|
||||||
|
workspaceLayout: { appsDir: string; libsDir: string }
|
||||||
|
) {
|
||||||
let groups = {};
|
let groups = {};
|
||||||
|
|
||||||
projects.forEach((project) => {
|
projects.forEach((project) => {
|
||||||
const workspaceRoot =
|
const workspaceRoot =
|
||||||
project.type === 'app' || project.type === 'e2e'
|
project.type === 'app' || project.type === 'e2e'
|
||||||
? window.workspaceLayout.appsDir
|
? workspaceLayout.appsDir
|
||||||
: window.workspaceLayout.libsDir;
|
: workspaceLayout.libsDir;
|
||||||
const directories = parseParentDirectoriesFromPilePath(
|
const directories = parseParentDirectoriesFromPilePath(
|
||||||
project.data.root,
|
project.data.root,
|
||||||
workspaceRoot
|
workspaceRoot
|
||||||
@ -203,7 +181,7 @@ export class ProjectList {
|
|||||||
);
|
);
|
||||||
const focusButtonElement: HTMLElement = element.querySelector('button');
|
const focusButtonElement: HTMLElement = element.querySelector('button');
|
||||||
focusButtonElement.addEventListener('click', () =>
|
focusButtonElement.addEventListener('click', () =>
|
||||||
this.focusProjectSubject.next(project.name)
|
this.send({ type: 'focusProject', projectName: project.name })
|
||||||
);
|
);
|
||||||
|
|
||||||
const projectNameElement: HTMLElement = element.querySelector('label');
|
const projectNameElement: HTMLElement = element.querySelector('label');
|
||||||
@ -214,12 +192,19 @@ export class ProjectList {
|
|||||||
|
|
||||||
projectNameElement.addEventListener('click', (event) => {
|
projectNameElement.addEventListener('click', (event) => {
|
||||||
const el = event.target as HTMLElement;
|
const el = event.target as HTMLElement;
|
||||||
el.dataset['active'] =
|
if (el.dataset['active'] === 'true') {
|
||||||
el.dataset['active'] === 'false' ? 'true' : 'false';
|
this.send({
|
||||||
el.dispatchEvent(new CustomEvent('change'));
|
type: 'deselectProject',
|
||||||
|
projectName: el.dataset['project'],
|
||||||
this.emitChanges();
|
});
|
||||||
|
} else {
|
||||||
|
this.send({
|
||||||
|
type: 'selectProject',
|
||||||
|
projectName: el.dataset['project'],
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
projectNameElement.addEventListener('change', (event) => {
|
projectNameElement.addEventListener('change', (event) => {
|
||||||
const el = event.target as HTMLElement;
|
const el = event.target as HTMLElement;
|
||||||
if (el.dataset['active'] === 'false') {
|
if (el.dataset['active'] === 'false') {
|
||||||
@ -231,7 +216,7 @@ export class ProjectList {
|
|||||||
projectNameElement.dispatchEvent(new Event('click'));
|
projectNameElement.dispatchEvent(new Event('click'));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.selectedItems[project.name] = projectNameElement;
|
this.projectItems[project.name] = projectNameElement;
|
||||||
|
|
||||||
formGroup.append(element);
|
formGroup.append(element);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,52 +1,28 @@
|
|||||||
import { ProjectGraphNode } from '@nrwl/devkit';
|
|
||||||
import { BehaviorSubject, combineLatest, fromEvent, Subject } from 'rxjs';
|
|
||||||
import { DisplayOptionsPanel } from './display-options-panel';
|
import { DisplayOptionsPanel } from './display-options-panel';
|
||||||
import { FocusedProjectPanel } from './focused-project-panel';
|
import { FocusedProjectPanel } from './focused-project-panel';
|
||||||
import { ProjectList } from './project-list';
|
import { ProjectList } from './project-list';
|
||||||
import { TextFilterChangeEvent, TextFilterPanel } from './text-filter-panel';
|
import { TextFilterPanel } from './text-filter-panel';
|
||||||
|
|
||||||
declare var ResizeObserver;
|
declare var ResizeObserver;
|
||||||
|
|
||||||
export class SidebarComponent {
|
export class SidebarComponent {
|
||||||
private selectedProjectsChangedSubject = new BehaviorSubject<string[]>([]);
|
|
||||||
private groupByFolderChangedSubject = new BehaviorSubject<boolean>(
|
|
||||||
window.groupByFolder
|
|
||||||
);
|
|
||||||
|
|
||||||
private focusProjectSubject = new Subject<string>();
|
|
||||||
private filterByTextSubject = new Subject<TextFilterChangeEvent>();
|
|
||||||
|
|
||||||
selectedProjectsChanged$ = this.selectedProjectsChangedSubject.asObservable();
|
|
||||||
groupByFolderChanged$ = this.groupByFolderChangedSubject.asObservable();
|
|
||||||
|
|
||||||
private displayOptionsPanel: DisplayOptionsPanel;
|
private displayOptionsPanel: DisplayOptionsPanel;
|
||||||
private focusedProjectPanel: FocusedProjectPanel;
|
private focusedProjectPanel: FocusedProjectPanel;
|
||||||
private textFilterPanel: TextFilterPanel;
|
private textFilterPanel: TextFilterPanel;
|
||||||
private projectList: ProjectList;
|
private projectList: ProjectList;
|
||||||
|
|
||||||
private groupByFolder = window.groupByFolder;
|
constructor() {
|
||||||
private selectedProjects: string[] = [];
|
|
||||||
|
|
||||||
set projects(projects: ProjectGraphNode[]) {
|
|
||||||
this.projectList.projects = projects;
|
|
||||||
this.focusedProjectPanel.unfocusProject();
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private affectedProjects: string[]) {
|
|
||||||
const showAffected = this.affectedProjects.length > 0;
|
|
||||||
|
|
||||||
const displayOptionsPanelContainer = document.getElementById(
|
const displayOptionsPanelContainer = document.getElementById(
|
||||||
'display-options-panel'
|
'display-options-panel'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.displayOptionsPanel = new DisplayOptionsPanel(
|
this.displayOptionsPanel = new DisplayOptionsPanel(
|
||||||
showAffected,
|
displayOptionsPanelContainer
|
||||||
this.groupByFolder
|
|
||||||
);
|
);
|
||||||
this.displayOptionsPanel.render(displayOptionsPanelContainer);
|
|
||||||
|
|
||||||
const focusedProjectPanelContainer =
|
const focusedProjectPanelContainer =
|
||||||
document.getElementById('focused-project');
|
document.getElementById('focused-project');
|
||||||
|
|
||||||
this.focusedProjectPanel = new FocusedProjectPanel(
|
this.focusedProjectPanel = new FocusedProjectPanel(
|
||||||
focusedProjectPanelContainer
|
focusedProjectPanelContainer
|
||||||
);
|
);
|
||||||
@ -57,232 +33,5 @@ export class SidebarComponent {
|
|||||||
|
|
||||||
const projectListContainer = document.getElementById('project-lists');
|
const projectListContainer = document.getElementById('project-lists');
|
||||||
this.projectList = new ProjectList(projectListContainer);
|
this.projectList = new ProjectList(projectListContainer);
|
||||||
|
|
||||||
this.projectList.projects = window.projects;
|
|
||||||
|
|
||||||
if (showAffected) {
|
|
||||||
this.selectAffectedProjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.focusProject = (projectId) => {
|
|
||||||
this.focusProjectSubject.next(projectId);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.excludeProject = (projectId) => {
|
|
||||||
this.excludeProject(projectId);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.listenForDOMEvents();
|
|
||||||
|
|
||||||
if (window.focusedProject !== null) {
|
|
||||||
this.focusProject(window.focusedProject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.exclude.length > 0) {
|
|
||||||
window.exclude.forEach((project) => this.excludeProject(project));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectProjects(projects: string[]) {
|
|
||||||
this.projectList.selectProjects(projects);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetSidebarVisibility() {
|
|
||||||
const sidebarElement = document.getElementById('sidebar');
|
|
||||||
|
|
||||||
if (sidebarElement.classList.contains('hidden')) {
|
|
||||||
sidebarElement.classList.remove('hidden');
|
|
||||||
sidebarElement.style.marginLeft = `0px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listenForDOMEvents() {
|
|
||||||
this.displayOptionsPanel.selectAll$.subscribe(() => {
|
|
||||||
this.selectAllProjects();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.displayOptionsPanel.deselectAll$.subscribe(() => {
|
|
||||||
this.deselectAllProjects();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.displayOptionsPanel.selectAffected$.subscribe(() => {
|
|
||||||
this.selectAffectedProjects();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.displayOptionsPanel.groupByFolder$.subscribe((groupByFolder) => {
|
|
||||||
this.groupByFolderChangedSubject.next(groupByFolder);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.focusedProjectPanel.unfocus$.subscribe(() => {
|
|
||||||
this.unfocusProject();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.textFilterPanel.changes$.subscribe((event) => {
|
|
||||||
this.filterByTextSubject.next(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
combineLatest([
|
|
||||||
this.filterByTextSubject,
|
|
||||||
this.displayOptionsPanel.searchDepth$,
|
|
||||||
]).subscribe(([event, searchDepth]) => {
|
|
||||||
if (event.text && !!event.text.length) {
|
|
||||||
this.filterProjectsByText(event.text, event.includeInPath, searchDepth);
|
|
||||||
} else this.deselectAllProjects();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.projectList.checkedProjectsChange$.subscribe((checkedProjects) => {
|
|
||||||
this.emitSelectedProjects(checkedProjects);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.projectList.focusProject$.subscribe((projectName) => {
|
|
||||||
this.focusProjectSubject.next(projectName);
|
|
||||||
});
|
|
||||||
|
|
||||||
combineLatest([
|
|
||||||
this.focusProjectSubject,
|
|
||||||
this.displayOptionsPanel.searchDepth$,
|
|
||||||
]).subscribe(([projectName, searchDepth]) => {
|
|
||||||
if (projectName) {
|
|
||||||
this.focusProject(projectName, searchDepth);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private setFocusedProject(projectId: string = null) {
|
|
||||||
window.focusedProject = projectId;
|
|
||||||
if (projectId) {
|
|
||||||
this.focusedProjectPanel.projectName = window.graph.nodes[projectId].name;
|
|
||||||
} else {
|
|
||||||
this.focusedProjectPanel.projectName = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAffectedProjects() {
|
|
||||||
this.setFocusedProject(null);
|
|
||||||
this.projectList.setCheckedProjects(this.affectedProjects);
|
|
||||||
this.emitSelectedProjects(this.affectedProjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAllProjects() {
|
|
||||||
this.setFocusedProject(null);
|
|
||||||
this.projectList.checkAllProjects();
|
|
||||||
this.emitSelectedProjects(window.projects.map((project) => project.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectAllProjects() {
|
|
||||||
this.setFocusedProject(null);
|
|
||||||
this.projectList.uncheckAllProjects();
|
|
||||||
this.emitSelectedProjects([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
focusProject(id: string, searchDepth: number = -1) {
|
|
||||||
this.filterByTextSubject.next({ text: null, includeInPath: false });
|
|
||||||
this.setFocusedProject(id);
|
|
||||||
|
|
||||||
const selectedProjects = window.projects
|
|
||||||
.map((project) => project.name)
|
|
||||||
.filter(
|
|
||||||
(projectName) =>
|
|
||||||
this.hasPath(id, projectName, [], 1, searchDepth) ||
|
|
||||||
this.hasPath(projectName, id, [], 1, searchDepth)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.projectList.setCheckedProjects(selectedProjects);
|
|
||||||
|
|
||||||
this.emitSelectedProjects(selectedProjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
unfocusProject() {
|
|
||||||
this.focusProjectSubject.next(null);
|
|
||||||
this.setFocusedProject(null);
|
|
||||||
|
|
||||||
this.projectList.uncheckAllProjects();
|
|
||||||
|
|
||||||
this.emitSelectedProjects([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
excludeProject(id: string) {
|
|
||||||
const selectedProjects = [...this.selectedProjects];
|
|
||||||
selectedProjects.splice(this.selectedProjects.indexOf(id), 1);
|
|
||||||
|
|
||||||
this.projectList.uncheckProject(id);
|
|
||||||
this.emitSelectedProjects(selectedProjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitSelectedProjects(selectedProjects: string[]) {
|
|
||||||
this.selectedProjects = selectedProjects;
|
|
||||||
|
|
||||||
this.selectedProjectsChangedSubject.next(selectedProjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
filterProjectsByText(
|
|
||||||
text: string,
|
|
||||||
includeInPath: boolean,
|
|
||||||
searchDepth: number
|
|
||||||
) {
|
|
||||||
this.focusProjectSubject.next(null);
|
|
||||||
this.setFocusedProject(null);
|
|
||||||
this.projectList.uncheckAllProjects();
|
|
||||||
|
|
||||||
const split = text.split(',').map((splitItem) => splitItem.trim());
|
|
||||||
|
|
||||||
const selectedProjects = new Set<string>();
|
|
||||||
|
|
||||||
window.projects
|
|
||||||
.map((project) => project.name)
|
|
||||||
.forEach((project) => {
|
|
||||||
const projectMatch =
|
|
||||||
split.findIndex((splitItem) => project.includes(splitItem)) > -1;
|
|
||||||
|
|
||||||
if (projectMatch) {
|
|
||||||
selectedProjects.add(project);
|
|
||||||
|
|
||||||
if (includeInPath) {
|
|
||||||
window.projects
|
|
||||||
.map((project) => project.name)
|
|
||||||
.forEach((projectInPath) => {
|
|
||||||
if (
|
|
||||||
this.hasPath(project, projectInPath, [], 1, searchDepth) ||
|
|
||||||
this.hasPath(projectInPath, project, [], 1, searchDepth)
|
|
||||||
) {
|
|
||||||
selectedProjects.add(projectInPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedProjectsArray = Array.from(selectedProjects);
|
|
||||||
this.projectList.setCheckedProjects(selectedProjectsArray);
|
|
||||||
this.emitSelectedProjects(selectedProjectsArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasPath(
|
|
||||||
target,
|
|
||||||
node,
|
|
||||||
visited,
|
|
||||||
currentSearchDepth: number,
|
|
||||||
maxSearchDepth: number = -1 // -1 indicates unlimited search depth
|
|
||||||
) {
|
|
||||||
if (target === node) return true;
|
|
||||||
|
|
||||||
if (maxSearchDepth === -1 || currentSearchDepth <= maxSearchDepth) {
|
|
||||||
for (let d of window.graph.dependencies[node] || []) {
|
|
||||||
if (visited.indexOf(d.target) > -1) continue;
|
|
||||||
visited.push(d.target);
|
|
||||||
if (
|
|
||||||
this.hasPath(
|
|
||||||
target,
|
|
||||||
d.target,
|
|
||||||
visited,
|
|
||||||
currentSearchDepth + 1,
|
|
||||||
maxSearchDepth
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { fromEvent, Subject, Subscription } from 'rxjs';
|
import { fromEvent, Subscription } from 'rxjs';
|
||||||
import { removeChildrenFromContainer } from '../util';
|
|
||||||
import { debounceTime, filter, map } from 'rxjs/operators';
|
import { debounceTime, filter, map } from 'rxjs/operators';
|
||||||
|
import { useDepGraphService } from '../machines/dep-graph.service';
|
||||||
|
import { DepGraphSend } from '../machines/interfaces';
|
||||||
|
import { removeChildrenFromContainer } from '../util';
|
||||||
|
|
||||||
export interface TextFilterChangeEvent {
|
export interface TextFilterChangeEvent {
|
||||||
text: string;
|
text: string;
|
||||||
@ -10,13 +12,11 @@ export interface TextFilterChangeEvent {
|
|||||||
export class TextFilterPanel {
|
export class TextFilterPanel {
|
||||||
private textInput: HTMLInputElement;
|
private textInput: HTMLInputElement;
|
||||||
private includeInPathCheckbox: HTMLInputElement;
|
private includeInPathCheckbox: HTMLInputElement;
|
||||||
private changesSubject = new Subject<TextFilterChangeEvent>();
|
private send: DepGraphSend;
|
||||||
private subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
changes$ = this.changesSubject.asObservable();
|
|
||||||
|
|
||||||
constructor(private container: HTMLElement) {
|
constructor(private container: HTMLElement) {
|
||||||
this.subscriptions.map((s) => s.unsubscribe());
|
const [_, send] = useDepGraphService();
|
||||||
|
this.send = send;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ export class TextFilterPanel {
|
|||||||
<div class="mt-4 px-4">
|
<div class="mt-4 px-4">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="flex items-center h-5">
|
<div class="flex items-center h-5">
|
||||||
<input id="includeInPath" name="textFilterCheckbox" type="checkbox" value="includeInPath" class="h-4 w-4 border-gray-300 rounded" disabled>
|
<input disabled id="includeInPath" name="textFilterCheckbox" type="checkbox" value="includeInPath" class="h-4 w-4 border-gray-300 rounded">
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 text-sm">
|
<div class="ml-3 text-sm">
|
||||||
<label for="includeInPath" class="font-medium text-gray-700 cursor-pointer">Include related libraries</label>
|
<label for="includeInPath" class="font-medium text-gray-700 cursor-pointer">Include related libraries</label>
|
||||||
@ -55,13 +55,6 @@ export class TextFilterPanel {
|
|||||||
return render.content.firstChild as HTMLElement;
|
return render.content.firstChild as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitChanges() {
|
|
||||||
this.changesSubject.next({
|
|
||||||
text: this.textInput.value.toLowerCase(),
|
|
||||||
includeInPath: this.includeInPathCheckbox.checked,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private render() {
|
private render() {
|
||||||
removeChildrenFromContainer(this.container);
|
removeChildrenFromContainer(this.container);
|
||||||
|
|
||||||
@ -72,7 +65,10 @@ export class TextFilterPanel {
|
|||||||
|
|
||||||
this.textInput = element.querySelector('input[type="text"]');
|
this.textInput = element.querySelector('input[type="text"]');
|
||||||
this.textInput.addEventListener('keyup', (event) => {
|
this.textInput.addEventListener('keyup', (event) => {
|
||||||
if (event.key === 'Enter') this.emitChanges();
|
if (event.key === 'Enter') {
|
||||||
|
this.send({ type: 'filterByText', search: this.textInput.value });
|
||||||
|
}
|
||||||
|
|
||||||
if (!!this.textInput.value.length) {
|
if (!!this.textInput.value.length) {
|
||||||
resetInputElement.classList.remove('hidden');
|
resetInputElement.classList.remove('hidden');
|
||||||
this.includeInPathCheckbox.disabled = false;
|
this.includeInPathCheckbox.disabled = false;
|
||||||
@ -82,19 +78,22 @@ export class TextFilterPanel {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subscriptions.push(
|
fromEvent(this.textInput, 'keyup')
|
||||||
fromEvent(this.textInput, 'keyup')
|
.pipe(
|
||||||
.pipe(
|
filter((event: KeyboardEvent) => event.key !== 'Enter'),
|
||||||
filter((event: KeyboardEvent) => event.key !== 'Enter'),
|
debounceTime(500),
|
||||||
debounceTime(500),
|
map(() =>
|
||||||
map(() => this.emitChanges())
|
this.send({ type: 'filterByText', search: this.textInput.value })
|
||||||
)
|
)
|
||||||
.subscribe()
|
)
|
||||||
);
|
.subscribe();
|
||||||
|
|
||||||
this.includeInPathCheckbox = element.querySelector('#includeInPath');
|
this.includeInPathCheckbox = element.querySelector('#includeInPath');
|
||||||
this.includeInPathCheckbox.addEventListener('change', () =>
|
this.includeInPathCheckbox.addEventListener('change', () =>
|
||||||
this.emitChanges()
|
this.send({
|
||||||
|
type: 'setIncludeProjectsByPath',
|
||||||
|
includeProjectsByPath: this.includeInPathCheckbox.checked,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
resetInputElement.addEventListener('click', () => {
|
resetInputElement.addEventListener('click', () => {
|
||||||
@ -102,7 +101,7 @@ export class TextFilterPanel {
|
|||||||
this.includeInPathCheckbox.checked = false;
|
this.includeInPathCheckbox.checked = false;
|
||||||
this.includeInPathCheckbox.disabled = true;
|
this.includeInPathCheckbox.disabled = true;
|
||||||
resetInputElement.classList.add('hidden');
|
resetInputElement.classList.add('hidden');
|
||||||
this.emitChanges();
|
this.send([{ type: 'clearTextFilter' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.container.appendChild(element);
|
this.container.appendChild(element);
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
||||||
|
|
||||||
export function removeChildrenFromContainer(container: HTMLElement) {
|
export function removeChildrenFromContainer(container: HTMLElement) {
|
||||||
Array.from(container.children).forEach((child) =>
|
Array.from(container.children).forEach((child) =>
|
||||||
container.removeChild(child)
|
container.removeChild(child)
|
||||||
@ -27,3 +29,115 @@ export function parseParentDirectoriesFromPilePath(
|
|||||||
|
|
||||||
return split;
|
return split;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasPath(
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||||
|
target: string,
|
||||||
|
node: string,
|
||||||
|
visited: string[],
|
||||||
|
currentSearchDepth: number,
|
||||||
|
maxSearchDepth: number = -1 // -1 indicates unlimited search depth
|
||||||
|
) {
|
||||||
|
if (target === node) return true;
|
||||||
|
|
||||||
|
if (maxSearchDepth === -1 || currentSearchDepth <= maxSearchDepth) {
|
||||||
|
for (let d of dependencies[node] || []) {
|
||||||
|
if (visited.indexOf(d.target) > -1) continue;
|
||||||
|
visited.push(d.target);
|
||||||
|
if (
|
||||||
|
hasPath(
|
||||||
|
dependencies,
|
||||||
|
target,
|
||||||
|
d.target,
|
||||||
|
visited,
|
||||||
|
currentSearchDepth + 1,
|
||||||
|
maxSearchDepth
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectProjectsForFocusedProject(
|
||||||
|
projects: ProjectGraphNode[],
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||||
|
focusedProjectName: string,
|
||||||
|
searchDepth: number
|
||||||
|
) {
|
||||||
|
return projects
|
||||||
|
.map((project) => project.name)
|
||||||
|
.filter(
|
||||||
|
(projectName) =>
|
||||||
|
hasPath(
|
||||||
|
dependencies,
|
||||||
|
focusedProjectName,
|
||||||
|
projectName,
|
||||||
|
[],
|
||||||
|
1,
|
||||||
|
searchDepth
|
||||||
|
) ||
|
||||||
|
hasPath(
|
||||||
|
dependencies,
|
||||||
|
projectName,
|
||||||
|
focusedProjectName,
|
||||||
|
[],
|
||||||
|
1,
|
||||||
|
searchDepth
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterProjectsByText(
|
||||||
|
text: string,
|
||||||
|
includeInPath: boolean,
|
||||||
|
searchDepth: number,
|
||||||
|
projects: ProjectGraphNode[],
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>
|
||||||
|
) {
|
||||||
|
const split = text.split(',').map((splitItem) => splitItem.trim());
|
||||||
|
|
||||||
|
const selectedProjects = new Set<string>();
|
||||||
|
|
||||||
|
projects
|
||||||
|
.map((project) => project.name)
|
||||||
|
.forEach((project) => {
|
||||||
|
const projectMatch =
|
||||||
|
split.findIndex((splitItem) => project.includes(splitItem)) > -1;
|
||||||
|
|
||||||
|
if (projectMatch) {
|
||||||
|
selectedProjects.add(project);
|
||||||
|
|
||||||
|
if (includeInPath) {
|
||||||
|
projects
|
||||||
|
.map((project) => project.name)
|
||||||
|
.forEach((projectInPath) => {
|
||||||
|
if (
|
||||||
|
hasPath(
|
||||||
|
dependencies,
|
||||||
|
project,
|
||||||
|
projectInPath,
|
||||||
|
[],
|
||||||
|
1,
|
||||||
|
searchDepth
|
||||||
|
) ||
|
||||||
|
hasPath(
|
||||||
|
dependencies,
|
||||||
|
projectInPath,
|
||||||
|
project,
|
||||||
|
[],
|
||||||
|
1,
|
||||||
|
searchDepth
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
selectedProjects.add(projectInPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(selectedProjects);
|
||||||
|
}
|
||||||
|
|||||||
38
dep-graph/dep-graph/src/assets/environment.dev.js
Normal file
38
dep-graph/dep-graph/src/assets/environment.dev.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
window.exclude = [];
|
||||||
|
window.focusedProject = null;
|
||||||
|
window.groupByFolder = false;
|
||||||
|
window.watch = false;
|
||||||
|
window.environment = 'dev';
|
||||||
|
window.useXstateInspect = false;
|
||||||
|
|
||||||
|
window.appConfig = {
|
||||||
|
showDebugger: true,
|
||||||
|
projectGraphs: [
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultProjectGraph: 'nx',
|
||||||
|
};
|
||||||
18
dep-graph/dep-graph/src/assets/environment.watch.js
Normal file
18
dep-graph/dep-graph/src/assets/environment.watch.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
window.exclude = [];
|
||||||
|
window.focusedProject = null;
|
||||||
|
window.groupByFolder = false;
|
||||||
|
window.watch = true;
|
||||||
|
window.environment = 'watch';
|
||||||
|
window.useXstateInspect = false;
|
||||||
|
|
||||||
|
window.appConfig = {
|
||||||
|
showDebugger: false,
|
||||||
|
projectGraphs: [
|
||||||
|
{
|
||||||
|
id: 'local',
|
||||||
|
label: 'local',
|
||||||
|
url: 'projectGraph.json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultProjectGraph: 'local',
|
||||||
|
};
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { FetchProjectGraphService } from '../app/fetch-project-graph-service';
|
|
||||||
import { Environment } from '../app/models';
|
|
||||||
import { projectGraphs } from '../graphs';
|
|
||||||
|
|
||||||
export const environment: Environment = {
|
|
||||||
environment: 'dev',
|
|
||||||
appConfig: {
|
|
||||||
showDebugger: true,
|
|
||||||
projectGraphs,
|
|
||||||
defaultProjectGraph: 'nx',
|
|
||||||
projectGraphService: new FetchProjectGraphService(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { FetchProjectGraphService } from '../app/fetch-project-graph-service';
|
|
||||||
import { Environment } from '../app/models';
|
|
||||||
|
|
||||||
export const environment: Environment = {
|
|
||||||
environment: 'release',
|
|
||||||
appConfig: {
|
|
||||||
showDebugger: false,
|
|
||||||
projectGraphs: [
|
|
||||||
{
|
|
||||||
id: 'local',
|
|
||||||
label: 'local',
|
|
||||||
url: 'projectGraph.json',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultProjectGraph: 'local',
|
|
||||||
projectGraphService: new FetchProjectGraphService(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { MockProjectGraphService } from '../app/mock-project-graph-service';
|
|
||||||
import { Environment } from '../app/models';
|
|
||||||
|
|
||||||
export const environment: Environment = {
|
|
||||||
environment: 'dev-watch',
|
|
||||||
appConfig: {
|
|
||||||
showDebugger: false,
|
|
||||||
projectGraphs: [
|
|
||||||
{
|
|
||||||
id: 'local',
|
|
||||||
label: 'local',
|
|
||||||
url: 'projectGraph.json',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultProjectGraph: 'local',
|
|
||||||
projectGraphService: new MockProjectGraphService(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
20
dep-graph/dep-graph/src/globals.d.ts
vendored
20
dep-graph/dep-graph/src/globals.d.ts
vendored
@ -2,27 +2,19 @@
|
|||||||
import { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph';
|
import { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph';
|
||||||
import { ProjectGraph, ProjectGraphNode } from '@nrwl/devkit';
|
import { ProjectGraph, ProjectGraphNode } from '@nrwl/devkit';
|
||||||
import { ProjectGraphList } from './graphs';
|
import { ProjectGraphList } from './graphs';
|
||||||
|
import { AppConfig } from './app/models';
|
||||||
|
|
||||||
export declare global {
|
export declare global {
|
||||||
export interface Window {
|
export interface Window {
|
||||||
watch: boolean;
|
|
||||||
projects: ProjectGraphNode[];
|
|
||||||
graph: ProjectGraph;
|
|
||||||
filteredProjects: ProjectGraphNode[];
|
|
||||||
affected: string[];
|
|
||||||
exclude: string[];
|
exclude: string[];
|
||||||
focusedProject: string;
|
focusedProject: string;
|
||||||
groupByFolder: boolean;
|
groupByFolder: boolean;
|
||||||
focusProject: Function;
|
watch: boolean;
|
||||||
excludeProject: Function;
|
|
||||||
projectGraphList: ProjectGraphList[];
|
|
||||||
selectedProjectGraph: string;
|
|
||||||
workspaceLayout: {
|
|
||||||
libsDir: string;
|
|
||||||
appsDir: string;
|
|
||||||
};
|
|
||||||
projectGraphResponse: DepGraphClientResponse;
|
|
||||||
localMode: 'serve' | 'build';
|
localMode: 'serve' | 'build';
|
||||||
|
projectGraphResponse?: DepGraphClientResponse;
|
||||||
|
environment: 'dev' | 'watch' | 'release';
|
||||||
|
appConfig: AppConfig;
|
||||||
|
useXstateInspect: boolean = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
href="https://fonts.googleapis.com/css?family=Montserrat&display=swap"
|
href="https://fonts.googleapis.com/css?family=Montserrat&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<script id="environment" src="environment.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
|
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
|
||||||
@ -143,17 +145,4 @@
|
|||||||
<div id="graph-container"></div>
|
<div id="graph-container"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
window.watch = false;
|
|
||||||
window.projects = [];
|
|
||||||
window.graph = {};
|
|
||||||
window.affected = [];
|
|
||||||
window.focusedProject = null;
|
|
||||||
window.filteredProjects = [];
|
|
||||||
window.groupByFolder = false;
|
|
||||||
window.exclude = [];
|
|
||||||
window.workspaceLayout = null;
|
|
||||||
window.localMode = 'serve';
|
|
||||||
window.projectGraphResponse = null;
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,13 +1,29 @@
|
|||||||
import { AppComponent } from './app/app';
|
import { AppComponent } from './app/app';
|
||||||
import { LocalProjectGraphService } from './app/local-project-graph-service';
|
import { LocalProjectGraphService } from './app/local-project-graph-service';
|
||||||
import { environment } from './environments/environment';
|
import { inspect } from '@xstate/inspect';
|
||||||
|
import { ProjectGraphService } from './app/models';
|
||||||
|
import { MockProjectGraphService } from './app/mock-project-graph-service';
|
||||||
|
import { FetchProjectGraphService } from './app/fetch-project-graph-service';
|
||||||
|
|
||||||
if (environment.environment === 'dev-watch') {
|
if (window.useXstateInspect === true) {
|
||||||
window.watch = true;
|
inspect({
|
||||||
|
url: 'https://stately.ai/viz?inspect',
|
||||||
|
iframe: false, // open in new window
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (environment.environment === 'release' && window.localMode === 'build') {
|
let projectGraphService: ProjectGraphService;
|
||||||
environment.appConfig.projectGraphService = new LocalProjectGraphService();
|
|
||||||
|
if (window.environment === 'dev') {
|
||||||
|
projectGraphService = new FetchProjectGraphService();
|
||||||
|
} else if (window.environment === 'watch') {
|
||||||
|
projectGraphService = new MockProjectGraphService();
|
||||||
|
} else if (window.environment === 'release') {
|
||||||
|
if (window.localMode === 'build') {
|
||||||
|
projectGraphService = new LocalProjectGraphService();
|
||||||
|
} else {
|
||||||
|
projectGraphService = new FetchProjectGraphService();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => new AppComponent(environment.appConfig));
|
setTimeout(() => new AppComponent(window.appConfig, projectGraphService));
|
||||||
|
|||||||
@ -116,6 +116,8 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "~4.33.0",
|
"@typescript-eslint/eslint-plugin": "~4.33.0",
|
||||||
"@typescript-eslint/experimental-utils": "~4.33.0",
|
"@typescript-eslint/experimental-utils": "~4.33.0",
|
||||||
"@typescript-eslint/parser": "~4.33.0",
|
"@typescript-eslint/parser": "~4.33.0",
|
||||||
|
"@xstate/immer": "^0.2.0",
|
||||||
|
"@xstate/inspect": "^0.5.1",
|
||||||
"angular": "1.8.0",
|
"angular": "1.8.0",
|
||||||
"autoprefixer": "^10.2.5",
|
"autoprefixer": "^10.2.5",
|
||||||
"babel-jest": "27.2.3",
|
"babel-jest": "27.2.3",
|
||||||
@ -136,8 +138,8 @@
|
|||||||
"cz-customizable": "^6.2.0",
|
"cz-customizable": "^6.2.0",
|
||||||
"depcheck": "^1.3.1",
|
"depcheck": "^1.3.1",
|
||||||
"dotenv": "~10.0.0",
|
"dotenv": "~10.0.0",
|
||||||
"enhanced-resolve": "^5.8.3",
|
|
||||||
"ejs": "^3.1.5",
|
"ejs": "^3.1.5",
|
||||||
|
"enhanced-resolve": "^5.8.3",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-config-next": "^12.0.0",
|
"eslint-config-next": "^12.0.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
@ -158,6 +160,7 @@
|
|||||||
"husky": "^6.0.0",
|
"husky": "^6.0.0",
|
||||||
"identity-obj-proxy": "3.0.0",
|
"identity-obj-proxy": "3.0.0",
|
||||||
"ignore": "^5.0.4",
|
"ignore": "^5.0.4",
|
||||||
|
"immer": "^9.0.6",
|
||||||
"import-fresh": "^3.1.0",
|
"import-fresh": "^3.1.0",
|
||||||
"injection-js": "^2.4.0",
|
"injection-js": "^2.4.0",
|
||||||
"is-ci": "^3.0.0",
|
"is-ci": "^3.0.0",
|
||||||
@ -254,6 +257,7 @@
|
|||||||
"webpack-sources": "^3.0.2",
|
"webpack-sources": "^3.0.2",
|
||||||
"webpack-subresource-integrity": "^1.5.2",
|
"webpack-subresource-integrity": "^1.5.2",
|
||||||
"worker-plugin": "3.2.0",
|
"worker-plugin": "3.2.0",
|
||||||
|
"xstate": "^4.25.0",
|
||||||
"yargs": "15.4.1",
|
"yargs": "15.4.1",
|
||||||
"yargs-parser": "20.0.0",
|
"yargs-parser": "20.0.0",
|
||||||
"zone.js": "~0.11.4"
|
"zone.js": "~0.11.4"
|
||||||
|
|||||||
@ -20,10 +20,6 @@ import {
|
|||||||
ProjectGraphNode,
|
ProjectGraphNode,
|
||||||
pruneExternalNodes,
|
pruneExternalNodes,
|
||||||
} from '../core/project-graph';
|
} from '../core/project-graph';
|
||||||
import {
|
|
||||||
cacheDirectory,
|
|
||||||
readCacheDirectoryProperty,
|
|
||||||
} from '../utilities/cache-directory';
|
|
||||||
import { writeJsonFile } from '../utilities/fileutils';
|
import { writeJsonFile } from '../utilities/fileutils';
|
||||||
import { output } from '../utilities/output';
|
import { output } from '../utilities/output';
|
||||||
|
|
||||||
@ -32,9 +28,6 @@ export interface DepGraphClientResponse {
|
|||||||
projects: ProjectGraphNode[];
|
projects: ProjectGraphNode[];
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
layout: { appsDir: string; libsDir: string };
|
layout: { appsDir: string; libsDir: string };
|
||||||
changes: {
|
|
||||||
added: string[];
|
|
||||||
};
|
|
||||||
affected: string[];
|
affected: string[];
|
||||||
focus: string;
|
focus: string;
|
||||||
groupByFolder: boolean;
|
groupByFolder: boolean;
|
||||||
@ -59,74 +52,46 @@ const mimeType = {
|
|||||||
'.ttf': 'aplication/font-sfnt',
|
'.ttf': 'aplication/font-sfnt',
|
||||||
};
|
};
|
||||||
|
|
||||||
const nxDepsDir = cacheDirectory(
|
function buildEnvironmentJs(
|
||||||
appRootPath,
|
exclude: string[],
|
||||||
readCacheDirectoryProperty(appRootPath)
|
|
||||||
);
|
|
||||||
|
|
||||||
async function projectsToHtml(
|
|
||||||
projects: ProjectGraphNode[],
|
|
||||||
graph: ProjectGraph,
|
|
||||||
affected: string[],
|
|
||||||
focus: string,
|
focus: string,
|
||||||
groupByFolder: boolean,
|
groupByFolder: boolean,
|
||||||
exclude: string[],
|
watchMode: boolean,
|
||||||
layout: { appsDir: string; libsDir: string },
|
localMode: 'build' | 'serve',
|
||||||
localMode: 'serve' | 'build',
|
depGraphClientResponse?: DepGraphClientResponse
|
||||||
watchMode: boolean = false
|
|
||||||
) {
|
) {
|
||||||
let f = readFileSync(
|
let environmentJs = `window.exclude = ${JSON.stringify(exclude)};
|
||||||
join(__dirname, '../core/dep-graph/index.html'),
|
window.groupByFolder = ${!!groupByFolder};
|
||||||
'utf-8'
|
window.watch = ${!!watchMode};
|
||||||
);
|
window.environment = 'release';
|
||||||
|
window.localMode = '${localMode}';
|
||||||
|
|
||||||
f = f
|
window.appConfig = {
|
||||||
.replace(
|
showDebugger: false,
|
||||||
`window.projects = []`,
|
projectGraphs: [
|
||||||
`window.projects = ${JSON.stringify(projects)}`
|
{
|
||||||
)
|
id: 'local',
|
||||||
.replace(`window.graph = {}`, `window.graph = ${JSON.stringify(graph)}`)
|
label: 'local',
|
||||||
.replace(
|
url: 'projectGraph.json',
|
||||||
`window.affected = []`,
|
}
|
||||||
`window.affected = ${JSON.stringify(affected)}`
|
],
|
||||||
)
|
defaultProjectGraph: 'local',
|
||||||
.replace(
|
};
|
||||||
`window.groupByFolder = false`,
|
`;
|
||||||
`window.groupByFolder = ${!!groupByFolder}`
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
`window.exclude = []`,
|
|
||||||
`window.exclude = ${JSON.stringify(exclude)}`
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
`window.workspaceLayout = null`,
|
|
||||||
`window.workspaceLayout = ${JSON.stringify(layout)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (focus) {
|
|
||||||
f = f.replace(
|
|
||||||
`window.focusedProject = null`,
|
|
||||||
`window.focusedProject = '${focus}'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (watchMode) {
|
|
||||||
f = f.replace(`window.watch = false`, `window.watch = true`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localMode === 'build') {
|
if (localMode === 'build') {
|
||||||
currentDepGraphClientResponse = await createDepGraphClientResponse();
|
environmentJs += `window.projectGraphResponse = ${JSON.stringify(
|
||||||
f = f.replace(
|
depGraphClientResponse
|
||||||
`window.projectGraphResponse = null`,
|
)};`;
|
||||||
`window.projectGraphResponse = ${JSON.stringify(
|
} else {
|
||||||
currentDepGraphClientResponse
|
environmentJs += `window.projectGraphResponse = null;`;
|
||||||
)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
f = f.replace(`window.localMode = 'serve'`, `window.localMode = 'build'`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return f;
|
if (!!focus) {
|
||||||
|
environmentJs += `window.focusedProject = '${focus}';`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return environmentJs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectExists(projects: ProjectGraphNode[], projectToFind: string) {
|
function projectExists(projects: ProjectGraphNode[], projectToFind: string) {
|
||||||
@ -242,23 +207,12 @@ export async function generateGraph(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let html: string;
|
let html = readFileSync(
|
||||||
|
join(__dirname, '../core/dep-graph/index.html'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
if (!args.file || extname(args.file) === '.html') {
|
graph = filterGraph(graph, args.focus || null, args.exclude || []);
|
||||||
html = await projectsToHtml(
|
|
||||||
projects,
|
|
||||||
graph,
|
|
||||||
affectedProjects,
|
|
||||||
args.focus || null,
|
|
||||||
args.groupByFolder || false,
|
|
||||||
args.exclude || [],
|
|
||||||
layout,
|
|
||||||
!!args.file && args.file.endsWith('html') ? 'build' : 'serve',
|
|
||||||
args.watch
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
graph = filterGraph(graph, args.focus || null, args.exclude || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.file) {
|
if (args.file) {
|
||||||
const workspaceFolder = appRootPath;
|
const workspaceFolder = appRootPath;
|
||||||
@ -281,14 +235,23 @@ export async function generateGraph(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
currentDepGraphClientResponse = await createDepGraphClientResponse();
|
const depGraphClientResponse = await createDepGraphClientResponse();
|
||||||
|
|
||||||
|
const environmentJs = buildEnvironmentJs(
|
||||||
|
args.exclude || [],
|
||||||
|
args.focus || null,
|
||||||
|
args.groupByFolder || false,
|
||||||
|
args.watch,
|
||||||
|
!!args.file && args.file.endsWith('html') ? 'build' : 'serve',
|
||||||
|
depGraphClientResponse
|
||||||
|
);
|
||||||
html = html.replace(/src="/g, 'src="static/');
|
html = html.replace(/src="/g, 'src="static/');
|
||||||
html = html.replace(/href="styles/g, 'href="static/styles');
|
html = html.replace(/href="styles/g, 'href="static/styles');
|
||||||
html = html.replace('<base href="/" />', '');
|
html = html.replace('<base href="/" />', '');
|
||||||
html = html.replace(/type="module"/g, '');
|
html = html.replace(/type="module"/g, '');
|
||||||
|
|
||||||
writeFileSync(fullFilePath, html);
|
writeFileSync(fullFilePath, html);
|
||||||
|
writeFileSync(join(assetsFolder, 'environment.js'), environmentJs);
|
||||||
|
|
||||||
output.success({
|
output.success({
|
||||||
title: `HTML output created in ${fileFolderPath}`,
|
title: `HTML output created in ${fileFolderPath}`,
|
||||||
@ -315,8 +278,17 @@ export async function generateGraph(
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const environmentJs = buildEnvironmentJs(
|
||||||
|
args.exclude || [],
|
||||||
|
args.focus || null,
|
||||||
|
args.groupByFolder || false,
|
||||||
|
args.watch,
|
||||||
|
!!args.file && args.file.endsWith('html') ? 'build' : 'serve'
|
||||||
|
);
|
||||||
|
|
||||||
await startServer(
|
await startServer(
|
||||||
html,
|
html,
|
||||||
|
environmentJs,
|
||||||
args.host || '127.0.0.1',
|
args.host || '127.0.0.1',
|
||||||
args.port || 4211,
|
args.port || 4211,
|
||||||
args.watch,
|
args.watch,
|
||||||
@ -331,6 +303,7 @@ export async function generateGraph(
|
|||||||
|
|
||||||
async function startServer(
|
async function startServer(
|
||||||
html: string,
|
html: string,
|
||||||
|
environmentJs: string,
|
||||||
host: string,
|
host: string,
|
||||||
port = 4211,
|
port = 4211,
|
||||||
watchForchanges = false,
|
watchForchanges = false,
|
||||||
@ -372,6 +345,12 @@ async function startServer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sanitizePath === 'environment.js') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||||
|
res.end(environmentJs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let pathname = join(__dirname, '../core/dep-graph/', sanitizePath);
|
let pathname = join(__dirname, '../core/dep-graph/', sanitizePath);
|
||||||
|
|
||||||
if (!existsSync(pathname)) {
|
if (!existsSync(pathname)) {
|
||||||
@ -383,7 +362,6 @@ async function startServer(
|
|||||||
|
|
||||||
// if is a directory, then look for index.html
|
// if is a directory, then look for index.html
|
||||||
if (statSync(pathname).isDirectory()) {
|
if (statSync(pathname).isDirectory()) {
|
||||||
// pathname += '/index.html';
|
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
res.end(html);
|
res.end(html);
|
||||||
return;
|
return;
|
||||||
@ -420,9 +398,6 @@ let currentDepGraphClientResponse: DepGraphClientResponse = {
|
|||||||
appsDir: '',
|
appsDir: '',
|
||||||
libsDir: '',
|
libsDir: '',
|
||||||
},
|
},
|
||||||
changes: {
|
|
||||||
added: [],
|
|
||||||
},
|
|
||||||
affected: [],
|
affected: [],
|
||||||
focus: null,
|
focus: null,
|
||||||
groupByFolder: false,
|
groupByFolder: false,
|
||||||
@ -522,21 +497,6 @@ async function createDepGraphClientResponse(): Promise<DepGraphClientResponse> {
|
|||||||
|
|
||||||
const hash = hasher.digest('hex');
|
const hash = hasher.digest('hex');
|
||||||
|
|
||||||
let added = [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentDepGraphClientResponse.hash !== null &&
|
|
||||||
hash !== currentDepGraphClientResponse.hash
|
|
||||||
) {
|
|
||||||
added = projects
|
|
||||||
.filter((project) => {
|
|
||||||
const result = currentDepGraphClientResponse.projects.find(
|
|
||||||
(previousProject) => previousProject.name === project.name
|
|
||||||
);
|
|
||||||
return !result;
|
|
||||||
})
|
|
||||||
.map((project) => project.name);
|
|
||||||
}
|
|
||||||
performance.mark('dep graph response generation:end');
|
performance.mark('dep graph response generation:end');
|
||||||
|
|
||||||
performance.measure(
|
performance.measure(
|
||||||
@ -557,8 +517,5 @@ async function createDepGraphClientResponse(): Promise<DepGraphClientResponse> {
|
|||||||
layout,
|
layout,
|
||||||
projects,
|
projects,
|
||||||
dependencies,
|
dependencies,
|
||||||
changes: {
|
|
||||||
added: [...currentDepGraphClientResponse.changes.added, ...added],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
12
scripts/copy-dep-graph-environment.ts
Normal file
12
scripts/copy-dep-graph-environment.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { copyFileSync } from 'fs';
|
||||||
|
import { argv } from 'yargs';
|
||||||
|
|
||||||
|
type Mode = 'dev' | 'watch';
|
||||||
|
const mode = argv._[0];
|
||||||
|
|
||||||
|
console.log(`Setting up dep-graph for ${mode}`);
|
||||||
|
|
||||||
|
copyFileSync(
|
||||||
|
`dep-graph/dep-graph/src/assets/environment.${mode}.js`,
|
||||||
|
`dep-graph/dep-graph/src/assets/environment.js`
|
||||||
|
);
|
||||||
21
yarn.lock
21
yarn.lock
@ -5392,6 +5392,18 @@
|
|||||||
"@webassemblyjs/wast-parser" "1.9.0"
|
"@webassemblyjs/wast-parser" "1.9.0"
|
||||||
"@xtuc/long" "4.2.2"
|
"@xtuc/long" "4.2.2"
|
||||||
|
|
||||||
|
"@xstate/immer@^0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@xstate/immer/-/immer-0.2.0.tgz#4f128947c3cbb3e68357b886485a36852d4e06b3"
|
||||||
|
integrity sha512-ZKwAwS84kfmN108lEtVHw8jztKDiFeaQsTxkOlOghpK1Lr7+13G8HhZZXyN1/pVkplloUUOPMH5EXVtitZDr8w==
|
||||||
|
|
||||||
|
"@xstate/inspect@^0.5.1":
|
||||||
|
version "0.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@xstate/inspect/-/inspect-0.5.1.tgz#12dbed78e6123e407458fde322273e7a64650f1e"
|
||||||
|
integrity sha512-m1zKFzyV/skUfpdiO/52w9o5EUporqIYEevryjcPpUEiIjVXKgti3EXl8TfXxggeNmsa2H9P0M0wZ5alM8M3Ng==
|
||||||
|
dependencies:
|
||||||
|
fast-safe-stringify "^2.0.7"
|
||||||
|
|
||||||
"@xtuc/ieee754@^1.2.0":
|
"@xtuc/ieee754@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||||
@ -10588,7 +10600,7 @@ fast-redact@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.2.tgz#c940ba7162dde3aeeefc522926ae8c5231412904"
|
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.0.2.tgz#c940ba7162dde3aeeefc522926ae8c5231412904"
|
||||||
integrity sha512-YN+CYfCVRVMUZOUPeinHNKgytM1wPI/C/UCLEi56EsY2dwwvI00kIJHJoI7pMVqGoMew8SMZ2SSfHKHULHXDsg==
|
integrity sha512-YN+CYfCVRVMUZOUPeinHNKgytM1wPI/C/UCLEi56EsY2dwwvI00kIJHJoI7pMVqGoMew8SMZ2SSfHKHULHXDsg==
|
||||||
|
|
||||||
fast-safe-stringify@2.1.1, fast-safe-stringify@^2.0.8:
|
fast-safe-stringify@2.1.1, fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||||
@ -12354,7 +12366,7 @@ immer@8.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
|
||||||
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
|
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
|
||||||
|
|
||||||
immer@^9.0.1:
|
immer@^9.0.1, immer@^9.0.6:
|
||||||
version "9.0.6"
|
version "9.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
||||||
integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
|
integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
|
||||||
@ -22960,6 +22972,11 @@ xmlhttprequest-ssl@~1.5.4, xmlhttprequest-ssl@~1.6.2:
|
|||||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
|
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
|
||||||
integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
|
integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
|
||||||
|
|
||||||
|
xstate@^4.25.0:
|
||||||
|
version "4.25.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.25.0.tgz#d902ef33137532043f7a88597af8e5e1c7ad6bdf"
|
||||||
|
integrity sha512-qP7lc/ypOuuWME4ArOBnzaCa90TfHkjiqYDmxpiCjPy6FcXstInA2vH6qRVAHbPXRK4KQIYfIEOk1X38P+TldQ==
|
||||||
|
|
||||||
xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
|
xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user