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
|
||||
.tool-versions
|
||||
/.verdaccio/build/local-registry
|
||||
dep-graph/dep-graph/src/assets/environment.js
|
||||
@ -8,7 +8,8 @@
|
||||
"options": {
|
||||
"cypressConfig": "dep-graph/dep-graph-e2e/cypress.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": {
|
||||
@ -16,7 +17,8 @@
|
||||
"options": {
|
||||
"cypressConfig": "dep-graph/dep-graph-e2e/cypress-watch-mode.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": {
|
||||
|
||||
@ -12,7 +12,12 @@ import {
|
||||
|
||||
describe('dep-graph-client', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('/assets/graphs/*').as('getGraph');
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
// wait for first graph to finish loading
|
||||
cy.wait('@getGraph');
|
||||
});
|
||||
|
||||
it('should display message to select projects', () => {
|
||||
@ -51,12 +56,12 @@ describe('dep-graph-client', () => {
|
||||
|
||||
describe('selecting projects', () => {
|
||||
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"]')
|
||||
.click({
|
||||
force: true,
|
||||
})
|
||||
.should('have.data', 'active', true);
|
||||
cy.get('[data-project="nx-dev"]').should('have.data', 'active', false);
|
||||
cy.get('[data-project="nx-dev"]').click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get('[data-project="nx-dev"]').should('have.data', 'active', true);
|
||||
});
|
||||
|
||||
it('should deselect a project by clicking on the project name again', () => {
|
||||
|
||||
@ -32,39 +32,16 @@
|
||||
},
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "dep-graph/dep-graph/src/environments/environment.ts",
|
||||
"with": "dep-graph/dep-graph/src/environments/environment.dev.ts"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [],
|
||||
"assets": [
|
||||
"dep-graph/dep-graph/src/favicon.ico",
|
||||
"dep-graph/dep-graph/src/assets"
|
||||
],
|
||||
"optimization": false,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": true,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": false,
|
||||
"vendorChunk": true,
|
||||
"budgets": [
|
||||
"dep-graph/dep-graph/src/assets/graphs/",
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"watch": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "dep-graph/dep-graph/src/environments/environment.ts",
|
||||
"with": "dep-graph/dep-graph/src/environments/environment.watch.ts"
|
||||
"input": "dep-graph/dep-graph/src/assets",
|
||||
"output": "/",
|
||||
"glob": "environment.js"
|
||||
}
|
||||
],
|
||||
"assets": [],
|
||||
"optimization": false,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": true,
|
||||
@ -83,15 +60,10 @@
|
||||
},
|
||||
"outputs": ["{options.outputPath}"]
|
||||
},
|
||||
"serve": {
|
||||
"serve-base": {
|
||||
"executor": "@nrwl/web:dev-server",
|
||||
"options": {
|
||||
"buildTarget": "dep-graph-dep-graph:build-base:dev"
|
||||
},
|
||||
"configurations": {
|
||||
"watch": {
|
||||
"buildTarget": "dep-graph-dep-graph:build-base:watch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
@ -112,6 +84,44 @@
|
||||
"jestConfig": "dep-graph/dep-graph/jest.config.js",
|
||||
"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"]
|
||||
|
||||
@ -1,31 +1,38 @@
|
||||
// nx-ignore-next-line
|
||||
import type { DepGraphClientResponse } from '@nrwl/workspace/src/command-line/dep-graph';
|
||||
import { ProjectGraph } from '@nrwl/devkit';
|
||||
import { combineLatest, fromEvent, Subject } from 'rxjs';
|
||||
import { startWith, takeUntil } from 'rxjs/operators';
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { startWith } from 'rxjs/operators';
|
||||
import { DebuggerPanel } from './debugger-panel';
|
||||
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 { SidebarComponent } from './ui-sidebar/sidebar';
|
||||
|
||||
export class AppComponent {
|
||||
private sidebar: SidebarComponent;
|
||||
private sidebar = new SidebarComponent();
|
||||
private tooltipService = new GraphTooltipService();
|
||||
private graph = new GraphComponent(this.tooltipService);
|
||||
private debuggerPanel: DebuggerPanel;
|
||||
|
||||
private windowResize$ = fromEvent(window, 'resize').pipe(startWith({}));
|
||||
private render$ = new Subject<{ newProjects: string[] }>();
|
||||
|
||||
constructor(private config: AppConfig = DEFAULT_CONFIG) {
|
||||
this.render$.subscribe((nextRenderConfig) => this.render(nextRenderConfig));
|
||||
private send: DepGraphSend;
|
||||
|
||||
constructor(
|
||||
private config: AppConfig = DEFAULT_CONFIG,
|
||||
private projectGraphService: ProjectGraphService
|
||||
) {
|
||||
const [_, send] = useDepGraphService();
|
||||
this.send = send;
|
||||
|
||||
this.loadProjectGraph(config.defaultProjectGraph);
|
||||
this.render();
|
||||
|
||||
if (window.watch === true) {
|
||||
setInterval(
|
||||
() => this.loadProjectGraph(config.defaultProjectGraph),
|
||||
() => this.updateProjectGraph(config.defaultProjectGraph),
|
||||
5000
|
||||
);
|
||||
}
|
||||
@ -37,39 +44,49 @@ export class AppComponent {
|
||||
);
|
||||
|
||||
const project: DepGraphClientResponse =
|
||||
await this.config.projectGraphService.getProjectGraph(projectInfo.url);
|
||||
await this.projectGraphService.getProjectGraph(projectInfo.url);
|
||||
|
||||
const workspaceLayout = project?.layout;
|
||||
|
||||
const nodes = Object.values(project.projects).reduce((acc, cur: any) => {
|
||||
acc[cur.name] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const newProjects = !!window.graph
|
||||
? project.changes.added.filter(
|
||||
(addedProject) => !window.graph.nodes[addedProject]
|
||||
)
|
||||
: project.changes.added;
|
||||
|
||||
window.projects = project.projects;
|
||||
window.graph = <ProjectGraph>{
|
||||
this.send({
|
||||
type: 'initGraph',
|
||||
projects: project.projects,
|
||||
dependencies: project.dependencies,
|
||||
nodes: nodes,
|
||||
};
|
||||
window.focusedProject = null;
|
||||
window.projectGraphList = this.config.projectGraphs;
|
||||
window.selectedProjectGraph = projectGraphId;
|
||||
window.workspaceLayout = workspaceLayout;
|
||||
affectedProjects: project.affected,
|
||||
workspaceLayout: workspaceLayout,
|
||||
});
|
||||
|
||||
if (this.sidebar) {
|
||||
this.render$.next({ newProjects });
|
||||
} else {
|
||||
this.render$.next();
|
||||
if (!!window.focusedProject) {
|
||||
this.send({
|
||||
type: 'focusProject',
|
||||
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');
|
||||
|
||||
if (this.config.showDebugger) {
|
||||
@ -78,59 +95,17 @@ export class AppComponent {
|
||||
|
||||
this.debuggerPanel = new DebuggerPanel(
|
||||
debuggerPanelContainer,
|
||||
window.projectGraphList
|
||||
this.config.projectGraphs,
|
||||
this.config.defaultProjectGraph
|
||||
);
|
||||
|
||||
this.debuggerPanel.selectProject$.subscribe((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$
|
||||
.pipe(takeUntil(this.render$))
|
||||
.subscribe(
|
||||
(renderTime) => (this.debuggerPanel.renderTime = renderTime)
|
||||
);
|
||||
this.graph.renderTimes$.subscribe(
|
||||
(renderTime) => (this.debuggerPanel.renderTime = renderTime)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,8 @@ export class DebuggerPanel {
|
||||
|
||||
constructor(
|
||||
private container: HTMLElement,
|
||||
private projectGraphs: ProjectGraphList[]
|
||||
private projectGraphs: ProjectGraphList[],
|
||||
private initialSelectedGraph: string
|
||||
) {
|
||||
this.render();
|
||||
}
|
||||
@ -40,7 +41,7 @@ export class DebuggerPanel {
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
select.value = window.selectedProjectGraph;
|
||||
select.value = this.initialSelectedGraph;
|
||||
select.dataset['cy'] = 'project-select';
|
||||
|
||||
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 cytoscapeDagre from 'cytoscape-dagre';
|
||||
import popper from 'cytoscape-popper';
|
||||
import { Subject } from 'rxjs';
|
||||
import type { Instance } from 'tippy.js';
|
||||
import { useDepGraphService } from './machines/dep-graph.service';
|
||||
import { ProjectNodeToolTip } from './project-node-tooltip';
|
||||
import { edgeStyles, nodeStyles } from './styles-graph';
|
||||
import { GraphTooltipService } from './tooltip-service';
|
||||
@ -13,7 +19,6 @@ import {
|
||||
ProjectEdge,
|
||||
ProjectNode,
|
||||
} from './util-cytoscape';
|
||||
import type { VirtualElement } from '@popperjs/core';
|
||||
|
||||
export interface GraphPerfReport {
|
||||
renderTime: number;
|
||||
@ -24,22 +29,57 @@ export class GraphComponent {
|
||||
private graph: cy.Core;
|
||||
private openTooltip: Instance = null;
|
||||
|
||||
affectedProjects: string[];
|
||||
projectGraph: ProjectGraph;
|
||||
|
||||
private renderTimesSubject = new Subject<GraphPerfReport>();
|
||||
renderTimes$ = this.renderTimesSubject.asObservable();
|
||||
|
||||
private send;
|
||||
constructor(private tooltipService: GraphTooltipService) {
|
||||
cy.use(cytoscapeDagre);
|
||||
cy.use(popper);
|
||||
|
||||
const [state$, send] = useDepGraphService();
|
||||
this.send = send;
|
||||
|
||||
state$.subscribe((state) => {
|
||||
const projects = state.context.selectedProjects.map((projectName) =>
|
||||
state.context.projects.find((project) => project.name === projectName)
|
||||
);
|
||||
this.render(
|
||||
projects,
|
||||
state.context.groupByFolder,
|
||||
state.context.workspaceLayout,
|
||||
state.context.focusedProject,
|
||||
state.context.affectedProjects,
|
||||
state.context.dependencies
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render(selectedProjects: ProjectGraphNode[], groupByFolder: boolean) {
|
||||
render(
|
||||
selectedProjects: ProjectGraphNode[],
|
||||
groupByFolder: boolean,
|
||||
workspaceLayout,
|
||||
focusedProject: string,
|
||||
affectedProjects: string[],
|
||||
dependencies: Record<string, ProjectGraphDependency[]>
|
||||
) {
|
||||
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.generateCytoscapeLayout(selectedProjects, groupByFolder);
|
||||
this.generateCytoscapeLayout(
|
||||
selectedProjects,
|
||||
groupByFolder,
|
||||
workspaceLayout,
|
||||
focusedProject,
|
||||
affectedProjects,
|
||||
dependencies
|
||||
);
|
||||
this.listenForProjectNodeClicks();
|
||||
this.listenForProjectNodeHovers();
|
||||
|
||||
@ -56,9 +96,20 @@ export class GraphComponent {
|
||||
|
||||
private generateCytoscapeLayout(
|
||||
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({
|
||||
container: document.getElementById('graph-container'),
|
||||
@ -84,7 +135,14 @@ export class GraphComponent {
|
||||
|
||||
private createElements(
|
||||
selectedProjects: ProjectGraphNode[],
|
||||
groupByFolder: boolean
|
||||
groupByFolder: boolean,
|
||||
workspaceLayout: {
|
||||
appsDir: string;
|
||||
libsDir: string;
|
||||
},
|
||||
focusedProject: string,
|
||||
affectedProjects: string[],
|
||||
dependencies: Record<string, ProjectGraphDependency[]>
|
||||
) {
|
||||
let elements: cy.ElementDefinition[] = [];
|
||||
const filteredProjectNames = selectedProjects.map(
|
||||
@ -101,21 +159,21 @@ export class GraphComponent {
|
||||
selectedProjects.forEach((project) => {
|
||||
const workspaceRoot =
|
||||
project.type === 'app' || project.type === 'e2e'
|
||||
? window.workspaceLayout.appsDir
|
||||
: window.workspaceLayout.libsDir;
|
||||
? workspaceLayout.appsDir
|
||||
: workspaceLayout.libsDir;
|
||||
|
||||
const projectNode = new ProjectNode(project, workspaceRoot);
|
||||
projectNode.focused = project.name === window.focusedProject;
|
||||
projectNode.affected = this.affectedProjects.includes(project.name);
|
||||
projectNode.focused = project.name === focusedProject;
|
||||
projectNode.affected = affectedProjects.includes(project.name);
|
||||
|
||||
projectNodes.push(projectNode);
|
||||
|
||||
this.projectGraph.dependencies[project.name].forEach((dep) => {
|
||||
dependencies[project.name].forEach((dep) => {
|
||||
if (filteredProjectNames.includes(dep.target)) {
|
||||
const edge = new ProjectEdge(dep);
|
||||
edge.affected =
|
||||
this.affectedProjects.includes(dep.source) &&
|
||||
this.affectedProjects.includes(dep.target);
|
||||
affectedProjects.includes(dep.source) &&
|
||||
affectedProjects.includes(dep.target);
|
||||
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': [],
|
||||
},
|
||||
changes: {
|
||||
added: [],
|
||||
},
|
||||
affected: [],
|
||||
focus: null,
|
||||
exclude: [],
|
||||
@ -88,8 +85,14 @@ export class MockProjectGraphService implements ProjectGraphService {
|
||||
type: 'static',
|
||||
},
|
||||
];
|
||||
this.response.projects.push(newProject);
|
||||
this.response.dependencies[newProject.name] = newDependency;
|
||||
this.response.changes.added.push(newProject.name);
|
||||
|
||||
this.response = {
|
||||
...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>;
|
||||
}
|
||||
export interface Environment {
|
||||
environment: 'dev' | 'dev-watch' | 'release';
|
||||
appConfig: AppConfig;
|
||||
environment: 'dev' | 'watch' | 'release';
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
showDebugger: boolean;
|
||||
projectGraphs: ProjectGraphList[];
|
||||
defaultProjectGraph: string;
|
||||
projectGraphService: ProjectGraphService;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: AppConfig = {
|
||||
showDebugger: false,
|
||||
projectGraphs: [],
|
||||
defaultProjectGraph: null,
|
||||
projectGraphService: null,
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import * as cy from 'cytoscape';
|
||||
import { useDepGraphService } from './machines/dep-graph.service';
|
||||
|
||||
export class ProjectNodeToolTip {
|
||||
constructor(private node: cy.NodeSingular) {}
|
||||
@ -53,13 +54,15 @@ export class ProjectNodeToolTip {
|
||||
|
||||
wrapper.classList.add('flex');
|
||||
|
||||
const [_, send] = useDepGraphService();
|
||||
|
||||
focusButton.addEventListener('click', () =>
|
||||
window.focusProject(this.node.attr('id'))
|
||||
send({ type: 'focusProject', projectName: this.node.attr('id') })
|
||||
);
|
||||
focusButton.innerText = 'Focus';
|
||||
|
||||
excludeButton.addEventListener('click', () => {
|
||||
window.excludeProject(this.node.attr('id'));
|
||||
send({ type: 'deselectProject', projectName: this.node.attr('id') });
|
||||
});
|
||||
|
||||
excludeButton.innerText = 'Exclude';
|
||||
|
||||
@ -1,51 +1,34 @@
|
||||
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, map, withLatestFrom } from 'rxjs/operators';
|
||||
import { useDepGraphService } from '../machines/dep-graph.service';
|
||||
import { DepGraphSend } from '../machines/interfaces';
|
||||
import { removeChildrenFromContainer } from '../util';
|
||||
|
||||
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;
|
||||
affectedButtonElement: HTMLElement;
|
||||
groupByFolderCheckboxElement: HTMLInputElement;
|
||||
|
||||
constructor(showAffected = false, groupByFolder = false) {
|
||||
this.showAffected = showAffected;
|
||||
this.groupByFolder = groupByFolder;
|
||||
send: DepGraphSend;
|
||||
|
||||
this.searchDepthChangesSubject
|
||||
.pipe(withLatestFrom(this.searchByDepthSubject))
|
||||
.subscribe(([action, current]) => {
|
||||
if (action === 'decrement' && current > 1) {
|
||||
this.searchByDepthSubject.next(current - 1);
|
||||
} else if (action === 'increment') {
|
||||
this.searchByDepthSubject.next(current + 1);
|
||||
}
|
||||
});
|
||||
constructor(private container: HTMLElement) {
|
||||
const [state$, send] = useDepGraphService();
|
||||
this.send = send;
|
||||
this.render();
|
||||
|
||||
this.searchByDepthSubject.subscribe((current) => {
|
||||
if (this.searchDepthDisplay) {
|
||||
this.searchDepthDisplay.innerText = current.toString();
|
||||
state$.subscribe((state) => {
|
||||
if (state.context.affectedProjects.length > 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
render(container: HTMLElement) {
|
||||
removeChildrenFromContainer(container);
|
||||
private render() {
|
||||
removeChildrenFromContainer(this.container);
|
||||
|
||||
const element = DisplayOptionsPanel.renderHtmlTemplate();
|
||||
|
||||
const affectedButtonElement: HTMLElement = element.querySelector(
|
||||
this.affectedButtonElement = element.querySelector(
|
||||
'[data-cy="affectedButton"]'
|
||||
);
|
||||
|
||||
if (this.showAffected) {
|
||||
affectedButtonElement.classList.remove('hidden');
|
||||
affectedButtonElement.addEventListener('click', () =>
|
||||
this.selectAffectedSubject.next()
|
||||
);
|
||||
}
|
||||
|
||||
const selectAllButtonElement: HTMLElement = element.querySelector(
|
||||
'[data-cy="selectAllButton"]'
|
||||
);
|
||||
selectAllButtonElement.addEventListener('click', () => {
|
||||
this.selectAllSubject.next();
|
||||
this.send({ type: 'selectAll' });
|
||||
});
|
||||
|
||||
const deselectAllButtonElement: HTMLElement = element.querySelector(
|
||||
'[data-cy="deselectAllButton"]'
|
||||
);
|
||||
deselectAllButtonElement.addEventListener('click', () => {
|
||||
this.deselectAllSubject.next();
|
||||
this.send({ type: 'deselectAll' });
|
||||
});
|
||||
|
||||
const groupByFolderCheckboxElement: HTMLInputElement =
|
||||
this.groupByFolderCheckboxElement =
|
||||
element.querySelector('#displayOptions');
|
||||
groupByFolderCheckboxElement.checked = this.groupByFolder;
|
||||
|
||||
groupByFolderCheckboxElement.addEventListener(
|
||||
this.groupByFolderCheckboxElement.addEventListener(
|
||||
'change',
|
||||
(event: InputEvent) =>
|
||||
this.groupByFolderSubject.next((<HTMLInputElement>event.target).checked)
|
||||
this.send({
|
||||
type: 'setGroupByFolder',
|
||||
groupByFolder: (event.target as HTMLInputElement).checked,
|
||||
})
|
||||
);
|
||||
|
||||
this.searchDepthDisplay = element.querySelector('#depthFilterValue');
|
||||
@ -170,18 +148,19 @@ export class DisplayOptionsPanel {
|
||||
element.querySelector('#depthFilter');
|
||||
|
||||
incrementButtonElement.addEventListener('click', () => {
|
||||
this.searchDepthChangesSubject.next('increment');
|
||||
this.send({ type: 'incrementSearchDepth' });
|
||||
});
|
||||
decrementButtonElement.addEventListener('click', () => {
|
||||
this.searchDepthChangesSubject.next('decrement');
|
||||
this.send({ type: 'decrementSearchDepth' });
|
||||
});
|
||||
|
||||
searchDepthEnabledElement.addEventListener('change', (event: InputEvent) =>
|
||||
this.searchByDepthEnabledSubject.next(
|
||||
(<HTMLInputElement>event.target).checked
|
||||
)
|
||||
this.send({
|
||||
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';
|
||||
|
||||
export class FocusedProjectPanel {
|
||||
private unfocusSubject = new Subject<void>();
|
||||
|
||||
set projectName(projectName: string) {
|
||||
this.render(projectName);
|
||||
}
|
||||
|
||||
unfocus$ = this.unfocusSubject.asObservable();
|
||||
private send: DepGraphSend;
|
||||
|
||||
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 {
|
||||
@ -39,10 +40,6 @@ export class FocusedProjectPanel {
|
||||
return render.content.firstChild as HTMLElement;
|
||||
}
|
||||
|
||||
unfocusProject() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render(projectName?: string) {
|
||||
removeChildrenFromContainer(this.container);
|
||||
|
||||
@ -62,7 +59,7 @@ export class FocusedProjectPanel {
|
||||
}
|
||||
|
||||
unfocusButtonElement.addEventListener('click', () =>
|
||||
this.unfocusSubject.next()
|
||||
this.send({ type: 'unfocusProject' })
|
||||
);
|
||||
|
||||
this.container.appendChild(element);
|
||||
|
||||
@ -1,35 +1,24 @@
|
||||
import type { ProjectGraphNode } from '@nrwl/devkit';
|
||||
import { Subject } from 'rxjs';
|
||||
import { useDepGraphService } from '../machines/dep-graph.service';
|
||||
import { DepGraphSend } from '../machines/interfaces';
|
||||
import {
|
||||
parseParentDirectoriesFromPilePath,
|
||||
removeChildrenFromContainer,
|
||||
} from '../util';
|
||||
|
||||
export class ProjectList {
|
||||
private focusProjectSubject = new Subject<string>();
|
||||
private checkedProjectsChangeSubject = new Subject<string[]>();
|
||||
private selectedItems: Record<string, HTMLElement> = {};
|
||||
checkedProjectsChange$ = this.checkedProjectsChangeSubject.asObservable();
|
||||
focusProject$ = this.focusProjectSubject.asObservable();
|
||||
private projectItems: Record<string, HTMLElement> = {};
|
||||
|
||||
private _projects: ProjectGraphNode[] = [];
|
||||
|
||||
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;
|
||||
}
|
||||
private send: DepGraphSend;
|
||||
|
||||
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 {
|
||||
@ -60,63 +49,49 @@ export class ProjectList {
|
||||
return render.content.firstChild as HTMLElement;
|
||||
}
|
||||
|
||||
selectProjects(projects: string[]) {
|
||||
projects.forEach((projectName) => {
|
||||
if (!!this.selectedItems[projectName]) {
|
||||
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
|
||||
setSelectedProjects(selectedProjects: string[]) {
|
||||
Object.keys(this.projectItems).forEach((projectName) => {
|
||||
this.projectItems[projectName].dataset['active'] = selectedProjects
|
||||
.includes(projectName)
|
||||
.toString();
|
||||
this.selectedItems[projectName].dispatchEvent(new CustomEvent('change'));
|
||||
this.projectItems[projectName].dispatchEvent(new CustomEvent('change'));
|
||||
});
|
||||
}
|
||||
|
||||
checkAllProjects() {
|
||||
Object.values(this.selectedItems).forEach((item) => {
|
||||
item.dataset['active'] = 'true';
|
||||
item.dispatchEvent(new CustomEvent('change'));
|
||||
});
|
||||
this.send({ type: 'selectAll' });
|
||||
}
|
||||
|
||||
uncheckAllProjects() {
|
||||
Object.values(this.selectedItems).forEach((item) => {
|
||||
item.dataset['active'] = 'false';
|
||||
item.dispatchEvent(new CustomEvent('change'));
|
||||
});
|
||||
this.send({ type: 'deselectAll' });
|
||||
}
|
||||
|
||||
uncheckProject(projectName: string) {
|
||||
this.selectedItems[projectName].dataset['active'] = 'false';
|
||||
this.selectedItems[projectName].dispatchEvent(new CustomEvent('change'));
|
||||
this.send({ type: 'deselectProject', projectName });
|
||||
}
|
||||
|
||||
private emitChanges() {
|
||||
const changes = Object.values(this.selectedItems)
|
||||
.filter((item) => item.dataset['active'] === 'true')
|
||||
.map((item) => item.dataset['project']);
|
||||
this.checkedProjectsChangeSubject.next(changes);
|
||||
}
|
||||
|
||||
private render() {
|
||||
private render(
|
||||
projects: ProjectGraphNode[],
|
||||
workspaceLayout: { appsDir: string; libsDir: string }
|
||||
) {
|
||||
removeChildrenFromContainer(this.container);
|
||||
|
||||
const appProjects = this.getProjectsByType('app');
|
||||
const libProjects = this.getProjectsByType('lib');
|
||||
const e2eProjects = this.getProjectsByType('e2e');
|
||||
const appProjects = this.getProjectsByType('app', projects);
|
||||
const libProjects = this.getProjectsByType('lib', projects);
|
||||
const e2eProjects = this.getProjectsByType('e2e', projects);
|
||||
|
||||
const appDirectoryGroups = this.groupProjectsByDirectory(appProjects);
|
||||
const libDirectoryGroups = this.groupProjectsByDirectory(libProjects);
|
||||
const e2eDirectoryGroups = this.groupProjectsByDirectory(e2eProjects);
|
||||
const appDirectoryGroups = this.groupProjectsByDirectory(
|
||||
appProjects,
|
||||
workspaceLayout
|
||||
);
|
||||
const libDirectoryGroups = this.groupProjectsByDirectory(
|
||||
libProjects,
|
||||
workspaceLayout
|
||||
);
|
||||
const e2eDirectoryGroups = this.groupProjectsByDirectory(
|
||||
e2eProjects,
|
||||
workspaceLayout
|
||||
);
|
||||
|
||||
const sortedAppDirectories = Object.keys(appDirectoryGroups).sort();
|
||||
const sortedLibDirectories = Object.keys(libDirectoryGroups).sort();
|
||||
@ -153,20 +128,23 @@ export class ProjectList {
|
||||
});
|
||||
}
|
||||
|
||||
private getProjectsByType(type) {
|
||||
return this.projects
|
||||
private getProjectsByType(type: string, projects: ProjectGraphNode[]) {
|
||||
return projects
|
||||
.filter((project) => project.type === type)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
private groupProjectsByDirectory(projects: ProjectGraphNode[]) {
|
||||
private groupProjectsByDirectory(
|
||||
projects: ProjectGraphNode[],
|
||||
workspaceLayout: { appsDir: string; libsDir: string }
|
||||
) {
|
||||
let groups = {};
|
||||
|
||||
projects.forEach((project) => {
|
||||
const workspaceRoot =
|
||||
project.type === 'app' || project.type === 'e2e'
|
||||
? window.workspaceLayout.appsDir
|
||||
: window.workspaceLayout.libsDir;
|
||||
? workspaceLayout.appsDir
|
||||
: workspaceLayout.libsDir;
|
||||
const directories = parseParentDirectoriesFromPilePath(
|
||||
project.data.root,
|
||||
workspaceRoot
|
||||
@ -203,7 +181,7 @@ export class ProjectList {
|
||||
);
|
||||
const focusButtonElement: HTMLElement = element.querySelector('button');
|
||||
focusButtonElement.addEventListener('click', () =>
|
||||
this.focusProjectSubject.next(project.name)
|
||||
this.send({ type: 'focusProject', projectName: project.name })
|
||||
);
|
||||
|
||||
const projectNameElement: HTMLElement = element.querySelector('label');
|
||||
@ -214,12 +192,19 @@ export class ProjectList {
|
||||
|
||||
projectNameElement.addEventListener('click', (event) => {
|
||||
const el = event.target as HTMLElement;
|
||||
el.dataset['active'] =
|
||||
el.dataset['active'] === 'false' ? 'true' : 'false';
|
||||
el.dispatchEvent(new CustomEvent('change'));
|
||||
|
||||
this.emitChanges();
|
||||
if (el.dataset['active'] === 'true') {
|
||||
this.send({
|
||||
type: 'deselectProject',
|
||||
projectName: el.dataset['project'],
|
||||
});
|
||||
} else {
|
||||
this.send({
|
||||
type: 'selectProject',
|
||||
projectName: el.dataset['project'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
projectNameElement.addEventListener('change', (event) => {
|
||||
const el = event.target as HTMLElement;
|
||||
if (el.dataset['active'] === 'false') {
|
||||
@ -231,7 +216,7 @@ export class ProjectList {
|
||||
projectNameElement.dispatchEvent(new Event('click'));
|
||||
});
|
||||
|
||||
this.selectedItems[project.name] = projectNameElement;
|
||||
this.projectItems[project.name] = projectNameElement;
|
||||
|
||||
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 { FocusedProjectPanel } from './focused-project-panel';
|
||||
import { ProjectList } from './project-list';
|
||||
import { TextFilterChangeEvent, TextFilterPanel } from './text-filter-panel';
|
||||
import { TextFilterPanel } from './text-filter-panel';
|
||||
|
||||
declare var ResizeObserver;
|
||||
|
||||
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 focusedProjectPanel: FocusedProjectPanel;
|
||||
private textFilterPanel: TextFilterPanel;
|
||||
private projectList: ProjectList;
|
||||
|
||||
private groupByFolder = window.groupByFolder;
|
||||
private selectedProjects: string[] = [];
|
||||
|
||||
set projects(projects: ProjectGraphNode[]) {
|
||||
this.projectList.projects = projects;
|
||||
this.focusedProjectPanel.unfocusProject();
|
||||
}
|
||||
|
||||
constructor(private affectedProjects: string[]) {
|
||||
const showAffected = this.affectedProjects.length > 0;
|
||||
|
||||
constructor() {
|
||||
const displayOptionsPanelContainer = document.getElementById(
|
||||
'display-options-panel'
|
||||
);
|
||||
|
||||
this.displayOptionsPanel = new DisplayOptionsPanel(
|
||||
showAffected,
|
||||
this.groupByFolder
|
||||
displayOptionsPanelContainer
|
||||
);
|
||||
this.displayOptionsPanel.render(displayOptionsPanelContainer);
|
||||
|
||||
const focusedProjectPanelContainer =
|
||||
document.getElementById('focused-project');
|
||||
|
||||
this.focusedProjectPanel = new FocusedProjectPanel(
|
||||
focusedProjectPanelContainer
|
||||
);
|
||||
@ -57,232 +33,5 @@ export class SidebarComponent {
|
||||
|
||||
const projectListContainer = document.getElementById('project-lists');
|
||||
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 { removeChildrenFromContainer } from '../util';
|
||||
import { fromEvent, Subscription } from 'rxjs';
|
||||
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 {
|
||||
text: string;
|
||||
@ -10,13 +12,11 @@ export interface TextFilterChangeEvent {
|
||||
export class TextFilterPanel {
|
||||
private textInput: HTMLInputElement;
|
||||
private includeInPathCheckbox: HTMLInputElement;
|
||||
private changesSubject = new Subject<TextFilterChangeEvent>();
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
changes$ = this.changesSubject.asObservable();
|
||||
private send: DepGraphSend;
|
||||
|
||||
constructor(private container: HTMLElement) {
|
||||
this.subscriptions.map((s) => s.unsubscribe());
|
||||
const [_, send] = useDepGraphService();
|
||||
this.send = send;
|
||||
this.render();
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ export class TextFilterPanel {
|
||||
<div class="mt-4 px-4">
|
||||
<div class="flex items-start">
|
||||
<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 class="ml-3 text-sm">
|
||||
<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;
|
||||
}
|
||||
|
||||
private emitChanges() {
|
||||
this.changesSubject.next({
|
||||
text: this.textInput.value.toLowerCase(),
|
||||
includeInPath: this.includeInPathCheckbox.checked,
|
||||
});
|
||||
}
|
||||
|
||||
private render() {
|
||||
removeChildrenFromContainer(this.container);
|
||||
|
||||
@ -72,7 +65,10 @@ export class TextFilterPanel {
|
||||
|
||||
this.textInput = element.querySelector('input[type="text"]');
|
||||
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) {
|
||||
resetInputElement.classList.remove('hidden');
|
||||
this.includeInPathCheckbox.disabled = false;
|
||||
@ -82,19 +78,22 @@ export class TextFilterPanel {
|
||||
}
|
||||
});
|
||||
|
||||
this.subscriptions.push(
|
||||
fromEvent(this.textInput, 'keyup')
|
||||
.pipe(
|
||||
filter((event: KeyboardEvent) => event.key !== 'Enter'),
|
||||
debounceTime(500),
|
||||
map(() => this.emitChanges())
|
||||
fromEvent(this.textInput, 'keyup')
|
||||
.pipe(
|
||||
filter((event: KeyboardEvent) => event.key !== 'Enter'),
|
||||
debounceTime(500),
|
||||
map(() =>
|
||||
this.send({ type: 'filterByText', search: this.textInput.value })
|
||||
)
|
||||
.subscribe()
|
||||
);
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.includeInPathCheckbox = element.querySelector('#includeInPath');
|
||||
this.includeInPathCheckbox.addEventListener('change', () =>
|
||||
this.emitChanges()
|
||||
this.send({
|
||||
type: 'setIncludeProjectsByPath',
|
||||
includeProjectsByPath: this.includeInPathCheckbox.checked,
|
||||
})
|
||||
);
|
||||
|
||||
resetInputElement.addEventListener('click', () => {
|
||||
@ -102,7 +101,7 @@ export class TextFilterPanel {
|
||||
this.includeInPathCheckbox.checked = false;
|
||||
this.includeInPathCheckbox.disabled = true;
|
||||
resetInputElement.classList.add('hidden');
|
||||
this.emitChanges();
|
||||
this.send([{ type: 'clearTextFilter' }]);
|
||||
});
|
||||
|
||||
this.container.appendChild(element);
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
||||
|
||||
export function removeChildrenFromContainer(container: HTMLElement) {
|
||||
Array.from(container.children).forEach((child) =>
|
||||
container.removeChild(child)
|
||||
@ -27,3 +29,115 @@ export function parseParentDirectoriesFromPilePath(
|
||||
|
||||
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 { ProjectGraph, ProjectGraphNode } from '@nrwl/devkit';
|
||||
import { ProjectGraphList } from './graphs';
|
||||
import { AppConfig } from './app/models';
|
||||
|
||||
export declare global {
|
||||
export interface Window {
|
||||
watch: boolean;
|
||||
projects: ProjectGraphNode[];
|
||||
graph: ProjectGraph;
|
||||
filteredProjects: ProjectGraphNode[];
|
||||
affected: string[];
|
||||
exclude: string[];
|
||||
focusedProject: string;
|
||||
groupByFolder: boolean;
|
||||
focusProject: Function;
|
||||
excludeProject: Function;
|
||||
projectGraphList: ProjectGraphList[];
|
||||
selectedProjectGraph: string;
|
||||
workspaceLayout: {
|
||||
libsDir: string;
|
||||
appsDir: string;
|
||||
};
|
||||
projectGraphResponse: DepGraphClientResponse;
|
||||
watch: boolean;
|
||||
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"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<script id="environment" src="environment.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
|
||||
@ -143,17 +145,4 @@
|
||||
<div id="graph-container"></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>
|
||||
|
||||
@ -1,13 +1,29 @@
|
||||
import { AppComponent } from './app/app';
|
||||
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') {
|
||||
window.watch = true;
|
||||
if (window.useXstateInspect === true) {
|
||||
inspect({
|
||||
url: 'https://stately.ai/viz?inspect',
|
||||
iframe: false, // open in new window
|
||||
});
|
||||
}
|
||||
|
||||
if (environment.environment === 'release' && window.localMode === 'build') {
|
||||
environment.appConfig.projectGraphService = new LocalProjectGraphService();
|
||||
let projectGraphService: ProjectGraphService;
|
||||
|
||||
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/experimental-utils": "~4.33.0",
|
||||
"@typescript-eslint/parser": "~4.33.0",
|
||||
"@xstate/immer": "^0.2.0",
|
||||
"@xstate/inspect": "^0.5.1",
|
||||
"angular": "1.8.0",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"babel-jest": "27.2.3",
|
||||
@ -136,8 +138,8 @@
|
||||
"cz-customizable": "^6.2.0",
|
||||
"depcheck": "^1.3.1",
|
||||
"dotenv": "~10.0.0",
|
||||
"enhanced-resolve": "^5.8.3",
|
||||
"ejs": "^3.1.5",
|
||||
"enhanced-resolve": "^5.8.3",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-next": "^12.0.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
@ -158,6 +160,7 @@
|
||||
"husky": "^6.0.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"ignore": "^5.0.4",
|
||||
"immer": "^9.0.6",
|
||||
"import-fresh": "^3.1.0",
|
||||
"injection-js": "^2.4.0",
|
||||
"is-ci": "^3.0.0",
|
||||
@ -254,6 +257,7 @@
|
||||
"webpack-sources": "^3.0.2",
|
||||
"webpack-subresource-integrity": "^1.5.2",
|
||||
"worker-plugin": "3.2.0",
|
||||
"xstate": "^4.25.0",
|
||||
"yargs": "15.4.1",
|
||||
"yargs-parser": "20.0.0",
|
||||
"zone.js": "~0.11.4"
|
||||
|
||||
@ -20,10 +20,6 @@ import {
|
||||
ProjectGraphNode,
|
||||
pruneExternalNodes,
|
||||
} from '../core/project-graph';
|
||||
import {
|
||||
cacheDirectory,
|
||||
readCacheDirectoryProperty,
|
||||
} from '../utilities/cache-directory';
|
||||
import { writeJsonFile } from '../utilities/fileutils';
|
||||
import { output } from '../utilities/output';
|
||||
|
||||
@ -32,9 +28,6 @@ export interface DepGraphClientResponse {
|
||||
projects: ProjectGraphNode[];
|
||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||
layout: { appsDir: string; libsDir: string };
|
||||
changes: {
|
||||
added: string[];
|
||||
};
|
||||
affected: string[];
|
||||
focus: string;
|
||||
groupByFolder: boolean;
|
||||
@ -59,74 +52,46 @@ const mimeType = {
|
||||
'.ttf': 'aplication/font-sfnt',
|
||||
};
|
||||
|
||||
const nxDepsDir = cacheDirectory(
|
||||
appRootPath,
|
||||
readCacheDirectoryProperty(appRootPath)
|
||||
);
|
||||
|
||||
async function projectsToHtml(
|
||||
projects: ProjectGraphNode[],
|
||||
graph: ProjectGraph,
|
||||
affected: string[],
|
||||
function buildEnvironmentJs(
|
||||
exclude: string[],
|
||||
focus: string,
|
||||
groupByFolder: boolean,
|
||||
exclude: string[],
|
||||
layout: { appsDir: string; libsDir: string },
|
||||
localMode: 'serve' | 'build',
|
||||
watchMode: boolean = false
|
||||
watchMode: boolean,
|
||||
localMode: 'build' | 'serve',
|
||||
depGraphClientResponse?: DepGraphClientResponse
|
||||
) {
|
||||
let f = readFileSync(
|
||||
join(__dirname, '../core/dep-graph/index.html'),
|
||||
'utf-8'
|
||||
);
|
||||
let environmentJs = `window.exclude = ${JSON.stringify(exclude)};
|
||||
window.groupByFolder = ${!!groupByFolder};
|
||||
window.watch = ${!!watchMode};
|
||||
window.environment = 'release';
|
||||
window.localMode = '${localMode}';
|
||||
|
||||
f = f
|
||||
.replace(
|
||||
`window.projects = []`,
|
||||
`window.projects = ${JSON.stringify(projects)}`
|
||||
)
|
||||
.replace(`window.graph = {}`, `window.graph = ${JSON.stringify(graph)}`)
|
||||
.replace(
|
||||
`window.affected = []`,
|
||||
`window.affected = ${JSON.stringify(affected)}`
|
||||
)
|
||||
.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`);
|
||||
}
|
||||
window.appConfig = {
|
||||
showDebugger: false,
|
||||
projectGraphs: [
|
||||
{
|
||||
id: 'local',
|
||||
label: 'local',
|
||||
url: 'projectGraph.json',
|
||||
}
|
||||
],
|
||||
defaultProjectGraph: 'local',
|
||||
};
|
||||
`;
|
||||
|
||||
if (localMode === 'build') {
|
||||
currentDepGraphClientResponse = await createDepGraphClientResponse();
|
||||
f = f.replace(
|
||||
`window.projectGraphResponse = null`,
|
||||
`window.projectGraphResponse = ${JSON.stringify(
|
||||
currentDepGraphClientResponse
|
||||
)}`
|
||||
);
|
||||
|
||||
f = f.replace(`window.localMode = 'serve'`, `window.localMode = 'build'`);
|
||||
environmentJs += `window.projectGraphResponse = ${JSON.stringify(
|
||||
depGraphClientResponse
|
||||
)};`;
|
||||
} else {
|
||||
environmentJs += `window.projectGraphResponse = null;`;
|
||||
}
|
||||
|
||||
return f;
|
||||
if (!!focus) {
|
||||
environmentJs += `window.focusedProject = '${focus}';`;
|
||||
}
|
||||
|
||||
return environmentJs;
|
||||
}
|
||||
|
||||
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') {
|
||||
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 || []);
|
||||
}
|
||||
graph = filterGraph(graph, args.focus || null, args.exclude || []);
|
||||
|
||||
if (args.file) {
|
||||
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(/href="styles/g, 'href="static/styles');
|
||||
html = html.replace('<base href="/" />', '');
|
||||
html = html.replace(/type="module"/g, '');
|
||||
|
||||
writeFileSync(fullFilePath, html);
|
||||
writeFileSync(join(assetsFolder, 'environment.js'), environmentJs);
|
||||
|
||||
output.success({
|
||||
title: `HTML output created in ${fileFolderPath}`,
|
||||
@ -315,8 +278,17 @@ export async function generateGraph(
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
const environmentJs = buildEnvironmentJs(
|
||||
args.exclude || [],
|
||||
args.focus || null,
|
||||
args.groupByFolder || false,
|
||||
args.watch,
|
||||
!!args.file && args.file.endsWith('html') ? 'build' : 'serve'
|
||||
);
|
||||
|
||||
await startServer(
|
||||
html,
|
||||
environmentJs,
|
||||
args.host || '127.0.0.1',
|
||||
args.port || 4211,
|
||||
args.watch,
|
||||
@ -331,6 +303,7 @@ export async function generateGraph(
|
||||
|
||||
async function startServer(
|
||||
html: string,
|
||||
environmentJs: string,
|
||||
host: string,
|
||||
port = 4211,
|
||||
watchForchanges = false,
|
||||
@ -372,6 +345,12 @@ async function startServer(
|
||||
return;
|
||||
}
|
||||
|
||||
if (sanitizePath === 'environment.js') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
res.end(environmentJs);
|
||||
return;
|
||||
}
|
||||
|
||||
let pathname = join(__dirname, '../core/dep-graph/', sanitizePath);
|
||||
|
||||
if (!existsSync(pathname)) {
|
||||
@ -383,7 +362,6 @@ async function startServer(
|
||||
|
||||
// if is a directory, then look for index.html
|
||||
if (statSync(pathname).isDirectory()) {
|
||||
// pathname += '/index.html';
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(html);
|
||||
return;
|
||||
@ -420,9 +398,6 @@ let currentDepGraphClientResponse: DepGraphClientResponse = {
|
||||
appsDir: '',
|
||||
libsDir: '',
|
||||
},
|
||||
changes: {
|
||||
added: [],
|
||||
},
|
||||
affected: [],
|
||||
focus: null,
|
||||
groupByFolder: false,
|
||||
@ -522,21 +497,6 @@ async function createDepGraphClientResponse(): Promise<DepGraphClientResponse> {
|
||||
|
||||
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.measure(
|
||||
@ -557,8 +517,5 @@ async function createDepGraphClientResponse(): Promise<DepGraphClientResponse> {
|
||||
layout,
|
||||
projects,
|
||||
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"
|
||||
"@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":
|
||||
version "1.2.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||
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"
|
||||
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
|
||||
|
||||
immer@^9.0.1:
|
||||
immer@^9.0.1, immer@^9.0.6:
|
||||
version "9.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
||||
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"
|
||||
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:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user