chore(graph): consume task graph data in client (#13092)
This commit is contained in:
parent
f08a3c3c44
commit
2be9a01272
3
.gitignore
vendored
3
.gitignore
vendored
@ -15,7 +15,8 @@ jest.debug.config.js
|
|||||||
/.verdaccio/build/local-registry
|
/.verdaccio/build/local-registry
|
||||||
/graph/client/src/assets/environment.js
|
/graph/client/src/assets/environment.js
|
||||||
/graph/client/src/assets/dev/environment.js
|
/graph/client/src/assets/dev/environment.js
|
||||||
/graph/client/src/assets/generated-graphs
|
/graph/client/src/assets/generated-project-graphs
|
||||||
|
/graph/client/src/assets/generated-task-graphs
|
||||||
/nx-dev/nx-dev/public/documentation
|
/nx-dev/nx-dev/public/documentation
|
||||||
/nx-dev/nx-dev/public/images/open-graph
|
/nx-dev/nx-dev/public/images/open-graph
|
||||||
# Lerna creates this
|
# Lerna creates this
|
||||||
|
|||||||
@ -23,9 +23,9 @@ import * as nxExamplesJson from '../fixtures/nx-examples.json';
|
|||||||
|
|
||||||
describe('graph-client', () => {
|
describe('graph-client', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.intercept('/assets/graphs/e2e.json', { fixture: 'nx-examples.json' }).as(
|
cy.intercept('/assets/project-graphs/e2e.json', {
|
||||||
'getGraph'
|
fixture: 'nx-examples.json',
|
||||||
);
|
}).as('getGraph');
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
|
||||||
// wait for initial graph to finish loading
|
// wait for initial graph to finish loading
|
||||||
@ -140,7 +140,7 @@ describe('graph-client', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should check all affected project items', () => {
|
it('should check all affected project items', () => {
|
||||||
cy.intercept('/assets/graphs/affected.json', {
|
cy.intercept('/assets/project-graphs/affected.json', {
|
||||||
fixture: 'affected.json',
|
fixture: 'affected.json',
|
||||||
}).as('getAffectedGraph');
|
}).as('getAffectedGraph');
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ describe('graph-client', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// switch back to Nx Examples graph before proceeding
|
// switch back to Nx Examples graph before proceeding
|
||||||
cy.intercept('/assets/graphs/e2e.json', {
|
cy.intercept('/assets/project-graphs/e2e.json', {
|
||||||
fixture: 'nx-examples.json',
|
fixture: 'nx-examples.json',
|
||||||
}).as('getGraph');
|
}).as('getGraph');
|
||||||
cy.get('[data-cy=project-select]').select('e2e', { force: true });
|
cy.get('[data-cy=project-select]').select('e2e', { force: true });
|
||||||
@ -308,9 +308,9 @@ describe('graph-client', () => {
|
|||||||
|
|
||||||
describe('loading graph client with url params', () => {
|
describe('loading graph client with url params', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('/assets/graphs/*', { fixture: 'nx-examples.json' }).as(
|
cy.intercept('/assets/project-graphs/*', {
|
||||||
'getGraph'
|
fixture: 'nx-examples.json',
|
||||||
);
|
}).as('getGraph');
|
||||||
});
|
});
|
||||||
|
|
||||||
// check that params work from old base url of /
|
// check that params work from old base url of /
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
describe('graph-client release', () => {
|
describe('graph-client release', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.intercept('/assets/graphs/*').as('getGraph');
|
cy.intercept('/assets/project-graphs/*').as('getGraph');
|
||||||
|
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
|
||||||
|
|||||||
@ -62,8 +62,9 @@
|
|||||||
"dev": {
|
"dev": {
|
||||||
"assets": [
|
"assets": [
|
||||||
"graph/client/src/favicon.ico",
|
"graph/client/src/favicon.ico",
|
||||||
"graph/client/src/assets/graphs/",
|
"graph/client/src/assets/project-graphs/",
|
||||||
"graph/client/src/assets/generated-graphs/",
|
"graph/client/src/assets/generated-project-graphs/",
|
||||||
|
"graph/client/src/assets/generated-task-graphs/",
|
||||||
{
|
{
|
||||||
"input": "graph/client/src/assets/dev",
|
"input": "graph/client/src/assets/dev",
|
||||||
"output": "/",
|
"output": "/",
|
||||||
@ -74,7 +75,7 @@
|
|||||||
"dev-e2e": {
|
"dev-e2e": {
|
||||||
"assets": [
|
"assets": [
|
||||||
"graph/client/src/favicon.ico",
|
"graph/client/src/favicon.ico",
|
||||||
"graph/client/src/assets/graphs/",
|
"graph/client/src/assets/project-graphs/",
|
||||||
{
|
{
|
||||||
"input": "graph/client/src/assets/dev-e2e",
|
"input": "graph/client/src/assets/dev-e2e",
|
||||||
"output": "/",
|
"output": "/",
|
||||||
@ -86,8 +87,8 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
"graph/client/src/favicon.ico",
|
"graph/client/src/favicon.ico",
|
||||||
{
|
{
|
||||||
"input": "graph/client/src/assets/graphs",
|
"input": "graph/client/src/assets/project-graphs",
|
||||||
"output": "/assets/graphs",
|
"output": "/assets/project-graphs",
|
||||||
"glob": "e2e.json"
|
"glob": "e2e.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -101,8 +102,8 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
"graph/client/src/favicon.ico",
|
"graph/client/src/favicon.ico",
|
||||||
{
|
{
|
||||||
"input": "graph/client/src/assets/graphs",
|
"input": "graph/client/src/assets/project-graphs",
|
||||||
"output": "/assets/graphs",
|
"output": "/assets/project-graphs",
|
||||||
"glob": "e2e.json"
|
"glob": "e2e.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -129,7 +130,7 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
"graph/client/src/favicon.ico",
|
"graph/client/src/favicon.ico",
|
||||||
{
|
{
|
||||||
"input": "graph/client/src/assets/graphs",
|
"input": "graph/client/src/assets/project-graphs",
|
||||||
"output": "/assets/graphs",
|
"output": "/assets/graphs",
|
||||||
"glob": "e2e.json"
|
"glob": "e2e.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
localStorageRankDirKey,
|
localStorageRankDirKey,
|
||||||
RankDir,
|
RankDir,
|
||||||
rankDirResolver,
|
rankDirResolver,
|
||||||
} from '../rankdir-resolver';
|
} from '../../rankdir-resolver';
|
||||||
|
|
||||||
export default function RankdirPanel(): JSX.Element {
|
export default function RankdirPanel(): JSX.Element {
|
||||||
const [rankDir, setRankDir] = useState(
|
const [rankDir, setRankDir] = useState(
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { useDebounce } from '../hooks/use-debounce';
|
import { useDebounce } from '../../hooks/use-debounce';
|
||||||
import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline';
|
import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline';
|
||||||
import DebouncedTextInput from '../ui-components/debounced-text-input';
|
import DebouncedTextInput from '../../ui-components/debounced-text-input';
|
||||||
|
|
||||||
export interface TextFilterPanelProps {
|
export interface TextFilterPanelProps {
|
||||||
textFilter: string;
|
textFilter: string;
|
||||||
@ -6,7 +6,11 @@ import {
|
|||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Fragment, useEffect, useState } from 'react';
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
import { localStorageThemeKey, Theme, themeResolver } from '../theme-resolver';
|
import {
|
||||||
|
localStorageThemeKey,
|
||||||
|
Theme,
|
||||||
|
themeResolver,
|
||||||
|
} from '../../theme-resolver';
|
||||||
|
|
||||||
export default function ThemePanel(): JSX.Element {
|
export default function ThemePanel(): JSX.Element {
|
||||||
const [theme, setTheme] = useState(
|
const [theme, setTheme] = useState(
|
||||||
@ -5,7 +5,7 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { TracingAlgorithmType } from '../machines/interfaces';
|
import { TracingAlgorithmType } from '../../machines/interfaces';
|
||||||
|
|
||||||
export interface TracingPanelProps {
|
export interface TracingPanelProps {
|
||||||
start: string;
|
start: string;
|
||||||
@ -13,14 +13,14 @@ import {
|
|||||||
searchDepthSelector,
|
searchDepthSelector,
|
||||||
textFilterSelector,
|
textFilterSelector,
|
||||||
} from '../machines/selectors';
|
} from '../machines/selectors';
|
||||||
import CollapseEdgesPanel from '../sidebar/collapse-edges-panel';
|
import CollapseEdgesPanel from './panels/collapse-edges-panel';
|
||||||
import FocusedProjectPanel from '../sidebar/focused-project-panel';
|
import FocusedProjectPanel from './panels/focused-project-panel';
|
||||||
import GroupByFolderPanel from '../sidebar/group-by-folder-panel';
|
import GroupByFolderPanel from './panels/group-by-folder-panel';
|
||||||
import ProjectList from '../sidebar/project-list';
|
import ProjectList from './project-list';
|
||||||
import SearchDepth from '../sidebar/search-depth';
|
import SearchDepth from './panels/search-depth';
|
||||||
import ShowHideProjects from '../sidebar/show-hide-projects';
|
import ShowHideProjects from './panels/show-hide-projects';
|
||||||
import TextFilterPanel from '../sidebar/text-filter-panel';
|
import TextFilterPanel from './panels/text-filter-panel';
|
||||||
import TracingPanel from '../sidebar/tracing-panel';
|
import TracingPanel from './panels/tracing-panel';
|
||||||
import { TracingAlgorithmType } from '../machines/interfaces';
|
import { TracingAlgorithmType } from '../machines/interfaces';
|
||||||
import { useEnvironmentConfig } from '../hooks/use-environment-config';
|
import { useEnvironmentConfig } from '../hooks/use-environment-config';
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { DocumentMagnifyingGlassIcon } from '@heroicons/react/24/solid';
|
import { DocumentMagnifyingGlassIcon } from '@heroicons/react/24/solid';
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { ProjectGraphNode, Task } from '@nrwl/devkit';
|
import type { ProjectGraphNode } from '@nrwl/devkit';
|
||||||
import { parseParentDirectoriesFromFilePath } from '../util';
|
import { parseParentDirectoriesFromFilePath } from '../util';
|
||||||
import { WorkspaceLayout } from '../interfaces';
|
import { WorkspaceLayout } from '../interfaces';
|
||||||
import Tag from '../ui-components/tag';
|
import Tag from '../ui-components/tag';
|
||||||
@ -55,6 +55,7 @@ function groupProjectsByDirectory(
|
|||||||
function ProjectListItem({
|
function ProjectListItem({
|
||||||
project,
|
project,
|
||||||
selectTask,
|
selectTask,
|
||||||
|
selectedTaskId,
|
||||||
}: {
|
}: {
|
||||||
project: SidebarProjectWithTargets;
|
project: SidebarProjectWithTargets;
|
||||||
selectTask: (
|
selectTask: (
|
||||||
@ -62,6 +63,7 @@ function ProjectListItem({
|
|||||||
targetName: string,
|
targetName: string,
|
||||||
configurationName: string
|
configurationName: string
|
||||||
) => void;
|
) => void;
|
||||||
|
selectedTaskId: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<li className="relative block cursor-default select-none py-1 pl-2 pr-6 text-xs text-slate-600 dark:text-slate-400">
|
<li className="relative block cursor-default select-none py-1 pl-2 pr-6 text-xs text-slate-600 dark:text-slate-400">
|
||||||
@ -73,6 +75,10 @@ function ProjectListItem({
|
|||||||
<br />
|
<br />
|
||||||
{target.configurations.map((configuration) => (
|
{target.configurations.map((configuration) => (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
{selectedTaskId ===
|
||||||
|
`${project.projectGraphNode.name}:${target.targetName}:${configuration.name}` ? (
|
||||||
|
<span>selected</span>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
data-cy={`focus-button-${configuration.name}`}
|
data-cy={`focus-button-${configuration.name}`}
|
||||||
type="button"
|
type="button"
|
||||||
@ -111,6 +117,7 @@ function SubProjectList({
|
|||||||
headerText = '',
|
headerText = '',
|
||||||
projects,
|
projects,
|
||||||
selectTask,
|
selectTask,
|
||||||
|
selectedTaskId,
|
||||||
}: {
|
}: {
|
||||||
headerText: string;
|
headerText: string;
|
||||||
projects: SidebarProjectWithTargets[];
|
projects: SidebarProjectWithTargets[];
|
||||||
@ -119,6 +126,7 @@ function SubProjectList({
|
|||||||
targetName: string,
|
targetName: string,
|
||||||
configurationName: string
|
configurationName: string
|
||||||
) => void;
|
) => void;
|
||||||
|
selectedTaskId: string;
|
||||||
}) {
|
}) {
|
||||||
let sortedProjects = [...projects];
|
let sortedProjects = [...projects];
|
||||||
sortedProjects.sort((a, b) => {
|
sortedProjects.sort((a, b) => {
|
||||||
@ -139,6 +147,7 @@ function SubProjectList({
|
|||||||
key={project.projectGraphNode.name}
|
key={project.projectGraphNode.name}
|
||||||
project={project}
|
project={project}
|
||||||
selectTask={selectTask}
|
selectTask={selectTask}
|
||||||
|
selectedTaskId={selectedTaskId}
|
||||||
></ProjectListItem>
|
></ProjectListItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -232,6 +241,7 @@ export function TaskList({
|
|||||||
mapToSidebarProjectWithTasks(project, selectedTask)
|
mapToSidebarProjectWithTasks(project, selectedTask)
|
||||||
)}
|
)}
|
||||||
selectTask={selectTask}
|
selectTask={selectTask}
|
||||||
|
selectedTaskId={selectedTask}
|
||||||
></SubProjectList>
|
></SubProjectList>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -249,6 +259,7 @@ export function TaskList({
|
|||||||
mapToSidebarProjectWithTasks(project, selectedTask)
|
mapToSidebarProjectWithTasks(project, selectedTask)
|
||||||
)}
|
)}
|
||||||
selectTask={selectTask}
|
selectTask={selectTask}
|
||||||
|
selectedTaskId={selectedTask}
|
||||||
></SubProjectList>
|
></SubProjectList>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -266,6 +277,7 @@ export function TaskList({
|
|||||||
mapToSidebarProjectWithTasks(project, selectedTask)
|
mapToSidebarProjectWithTasks(project, selectedTask)
|
||||||
)}
|
)}
|
||||||
selectTask={selectTask}
|
selectTask={selectTask}
|
||||||
|
selectedTaskId={selectedTask}
|
||||||
></SubProjectList>
|
></SubProjectList>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -1,64 +1,41 @@
|
|||||||
import TaskList from '../sidebar/task-list';
|
import TaskList from './task-list';
|
||||||
/* nx-ignore-next-line */
|
/* nx-ignore-next-line */
|
||||||
import { ProjectGraphNode } from 'nx/src/config/project-graph';
|
import { ProjectGraphNode } from 'nx/src/config/project-graph';
|
||||||
|
import { useDepGraphService } from '../hooks/use-dep-graph';
|
||||||
|
import { useDepGraphSelector } from '../hooks/use-dep-graph-selector';
|
||||||
|
import {
|
||||||
|
allProjectsSelector,
|
||||||
|
workspaceLayoutSelector,
|
||||||
|
} from '../machines/selectors';
|
||||||
|
import { useTaskGraphSelector } from '../hooks/use-task-graph-selector';
|
||||||
|
import { getTaskGraphService } from '../machines/get-services';
|
||||||
|
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
export interface TasksSidebarProps {}
|
export interface TasksSidebarProps {}
|
||||||
|
|
||||||
export function TasksSidebar(props: TasksSidebarProps) {
|
export function TasksSidebar(props: TasksSidebarProps) {
|
||||||
const mockProjects: ProjectGraphNode[] = [
|
const projects = useDepGraphSelector(allProjectsSelector);
|
||||||
{
|
const workspaceLayout = useDepGraphSelector(workspaceLayoutSelector);
|
||||||
name: 'app1',
|
const taskGraph = getTaskGraphService();
|
||||||
type: 'app',
|
|
||||||
data: {
|
|
||||||
root: 'apps/app1',
|
|
||||||
targets: {
|
|
||||||
build: {
|
|
||||||
configurations: { production: {}, development: {} },
|
|
||||||
defaultConfiguration: 'production',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'nested-app',
|
|
||||||
type: 'app',
|
|
||||||
data: {
|
|
||||||
root: 'apps/nested/app',
|
|
||||||
targets: { build: { configurations: { production: {} } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'app1-e2e',
|
|
||||||
type: 'e2e',
|
|
||||||
data: {
|
|
||||||
root: 'apps/app1-e2e',
|
|
||||||
targets: { e2e: { configurations: { production: {} } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lib1',
|
|
||||||
type: 'lib',
|
|
||||||
data: {
|
|
||||||
root: 'libs/lib1',
|
|
||||||
targets: { lint: { configurations: { production: {} } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockWorkspaceLayout = {
|
const selectedTask = useTaskGraphSelector(
|
||||||
appsDir: 'apps',
|
(state) => state.context.selectedTaskId
|
||||||
libsDir: 'libs',
|
);
|
||||||
};
|
function selectTask(
|
||||||
|
projectName: string,
|
||||||
const mockSelectedTask = 'app1:build:production';
|
targetName: string,
|
||||||
|
configurationName: string
|
||||||
|
) {
|
||||||
|
const taskId = `${projectName}:${targetName}:${configurationName}`;
|
||||||
|
taskGraph.send({ type: 'selectTask', taskId });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TaskList
|
<TaskList
|
||||||
projects={mockProjects}
|
projects={projects}
|
||||||
workspaceLayout={mockWorkspaceLayout}
|
workspaceLayout={workspaceLayout}
|
||||||
selectedTask={mockSelectedTask}
|
selectedTask={selectedTask}
|
||||||
selectTask={console.log}
|
selectTask={selectTask}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { DepGraphClientResponse } from 'nx/src/command-line/dep-graph';
|
import type {
|
||||||
|
DepGraphClientResponse,
|
||||||
|
TaskGraphClientResponse,
|
||||||
|
} from 'nx/src/command-line/dep-graph';
|
||||||
import { ProjectGraphService } from './interfaces';
|
import { ProjectGraphService } from './interfaces';
|
||||||
|
|
||||||
export class FetchProjectGraphService implements ProjectGraphService {
|
export class FetchProjectGraphService implements ProjectGraphService {
|
||||||
@ -18,4 +21,12 @@ export class FetchProjectGraphService implements ProjectGraphService {
|
|||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
||||||
|
const request = new Request(url, { mode: 'no-cors' });
|
||||||
|
|
||||||
|
const response = await fetch(request);
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
graph/client/src/app/hooks/use-task-graph-selector.ts
Normal file
15
graph/client/src/app/hooks/use-task-graph-selector.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useSelector } from '@xstate/react';
|
||||||
|
import { DepGraphState, TaskGraphState } from '../machines/interfaces';
|
||||||
|
import { useDepGraphService } from './use-dep-graph';
|
||||||
|
import { getTaskGraphService } from '../machines/get-services';
|
||||||
|
|
||||||
|
export type TaskGraphSelector<T> = (depGraphState: TaskGraphState) => T;
|
||||||
|
|
||||||
|
export function useTaskGraphSelector<T>(selectorFunc: TaskGraphSelector<T>): T {
|
||||||
|
const taskGraphMachine = getTaskGraphService();
|
||||||
|
|
||||||
|
return useSelector<typeof taskGraphMachine, T>(
|
||||||
|
taskGraphMachine,
|
||||||
|
selectorFunc
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,10 +1,14 @@
|
|||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { DepGraphClientResponse } from 'nx/src/command-line/dep-graph';
|
import type {
|
||||||
|
DepGraphClientResponse,
|
||||||
|
TaskGraphClientResponse,
|
||||||
|
} from 'nx/src/command-line/dep-graph';
|
||||||
|
|
||||||
export interface ProjectGraphList {
|
export interface GraphListItem {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
url: string;
|
projectGraphUrl: string;
|
||||||
|
taskGraphUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceLayout {
|
export interface WorkspaceLayout {
|
||||||
@ -15,6 +19,7 @@ export interface WorkspaceLayout {
|
|||||||
export interface ProjectGraphService {
|
export interface ProjectGraphService {
|
||||||
getHash: () => Promise<string>;
|
getHash: () => Promise<string>;
|
||||||
getProjectGraph: (url: string) => Promise<DepGraphClientResponse>;
|
getProjectGraph: (url: string) => Promise<DepGraphClientResponse>;
|
||||||
|
getTaskGraph: (url: string) => Promise<TaskGraphClientResponse>;
|
||||||
}
|
}
|
||||||
export interface Environment {
|
export interface Environment {
|
||||||
environment: 'dev' | 'watch' | 'release';
|
environment: 'dev' | 'watch' | 'release';
|
||||||
@ -23,6 +28,6 @@ export interface Environment {
|
|||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
showDebugger: boolean;
|
showDebugger: boolean;
|
||||||
showExperimentalFeatures: boolean;
|
showExperimentalFeatures: boolean;
|
||||||
projectGraphs: ProjectGraphList[];
|
projects: GraphListItem[];
|
||||||
defaultProjectGraph: string;
|
defaultProject: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { DepGraphClientResponse } from 'nx/src/command-line/dep-graph';
|
import type {
|
||||||
|
DepGraphClientResponse,
|
||||||
|
TaskGraphClientResponse,
|
||||||
|
} from 'nx/src/command-line/dep-graph';
|
||||||
import { ProjectGraphService } from './interfaces';
|
import { ProjectGraphService } from './interfaces';
|
||||||
|
|
||||||
export class LocalProjectGraphService implements ProjectGraphService {
|
export class LocalProjectGraphService implements ProjectGraphService {
|
||||||
@ -10,4 +13,8 @@ export class LocalProjectGraphService implements ProjectGraphService {
|
|||||||
async getProjectGraph(url: string): Promise<DepGraphClientResponse> {
|
async getProjectGraph(url: string): Promise<DepGraphClientResponse> {
|
||||||
return new Promise((resolve) => resolve(window.projectGraphResponse));
|
return new Promise((resolve) => resolve(window.projectGraphResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
||||||
|
return new Promise((resolve) => resolve(window.taskGraphResponse));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
155
graph/client/src/app/machines/app.machine.ts
Normal file
155
graph/client/src/app/machines/app.machine.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
import { ActorRef, createMachine, Machine, send, spawn } from 'xstate';
|
||||||
|
import {
|
||||||
|
ProjectGraphContext,
|
||||||
|
ProjectGraphEvents,
|
||||||
|
GraphPerfReport,
|
||||||
|
} from './interfaces';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import {
|
||||||
|
ProjectGraphDependency,
|
||||||
|
ProjectGraphProjectNode,
|
||||||
|
} from 'nx/src/config/project-graph';
|
||||||
|
import { projectGraphMachine } from './project-graph.machine';
|
||||||
|
import { taskGraphMachine, TaskGraphRecord } from './task-graph.machine';
|
||||||
|
|
||||||
|
export interface AppContext {
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
taskGraphs: TaskGraphRecord;
|
||||||
|
projectGraphActor: ActorRef<any>;
|
||||||
|
taskGraphActor: ActorRef<any>;
|
||||||
|
lastPerfReport: GraphPerfReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialContext: AppContext = {
|
||||||
|
projects: [],
|
||||||
|
dependencies: {},
|
||||||
|
affectedProjects: [],
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: '',
|
||||||
|
appsDir: '',
|
||||||
|
},
|
||||||
|
taskGraphs: {},
|
||||||
|
projectGraphActor: null,
|
||||||
|
taskGraphActor: null,
|
||||||
|
lastPerfReport: {
|
||||||
|
numEdges: 0,
|
||||||
|
numNodes: 0,
|
||||||
|
renderTime: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AppSchema {
|
||||||
|
states: {
|
||||||
|
idle: {};
|
||||||
|
initialized: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppEvents =
|
||||||
|
| {
|
||||||
|
type: 'setProjects';
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'setTaskGraphs';
|
||||||
|
taskGraphs: TaskGraphRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appMachine = createMachine<AppContext, AppEvents>(
|
||||||
|
{
|
||||||
|
predictableActionArguments: true,
|
||||||
|
id: 'App',
|
||||||
|
initial: 'idle',
|
||||||
|
context: initialContext,
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
entry: assign((ctx) => {
|
||||||
|
ctx.projectGraphActor = spawn(projectGraphMachine, {
|
||||||
|
name: 'projectGraphActor',
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.taskGraphActor = spawn(taskGraphMachine, {
|
||||||
|
name: 'taskGraphActor',
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
initialized: {},
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
setProjects: {
|
||||||
|
target: 'initialized',
|
||||||
|
actions: [
|
||||||
|
'setProjects',
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyProjectGraphSetProjects',
|
||||||
|
projects: ctx.projects,
|
||||||
|
dependencies: ctx.dependencies,
|
||||||
|
affectedProjects: ctx.affectedProjects,
|
||||||
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
to: (context) => context.projectGraphActor,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyTaskGraphSetProjects',
|
||||||
|
projects: ctx.projects,
|
||||||
|
taskGraphs: ctx.taskGraphs,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
to: (context) => context.taskGraphActor,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
setTaskGraphs: {
|
||||||
|
target: 'initialized',
|
||||||
|
actions: [
|
||||||
|
'setTaskGraphs',
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyTaskGraphSetTaskGraphs',
|
||||||
|
projects: ctx.projects,
|
||||||
|
taskGraphs: ctx.taskGraphs,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
to: (context) => context.taskGraphActor,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
setProjects: assign((ctx, event) => {
|
||||||
|
if (event.type !== 'setProjects') return;
|
||||||
|
|
||||||
|
ctx.projects = event.projects;
|
||||||
|
ctx.dependencies = event.dependencies;
|
||||||
|
ctx.workspaceLayout = event.workspaceLayout;
|
||||||
|
ctx.affectedProjects = event.affectedProjects;
|
||||||
|
}),
|
||||||
|
setTaskGraphs: assign((ctx, event) => {
|
||||||
|
if (event.type !== 'setTaskGraphs') return;
|
||||||
|
|
||||||
|
ctx.taskGraphs = event.taskGraphs;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { interpret, InterpreterStatus } from 'xstate';
|
|
||||||
import { depGraphMachine } from './dep-graph.machine';
|
|
||||||
|
|
||||||
// TODO: figure out what happened to make the interpret return type get so weird
|
|
||||||
let depGraphService = interpret(depGraphMachine, {
|
|
||||||
devTools: !!window.useXstateInspect,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function getDepGraphService() {
|
|
||||||
if (depGraphService.status === InterpreterStatus.NotStarted) {
|
|
||||||
depGraphService.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
return depGraphService;
|
|
||||||
}
|
|
||||||
@ -1,9 +1,12 @@
|
|||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { ProjectGraphDependency, ProjectGraphNode } from '@nrwl/devkit';
|
import type {
|
||||||
|
ProjectGraphDependency,
|
||||||
|
ProjectGraphProjectNode,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
import { interpret } from 'xstate';
|
import { interpret } from 'xstate';
|
||||||
import { depGraphMachine } from './dep-graph.machine';
|
import { projectGraphMachine } from './project-graph.machine';
|
||||||
|
|
||||||
export const mockProjects: ProjectGraphNode[] = [
|
export const mockProjects: ProjectGraphProjectNode[] = [
|
||||||
{
|
{
|
||||||
name: 'app1',
|
name: 'app1',
|
||||||
type: 'app',
|
type: 'app',
|
||||||
@ -94,13 +97,16 @@ export const mockDependencies: Record<string, ProjectGraphDependency[]> = {
|
|||||||
describe('dep-graph machine', () => {
|
describe('dep-graph machine', () => {
|
||||||
describe('initGraph', () => {
|
describe('initGraph', () => {
|
||||||
it('should set projects, dependencies, and workspaceLayout', () => {
|
it('should set projects, dependencies, and workspaceLayout', () => {
|
||||||
const result = depGraphMachine.transition(depGraphMachine.initialState, {
|
const result = projectGraphMachine.transition(
|
||||||
type: 'initGraph',
|
projectGraphMachine.initialState,
|
||||||
projects: mockProjects,
|
{
|
||||||
dependencies: mockDependencies,
|
type: 'notifyProjectGraphSetProjects',
|
||||||
affectedProjects: [],
|
projects: mockProjects,
|
||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
dependencies: mockDependencies,
|
||||||
});
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
}
|
||||||
|
);
|
||||||
expect(result.context.projects).toEqual(mockProjects);
|
expect(result.context.projects).toEqual(mockProjects);
|
||||||
expect(result.context.dependencies).toEqual(mockDependencies);
|
expect(result.context.dependencies).toEqual(mockDependencies);
|
||||||
expect(result.context.workspaceLayout).toEqual({
|
expect(result.context.workspaceLayout).toEqual({
|
||||||
@ -110,13 +116,16 @@ describe('dep-graph machine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should start with no projects selected', () => {
|
it('should start with no projects selected', () => {
|
||||||
const result = depGraphMachine.transition(depGraphMachine.initialState, {
|
const result = projectGraphMachine.transition(
|
||||||
type: 'initGraph',
|
projectGraphMachine.initialState,
|
||||||
projects: mockProjects,
|
{
|
||||||
dependencies: mockDependencies,
|
type: 'notifyProjectGraphSetProjects',
|
||||||
affectedProjects: [],
|
projects: mockProjects,
|
||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
dependencies: mockDependencies,
|
||||||
});
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.value).toEqual('unselected');
|
expect(result.value).toEqual('unselected');
|
||||||
expect(result.context.selectedProjects).toEqual([]);
|
expect(result.context.selectedProjects).toEqual([]);
|
||||||
@ -125,7 +134,7 @@ describe('dep-graph machine', () => {
|
|||||||
|
|
||||||
describe('selecting projects', () => {
|
describe('selecting projects', () => {
|
||||||
it('should select projects', (done) => {
|
it('should select projects', (done) => {
|
||||||
let service = interpret(depGraphMachine).onTransition((state) => {
|
let service = interpret(projectGraphMachine).onTransition((state) => {
|
||||||
if (
|
if (
|
||||||
state.matches('customSelected') &&
|
state.matches('customSelected') &&
|
||||||
state.context.selectedProjects.includes('app1') &&
|
state.context.selectedProjects.includes('app1') &&
|
||||||
@ -138,7 +147,7 @@ describe('dep-graph machine', () => {
|
|||||||
service.start();
|
service.start();
|
||||||
|
|
||||||
service.send({
|
service.send({
|
||||||
type: 'initGraph',
|
type: 'notifyProjectGraphSetProjects',
|
||||||
projects: mockProjects,
|
projects: mockProjects,
|
||||||
dependencies: mockDependencies,
|
dependencies: mockDependencies,
|
||||||
affectedProjects: [],
|
affectedProjects: [],
|
||||||
@ -159,7 +168,7 @@ describe('dep-graph machine', () => {
|
|||||||
|
|
||||||
describe('deselecting projects', () => {
|
describe('deselecting projects', () => {
|
||||||
it('should deselect projects', (done) => {
|
it('should deselect projects', (done) => {
|
||||||
let service = interpret(depGraphMachine).onTransition((state) => {
|
let service = interpret(projectGraphMachine).onTransition((state) => {
|
||||||
if (
|
if (
|
||||||
state.matches('customSelected') &&
|
state.matches('customSelected') &&
|
||||||
!state.context.selectedProjects.includes('app1') &&
|
!state.context.selectedProjects.includes('app1') &&
|
||||||
@ -172,7 +181,7 @@ describe('dep-graph machine', () => {
|
|||||||
service.start();
|
service.start();
|
||||||
|
|
||||||
service.send({
|
service.send({
|
||||||
type: 'initGraph',
|
type: 'notifyProjectGraphSetProjects',
|
||||||
projects: mockProjects,
|
projects: mockProjects,
|
||||||
dependencies: mockDependencies,
|
dependencies: mockDependencies,
|
||||||
affectedProjects: [],
|
affectedProjects: [],
|
||||||
@ -196,30 +205,33 @@ describe('dep-graph machine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should go to unselected when last project is deselected', () => {
|
it('should go to unselected when last project is deselected', () => {
|
||||||
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
let result = projectGraphMachine.transition(
|
||||||
type: 'initGraph',
|
projectGraphMachine.initialState,
|
||||||
projects: mockProjects,
|
{
|
||||||
dependencies: mockDependencies,
|
type: 'notifyProjectGraphSetProjects',
|
||||||
affectedProjects: [],
|
projects: mockProjects,
|
||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
dependencies: mockDependencies,
|
||||||
});
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'selectProject',
|
type: 'selectProject',
|
||||||
projectName: 'app1',
|
projectName: 'app1',
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'selectProject',
|
type: 'selectProject',
|
||||||
projectName: 'app2',
|
projectName: 'app2',
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'deselectProject',
|
type: 'deselectProject',
|
||||||
projectName: 'app1',
|
projectName: 'app1',
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'deselectProject',
|
type: 'deselectProject',
|
||||||
projectName: 'app2',
|
projectName: 'app2',
|
||||||
});
|
});
|
||||||
@ -231,15 +243,18 @@ describe('dep-graph machine', () => {
|
|||||||
|
|
||||||
describe('focusing projects', () => {
|
describe('focusing projects', () => {
|
||||||
it('should set the focused project', () => {
|
it('should set the focused project', () => {
|
||||||
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
let result = projectGraphMachine.transition(
|
||||||
type: 'initGraph',
|
projectGraphMachine.initialState,
|
||||||
projects: mockProjects,
|
{
|
||||||
dependencies: mockDependencies,
|
type: 'notifyProjectGraphSetProjects',
|
||||||
affectedProjects: [],
|
projects: mockProjects,
|
||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
dependencies: mockDependencies,
|
||||||
});
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'focusProject',
|
type: 'focusProject',
|
||||||
projectName: 'app1',
|
projectName: 'app1',
|
||||||
});
|
});
|
||||||
@ -249,7 +264,7 @@ describe('dep-graph machine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should select the projects by the focused project', (done) => {
|
it('should select the projects by the focused project', (done) => {
|
||||||
let service = interpret(depGraphMachine).onTransition((state) => {
|
let service = interpret(projectGraphMachine).onTransition((state) => {
|
||||||
if (
|
if (
|
||||||
state.matches('focused') &&
|
state.matches('focused') &&
|
||||||
state.context.selectedProjects.includes('app1') &&
|
state.context.selectedProjects.includes('app1') &&
|
||||||
@ -264,7 +279,7 @@ describe('dep-graph machine', () => {
|
|||||||
service.start();
|
service.start();
|
||||||
|
|
||||||
service.send({
|
service.send({
|
||||||
type: 'initGraph',
|
type: 'notifyProjectGraphSetProjects',
|
||||||
projects: mockProjects,
|
projects: mockProjects,
|
||||||
dependencies: mockDependencies,
|
dependencies: mockDependencies,
|
||||||
affectedProjects: [],
|
affectedProjects: [],
|
||||||
@ -278,20 +293,23 @@ describe('dep-graph machine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should select no projects on unfocus', () => {
|
it('should select no projects on unfocus', () => {
|
||||||
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
let result = projectGraphMachine.transition(
|
||||||
type: 'initGraph',
|
projectGraphMachine.initialState,
|
||||||
projects: mockProjects,
|
{
|
||||||
dependencies: mockDependencies,
|
type: 'notifyProjectGraphSetProjects',
|
||||||
affectedProjects: [],
|
projects: mockProjects,
|
||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
dependencies: mockDependencies,
|
||||||
});
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'focusProject',
|
type: 'focusProject',
|
||||||
projectName: 'app1',
|
projectName: 'app1',
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'unfocusProject',
|
type: 'unfocusProject',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -302,60 +320,63 @@ describe('dep-graph machine', () => {
|
|||||||
|
|
||||||
describe('search depth', () => {
|
describe('search depth', () => {
|
||||||
it('should not decrement search depth below 1', () => {
|
it('should not decrement search depth below 1', () => {
|
||||||
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
let result = projectGraphMachine.transition(
|
||||||
type: 'initGraph',
|
projectGraphMachine.initialState,
|
||||||
projects: mockProjects,
|
{
|
||||||
dependencies: mockDependencies,
|
type: 'notifyProjectGraphSetProjects',
|
||||||
affectedProjects: [],
|
projects: mockProjects,
|
||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
dependencies: mockDependencies,
|
||||||
});
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'filterByText',
|
type: 'filterByText',
|
||||||
search: 'app1',
|
search: 'app1',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepth).toEqual(1);
|
expect(result.context.searchDepth).toEqual(1);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'incrementSearchDepth',
|
type: 'incrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepth).toEqual(2);
|
expect(result.context.searchDepth).toEqual(2);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'incrementSearchDepth',
|
type: 'incrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'incrementSearchDepth',
|
type: 'incrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepth).toEqual(4);
|
expect(result.context.searchDepth).toEqual(4);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'decrementSearchDepth',
|
type: 'decrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'decrementSearchDepth',
|
type: 'decrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepth).toEqual(2);
|
expect(result.context.searchDepth).toEqual(2);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'decrementSearchDepth',
|
type: 'decrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepth).toEqual(1);
|
expect(result.context.searchDepth).toEqual(1);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'decrementSearchDepth',
|
type: 'decrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepth).toEqual(1);
|
expect(result.context.searchDepth).toEqual(1);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'decrementSearchDepth',
|
type: 'decrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -363,35 +384,38 @@ describe('dep-graph machine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should activate search depth if incremented or decremented', () => {
|
it('should activate search depth if incremented or decremented', () => {
|
||||||
let result = depGraphMachine.transition(depGraphMachine.initialState, {
|
let result = projectGraphMachine.transition(
|
||||||
type: 'initGraph',
|
projectGraphMachine.initialState,
|
||||||
projects: mockProjects,
|
{
|
||||||
dependencies: mockDependencies,
|
type: 'notifyProjectGraphSetProjects',
|
||||||
affectedProjects: [],
|
projects: mockProjects,
|
||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
dependencies: mockDependencies,
|
||||||
});
|
affectedProjects: [],
|
||||||
|
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'setSearchDepthEnabled',
|
type: 'setSearchDepthEnabled',
|
||||||
searchDepthEnabled: false,
|
searchDepthEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepthEnabled).toBe(false);
|
expect(result.context.searchDepthEnabled).toBe(false);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'incrementSearchDepth',
|
type: 'incrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepthEnabled).toBe(true);
|
expect(result.context.searchDepthEnabled).toBe(true);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'setSearchDepthEnabled',
|
type: 'setSearchDepthEnabled',
|
||||||
searchDepthEnabled: false,
|
searchDepthEnabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.context.searchDepthEnabled).toBe(false);
|
expect(result.context.searchDepthEnabled).toBe(false);
|
||||||
|
|
||||||
result = depGraphMachine.transition(result, {
|
result = projectGraphMachine.transition(result, {
|
||||||
type: 'decrementSearchDepth',
|
type: 'decrementSearchDepth',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { getDepGraphService } from './dep-graph.service';
|
import { getProjectGraphService } from './get-services';
|
||||||
|
|
||||||
export class ExternalApi {
|
export class ExternalApi {
|
||||||
depGraphService = getDepGraphService();
|
depGraphService = getProjectGraphService();
|
||||||
|
|
||||||
focusProject(projectName: string) {
|
focusProject(projectName: string) {
|
||||||
this.depGraphService.send({ type: 'focusProject', projectName });
|
this.depGraphService.send({ type: 'focusProject', projectName });
|
||||||
|
|||||||
25
graph/client/src/app/machines/get-services.ts
Normal file
25
graph/client/src/app/machines/get-services.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { interpret, InterpreterStatus } from 'xstate';
|
||||||
|
import { appMachine } from './app.machine';
|
||||||
|
|
||||||
|
let appService = interpret(appMachine, {
|
||||||
|
devTools: !!window.useXstateInspect,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getAppService() {
|
||||||
|
if (appService.status === InterpreterStatus.NotStarted) {
|
||||||
|
appService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return appService;
|
||||||
|
}
|
||||||
|
export function getProjectGraphService() {
|
||||||
|
const appService = getAppService();
|
||||||
|
const depGraphService = appService.getSnapshot().context.projectGraphActor;
|
||||||
|
return depGraphService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTaskGraphService() {
|
||||||
|
const appService = getAppService();
|
||||||
|
const taskGraph = appService.getSnapshot().context.taskGraphActor;
|
||||||
|
return taskGraph;
|
||||||
|
}
|
||||||
@ -4,9 +4,10 @@ import type {
|
|||||||
ProjectGraphProjectNode,
|
ProjectGraphProjectNode,
|
||||||
} from '@nrwl/devkit';
|
} from '@nrwl/devkit';
|
||||||
import { ActionObject, ActorRef, State, StateNodeConfig } from 'xstate';
|
import { ActionObject, ActorRef, State, StateNodeConfig } from 'xstate';
|
||||||
|
import { TaskGraphContext, TaskGraphEvents } from './task-graph.machine';
|
||||||
|
|
||||||
// The hierarchical (recursive) schema for the states
|
// The hierarchical (recursive) schema for the states
|
||||||
export interface DepGraphSchema {
|
export interface ProjectGraphSchema {
|
||||||
states: {
|
states: {
|
||||||
idle: {};
|
idle: {};
|
||||||
unselected: {};
|
unselected: {};
|
||||||
@ -26,7 +27,7 @@ export interface GraphPerfReport {
|
|||||||
export type TracingAlgorithmType = 'shortest' | 'all';
|
export type TracingAlgorithmType = 'shortest' | 'all';
|
||||||
// The events that the machine handles
|
// The events that the machine handles
|
||||||
|
|
||||||
export type DepGraphUIEvents =
|
export type ProjectGraphEvents =
|
||||||
| {
|
| {
|
||||||
type: 'setSelectedProjectsFromGraph';
|
type: 'setSelectedProjectsFromGraph';
|
||||||
selectedProjectNames: string[];
|
selectedProjectNames: string[];
|
||||||
@ -56,7 +57,7 @@ export type DepGraphUIEvents =
|
|||||||
| { type: 'filterByText'; search: string }
|
| { type: 'filterByText'; search: string }
|
||||||
| { type: 'clearTextFilter' }
|
| { type: 'clearTextFilter' }
|
||||||
| {
|
| {
|
||||||
type: 'initGraph';
|
type: 'notifyProjectGraphSetProjects';
|
||||||
projects: ProjectGraphProjectNode[];
|
projects: ProjectGraphProjectNode[];
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
affectedProjects: string[];
|
affectedProjects: string[];
|
||||||
@ -169,10 +170,8 @@ export type RouteEvents =
|
|||||||
algorithm: TracingAlgorithmType;
|
algorithm: TracingAlgorithmType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AllEvents = DepGraphUIEvents | GraphRenderEvents | RouteEvents;
|
|
||||||
|
|
||||||
// The context (extended state) of the machine
|
// The context (extended state) of the machine
|
||||||
export interface DepGraphContext {
|
export interface ProjectGraphContext {
|
||||||
projects: ProjectGraphProjectNode[];
|
projects: ProjectGraphProjectNode[];
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
affectedProjects: string[];
|
affectedProjects: string[];
|
||||||
@ -190,7 +189,7 @@ export interface DepGraphContext {
|
|||||||
};
|
};
|
||||||
graphActor: ActorRef<GraphRenderEvents>;
|
graphActor: ActorRef<GraphRenderEvents>;
|
||||||
routeSetterActor: ActorRef<RouteEvents>;
|
routeSetterActor: ActorRef<RouteEvents>;
|
||||||
routeListenerActor: ActorRef<DepGraphUIEvents>;
|
routeListenerActor: ActorRef<ProjectGraphEvents>;
|
||||||
lastPerfReport: GraphPerfReport;
|
lastPerfReport: GraphPerfReport;
|
||||||
tracing: {
|
tracing: {
|
||||||
start: string;
|
start: string;
|
||||||
@ -200,22 +199,32 @@ export interface DepGraphContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type DepGraphStateNodeConfig = StateNodeConfig<
|
export type DepGraphStateNodeConfig = StateNodeConfig<
|
||||||
DepGraphContext,
|
ProjectGraphContext,
|
||||||
{},
|
{},
|
||||||
DepGraphUIEvents,
|
ProjectGraphEvents,
|
||||||
ActionObject<DepGraphContext, DepGraphUIEvents>
|
ActionObject<ProjectGraphContext, ProjectGraphEvents>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type DepGraphSend = (
|
export type DepGraphSend = (
|
||||||
event: DepGraphUIEvents | DepGraphUIEvents[]
|
event: ProjectGraphEvents | ProjectGraphEvents[]
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export type DepGraphState = State<
|
export type DepGraphState = State<
|
||||||
DepGraphContext,
|
ProjectGraphContext,
|
||||||
DepGraphUIEvents,
|
ProjectGraphEvents,
|
||||||
any,
|
any,
|
||||||
{
|
{
|
||||||
value: any;
|
value: any;
|
||||||
context: DepGraphContext;
|
context: ProjectGraphContext;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TaskGraphState = State<
|
||||||
|
TaskGraphContext,
|
||||||
|
TaskGraphEvents,
|
||||||
|
any,
|
||||||
|
{
|
||||||
|
value: any;
|
||||||
|
context: TaskGraphContext;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import { assign } from '@xstate/immer';
|
import { assign } from '@xstate/immer';
|
||||||
import { Machine, send, spawn } from 'xstate';
|
import { createMachine, Machine, send, spawn } from 'xstate';
|
||||||
import { customSelectedStateConfig } from './custom-selected.state';
|
import { customSelectedStateConfig } from './custom-selected.state';
|
||||||
import { focusedStateConfig } from './focused.state';
|
import { focusedStateConfig } from './focused.state';
|
||||||
import { graphActor } from './graph.actor';
|
import { graphActor } from './graph.actor';
|
||||||
import {
|
import {
|
||||||
DepGraphContext,
|
ProjectGraphContext,
|
||||||
DepGraphSchema,
|
ProjectGraphSchema,
|
||||||
DepGraphUIEvents,
|
ProjectGraphEvents,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { createRouteMachine } from './route-setter.machine';
|
import { createRouteMachine } from './route-setter.machine';
|
||||||
import { textFilteredStateConfig } from './text-filtered.state';
|
import { textFilteredStateConfig } from './text-filtered.state';
|
||||||
import { tracingStateConfig } from './tracing.state';
|
import { tracingStateConfig } from './tracing.state';
|
||||||
import { unselectedStateConfig } from './unselected.state';
|
import { unselectedStateConfig } from './unselected.state';
|
||||||
|
|
||||||
export const initialContext: DepGraphContext = {
|
export const initialContext: ProjectGraphContext = {
|
||||||
projects: [],
|
projects: [],
|
||||||
dependencies: {},
|
dependencies: {},
|
||||||
affectedProjects: [],
|
affectedProjects: [],
|
||||||
@ -44,12 +44,12 @@ export const initialContext: DepGraphContext = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const depGraphMachine = Machine<
|
export const projectGraphMachine = createMachine<
|
||||||
DepGraphContext,
|
ProjectGraphContext,
|
||||||
DepGraphSchema,
|
ProjectGraphEvents
|
||||||
DepGraphUIEvents
|
|
||||||
>(
|
>(
|
||||||
{
|
{
|
||||||
|
predictableActionArguments: true,
|
||||||
id: 'DepGraph',
|
id: 'DepGraph',
|
||||||
initial: 'idle',
|
initial: 'idle',
|
||||||
context: initialContext,
|
context: initialContext,
|
||||||
@ -62,7 +62,7 @@ export const depGraphMachine = Machine<
|
|||||||
tracing: tracingStateConfig,
|
tracing: tracingStateConfig,
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
initGraph: {
|
notifyProjectGraphSetProjects: {
|
||||||
target: 'unselected',
|
target: 'unselected',
|
||||||
actions: [
|
actions: [
|
||||||
'setGraph',
|
'setGraph',
|
||||||
@ -284,7 +284,11 @@ export const depGraphMachine = Machine<
|
|||||||
ctx.includePath = event.includeProjectsByPath;
|
ctx.includePath = event.includeProjectsByPath;
|
||||||
}),
|
}),
|
||||||
setGraph: assign((ctx, event) => {
|
setGraph: assign((ctx, event) => {
|
||||||
if (event.type !== 'initGraph' && event.type !== 'updateGraph') return;
|
if (
|
||||||
|
event.type !== 'notifyProjectGraphSetProjects' &&
|
||||||
|
event.type !== 'updateGraph'
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
ctx.projects = event.projects;
|
ctx.projects = event.projects;
|
||||||
ctx.dependencies = event.dependencies;
|
ctx.dependencies = event.dependencies;
|
||||||
@ -293,7 +297,7 @@ export const depGraphMachine = Machine<
|
|||||||
name: 'route',
|
name: 'route',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (event.type === 'initGraph') {
|
if (event.type === 'notifyProjectGraphSetProjects') {
|
||||||
ctx.workspaceLayout = event.workspaceLayout;
|
ctx.workspaceLayout = event.workspaceLayout;
|
||||||
ctx.affectedProjects = event.affectedProjects;
|
ctx.affectedProjects = event.affectedProjects;
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import { InvokeCallback } from 'xstate';
|
import { InvokeCallback } from 'xstate';
|
||||||
import { DepGraphUIEvents } from './interfaces';
|
import { ProjectGraphEvents } from './interfaces';
|
||||||
|
|
||||||
function parseSearchParamsToEvents(searchParams: string): DepGraphUIEvents[] {
|
function parseSearchParamsToEvents(searchParams: string): ProjectGraphEvents[] {
|
||||||
const events: DepGraphUIEvents[] = [];
|
const events: ProjectGraphEvents[] = [];
|
||||||
const params = new URLSearchParams(searchParams);
|
const params = new URLSearchParams(searchParams);
|
||||||
|
|
||||||
params.forEach((value, key) => {
|
params.forEach((value, key) => {
|
||||||
@ -60,8 +60,8 @@ function parseSearchParamsToEvents(searchParams: string): DepGraphUIEvents[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const routeListener: InvokeCallback<
|
export const routeListener: InvokeCallback<
|
||||||
DepGraphUIEvents,
|
ProjectGraphEvents,
|
||||||
DepGraphUIEvents
|
ProjectGraphEvents
|
||||||
> = (callback) => {
|
> = (callback) => {
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { assign } from '@xstate/immer';
|
import { assign } from '@xstate/immer';
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import { Machine } from 'xstate';
|
import { createMachine, Machine } from 'xstate';
|
||||||
import { RouteEvents } from './interfaces';
|
import {
|
||||||
|
ProjectGraphContext,
|
||||||
|
ProjectGraphEvents,
|
||||||
|
RouteEvents,
|
||||||
|
} from './interfaces';
|
||||||
|
|
||||||
type ParamKeys =
|
type ParamKeys =
|
||||||
| 'focus'
|
| 'focus'
|
||||||
@ -26,6 +30,11 @@ function reduceParamRecordToQueryString(params: ParamRecord): string {
|
|||||||
return new URLSearchParams(newParams).toString();
|
return new URLSearchParams(newParams).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RouteSetterContext {
|
||||||
|
currentParamString: string;
|
||||||
|
params: Record<ParamKeys, string | null>;
|
||||||
|
}
|
||||||
|
|
||||||
export const createRouteMachine = () => {
|
export const createRouteMachine = () => {
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
@ -41,17 +50,14 @@ export const createRouteMachine = () => {
|
|||||||
traceAlgorithm: params.get('traceAlgorithm'),
|
traceAlgorithm: params.get('traceAlgorithm'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialContext = {
|
const initialContext: RouteSetterContext = {
|
||||||
currentParamString: reduceParamRecordToQueryString(paramRecord),
|
currentParamString: reduceParamRecordToQueryString(paramRecord),
|
||||||
params: paramRecord,
|
params: paramRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Machine<
|
return createMachine<RouteSetterContext, RouteEvents>(
|
||||||
{ currentParamString: string; params: Record<ParamKeys, string | null> },
|
|
||||||
{},
|
|
||||||
RouteEvents
|
|
||||||
>(
|
|
||||||
{
|
{
|
||||||
|
predictableActionArguments: true,
|
||||||
id: 'route',
|
id: 'route',
|
||||||
context: {
|
context: {
|
||||||
currentParamString: '',
|
currentParamString: '',
|
||||||
|
|||||||
75
graph/client/src/app/machines/task-graph.machine.ts
Normal file
75
graph/client/src/app/machines/task-graph.machine.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { createMachine } from 'xstate';
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
|
||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
|
||||||
|
export type TaskGraphRecord = Record<
|
||||||
|
string,
|
||||||
|
Record<string, Record<string, any>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface TaskGraphContext {
|
||||||
|
selectedTaskId: string;
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
taskGraphs: TaskGraphRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialContext: TaskGraphContext = {
|
||||||
|
selectedTaskId: null,
|
||||||
|
projects: [],
|
||||||
|
taskGraphs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaskGraphEvents =
|
||||||
|
| {
|
||||||
|
type: 'notifyTaskGraphSetProjects';
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
taskGraphs: TaskGraphRecord;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'selectTask';
|
||||||
|
taskId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const taskGraphMachine = createMachine<
|
||||||
|
TaskGraphContext,
|
||||||
|
TaskGraphEvents
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
predictableActionArguments: true,
|
||||||
|
initial: 'idle',
|
||||||
|
context: initialContext,
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
notifyTaskGraphSetProjects: {
|
||||||
|
actions: ['setProjects'],
|
||||||
|
target: 'initialized',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initialized: {
|
||||||
|
on: {
|
||||||
|
selectTask: {
|
||||||
|
actions: ['selectTask'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
setProjects: assign((ctx, event) => {
|
||||||
|
if (event.type !== 'notifyTaskGraphSetProjects') return;
|
||||||
|
|
||||||
|
ctx.projects = event.projects;
|
||||||
|
ctx.taskGraphs = event.taskGraphs;
|
||||||
|
}),
|
||||||
|
selectTask: assign((ctx, event) => {
|
||||||
|
if (event.type !== 'selectTask') return;
|
||||||
|
|
||||||
|
ctx.selectedTaskId = event.taskId;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
@ -4,11 +4,14 @@ import type {
|
|||||||
ProjectGraphProjectNode,
|
ProjectGraphProjectNode,
|
||||||
} from '@nrwl/devkit';
|
} from '@nrwl/devkit';
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { DepGraphClientResponse } from 'nx/src/command-line/dep-graph';
|
import type {
|
||||||
|
DepGraphClientResponse,
|
||||||
|
TaskGraphClientResponse,
|
||||||
|
} from 'nx/src/command-line/dep-graph';
|
||||||
import { ProjectGraphService } from '../app/interfaces';
|
import { ProjectGraphService } from '../app/interfaces';
|
||||||
|
|
||||||
export class MockProjectGraphService implements ProjectGraphService {
|
export class MockProjectGraphService implements ProjectGraphService {
|
||||||
private response: DepGraphClientResponse = {
|
private projectGraphsResponse: DepGraphClientResponse = {
|
||||||
hash: '79054025255fb1a26e4bc422aef54eb4',
|
hash: '79054025255fb1a26e4bc422aef54eb4',
|
||||||
layout: {
|
layout: {
|
||||||
appsDir: 'apps',
|
appsDir: 'apps',
|
||||||
@ -56,21 +59,27 @@ export class MockProjectGraphService implements ProjectGraphService {
|
|||||||
groupByFolder: false,
|
groupByFolder: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private taskGraphsResponse: TaskGraphClientResponse = { dependencies: {} };
|
||||||
|
|
||||||
constructor(updateFrequency: number = 5000) {
|
constructor(updateFrequency: number = 5000) {
|
||||||
setInterval(() => this.updateResponse(), updateFrequency);
|
setInterval(() => this.updateResponse(), updateFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getHash(): Promise<string> {
|
async getHash(): Promise<string> {
|
||||||
return new Promise((resolve) => resolve(this.response.hash));
|
return new Promise((resolve) => resolve(this.projectGraphsResponse.hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjectGraph(url: string): Promise<DepGraphClientResponse> {
|
getProjectGraph(url: string): Promise<DepGraphClientResponse> {
|
||||||
return new Promise((resolve) => resolve(this.response));
|
return new Promise((resolve) => resolve(this.projectGraphsResponse));
|
||||||
|
}
|
||||||
|
|
||||||
|
getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
||||||
|
return new Promise((resolve) => resolve(this.taskGraphsResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
private createNewProject(): ProjectGraphProjectNode {
|
private createNewProject(): ProjectGraphProjectNode {
|
||||||
const type = Math.random() > 0.25 ? 'lib' : 'app';
|
const type = Math.random() > 0.25 ? 'lib' : 'app';
|
||||||
const name = `${type}-${this.response.projects.length + 1}`;
|
const name = `${type}-${this.projectGraphsResponse.projects.length + 1}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@ -85,7 +94,7 @@ export class MockProjectGraphService implements ProjectGraphService {
|
|||||||
|
|
||||||
private updateResponse() {
|
private updateResponse() {
|
||||||
const newProject = this.createNewProject();
|
const newProject = this.createNewProject();
|
||||||
const libProjects = this.response.projects.filter(
|
const libProjects = this.projectGraphsResponse.projects.filter(
|
||||||
(project) => project.type === 'lib'
|
(project) => project.type === 'lib'
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -99,11 +108,11 @@ export class MockProjectGraphService implements ProjectGraphService {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
this.response = {
|
this.projectGraphsResponse = {
|
||||||
...this.response,
|
...this.projectGraphsResponse,
|
||||||
projects: [...this.response.projects, newProject],
|
projects: [...this.projectGraphsResponse.projects, newProject],
|
||||||
dependencies: {
|
dependencies: {
|
||||||
...this.response.dependencies,
|
...this.projectGraphsResponse.dependencies,
|
||||||
[newProject.name]: newDependency,
|
[newProject.name]: newDependency,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { redirect } from 'react-router-dom';
|
|||||||
import ProjectsSidebar from './feature-projects/projects-sidebar';
|
import ProjectsSidebar from './feature-projects/projects-sidebar';
|
||||||
import TasksSidebar from './feature-tasks/tasks-sidebar';
|
import TasksSidebar from './feature-tasks/tasks-sidebar';
|
||||||
import { getEnvironmentConfig } from './hooks/use-environment-config';
|
import { getEnvironmentConfig } from './hooks/use-environment-config';
|
||||||
import { getDepGraphService } from './machines/dep-graph.service';
|
import { getProjectGraphService } from './machines/get-services';
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import { DepGraphClientResponse } from 'nx/src/command-line/dep-graph';
|
import { DepGraphClientResponse } from 'nx/src/command-line/dep-graph';
|
||||||
import { getProjectGraphDataService } from './hooks/get-project-graph-data-service';
|
import { getProjectGraphDataService } from './hooks/get-project-graph-data-service';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ArrowLeftCircleIcon,
|
|
||||||
ArrowDownTrayIcon,
|
ArrowDownTrayIcon,
|
||||||
|
ArrowLeftCircleIcon,
|
||||||
InformationCircleIcon,
|
InformationCircleIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import Tippy from '@tippyjs/react';
|
import Tippy from '@tippyjs/react';
|
||||||
@ -10,8 +10,7 @@ import type { DepGraphClientResponse } from 'nx/src/command-line/dep-graph';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
||||||
|
|
||||||
import DebuggerPanel from './debugger-panel';
|
import DebuggerPanel from './ui-components/debugger-panel';
|
||||||
import { useDepGraphService } from './hooks/use-dep-graph';
|
|
||||||
import { useDepGraphSelector } from './hooks/use-dep-graph-selector';
|
import { useDepGraphSelector } from './hooks/use-dep-graph-selector';
|
||||||
import { useEnvironmentConfig } from './hooks/use-environment-config';
|
import { useEnvironmentConfig } from './hooks/use-environment-config';
|
||||||
import { useIntervalWhen } from './hooks/use-interval-when';
|
import { useIntervalWhen } from './hooks/use-interval-when';
|
||||||
@ -21,27 +20,19 @@ import {
|
|||||||
lastPerfReportSelector,
|
lastPerfReportSelector,
|
||||||
projectIsSelectedSelector,
|
projectIsSelectedSelector,
|
||||||
} from './machines/selectors';
|
} from './machines/selectors';
|
||||||
import ProjectsSidebar from './feature-projects/projects-sidebar';
|
|
||||||
import { selectValueByThemeStatic } from './theme-resolver';
|
import { selectValueByThemeStatic } from './theme-resolver';
|
||||||
import { getTooltipService } from './tooltip-service';
|
|
||||||
import ProjectNodeToolTip from './project-node-tooltip';
|
|
||||||
import EdgeNodeTooltip from './edge-tooltip';
|
|
||||||
import { Outlet, useNavigate } from 'react-router-dom';
|
import { Outlet, useNavigate } from 'react-router-dom';
|
||||||
import ThemePanel from './sidebar/theme-panel';
|
import ThemePanel from './feature-projects/panels/theme-panel';
|
||||||
import Dropdown from './ui-components/dropdown';
|
import Dropdown from './ui-components/dropdown';
|
||||||
import { useCurrentPath } from './hooks/use-current-path';
|
import { useCurrentPath } from './hooks/use-current-path';
|
||||||
import ExperimentalFeature from './experimental-feature';
|
import ExperimentalFeature from './experimental-feature';
|
||||||
import RankdirPanel from './sidebar/rankdir-panel';
|
import RankdirPanel from './feature-projects/panels/rankdir-panel';
|
||||||
|
import { getAppService, getProjectGraphService } from './machines/get-services';
|
||||||
const tooltipService = getTooltipService();
|
import TooltipDisplay from './ui-tooltips/graph-tooltip-display';
|
||||||
|
|
||||||
export function Shell(): JSX.Element {
|
export function Shell(): JSX.Element {
|
||||||
const depGraphService = useDepGraphService();
|
const appService = getAppService();
|
||||||
|
const depGraphService = getProjectGraphService();
|
||||||
const currentTooltip = useSyncExternalStore(
|
|
||||||
(callback) => tooltipService.subscribe(callback),
|
|
||||||
() => tooltipService.currentTooltip
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectGraphService = getProjectGraphDataService();
|
const projectGraphService = getProjectGraphDataService();
|
||||||
const environment = useEnvironmentConfig();
|
const environment = useEnvironmentConfig();
|
||||||
@ -50,7 +41,7 @@ export function Shell(): JSX.Element {
|
|||||||
const environmentConfig = useEnvironmentConfig();
|
const environmentConfig = useEnvironmentConfig();
|
||||||
|
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState<string>(
|
const [selectedProjectId, setSelectedProjectId] = useState<string>(
|
||||||
environment.appConfig.defaultProjectGraph
|
environment.appConfig.defaultProject
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -71,36 +62,54 @@ export function Shell(): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { appConfig } = environment;
|
const { appConfig } = environment;
|
||||||
|
|
||||||
const projectInfo = appConfig.projectGraphs.find(
|
const projectInfo = appConfig.projects.find(
|
||||||
(graph) => graph.id === selectedProjectId
|
(graph) => graph.id === selectedProjectId
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchProjectGraph = async () => {
|
const fetchProjectGraph = async () => {
|
||||||
const project: DepGraphClientResponse =
|
const projectGraph: DepGraphClientResponse =
|
||||||
await projectGraphService.getProjectGraph(projectInfo.url);
|
await projectGraphService.getProjectGraph(projectInfo.projectGraphUrl);
|
||||||
|
|
||||||
const workspaceLayout = project?.layout;
|
const workspaceLayout = projectGraph?.layout;
|
||||||
|
|
||||||
depGraphService.send({
|
appService.send({
|
||||||
type: 'initGraph',
|
type: 'setProjects',
|
||||||
projects: project.projects,
|
projects: projectGraph.projects,
|
||||||
dependencies: project.dependencies,
|
dependencies: projectGraph.dependencies,
|
||||||
affectedProjects: project.affected,
|
affectedProjects: projectGraph.affected,
|
||||||
workspaceLayout: workspaceLayout,
|
workspaceLayout: workspaceLayout,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchTaskGraphs = async () => {
|
||||||
|
const taskGraphs = await projectGraphService.getTaskGraph(
|
||||||
|
projectInfo.taskGraphUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
appService.send({
|
||||||
|
type: 'setTaskGraphs',
|
||||||
|
taskGraphs: taskGraphs.dependencies,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
fetchProjectGraph();
|
fetchProjectGraph();
|
||||||
}, [selectedProjectId, environment, depGraphService, projectGraphService]);
|
|
||||||
|
if (currentRoute === '/tasks') {
|
||||||
|
fetchTaskGraphs();
|
||||||
|
}
|
||||||
|
}, [selectedProjectId, environment, appService, projectGraphService]);
|
||||||
|
|
||||||
useIntervalWhen(
|
useIntervalWhen(
|
||||||
() => {
|
() => {
|
||||||
const projectInfo = environment.appConfig.projectGraphs.find(
|
const projectInfo = environment.appConfig.projects.find(
|
||||||
(graph) => graph.id === selectedProjectId
|
(graph) => graph.id === selectedProjectId
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchProjectGraph = async () => {
|
const fetchProjectGraph = async () => {
|
||||||
const project: DepGraphClientResponse =
|
const project: DepGraphClientResponse =
|
||||||
await projectGraphService.getProjectGraph(projectInfo.url);
|
await projectGraphService.getProjectGraph(
|
||||||
|
projectInfo.projectGraphUrl
|
||||||
|
);
|
||||||
|
|
||||||
depGraphService.send({
|
depGraphService.send({
|
||||||
type: 'updateGraph',
|
type: 'updateGraph',
|
||||||
@ -205,10 +214,10 @@ export function Shell(): JSX.Element {
|
|||||||
>
|
>
|
||||||
{environment.appConfig.showDebugger ? (
|
{environment.appConfig.showDebugger ? (
|
||||||
<DebuggerPanel
|
<DebuggerPanel
|
||||||
projectGraphs={environment.appConfig.projectGraphs}
|
projects={environment.appConfig.projects}
|
||||||
selectedProjectGraph={selectedProjectId}
|
selectedProject={selectedProjectId}
|
||||||
lastPerfReport={lastPerfReport}
|
lastPerfReport={lastPerfReport}
|
||||||
projectGraphChange={projectChange}
|
selectedProjectChange={projectChange}
|
||||||
></DebuggerPanel>
|
></DebuggerPanel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -223,25 +232,7 @@ export function Shell(): JSX.Element {
|
|||||||
) : null}
|
) : null}
|
||||||
<div id="graph-container">
|
<div id="graph-container">
|
||||||
<div id="cytoscape-graph"></div>
|
<div id="cytoscape-graph"></div>
|
||||||
{currentTooltip ? (
|
<TooltipDisplay></TooltipDisplay>
|
||||||
<Tippy
|
|
||||||
content={
|
|
||||||
currentTooltip.type === 'node' ? (
|
|
||||||
<ProjectNodeToolTip
|
|
||||||
{...currentTooltip.props}
|
|
||||||
></ProjectNodeToolTip>
|
|
||||||
) : (
|
|
||||||
<EdgeNodeTooltip {...currentTooltip.props}></EdgeNodeTooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
visible={true}
|
|
||||||
getReferenceClientRect={currentTooltip.ref.getBoundingClientRect}
|
|
||||||
theme={selectValueByThemeStatic('dark-nx', 'nx')}
|
|
||||||
interactive={true}
|
|
||||||
appendTo={document.body}
|
|
||||||
maxWidth="none"
|
|
||||||
></Tippy>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Tippy
|
<Tippy
|
||||||
content="Download Graph as PNG"
|
content="Download Graph as PNG"
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { InterpreterFrom } from 'xstate';
|
import { InterpreterFrom } from 'xstate';
|
||||||
import { depGraphMachine } from './machines/dep-graph.machine';
|
import { projectGraphMachine } from './machines/project-graph.machine';
|
||||||
import { getDepGraphService } from './machines/dep-graph.service';
|
import { getProjectGraphService } from './machines/get-services';
|
||||||
|
|
||||||
export const GlobalStateContext = createContext<
|
export const GlobalStateContext = createContext<
|
||||||
InterpreterFrom<typeof depGraphMachine>
|
InterpreterFrom<typeof projectGraphMachine>
|
||||||
>({} as InterpreterFrom<typeof depGraphMachine>);
|
>({} as InterpreterFrom<typeof projectGraphMachine>);
|
||||||
|
|
||||||
export const GlobalStateProvider = (props) => {
|
export const GlobalStateProvider = (props) => {
|
||||||
const depGraphService = getDepGraphService();
|
const depGraphService = getProjectGraphService();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlobalStateContext.Provider value={depGraphService as any}>
|
<GlobalStateContext.Provider value={depGraphService as any}>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export default {
|
|||||||
component: DebuggerPanel,
|
component: DebuggerPanel,
|
||||||
title: 'Shell/DebuggerPanel',
|
title: 'Shell/DebuggerPanel',
|
||||||
argTypes: {
|
argTypes: {
|
||||||
projectGraphChange: { action: 'projectGraphChange' },
|
selectedProjectChange: { action: 'projectGraphChange' },
|
||||||
},
|
},
|
||||||
} as ComponentMeta<typeof DebuggerPanel>;
|
} as ComponentMeta<typeof DebuggerPanel>;
|
||||||
|
|
||||||
@ -1,19 +1,19 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { ProjectGraphList } from './interfaces';
|
import { GraphListItem } from '../interfaces';
|
||||||
import { GraphPerfReport } from './machines/interfaces';
|
import { GraphPerfReport } from '../machines/interfaces';
|
||||||
import Dropdown from './ui-components/dropdown';
|
import Dropdown from './dropdown';
|
||||||
|
|
||||||
export interface DebuggerPanelProps {
|
export interface DebuggerPanelProps {
|
||||||
projectGraphs: ProjectGraphList[];
|
projects: GraphListItem[];
|
||||||
selectedProjectGraph: string;
|
selectedProject: string;
|
||||||
projectGraphChange: (projectName: string) => void;
|
selectedProjectChange: (projectName: string) => void;
|
||||||
lastPerfReport: GraphPerfReport;
|
lastPerfReport: GraphPerfReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DebuggerPanel = memo(function ({
|
export const DebuggerPanel = memo(function ({
|
||||||
projectGraphs,
|
projects,
|
||||||
selectedProjectGraph,
|
selectedProject,
|
||||||
projectGraphChange,
|
selectedProjectChange,
|
||||||
lastPerfReport,
|
lastPerfReport,
|
||||||
}: DebuggerPanelProps) {
|
}: DebuggerPanelProps) {
|
||||||
return (
|
return (
|
||||||
@ -26,14 +26,14 @@ export const DebuggerPanel = memo(function ({
|
|||||||
</h4>
|
</h4>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data-cy="project-select"
|
data-cy="project-select"
|
||||||
onChange={(event) => projectGraphChange(event.currentTarget.value)}
|
onChange={(event) => selectedProjectChange(event.currentTarget.value)}
|
||||||
>
|
>
|
||||||
{projectGraphs.map((projectGraph) => {
|
{projects.map((projectGraph) => {
|
||||||
return (
|
return (
|
||||||
<option
|
<option
|
||||||
key={projectGraph.id}
|
key={projectGraph.id}
|
||||||
value={projectGraph.id}
|
value={projectGraph.id}
|
||||||
selected={projectGraph.id === selectedProjectGraph}
|
selected={projectGraph.id === selectedProject}
|
||||||
>
|
>
|
||||||
{projectGraph.label}
|
{projectGraph.label}
|
||||||
</option>
|
</option>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
import { EdgeNodeTooltip, EdgeNodeTooltipProps } from './edge-tooltip';
|
import { EdgeNodeTooltip, EdgeNodeTooltipProps } from './edge-tooltip';
|
||||||
import ProjectNodeToolTip from './project-node-tooltip';
|
import ProjectNodeToolTip from './project-node-tooltip';
|
||||||
import { selectValueByThemeStatic } from './theme-resolver';
|
import { selectValueByThemeStatic } from '../theme-resolver';
|
||||||
import Tippy from '@tippyjs/react';
|
import Tippy from '@tippyjs/react';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import Tag from './ui-components/tag';
|
import Tag from '../ui-components/tag';
|
||||||
|
|
||||||
export interface EdgeNodeTooltipProps {
|
export interface EdgeNodeTooltipProps {
|
||||||
type: string;
|
type: string;
|
||||||
35
graph/client/src/app/ui-tooltips/graph-tooltip-display.tsx
Normal file
35
graph/client/src/app/ui-tooltips/graph-tooltip-display.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import Tippy from '@tippyjs/react';
|
||||||
|
import ProjectNodeToolTip from './project-node-tooltip';
|
||||||
|
import EdgeNodeTooltip from './edge-tooltip';
|
||||||
|
import { selectValueByThemeStatic } from '../theme-resolver';
|
||||||
|
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
||||||
|
import { getTooltipService } from './tooltip-service';
|
||||||
|
|
||||||
|
const tooltipService = getTooltipService();
|
||||||
|
|
||||||
|
export function TooltipDisplay() {
|
||||||
|
const currentTooltip = useSyncExternalStore(
|
||||||
|
(callback) => tooltipService.subscribe(callback),
|
||||||
|
() => tooltipService.currentTooltip
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentTooltip ? (
|
||||||
|
<Tippy
|
||||||
|
content={
|
||||||
|
currentTooltip.type === 'node' ? (
|
||||||
|
<ProjectNodeToolTip {...currentTooltip.props}></ProjectNodeToolTip>
|
||||||
|
) : (
|
||||||
|
<EdgeNodeTooltip {...currentTooltip.props}></EdgeNodeTooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
visible={true}
|
||||||
|
getReferenceClientRect={currentTooltip.ref.getBoundingClientRect}
|
||||||
|
theme={selectValueByThemeStatic('dark-nx', 'nx')}
|
||||||
|
interactive={true}
|
||||||
|
appendTo={document.body}
|
||||||
|
maxWidth="none"
|
||||||
|
></Tippy>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TooltipDisplay;
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { getDepGraphService } from './machines/dep-graph.service';
|
import { getProjectGraphService } from '../machines/get-services';
|
||||||
import {
|
import {
|
||||||
DocumentMagnifyingGlassIcon,
|
DocumentMagnifyingGlassIcon,
|
||||||
FlagIcon,
|
FlagIcon,
|
||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import Tag from './ui-components/tag';
|
import Tag from '../ui-components/tag';
|
||||||
|
|
||||||
export interface ProjectNodeToolTipProps {
|
export interface ProjectNodeToolTipProps {
|
||||||
type: 'app' | 'lib' | 'e2e';
|
type: 'app' | 'lib' | 'e2e';
|
||||||
@ -17,7 +17,7 @@ export function ProjectNodeToolTip({
|
|||||||
id,
|
id,
|
||||||
tags,
|
tags,
|
||||||
}: ProjectNodeToolTipProps) {
|
}: ProjectNodeToolTipProps) {
|
||||||
const depGraphService = getDepGraphService();
|
const depGraphService = getProjectGraphService();
|
||||||
|
|
||||||
function onFocus() {
|
function onFocus() {
|
||||||
depGraphService.send({
|
depGraphService.send({
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { getGraphService } from './machines/graph.service';
|
import { getGraphService } from '../machines/graph.service';
|
||||||
|
|
||||||
import { VirtualElement } from '@popperjs/core';
|
import { VirtualElement } from '@popperjs/core';
|
||||||
import { ProjectNodeToolTipProps } from './project-node-tooltip';
|
import { ProjectNodeToolTipProps } from './project-node-tooltip';
|
||||||
@ -6,17 +6,19 @@ window.useXstateInspect = false;
|
|||||||
window.appConfig = {
|
window.appConfig = {
|
||||||
showDebugger: true,
|
showDebugger: true,
|
||||||
showExperimentalFeatures: true,
|
showExperimentalFeatures: true,
|
||||||
projectGraphs: [
|
projects: [
|
||||||
{
|
{
|
||||||
id: 'e2e',
|
id: 'e2e',
|
||||||
label: 'e2e',
|
label: 'e2e',
|
||||||
url: 'assets/graphs/e2e.json',
|
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
||||||
|
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'affected',
|
id: 'affected',
|
||||||
label: 'affected',
|
label: 'affected',
|
||||||
url: 'assets/graphs/affected.json',
|
projectGraphUrl: 'assets/project-graphs/affected.json',
|
||||||
|
taskGraphUrl: 'assets/task-graphs/affected.json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultProjectGraph: 'e2e',
|
defaultProject: 'e2e',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,12 +6,13 @@ window.useXstateInspect = false;
|
|||||||
window.appConfig = {
|
window.appConfig = {
|
||||||
showDebugger: false,
|
showDebugger: false,
|
||||||
showExperimentalFeatures: false,
|
showExperimentalFeatures: false,
|
||||||
projectGraphs: [
|
projects: [
|
||||||
{
|
{
|
||||||
id: 'local',
|
id: 'local',
|
||||||
label: 'local',
|
label: 'local',
|
||||||
url: 'assets/graphs/e2e.json',
|
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
||||||
|
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultProjectGraph: 'local',
|
defaultProject: 'local',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,14 +7,15 @@ window.localMode = 'build';
|
|||||||
window.appConfig = {
|
window.appConfig = {
|
||||||
showDebugger: false,
|
showDebugger: false,
|
||||||
showExperimentalFeatures: false,
|
showExperimentalFeatures: false,
|
||||||
projectGraphs: [
|
projects: [
|
||||||
{
|
{
|
||||||
id: 'local',
|
id: 'local',
|
||||||
label: 'local',
|
label: 'local',
|
||||||
url: 'assets/graphs/e2e.json',
|
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
||||||
|
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultProjectGraph: 'local',
|
defaultProject: 'local',
|
||||||
};
|
};
|
||||||
|
|
||||||
window.projectGraphResponse = {
|
window.projectGraphResponse = {
|
||||||
|
|||||||
@ -6,12 +6,13 @@ window.useXstateInspect = false;
|
|||||||
window.appConfig = {
|
window.appConfig = {
|
||||||
showDebugger: false,
|
showDebugger: false,
|
||||||
showExperimentalFeatures: false,
|
showExperimentalFeatures: false,
|
||||||
projectGraphs: [
|
projects: [
|
||||||
{
|
{
|
||||||
id: 'local',
|
id: 'local',
|
||||||
label: 'local',
|
label: 'local',
|
||||||
url: 'assets/graphs/e2e.json',
|
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
||||||
|
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultProjectGraph: 'local',
|
defaultProject: 'local',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,12 +6,13 @@ window.useXstateInspect = false;
|
|||||||
window.appConfig = {
|
window.appConfig = {
|
||||||
showDebugger: false,
|
showDebugger: false,
|
||||||
showExperimentalFeatures: true,
|
showExperimentalFeatures: true,
|
||||||
projectGraphs: [
|
projects: [
|
||||||
{
|
{
|
||||||
id: 'local',
|
id: 'local',
|
||||||
label: 'local',
|
label: 'local',
|
||||||
url: 'project-graph.json',
|
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
||||||
|
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultProjectGraph: 'local',
|
defaultProject: 'local',
|
||||||
};
|
};
|
||||||
|
|||||||
6
graph/client/src/globals.d.ts
vendored
6
graph/client/src/globals.d.ts
vendored
@ -1,5 +1,8 @@
|
|||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { DepGraphClientResponse } from 'nx/src/command-line/dep-graph';
|
import type {
|
||||||
|
DepGraphClientResponse,
|
||||||
|
TaskGraphClientResponse,
|
||||||
|
} from 'nx/src/command-line/dep-graph';
|
||||||
import { AppConfig } from './app/interfaces';
|
import { AppConfig } from './app/interfaces';
|
||||||
import { ExternalApi } from './app/machines/externalApi';
|
import { ExternalApi } from './app/machines/externalApi';
|
||||||
|
|
||||||
@ -9,6 +12,7 @@ export declare global {
|
|||||||
watch: boolean;
|
watch: boolean;
|
||||||
localMode: 'serve' | 'build';
|
localMode: 'serve' | 'build';
|
||||||
projectGraphResponse?: DepGraphClientResponse;
|
projectGraphResponse?: DepGraphClientResponse;
|
||||||
|
taskGraphResponse?: TaskGraphClientResponse;
|
||||||
environment: 'dev' | 'watch' | 'release' | 'nx-console';
|
environment: 'dev' | 'watch' | 'release' | 'nx-console';
|
||||||
appConfig: AppConfig;
|
appConfig: AppConfig;
|
||||||
useXstateInspect: boolean;
|
useXstateInspect: boolean;
|
||||||
|
|||||||
@ -1,94 +1,42 @@
|
|||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type {
|
import { CollectionReturnValue, use } from 'cytoscape';
|
||||||
ProjectGraphDependency,
|
|
||||||
ProjectGraphProjectNode,
|
|
||||||
} from '@nrwl/devkit';
|
|
||||||
import type { VirtualElement } from '@popperjs/core';
|
|
||||||
import cy from 'cytoscape';
|
|
||||||
import cytoscapeDagre from 'cytoscape-dagre';
|
import cytoscapeDagre from 'cytoscape-dagre';
|
||||||
import popper from 'cytoscape-popper';
|
import popper from 'cytoscape-popper';
|
||||||
import { edgeStyles, nodeStyles } from './styles-graph';
|
|
||||||
import {
|
|
||||||
CytoscapeDagreConfig,
|
|
||||||
ParentNode,
|
|
||||||
ProjectEdge,
|
|
||||||
ProjectNode,
|
|
||||||
} from './util-cytoscape';
|
|
||||||
import { GraphPerfReport, GraphRenderEvents } from './interfaces';
|
import { GraphPerfReport, GraphRenderEvents } from './interfaces';
|
||||||
import {
|
|
||||||
darkModeScratchKey,
|
|
||||||
switchValueByDarkMode,
|
|
||||||
} from './styles-graph/dark-mode';
|
|
||||||
import { GraphInteractionEvents } from './graph-interaction-events';
|
import { GraphInteractionEvents } from './graph-interaction-events';
|
||||||
|
import { RenderGraph } from './util-cytoscape/render-graph';
|
||||||
const cytoscapeDagreConfig = {
|
import { ProjectTraversalGraph } from './util-cytoscape/project-traversal-graph';
|
||||||
name: 'dagre',
|
|
||||||
nodeDimensionsIncludeLabels: true,
|
|
||||||
rankSep: 75,
|
|
||||||
rankDir: 'TB',
|
|
||||||
edgeSep: 50,
|
|
||||||
ranker: 'network-simplex',
|
|
||||||
} as CytoscapeDagreConfig;
|
|
||||||
|
|
||||||
export class GraphService {
|
export class GraphService {
|
||||||
private traversalGraph: cy.Core;
|
private traversalGraph: ProjectTraversalGraph;
|
||||||
private renderGraph: cy.Core;
|
private renderGraph: RenderGraph;
|
||||||
|
|
||||||
private collapseEdges = false;
|
|
||||||
|
|
||||||
private listeners = new Map<
|
private listeners = new Map<
|
||||||
number,
|
number,
|
||||||
(event: GraphInteractionEvents) => void
|
(event: GraphInteractionEvents) => void
|
||||||
>();
|
>();
|
||||||
|
|
||||||
private _theme: 'light' | 'dark';
|
|
||||||
private _rankDir: 'TB' | 'LR' = 'TB';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private container: string | HTMLElement,
|
container: string | HTMLElement,
|
||||||
|
|
||||||
theme: 'light' | 'dark',
|
theme: 'light' | 'dark',
|
||||||
private renderMode?: 'nx-console' | 'nx-docs',
|
renderMode?: 'nx-console' | 'nx-docs',
|
||||||
rankDir: 'TB' | 'LR' = 'TB'
|
rankDir: 'TB' | 'LR' = 'TB'
|
||||||
) {
|
) {
|
||||||
cy.use(cytoscapeDagre);
|
use(cytoscapeDagre);
|
||||||
cy.use(popper);
|
use(popper);
|
||||||
|
|
||||||
this._theme = theme;
|
this.renderGraph = new RenderGraph(container, theme, renderMode, rankDir);
|
||||||
this._rankDir = rankDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
get activeContainer() {
|
this.renderGraph.listen((event) => this.broadcast(event));
|
||||||
return typeof this.container === 'string'
|
this.traversalGraph = new ProjectTraversalGraph();
|
||||||
? document.getElementById(this.container)
|
|
||||||
: this.container;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set theme(theme: 'light' | 'dark') {
|
set theme(theme: 'light' | 'dark') {
|
||||||
this._theme = theme;
|
this.renderGraph.theme = theme;
|
||||||
|
|
||||||
if (this.renderGraph) {
|
|
||||||
this.renderGraph.unmount();
|
|
||||||
const useDarkMode = theme === 'dark';
|
|
||||||
|
|
||||||
this.renderGraph.scratch(darkModeScratchKey, useDarkMode);
|
|
||||||
this.renderGraph.elements().scratch(darkModeScratchKey, useDarkMode);
|
|
||||||
|
|
||||||
this.renderGraph.mount(this.activeContainer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set rankDir(rankDir: 'TB' | 'LR') {
|
set rankDir(rankDir: 'TB' | 'LR') {
|
||||||
this._rankDir = rankDir;
|
this.renderGraph.rankDir = rankDir;
|
||||||
if (this.renderGraph) {
|
|
||||||
const elements = this.renderGraph.elements();
|
|
||||||
elements
|
|
||||||
.layout({
|
|
||||||
...cytoscapeDagreConfig,
|
|
||||||
...{ rankDir: rankDir },
|
|
||||||
} as CytoscapeDagreConfig)
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listen(callback: (event: GraphInteractionEvents) => void) {
|
listen(callback: (event: GraphInteractionEvents) => void) {
|
||||||
@ -110,16 +58,19 @@ export class GraphService {
|
|||||||
} {
|
} {
|
||||||
const time = Date.now();
|
const time = Date.now();
|
||||||
|
|
||||||
if (this.renderGraph && event.type !== 'notifyGraphUpdateGraph') {
|
if (event.type !== 'notifyGraphUpdateGraph') {
|
||||||
this.renderGraph.nodes('.focused').removeClass('focused');
|
this.renderGraph.clearFocussedElement();
|
||||||
this.renderGraph.unmount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.broadcast({ type: 'GraphRegenerated' });
|
this.broadcast({ type: 'GraphRegenerated' });
|
||||||
|
|
||||||
|
let elementsToSendToRender: CollectionReturnValue;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'notifyGraphInitGraph':
|
case 'notifyGraphInitGraph':
|
||||||
this.initGraph(
|
this.renderGraph.collapseEdges = event.collapseEdges;
|
||||||
|
this.broadcast({ type: 'GraphRegenerated' });
|
||||||
|
this.traversalGraph.initGraph(
|
||||||
event.projects,
|
event.projects,
|
||||||
event.groupByFolder,
|
event.groupByFolder,
|
||||||
event.workspaceLayout,
|
event.workspaceLayout,
|
||||||
@ -130,7 +81,9 @@ export class GraphService {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphUpdateGraph':
|
case 'notifyGraphUpdateGraph':
|
||||||
this.initGraph(
|
this.renderGraph.collapseEdges = event.collapseEdges;
|
||||||
|
this.broadcast({ type: 'GraphRegenerated' });
|
||||||
|
this.traversalGraph.initGraph(
|
||||||
event.projects,
|
event.projects,
|
||||||
event.groupByFolder,
|
event.groupByFolder,
|
||||||
event.workspaceLayout,
|
event.workspaceLayout,
|
||||||
@ -138,19 +91,23 @@ export class GraphService {
|
|||||||
event.affectedProjects,
|
event.affectedProjects,
|
||||||
event.collapseEdges
|
event.collapseEdges
|
||||||
);
|
);
|
||||||
this.setShownProjects(
|
elementsToSendToRender = this.traversalGraph.setShownProjects(
|
||||||
event.selectedProjects.length > 0
|
event.selectedProjects.length > 0
|
||||||
? event.selectedProjects
|
? event.selectedProjects
|
||||||
: this.renderGraph.nodes(':childless').map((node) => node.id())
|
: this.renderGraph.getCurrentlyShownProjectIds()
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphFocusProject':
|
case 'notifyGraphFocusProject':
|
||||||
this.focusProject(event.projectName, event.searchDepth);
|
elementsToSendToRender = this.traversalGraph.focusProject(
|
||||||
|
event.projectName,
|
||||||
|
event.searchDepth
|
||||||
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphFilterProjectsByText':
|
case 'notifyGraphFilterProjectsByText':
|
||||||
this.filterProjectsByText(
|
elementsToSendToRender = this.traversalGraph.filterProjectsByText(
|
||||||
event.search,
|
event.search,
|
||||||
event.includeProjectsByPath,
|
event.includeProjectsByPath,
|
||||||
event.searchDepth
|
event.searchDepth
|
||||||
@ -158,31 +115,43 @@ export class GraphService {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphShowProjects':
|
case 'notifyGraphShowProjects':
|
||||||
this.showProjects(event.projectNames);
|
elementsToSendToRender = this.traversalGraph.showProjects(
|
||||||
|
event.projectNames,
|
||||||
|
this.renderGraph.getCurrentlyShownProjectIds()
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphHideProjects':
|
case 'notifyGraphHideProjects':
|
||||||
this.hideProjects(event.projectNames);
|
elementsToSendToRender = this.traversalGraph.hideProjects(
|
||||||
|
event.projectNames,
|
||||||
|
this.renderGraph.getCurrentlyShownProjectIds()
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphShowAllProjects':
|
case 'notifyGraphShowAllProjects':
|
||||||
this.showAllProjects();
|
elementsToSendToRender = this.traversalGraph.showAllProjects();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphHideAllProjects':
|
case 'notifyGraphHideAllProjects':
|
||||||
this.hideAllProjects();
|
elementsToSendToRender = this.traversalGraph.hideAllProjects();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphShowAffectedProjects':
|
case 'notifyGraphShowAffectedProjects':
|
||||||
this.showAffectedProjects();
|
elementsToSendToRender = this.traversalGraph.showAffectedProjects();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'notifyGraphTracing':
|
case 'notifyGraphTracing':
|
||||||
if (event.start && event.end) {
|
if (event.start && event.end) {
|
||||||
if (event.algorithm === 'shortest') {
|
if (event.algorithm === 'shortest') {
|
||||||
this.traceProjects(event.start, event.end);
|
elementsToSendToRender = this.traversalGraph.traceProjects(
|
||||||
|
event.start,
|
||||||
|
event.end
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.traceAllProjects(event.start, event.end);
|
elementsToSendToRender = this.traversalGraph.traceAllProjects(
|
||||||
|
event.start,
|
||||||
|
event.end
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -195,572 +164,32 @@ export class GraphService {
|
|||||||
renderTime: 0,
|
renderTime: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.renderGraph) {
|
if (this.renderGraph && elementsToSendToRender) {
|
||||||
const elements = this.renderGraph.elements().sort((a, b) => {
|
this.renderGraph.setElements(elementsToSendToRender);
|
||||||
return a.id().localeCompare(b.id());
|
|
||||||
});
|
|
||||||
|
|
||||||
elements
|
if (event.type === 'notifyGraphFocusProject') {
|
||||||
.layout({
|
this.renderGraph.setFocussedElement(event.projectName);
|
||||||
...cytoscapeDagreConfig,
|
|
||||||
...{ rankDir: this._rankDir },
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
|
|
||||||
if (this.collapseEdges) {
|
|
||||||
this.renderGraph.remove(this.renderGraph.edges());
|
|
||||||
|
|
||||||
elements.edges().forEach((edge) => {
|
|
||||||
const sourceNode = edge.source();
|
|
||||||
const targetNode = edge.target();
|
|
||||||
|
|
||||||
if (
|
|
||||||
sourceNode.parent().first().id() ===
|
|
||||||
targetNode.parent().first().id()
|
|
||||||
) {
|
|
||||||
this.renderGraph.add(edge);
|
|
||||||
} else {
|
|
||||||
let sourceAncestors, targetAncestors;
|
|
||||||
const commonAncestors = edge.connectedNodes().commonAncestors();
|
|
||||||
|
|
||||||
if (commonAncestors.length > 0) {
|
|
||||||
sourceAncestors = sourceNode
|
|
||||||
.ancestors()
|
|
||||||
.filter((anc) => !commonAncestors.contains(anc));
|
|
||||||
targetAncestors = targetNode
|
|
||||||
.ancestors()
|
|
||||||
.filter((anc) => !commonAncestors.contains(anc));
|
|
||||||
} else {
|
|
||||||
sourceAncestors = sourceNode.ancestors();
|
|
||||||
targetAncestors = targetNode.ancestors();
|
|
||||||
}
|
|
||||||
|
|
||||||
let sourceId, targetId;
|
|
||||||
|
|
||||||
if (sourceAncestors.length > 0 && targetAncestors.length === 0) {
|
|
||||||
sourceId = sourceAncestors.last().id();
|
|
||||||
targetId = targetNode.id();
|
|
||||||
} else if (
|
|
||||||
targetAncestors.length > 0 &&
|
|
||||||
sourceAncestors.length === 0
|
|
||||||
) {
|
|
||||||
sourceId = sourceNode.id();
|
|
||||||
targetId = targetAncestors.last().id();
|
|
||||||
} else {
|
|
||||||
sourceId = sourceAncestors.last().id();
|
|
||||||
targetId = targetAncestors.last().id();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceId !== undefined && targetId !== undefined) {
|
|
||||||
const edgeId = `${sourceId}|${targetId}`;
|
|
||||||
|
|
||||||
if (this.renderGraph.$id(edgeId).length === 0) {
|
|
||||||
const ancestorEdge: cy.EdgeDefinition = {
|
|
||||||
group: 'edges',
|
|
||||||
data: {
|
|
||||||
id: edgeId,
|
|
||||||
source: sourceId,
|
|
||||||
target: targetId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderGraph.add(ancestorEdge);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`Couldn't figure out how to draw edge ${edge.id()}`);
|
|
||||||
console.log(
|
|
||||||
'source ancestors',
|
|
||||||
sourceAncestors.map((anc) => anc.id())
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'target ancestors',
|
|
||||||
targetAncestors.map((anc) => anc.id())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.renderMode === 'nx-console') {
|
const { numEdges, numNodes } = this.renderGraph.render();
|
||||||
// when in the nx-console environment, adjust graph width and position to be to right of floating panel
|
|
||||||
// 175 is a magic number that represents the width of the floating panels divided in half plus some padding
|
|
||||||
this.renderGraph
|
|
||||||
.fit(this.renderGraph.elements(), 175)
|
|
||||||
.center()
|
|
||||||
.resize()
|
|
||||||
.panBy({ x: 150, y: 0 });
|
|
||||||
} else {
|
|
||||||
this.renderGraph.fit(this.renderGraph.elements(), 25).center().resize();
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedProjectNames = this.renderGraph
|
selectedProjectNames = (
|
||||||
.nodes('[type!="dir"]')
|
elementsToSendToRender.nodes('[type!="dir"]') ?? []
|
||||||
.map((node) => node.id());
|
).map((node) => node.id());
|
||||||
|
|
||||||
this.renderGraph.scratch(darkModeScratchKey, this._theme === 'dark');
|
|
||||||
this.renderGraph
|
|
||||||
.elements()
|
|
||||||
.scratch(darkModeScratchKey, this._theme === 'dark');
|
|
||||||
|
|
||||||
this.renderGraph.mount(this.activeContainer);
|
|
||||||
|
|
||||||
const renderTime = Date.now() - time;
|
const renderTime = Date.now() - time;
|
||||||
|
|
||||||
perfReport = {
|
perfReport = {
|
||||||
renderTime,
|
renderTime,
|
||||||
numNodes: this.renderGraph.nodes().length,
|
numNodes,
|
||||||
numEdges: this.renderGraph.edges().length,
|
numEdges,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { selectedProjectNames, perfReport };
|
return { selectedProjectNames, perfReport };
|
||||||
}
|
}
|
||||||
|
|
||||||
setShownProjects(selectedProjectNames: string[]) {
|
|
||||||
let nodesToAdd = this.traversalGraph.collection();
|
|
||||||
|
|
||||||
selectedProjectNames.forEach((name) => {
|
|
||||||
nodesToAdd = nodesToAdd.union(this.traversalGraph.$id(name));
|
|
||||||
});
|
|
||||||
|
|
||||||
const ancestorsToAdd = nodesToAdd.ancestors();
|
|
||||||
|
|
||||||
const nodesToRender = nodesToAdd.union(ancestorsToAdd);
|
|
||||||
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
|
||||||
|
|
||||||
this.transferToRenderGraph(nodesToRender.union(edgesToRender));
|
|
||||||
}
|
|
||||||
|
|
||||||
showProjects(selectedProjectNames: string[]) {
|
|
||||||
const currentNodes =
|
|
||||||
this.renderGraph?.nodes() ?? this.traversalGraph.collection();
|
|
||||||
|
|
||||||
let nodesToAdd = this.traversalGraph.collection();
|
|
||||||
|
|
||||||
selectedProjectNames.forEach((name) => {
|
|
||||||
nodesToAdd = nodesToAdd.union(this.traversalGraph.$id(name));
|
|
||||||
});
|
|
||||||
|
|
||||||
const ancestorsToAdd = nodesToAdd.ancestors();
|
|
||||||
|
|
||||||
const nodesToRender = currentNodes.union(nodesToAdd).union(ancestorsToAdd);
|
|
||||||
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
|
||||||
|
|
||||||
this.transferToRenderGraph(nodesToRender.union(edgesToRender));
|
|
||||||
}
|
|
||||||
|
|
||||||
hideProjects(projectNames: string[]) {
|
|
||||||
const currentNodes =
|
|
||||||
this.renderGraph?.nodes() ?? this.traversalGraph.collection();
|
|
||||||
let nodesToHide = this.renderGraph.collection();
|
|
||||||
|
|
||||||
projectNames.forEach((projectName) => {
|
|
||||||
nodesToHide = nodesToHide.union(this.renderGraph.$id(projectName));
|
|
||||||
});
|
|
||||||
|
|
||||||
const nodesToAdd = currentNodes
|
|
||||||
.difference(nodesToHide)
|
|
||||||
.difference(nodesToHide.ancestors());
|
|
||||||
const ancestorsToAdd = nodesToAdd.ancestors();
|
|
||||||
|
|
||||||
let nodesToRender = nodesToAdd.union(ancestorsToAdd);
|
|
||||||
|
|
||||||
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
|
||||||
|
|
||||||
this.transferToRenderGraph(nodesToRender.union(edgesToRender));
|
|
||||||
}
|
|
||||||
|
|
||||||
showAffectedProjects() {
|
|
||||||
const affectedProjects = this.traversalGraph.nodes('.affected');
|
|
||||||
const affectedAncestors = affectedProjects.ancestors();
|
|
||||||
|
|
||||||
const affectedNodes = affectedProjects.union(affectedAncestors);
|
|
||||||
const affectedEdges = affectedNodes.edgesTo(affectedNodes);
|
|
||||||
|
|
||||||
this.transferToRenderGraph(affectedNodes.union(affectedEdges));
|
|
||||||
}
|
|
||||||
|
|
||||||
focusProject(focusedProjectName: string, searchDepth: number = 1) {
|
|
||||||
const focusedProject = this.traversalGraph.$id(focusedProjectName);
|
|
||||||
|
|
||||||
const includedProjects = this.includeProjectsByDepth(
|
|
||||||
focusedProject,
|
|
||||||
searchDepth
|
|
||||||
);
|
|
||||||
|
|
||||||
const includedNodes = focusedProject.union(includedProjects);
|
|
||||||
|
|
||||||
const includedAncestors = includedNodes.ancestors();
|
|
||||||
|
|
||||||
const nodesToRender = includedNodes.union(includedAncestors);
|
|
||||||
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
|
||||||
|
|
||||||
this.transferToRenderGraph(nodesToRender.union(edgesToRender));
|
|
||||||
|
|
||||||
this.renderGraph.$id(focusedProjectName).addClass('focused');
|
|
||||||
}
|
|
||||||
|
|
||||||
showAllProjects() {
|
|
||||||
this.transferToRenderGraph(this.traversalGraph.elements());
|
|
||||||
}
|
|
||||||
|
|
||||||
hideAllProjects() {
|
|
||||||
this.transferToRenderGraph(this.traversalGraph.collection());
|
|
||||||
}
|
|
||||||
|
|
||||||
filterProjectsByText(
|
|
||||||
search: string,
|
|
||||||
includePath: boolean,
|
|
||||||
searchDepth: number = -1
|
|
||||||
) {
|
|
||||||
if (search === '') {
|
|
||||||
this.transferToRenderGraph(this.traversalGraph.collection());
|
|
||||||
} else {
|
|
||||||
const split = search.split(',');
|
|
||||||
|
|
||||||
let filteredProjects = this.traversalGraph.nodes().filter((node) => {
|
|
||||||
return (
|
|
||||||
split.findIndex((splitItem) => node.id().includes(splitItem)) > -1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (includePath) {
|
|
||||||
filteredProjects = filteredProjects.union(
|
|
||||||
this.includeProjectsByDepth(filteredProjects, searchDepth)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredProjects = filteredProjects.union(filteredProjects.ancestors());
|
|
||||||
const edgesToRender = filteredProjects.edgesTo(filteredProjects);
|
|
||||||
|
|
||||||
this.transferToRenderGraph(filteredProjects.union(edgesToRender));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
traceProjects(start: string, end: string) {
|
|
||||||
const dijkstra = this.traversalGraph
|
|
||||||
.elements()
|
|
||||||
.dijkstra({ root: `[id = "${start}"]`, directed: true });
|
|
||||||
|
|
||||||
const path = dijkstra.pathTo(this.traversalGraph.$(`[id = "${end}"]`));
|
|
||||||
|
|
||||||
this.transferToRenderGraph(path.union(path.ancestors()));
|
|
||||||
}
|
|
||||||
|
|
||||||
traceAllProjects(start: string, end: string) {
|
|
||||||
const startNode = this.traversalGraph.$id(start).nodes().first();
|
|
||||||
|
|
||||||
const queue: cy.NodeSingular[][] = [[startNode]];
|
|
||||||
|
|
||||||
const paths: cy.NodeSingular[][] = [];
|
|
||||||
let iterations = 0;
|
|
||||||
|
|
||||||
while (queue.length > 0 && iterations <= 1000) {
|
|
||||||
const currentPath = queue.pop();
|
|
||||||
|
|
||||||
const nodeToTest = currentPath[currentPath.length - 1];
|
|
||||||
|
|
||||||
const outgoers = nodeToTest.outgoers('node');
|
|
||||||
|
|
||||||
if (outgoers.length > 0) {
|
|
||||||
outgoers.forEach((outgoer) => {
|
|
||||||
const newPath = [...currentPath, outgoer];
|
|
||||||
if (outgoer.id() === end) {
|
|
||||||
paths.push(newPath);
|
|
||||||
} else {
|
|
||||||
queue.push(newPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
iterations++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iterations >= 1000) {
|
|
||||||
console.log('failsafe triggered!');
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalCollection = this.traversalGraph.collection();
|
|
||||||
|
|
||||||
paths.forEach((path) => {
|
|
||||||
for (let i = 0; i < path.length; i++) {
|
|
||||||
finalCollection = finalCollection.union(path[i]);
|
|
||||||
|
|
||||||
const nextIndex = i + 1;
|
|
||||||
if (nextIndex < path.length) {
|
|
||||||
finalCollection = finalCollection.union(
|
|
||||||
path[i].edgesTo(path[nextIndex])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
finalCollection.union(finalCollection.ancestors());
|
|
||||||
this.transferToRenderGraph(
|
|
||||||
finalCollection.union(finalCollection.ancestors())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private transferToRenderGraph(elements: cy.Collection) {
|
|
||||||
let currentFocusedProjectName;
|
|
||||||
if (this.renderGraph) {
|
|
||||||
currentFocusedProjectName = this.renderGraph
|
|
||||||
.nodes('.focused')
|
|
||||||
.first()
|
|
||||||
.id();
|
|
||||||
this.renderGraph.destroy();
|
|
||||||
delete this.renderGraph;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderGraph = cy({
|
|
||||||
headless: this.activeContainer === null,
|
|
||||||
container: this.activeContainer,
|
|
||||||
boxSelectionEnabled: false,
|
|
||||||
style: [...nodeStyles, ...edgeStyles],
|
|
||||||
panningEnabled: true,
|
|
||||||
userZoomingEnabled: this.renderMode !== 'nx-docs',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.renderGraph.add(elements);
|
|
||||||
|
|
||||||
if (!!currentFocusedProjectName) {
|
|
||||||
this.renderGraph.$id(currentFocusedProjectName).addClass('focused');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderGraph.on('zoom pan', () => {
|
|
||||||
this.broadcast({ type: 'GraphRegenerated' });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.listenForProjectNodeClicks();
|
|
||||||
this.listenForEdgeNodeClicks();
|
|
||||||
this.listenForProjectNodeHovers();
|
|
||||||
}
|
|
||||||
|
|
||||||
private includeProjectsByDepth(
|
|
||||||
projects: cy.NodeCollection | cy.NodeSingular,
|
|
||||||
depth: number = -1
|
|
||||||
) {
|
|
||||||
let predecessors = this.traversalGraph.collection();
|
|
||||||
|
|
||||||
if (depth === -1) {
|
|
||||||
predecessors = projects.predecessors();
|
|
||||||
} else {
|
|
||||||
predecessors = projects.incomers();
|
|
||||||
|
|
||||||
for (let i = 1; i < depth; i++) {
|
|
||||||
predecessors = predecessors.union(predecessors.incomers());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let successors = this.traversalGraph.collection();
|
|
||||||
|
|
||||||
if (depth === -1) {
|
|
||||||
successors = projects.successors();
|
|
||||||
} else {
|
|
||||||
successors = projects.outgoers();
|
|
||||||
|
|
||||||
for (let i = 1; i < depth; i++) {
|
|
||||||
successors = successors.union(successors.outgoers());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return projects.union(predecessors).union(successors);
|
|
||||||
}
|
|
||||||
|
|
||||||
initGraph(
|
|
||||||
allProjects: ProjectGraphProjectNode[],
|
|
||||||
groupByFolder: boolean,
|
|
||||||
workspaceLayout,
|
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>,
|
|
||||||
affectedProjectIds: string[],
|
|
||||||
collapseEdges: boolean
|
|
||||||
) {
|
|
||||||
this.collapseEdges = collapseEdges;
|
|
||||||
this.broadcast({ type: 'GraphRegenerated' });
|
|
||||||
|
|
||||||
this.generateCytoscapeLayout(
|
|
||||||
allProjects,
|
|
||||||
groupByFolder,
|
|
||||||
workspaceLayout,
|
|
||||||
dependencies,
|
|
||||||
affectedProjectIds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateCytoscapeLayout(
|
|
||||||
allProjects: ProjectGraphProjectNode[],
|
|
||||||
groupByFolder: boolean,
|
|
||||||
workspaceLayout,
|
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>,
|
|
||||||
affectedProjectIds: string[]
|
|
||||||
) {
|
|
||||||
const elements = this.createElements(
|
|
||||||
allProjects,
|
|
||||||
groupByFolder,
|
|
||||||
workspaceLayout,
|
|
||||||
dependencies,
|
|
||||||
affectedProjectIds
|
|
||||||
);
|
|
||||||
|
|
||||||
this.traversalGraph = cy({
|
|
||||||
headless: true,
|
|
||||||
elements: [...elements],
|
|
||||||
boxSelectionEnabled: false,
|
|
||||||
style: [...nodeStyles, ...edgeStyles],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private createElements(
|
|
||||||
projects: ProjectGraphProjectNode[],
|
|
||||||
groupByFolder: boolean,
|
|
||||||
workspaceLayout: {
|
|
||||||
appsDir: string;
|
|
||||||
libsDir: string;
|
|
||||||
},
|
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>,
|
|
||||||
affectedProjectIds: string[]
|
|
||||||
) {
|
|
||||||
let elements: cy.ElementDefinition[] = [];
|
|
||||||
const filteredProjectNames = projects.map((project) => project.name);
|
|
||||||
|
|
||||||
const projectNodes: ProjectNode[] = [];
|
|
||||||
const edgeNodes: ProjectEdge[] = [];
|
|
||||||
const parents: Record<
|
|
||||||
string,
|
|
||||||
{ id: string; parentId: string; label: string }
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
projects.forEach((project) => {
|
|
||||||
const workspaceRoot =
|
|
||||||
project.type === 'app' || project.type === 'e2e'
|
|
||||||
? workspaceLayout.appsDir
|
|
||||||
: workspaceLayout.libsDir;
|
|
||||||
|
|
||||||
const projectNode = new ProjectNode(project, workspaceRoot);
|
|
||||||
|
|
||||||
projectNode.affected = affectedProjectIds.includes(project.name);
|
|
||||||
|
|
||||||
projectNodes.push(projectNode);
|
|
||||||
|
|
||||||
dependencies[project.name].forEach((dep) => {
|
|
||||||
if (filteredProjectNames.includes(dep.target)) {
|
|
||||||
const edge = new ProjectEdge(dep);
|
|
||||||
edgeNodes.push(edge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (groupByFolder) {
|
|
||||||
const ancestors = projectNode.getAncestors();
|
|
||||||
ancestors.forEach((ancestor) => (parents[ancestor.id] = ancestor));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const projectElements = projectNodes.map((projectNode) =>
|
|
||||||
projectNode.getCytoscapeNodeDef(groupByFolder)
|
|
||||||
);
|
|
||||||
|
|
||||||
const edgeElements = edgeNodes.map((edgeNode) =>
|
|
||||||
edgeNode.getCytosacpeNodeDef()
|
|
||||||
);
|
|
||||||
|
|
||||||
elements = projectElements.concat(edgeElements);
|
|
||||||
|
|
||||||
if (groupByFolder) {
|
|
||||||
const parentElements = Object.keys(parents).map((id) =>
|
|
||||||
new ParentNode(parents[id]).getCytoscapeNodeDef()
|
|
||||||
);
|
|
||||||
elements = parentElements.concat(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
listenForProjectNodeClicks() {
|
|
||||||
this.renderGraph.$('node:childless').on('click', (event) => {
|
|
||||||
const node = event.target;
|
|
||||||
|
|
||||||
let ref: VirtualElement = node.popperRef(); // used only for positioning
|
|
||||||
|
|
||||||
this.broadcast({
|
|
||||||
type: 'NodeClick',
|
|
||||||
ref,
|
|
||||||
id: node.id(),
|
|
||||||
|
|
||||||
data: {
|
|
||||||
id: node.id(),
|
|
||||||
type: node.data('type'),
|
|
||||||
tags: node.data('tags'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
listenForEdgeNodeClicks() {
|
|
||||||
this.renderGraph.$('edge').on('click', (event) => {
|
|
||||||
const edge: cy.EdgeSingular = event.target;
|
|
||||||
let ref: VirtualElement = edge.popperRef(); // used only for positioning
|
|
||||||
|
|
||||||
this.broadcast({
|
|
||||||
type: 'EdgeClick',
|
|
||||||
ref,
|
|
||||||
id: edge.id(),
|
|
||||||
|
|
||||||
data: {
|
|
||||||
type: edge.data('type'),
|
|
||||||
source: edge.source().id(),
|
|
||||||
target: edge.target().id(),
|
|
||||||
fileDependencies: edge
|
|
||||||
.source()
|
|
||||||
.data('files')
|
|
||||||
.filter(
|
|
||||||
(file) => file.deps && file.deps.includes(edge.target().id())
|
|
||||||
)
|
|
||||||
.map((file) => {
|
|
||||||
return {
|
|
||||||
fileName: file.file.replace(
|
|
||||||
`${edge.source().data('root')}/`,
|
|
||||||
''
|
|
||||||
),
|
|
||||||
target: edge.target().id(),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
listenForProjectNodeHovers(): void {
|
|
||||||
this.renderGraph.on('mouseover', (event) => {
|
|
||||||
const node = event.target;
|
|
||||||
if (!node.isNode || !node.isNode() || node.isParent()) return;
|
|
||||||
|
|
||||||
this.renderGraph
|
|
||||||
.elements()
|
|
||||||
.difference(node.outgoers().union(node.incomers()))
|
|
||||||
.not(node)
|
|
||||||
.addClass('transparent');
|
|
||||||
node
|
|
||||||
.addClass('highlight')
|
|
||||||
.outgoers()
|
|
||||||
.union(node.incomers())
|
|
||||||
.addClass('highlight');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.renderGraph.on('mouseout', (event) => {
|
|
||||||
const node = event.target;
|
|
||||||
if (!node.isNode || !node.isNode() || node.isParent()) return;
|
|
||||||
|
|
||||||
this.renderGraph.elements().removeClass('transparent');
|
|
||||||
node
|
|
||||||
.removeClass('highlight')
|
|
||||||
.outgoers()
|
|
||||||
.union(node.incomers())
|
|
||||||
.removeClass('highlight');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getImage() {
|
getImage() {
|
||||||
const bg = switchValueByDarkMode(this.renderGraph, '#0F172A', '#FFFFFF');
|
return this.renderGraph.getImage();
|
||||||
return this.renderGraph.png({ bg, full: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
340
graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts
Normal file
340
graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
import cytoscape, {
|
||||||
|
CollectionReturnValue,
|
||||||
|
Core,
|
||||||
|
ElementDefinition,
|
||||||
|
NodeCollection,
|
||||||
|
NodeSingular,
|
||||||
|
} from 'cytoscape';
|
||||||
|
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import {
|
||||||
|
ProjectGraphDependency,
|
||||||
|
ProjectGraphProjectNode,
|
||||||
|
} from 'nx/src/config/project-graph';
|
||||||
|
import { edgeStyles, nodeStyles } from '../styles-graph';
|
||||||
|
import { ProjectNode } from './project-node';
|
||||||
|
import { ProjectEdge } from './edge';
|
||||||
|
import { ParentNode } from './parent-node';
|
||||||
|
|
||||||
|
export class ProjectTraversalGraph {
|
||||||
|
private cy?: Core;
|
||||||
|
|
||||||
|
setShownProjects(selectedProjectNames: string[]) {
|
||||||
|
let nodesToAdd = this.cy.collection();
|
||||||
|
|
||||||
|
selectedProjectNames.forEach((name) => {
|
||||||
|
nodesToAdd = nodesToAdd.union(this.cy.$id(name));
|
||||||
|
});
|
||||||
|
|
||||||
|
const ancestorsToAdd = nodesToAdd.ancestors();
|
||||||
|
|
||||||
|
const nodesToRender = nodesToAdd.union(ancestorsToAdd);
|
||||||
|
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
||||||
|
|
||||||
|
return nodesToRender.union(edgesToRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
showProjects(selectedProjectNames: string[], alreadyShownProjects: string[]) {
|
||||||
|
let nodesToAdd = this.cy.collection();
|
||||||
|
|
||||||
|
selectedProjectNames.forEach((name) => {
|
||||||
|
nodesToAdd = nodesToAdd.union(this.cy.$id(name));
|
||||||
|
});
|
||||||
|
|
||||||
|
alreadyShownProjects.forEach((name) => {
|
||||||
|
nodesToAdd = nodesToAdd.union(this.cy.$id(name));
|
||||||
|
});
|
||||||
|
|
||||||
|
const ancestorsToAdd = nodesToAdd.ancestors();
|
||||||
|
|
||||||
|
const nodesToRender = nodesToAdd.union(ancestorsToAdd);
|
||||||
|
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
||||||
|
|
||||||
|
return nodesToRender.union(edgesToRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideProjects(projectNames: string[], alreadyShownProjects: string[]) {
|
||||||
|
let currentNodes = this.cy.collection();
|
||||||
|
alreadyShownProjects.forEach((name) => {
|
||||||
|
currentNodes = currentNodes.union(this.cy.$id(name));
|
||||||
|
});
|
||||||
|
let nodesToHide = this.cy.collection();
|
||||||
|
|
||||||
|
projectNames.forEach((projectName) => {
|
||||||
|
nodesToHide = nodesToHide.union(this.cy.$id(projectName));
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodesToAdd = currentNodes
|
||||||
|
.difference(nodesToHide)
|
||||||
|
.difference(nodesToHide.ancestors());
|
||||||
|
const ancestorsToAdd = nodesToAdd.ancestors();
|
||||||
|
|
||||||
|
let nodesToRender = nodesToAdd.union(ancestorsToAdd);
|
||||||
|
|
||||||
|
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
||||||
|
|
||||||
|
return nodesToRender.union(edgesToRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAffectedProjects() {
|
||||||
|
const affectedProjects = this.cy.nodes('.affected');
|
||||||
|
const affectedAncestors = affectedProjects.ancestors();
|
||||||
|
|
||||||
|
const affectedNodes = affectedProjects.union(affectedAncestors);
|
||||||
|
const affectedEdges = affectedNodes.edgesTo(affectedNodes);
|
||||||
|
|
||||||
|
return affectedNodes.union(affectedEdges);
|
||||||
|
}
|
||||||
|
|
||||||
|
focusProject(focusedProjectName: string, searchDepth: number = 1) {
|
||||||
|
const focusedProject = this.cy.$id(focusedProjectName);
|
||||||
|
|
||||||
|
const includedProjects = this.includeProjectsByDepth(
|
||||||
|
focusedProject,
|
||||||
|
searchDepth
|
||||||
|
);
|
||||||
|
|
||||||
|
const includedNodes = focusedProject.union(includedProjects);
|
||||||
|
|
||||||
|
const includedAncestors = includedNodes.ancestors();
|
||||||
|
|
||||||
|
const nodesToRender = includedNodes.union(includedAncestors);
|
||||||
|
const edgesToRender = nodesToRender.edgesTo(nodesToRender);
|
||||||
|
|
||||||
|
return nodesToRender.union(edgesToRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAllProjects() {
|
||||||
|
return this.cy.elements();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAllProjects() {
|
||||||
|
return this.cy.collection();
|
||||||
|
}
|
||||||
|
|
||||||
|
filterProjectsByText(
|
||||||
|
search: string,
|
||||||
|
includePath: boolean,
|
||||||
|
searchDepth: number = -1
|
||||||
|
) {
|
||||||
|
if (search === '') {
|
||||||
|
return this.cy.collection();
|
||||||
|
} else {
|
||||||
|
const split = search.split(',');
|
||||||
|
|
||||||
|
let filteredProjects = this.cy.nodes().filter((node) => {
|
||||||
|
return (
|
||||||
|
split.findIndex((splitItem) => node.id().includes(splitItem)) > -1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (includePath) {
|
||||||
|
filteredProjects = filteredProjects.union(
|
||||||
|
this.includeProjectsByDepth(filteredProjects, searchDepth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredProjects = filteredProjects.union(filteredProjects.ancestors());
|
||||||
|
const edgesToRender = filteredProjects.edgesTo(filteredProjects);
|
||||||
|
|
||||||
|
return filteredProjects.union(edgesToRender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traceProjects(start: string, end: string) {
|
||||||
|
const dijkstra = this.cy
|
||||||
|
.elements()
|
||||||
|
.dijkstra({ root: `[id = "${start}"]`, directed: true });
|
||||||
|
|
||||||
|
const path = dijkstra.pathTo(this.cy.$(`[id = "${end}"]`));
|
||||||
|
|
||||||
|
return path.union(path.ancestors());
|
||||||
|
}
|
||||||
|
|
||||||
|
traceAllProjects(start: string, end: string) {
|
||||||
|
const startNode = this.cy.$id(start).nodes().first();
|
||||||
|
|
||||||
|
const queue: NodeSingular[][] = [[startNode]];
|
||||||
|
|
||||||
|
const paths: NodeSingular[][] = [];
|
||||||
|
let iterations = 0;
|
||||||
|
|
||||||
|
while (queue.length > 0 && iterations <= 1000) {
|
||||||
|
const currentPath = queue.pop();
|
||||||
|
|
||||||
|
const nodeToTest = currentPath[currentPath.length - 1];
|
||||||
|
|
||||||
|
const outgoers = nodeToTest.outgoers('node');
|
||||||
|
|
||||||
|
if (outgoers.length > 0) {
|
||||||
|
outgoers.forEach((outgoer) => {
|
||||||
|
const newPath = [...currentPath, outgoer];
|
||||||
|
if (outgoer.id() === end) {
|
||||||
|
paths.push(newPath);
|
||||||
|
} else {
|
||||||
|
queue.push(newPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
iterations++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iterations >= 1000) {
|
||||||
|
console.log('failsafe triggered!');
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalCollection = this.cy.collection();
|
||||||
|
|
||||||
|
paths.forEach((path) => {
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
finalCollection = finalCollection.union(path[i]);
|
||||||
|
|
||||||
|
const nextIndex = i + 1;
|
||||||
|
if (nextIndex < path.length) {
|
||||||
|
finalCollection = finalCollection.union(
|
||||||
|
path[i].edgesTo(path[nextIndex])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return finalCollection.union(finalCollection.ancestors());
|
||||||
|
}
|
||||||
|
|
||||||
|
private includeProjectsByDepth(
|
||||||
|
projects: NodeCollection | NodeSingular,
|
||||||
|
depth: number = -1
|
||||||
|
) {
|
||||||
|
let predecessors: CollectionReturnValue;
|
||||||
|
|
||||||
|
if (depth === -1) {
|
||||||
|
predecessors = projects.predecessors();
|
||||||
|
} else {
|
||||||
|
predecessors = projects.incomers();
|
||||||
|
|
||||||
|
for (let i = 1; i < depth; i++) {
|
||||||
|
predecessors = predecessors.union(predecessors.incomers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let successors: CollectionReturnValue;
|
||||||
|
|
||||||
|
if (depth === -1) {
|
||||||
|
successors = projects.successors();
|
||||||
|
} else {
|
||||||
|
successors = projects.outgoers();
|
||||||
|
|
||||||
|
for (let i = 1; i < depth; i++) {
|
||||||
|
successors = successors.union(successors.outgoers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects.union(predecessors).union(successors);
|
||||||
|
}
|
||||||
|
|
||||||
|
initGraph(
|
||||||
|
allProjects: ProjectGraphProjectNode[],
|
||||||
|
groupByFolder: boolean,
|
||||||
|
workspaceLayout,
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||||
|
affectedProjectIds: string[],
|
||||||
|
collapseEdges: boolean
|
||||||
|
) {
|
||||||
|
this.generateCytoscapeLayout(
|
||||||
|
allProjects,
|
||||||
|
groupByFolder,
|
||||||
|
workspaceLayout,
|
||||||
|
dependencies,
|
||||||
|
affectedProjectIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCytoscapeLayout(
|
||||||
|
allProjects: ProjectGraphProjectNode[],
|
||||||
|
groupByFolder: boolean,
|
||||||
|
workspaceLayout,
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||||
|
affectedProjectIds: string[]
|
||||||
|
) {
|
||||||
|
const elements = this.createElements(
|
||||||
|
allProjects,
|
||||||
|
groupByFolder,
|
||||||
|
workspaceLayout,
|
||||||
|
dependencies,
|
||||||
|
affectedProjectIds
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cy = cytoscape({
|
||||||
|
headless: true,
|
||||||
|
elements: [...elements],
|
||||||
|
boxSelectionEnabled: false,
|
||||||
|
style: [...nodeStyles, ...edgeStyles],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createElements(
|
||||||
|
projects: ProjectGraphProjectNode[],
|
||||||
|
groupByFolder: boolean,
|
||||||
|
workspaceLayout: {
|
||||||
|
appsDir: string;
|
||||||
|
libsDir: string;
|
||||||
|
},
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||||
|
affectedProjectIds: string[]
|
||||||
|
) {
|
||||||
|
let elements: ElementDefinition[] = [];
|
||||||
|
const filteredProjectNames = projects.map((project) => project.name);
|
||||||
|
|
||||||
|
const projectNodes: ProjectNode[] = [];
|
||||||
|
const edgeNodes: ProjectEdge[] = [];
|
||||||
|
const parents: Record<
|
||||||
|
string,
|
||||||
|
{ id: string; parentId: string; label: string }
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
projects.forEach((project) => {
|
||||||
|
const workspaceRoot =
|
||||||
|
project.type === 'app' || project.type === 'e2e'
|
||||||
|
? workspaceLayout.appsDir
|
||||||
|
: workspaceLayout.libsDir;
|
||||||
|
|
||||||
|
const projectNode = new ProjectNode(project, workspaceRoot);
|
||||||
|
|
||||||
|
projectNode.affected = affectedProjectIds.includes(project.name);
|
||||||
|
|
||||||
|
projectNodes.push(projectNode);
|
||||||
|
|
||||||
|
dependencies[project.name].forEach((dep) => {
|
||||||
|
if (filteredProjectNames.includes(dep.target)) {
|
||||||
|
const edge = new ProjectEdge(dep);
|
||||||
|
edgeNodes.push(edge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (groupByFolder) {
|
||||||
|
const ancestors = projectNode.getAncestors();
|
||||||
|
ancestors.forEach((ancestor) => (parents[ancestor.id] = ancestor));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectElements = projectNodes.map((projectNode) =>
|
||||||
|
projectNode.getCytoscapeNodeDef(groupByFolder)
|
||||||
|
);
|
||||||
|
|
||||||
|
const edgeElements = edgeNodes.map((edgeNode) =>
|
||||||
|
edgeNode.getCytosacpeNodeDef()
|
||||||
|
);
|
||||||
|
|
||||||
|
elements = projectElements.concat(edgeElements);
|
||||||
|
|
||||||
|
if (groupByFolder) {
|
||||||
|
const parentElements = Object.keys(parents).map((id) =>
|
||||||
|
new ParentNode(parents[id]).getCytoscapeNodeDef()
|
||||||
|
);
|
||||||
|
elements = parentElements.concat(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
||||||
336
graph/ui-graph/src/lib/util-cytoscape/render-graph.ts
Normal file
336
graph/ui-graph/src/lib/util-cytoscape/render-graph.ts
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
import cytoscape, {
|
||||||
|
Collection,
|
||||||
|
Core,
|
||||||
|
EdgeDefinition,
|
||||||
|
EdgeSingular,
|
||||||
|
} from 'cytoscape';
|
||||||
|
import { edgeStyles, nodeStyles } from '../styles-graph';
|
||||||
|
import { GraphInteractionEvents } from '@nrwl/graph/ui-graph';
|
||||||
|
import { VirtualElement } from '@popperjs/core';
|
||||||
|
import {
|
||||||
|
darkModeScratchKey,
|
||||||
|
switchValueByDarkMode,
|
||||||
|
} from '../styles-graph/dark-mode';
|
||||||
|
import { CytoscapeDagreConfig } from './cytoscape.models';
|
||||||
|
|
||||||
|
const cytoscapeDagreConfig = {
|
||||||
|
name: 'dagre',
|
||||||
|
nodeDimensionsIncludeLabels: true,
|
||||||
|
rankSep: 75,
|
||||||
|
rankDir: 'TB',
|
||||||
|
edgeSep: 50,
|
||||||
|
ranker: 'network-simplex',
|
||||||
|
} as CytoscapeDagreConfig;
|
||||||
|
|
||||||
|
export class RenderGraph {
|
||||||
|
private cy?: Core;
|
||||||
|
collapseEdges = false;
|
||||||
|
|
||||||
|
private _theme: 'light' | 'dark';
|
||||||
|
private _rankDir: 'TB' | 'LR' = 'TB';
|
||||||
|
|
||||||
|
private listeners = new Map<
|
||||||
|
number,
|
||||||
|
(event: GraphInteractionEvents) => void
|
||||||
|
>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private container: string | HTMLElement,
|
||||||
|
theme: 'light' | 'dark',
|
||||||
|
private renderMode?: 'nx-console' | 'nx-docs',
|
||||||
|
rankDir: 'TB' | 'LR' = 'TB'
|
||||||
|
) {
|
||||||
|
this._theme = theme;
|
||||||
|
this._rankDir = rankDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
set theme(theme: 'light' | 'dark') {
|
||||||
|
this._theme = theme;
|
||||||
|
|
||||||
|
if (this.cy) {
|
||||||
|
this.cy.unmount();
|
||||||
|
const useDarkMode = theme === 'dark';
|
||||||
|
|
||||||
|
this.cy.scratch(darkModeScratchKey, useDarkMode);
|
||||||
|
this.cy.elements().scratch(darkModeScratchKey, useDarkMode);
|
||||||
|
|
||||||
|
this.cy.mount(this.activeContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set rankDir(rankDir: 'LR' | 'TB') {
|
||||||
|
this._rankDir = rankDir;
|
||||||
|
if (this.cy) {
|
||||||
|
const elements = this.cy.elements();
|
||||||
|
elements
|
||||||
|
.layout({
|
||||||
|
...cytoscapeDagreConfig,
|
||||||
|
...{ rankDir: rankDir },
|
||||||
|
} as CytoscapeDagreConfig)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeContainer() {
|
||||||
|
return typeof this.container === 'string'
|
||||||
|
? document.getElementById(this.container)
|
||||||
|
: this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private broadcast(event: GraphInteractionEvents) {
|
||||||
|
this.listeners.forEach((callback) => callback(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(callback: (event: GraphInteractionEvents) => void) {
|
||||||
|
const listenerId = this.listeners.size + 1;
|
||||||
|
this.listeners.set(listenerId, callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listenerId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setElements(elements: Collection) {
|
||||||
|
let currentFocusedProjectName;
|
||||||
|
if (this.cy) {
|
||||||
|
currentFocusedProjectName = this.cy.nodes('.focused').first().id();
|
||||||
|
this.cy.destroy();
|
||||||
|
delete this.cy;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cy = cytoscape({
|
||||||
|
headless: this.activeContainer === null,
|
||||||
|
container: this.activeContainer,
|
||||||
|
boxSelectionEnabled: false,
|
||||||
|
style: [...nodeStyles, ...edgeStyles],
|
||||||
|
panningEnabled: true,
|
||||||
|
userZoomingEnabled: this.renderMode !== 'nx-docs',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cy.add(elements);
|
||||||
|
|
||||||
|
if (!!currentFocusedProjectName) {
|
||||||
|
this.cy.$id(currentFocusedProjectName).addClass('focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cy.on('zoom pan', () => {
|
||||||
|
this.broadcast({ type: 'GraphRegenerated' });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.listenForProjectNodeClicks();
|
||||||
|
this.listenForEdgeNodeClicks();
|
||||||
|
this.listenForProjectNodeHovers();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): { numEdges: number; numNodes: number } {
|
||||||
|
if (this.cy) {
|
||||||
|
const elements = this.cy.elements().sort((a, b) => {
|
||||||
|
return a.id().localeCompare(b.id());
|
||||||
|
});
|
||||||
|
|
||||||
|
elements
|
||||||
|
.layout({
|
||||||
|
...cytoscapeDagreConfig,
|
||||||
|
...{ rankDir: this._rankDir },
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
if (this.collapseEdges) {
|
||||||
|
this.cy.remove(this.cy.edges());
|
||||||
|
|
||||||
|
elements.edges().forEach((edge) => {
|
||||||
|
const sourceNode = edge.source();
|
||||||
|
const targetNode = edge.target();
|
||||||
|
|
||||||
|
if (
|
||||||
|
sourceNode.parent().first().id() ===
|
||||||
|
targetNode.parent().first().id()
|
||||||
|
) {
|
||||||
|
this.cy.add(edge);
|
||||||
|
} else {
|
||||||
|
let sourceAncestors, targetAncestors;
|
||||||
|
const commonAncestors = edge.connectedNodes().commonAncestors();
|
||||||
|
|
||||||
|
if (commonAncestors.length > 0) {
|
||||||
|
sourceAncestors = sourceNode
|
||||||
|
.ancestors()
|
||||||
|
.filter((anc) => !commonAncestors.contains(anc));
|
||||||
|
targetAncestors = targetNode
|
||||||
|
.ancestors()
|
||||||
|
.filter((anc) => !commonAncestors.contains(anc));
|
||||||
|
} else {
|
||||||
|
sourceAncestors = sourceNode.ancestors();
|
||||||
|
targetAncestors = targetNode.ancestors();
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceId, targetId;
|
||||||
|
|
||||||
|
if (sourceAncestors.length > 0 && targetAncestors.length === 0) {
|
||||||
|
sourceId = sourceAncestors.last().id();
|
||||||
|
targetId = targetNode.id();
|
||||||
|
} else if (
|
||||||
|
targetAncestors.length > 0 &&
|
||||||
|
sourceAncestors.length === 0
|
||||||
|
) {
|
||||||
|
sourceId = sourceNode.id();
|
||||||
|
targetId = targetAncestors.last().id();
|
||||||
|
} else {
|
||||||
|
sourceId = sourceAncestors.last().id();
|
||||||
|
targetId = targetAncestors.last().id();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceId !== undefined && targetId !== undefined) {
|
||||||
|
const edgeId = `${sourceId}|${targetId}`;
|
||||||
|
|
||||||
|
if (this.cy.$id(edgeId).length === 0) {
|
||||||
|
const ancestorEdge: EdgeDefinition = {
|
||||||
|
group: 'edges',
|
||||||
|
data: {
|
||||||
|
id: edgeId,
|
||||||
|
source: sourceId,
|
||||||
|
target: targetId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cy.add(ancestorEdge);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Couldn't figure out how to draw edge ${edge.id()}`);
|
||||||
|
console.log(
|
||||||
|
'source ancestors',
|
||||||
|
sourceAncestors.map((anc) => anc.id())
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'target ancestors',
|
||||||
|
targetAncestors.map((anc) => anc.id())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.renderMode === 'nx-console') {
|
||||||
|
// when in the nx-console environment, adjust graph width and position to be to right of floating panel
|
||||||
|
// 175 is a magic number that represents the width of the floating panels divided in half plus some padding
|
||||||
|
this.cy
|
||||||
|
.fit(this.cy.elements(), 175)
|
||||||
|
.center()
|
||||||
|
.resize()
|
||||||
|
.panBy({ x: 150, y: 0 });
|
||||||
|
} else {
|
||||||
|
this.cy.fit(this.cy.elements(), 25).center().resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cy.scratch(darkModeScratchKey, this._theme === 'dark');
|
||||||
|
this.cy.elements().scratch(darkModeScratchKey, this._theme === 'dark');
|
||||||
|
|
||||||
|
this.cy.mount(this.activeContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
numNodes: this.cy?.nodes().length ?? 0,
|
||||||
|
numEdges: this.cy?.edges().length ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private listenForProjectNodeClicks() {
|
||||||
|
this.cy.$('node:childless').on('click', (event) => {
|
||||||
|
const node = event.target;
|
||||||
|
|
||||||
|
let ref: VirtualElement = node.popperRef(); // used only for positioning
|
||||||
|
|
||||||
|
this.broadcast({
|
||||||
|
type: 'NodeClick',
|
||||||
|
ref,
|
||||||
|
id: node.id(),
|
||||||
|
|
||||||
|
data: {
|
||||||
|
id: node.id(),
|
||||||
|
type: node.data('type'),
|
||||||
|
tags: node.data('tags'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private listenForEdgeNodeClicks() {
|
||||||
|
this.cy.$('edge').on('click', (event) => {
|
||||||
|
const edge: EdgeSingular = event.target;
|
||||||
|
let ref: VirtualElement = edge.popperRef(); // used only for positioning
|
||||||
|
|
||||||
|
this.broadcast({
|
||||||
|
type: 'EdgeClick',
|
||||||
|
ref,
|
||||||
|
id: edge.id(),
|
||||||
|
|
||||||
|
data: {
|
||||||
|
type: edge.data('type'),
|
||||||
|
source: edge.source().id(),
|
||||||
|
target: edge.target().id(),
|
||||||
|
fileDependencies: edge
|
||||||
|
.source()
|
||||||
|
.data('files')
|
||||||
|
.filter(
|
||||||
|
(file) => file.deps && file.deps.includes(edge.target().id())
|
||||||
|
)
|
||||||
|
.map((file) => {
|
||||||
|
return {
|
||||||
|
fileName: file.file.replace(
|
||||||
|
`${edge.source().data('root')}/`,
|
||||||
|
''
|
||||||
|
),
|
||||||
|
target: edge.target().id(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private listenForProjectNodeHovers(): void {
|
||||||
|
this.cy.on('mouseover', (event) => {
|
||||||
|
const node = event.target;
|
||||||
|
if (!node.isNode || !node.isNode() || node.isParent()) return;
|
||||||
|
|
||||||
|
this.cy
|
||||||
|
.elements()
|
||||||
|
.difference(node.outgoers().union(node.incomers()))
|
||||||
|
.not(node)
|
||||||
|
.addClass('transparent');
|
||||||
|
node
|
||||||
|
.addClass('highlight')
|
||||||
|
.outgoers()
|
||||||
|
.union(node.incomers())
|
||||||
|
.addClass('highlight');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cy.on('mouseout', (event) => {
|
||||||
|
const node = event.target;
|
||||||
|
if (!node.isNode || !node.isNode() || node.isParent()) return;
|
||||||
|
|
||||||
|
this.cy.elements().removeClass('transparent');
|
||||||
|
node
|
||||||
|
.removeClass('highlight')
|
||||||
|
.outgoers()
|
||||||
|
.union(node.incomers())
|
||||||
|
.removeClass('highlight');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getImage() {
|
||||||
|
const bg = switchValueByDarkMode(this.cy, '#0F172A', '#FFFFFF');
|
||||||
|
return this.cy.png({ bg, full: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
setFocussedElement(id: string) {
|
||||||
|
this.cy.$id(id).addClass('focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFocussedElement() {
|
||||||
|
this.cy?.nodes('.focused').removeClass('focused');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentlyShownProjectIds(): string[] {
|
||||||
|
return this.cy?.nodes().map((node) => node.data('id')) ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -114,9 +114,9 @@
|
|||||||
"@typescript-eslint/parser": "5.38.1",
|
"@typescript-eslint/parser": "5.38.1",
|
||||||
"@typescript-eslint/type-utils": "^5.36.1",
|
"@typescript-eslint/type-utils": "^5.36.1",
|
||||||
"@typescript-eslint/utils": "5.38.1",
|
"@typescript-eslint/utils": "5.38.1",
|
||||||
"@xstate/immer": "^0.2.0",
|
"@xstate/immer": "^0.3.1",
|
||||||
"@xstate/inspect": "^0.5.1",
|
"@xstate/inspect": "^0.7.0",
|
||||||
"@xstate/react": "^1.6.3",
|
"@xstate/react": "^3.0.1",
|
||||||
"ajv": "^8.11.0",
|
"ajv": "^8.11.0",
|
||||||
"autoprefixer": "10.4.12",
|
"autoprefixer": "10.4.12",
|
||||||
"babel-jest": "28.1.3",
|
"babel-jest": "28.1.3",
|
||||||
@ -246,7 +246,7 @@
|
|||||||
"webpack-node-externals": "^3.0.0",
|
"webpack-node-externals": "^3.0.0",
|
||||||
"webpack-sources": "^3.2.3",
|
"webpack-sources": "^3.2.3",
|
||||||
"webpack-subresource-integrity": "^5.1.0",
|
"webpack-subresource-integrity": "^5.1.0",
|
||||||
"xstate": "^4.25.0",
|
"xstate": "^4.34.0",
|
||||||
"yargs": "^17.6.2",
|
"yargs": "^17.6.2",
|
||||||
"yargs-parser": "21.1.1"
|
"yargs-parser": "21.1.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -77,14 +77,15 @@ function buildEnvironmentJs(
|
|||||||
window.appConfig = {
|
window.appConfig = {
|
||||||
showDebugger: false,
|
showDebugger: false,
|
||||||
showExperimentalFeatures: false,
|
showExperimentalFeatures: false,
|
||||||
projectGraphs: [
|
projects: [
|
||||||
{
|
{
|
||||||
id: 'local',
|
id: 'local',
|
||||||
label: 'local',
|
label: 'local',
|
||||||
url: 'project-graph.json',
|
projectGraphUrl: 'project-graph.json',
|
||||||
|
taskGraphUrl: 'task-graph.json'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultProjectGraph: 'local',
|
defaultProject: 'local',
|
||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import * as yargs from 'yargs';
|
|
||||||
import { ensureDirSync } from 'fs-extra';
|
import { ensureDirSync } from 'fs-extra';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { writeFileSync, readdirSync } from 'fs';
|
import { readdirSync, writeFileSync } from 'fs';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
|
|
||||||
function generateFileContent(
|
function generateFileContent(
|
||||||
@ -9,17 +8,16 @@ function generateFileContent(
|
|||||||
) {
|
) {
|
||||||
return `
|
return `
|
||||||
window.exclude = [];
|
window.exclude = [];
|
||||||
window.watch = false;
|
window.watch = false;
|
||||||
window.environment = 'dev';
|
window.environment = 'dev';
|
||||||
window.useXstateInspect = false;
|
window.useXstateInspect = false;
|
||||||
|
|
||||||
window.appConfig = {
|
window.appConfig = {
|
||||||
showDebugger: true,
|
showDebugger: true,
|
||||||
showExperimentalFeatures: true,
|
showExperimentalFeatures: true,
|
||||||
projectGraphs: ${JSON.stringify(projects)},
|
projects: ${JSON.stringify(projects)},
|
||||||
defaultProjectGraph: '${projects[0].id}',
|
defaultProject: '${projects[0].id}',
|
||||||
};
|
};
|
||||||
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,13 +25,14 @@ function writeFile() {
|
|||||||
let generatedGraphs;
|
let generatedGraphs;
|
||||||
try {
|
try {
|
||||||
generatedGraphs = readdirSync(
|
generatedGraphs = readdirSync(
|
||||||
join(__dirname, '../graph/client/src/assets/generated-graphs')
|
join(__dirname, '../graph/client/src/assets/generated-project-graphs')
|
||||||
).map((filename) => {
|
).map((filename) => {
|
||||||
const id = filename.substring(0, filename.length - 5);
|
const id = filename.substring(0, filename.length - 5);
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label: id,
|
label: id,
|
||||||
url: join('assets/generated-graphs/', filename),
|
projectGraphUrl: join('assets/generated-project-graphs/', filename),
|
||||||
|
taskGraphUrl: join('assets/generated-task-graphs/', filename),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@ -43,13 +42,13 @@ function writeFile() {
|
|||||||
let pregeneratedGraphs;
|
let pregeneratedGraphs;
|
||||||
try {
|
try {
|
||||||
pregeneratedGraphs = readdirSync(
|
pregeneratedGraphs = readdirSync(
|
||||||
join(__dirname, '../graph/client/src/assets/graphs')
|
join(__dirname, '../graph/client/src/assets/project-graphs')
|
||||||
).map((filename) => {
|
).map((filename) => {
|
||||||
const id = filename.substring(0, filename.length - 5);
|
const id = filename.substring(0, filename.length - 5);
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label: id,
|
label: id,
|
||||||
url: join('assets/graphs/', filename),
|
url: join('assets/project-graphs/', filename),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -28,18 +28,34 @@ async function generateGraph(directory: string, name: string) {
|
|||||||
/window.projectGraphResponse = (.*?);/
|
/window.projectGraphResponse = (.*?);/
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const taskGraphResponse = environmentJs.match(
|
||||||
|
/window.taskGraphResponse = (.*?);/
|
||||||
|
);
|
||||||
|
|
||||||
ensureDirSync(
|
ensureDirSync(
|
||||||
join(__dirname, '../graph/client/src/assets/generated-graphs/')
|
join(__dirname, '../graph/client/src/assets/generated-project-graphs/')
|
||||||
|
);
|
||||||
|
ensureDirSync(
|
||||||
|
join(__dirname, '../graph/client/src/assets/generated-task-graphs/')
|
||||||
);
|
);
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(
|
join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../graph/client/src/assets/generated-graphs/',
|
'../graph/client/src/assets/generated-project-graphs/',
|
||||||
`${name}.json`
|
`${name}.json`
|
||||||
),
|
),
|
||||||
projectGraphResponse[1]
|
projectGraphResponse[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(
|
||||||
|
__dirname,
|
||||||
|
'../graph/client/src/assets/generated-task-graphs/',
|
||||||
|
`${name}.json`
|
||||||
|
),
|
||||||
|
taskGraphResponse[1]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -49,8 +65,7 @@ async function generateGraph(directory: string, name: string) {
|
|||||||
.option('name', {
|
.option('name', {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
requiresArg: true,
|
requiresArg: true,
|
||||||
description:
|
description: 'The snake-case name of the file created',
|
||||||
'The version to publish. This does not need to be passed and can be inferred.',
|
|
||||||
})
|
})
|
||||||
.option('directory', {
|
.option('directory', {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -59,5 +74,5 @@ async function generateGraph(directory: string, name: string) {
|
|||||||
})
|
})
|
||||||
.parseSync();
|
.parseSync();
|
||||||
|
|
||||||
generateGraph(parsedArgs.directory, parsedArgs.name);
|
await generateGraph(parsedArgs.directory, parsedArgs.name);
|
||||||
})();
|
})();
|
||||||
|
|||||||
47
yarn.lock
47
yarn.lock
@ -6715,25 +6715,25 @@
|
|||||||
"@webassemblyjs/wast-parser" "1.9.0"
|
"@webassemblyjs/wast-parser" "1.9.0"
|
||||||
"@xtuc/long" "4.2.2"
|
"@xtuc/long" "4.2.2"
|
||||||
|
|
||||||
"@xstate/immer@^0.2.0":
|
"@xstate/immer@^0.3.1":
|
||||||
version "0.2.0"
|
version "0.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@xstate/immer/-/immer-0.2.0.tgz#4f128947c3cbb3e68357b886485a36852d4e06b3"
|
resolved "https://registry.yarnpkg.com/@xstate/immer/-/immer-0.3.1.tgz#73a7948f7f248e00dc287b55290a949cd8276b3d"
|
||||||
integrity sha512-ZKwAwS84kfmN108lEtVHw8jztKDiFeaQsTxkOlOghpK1Lr7+13G8HhZZXyN1/pVkplloUUOPMH5EXVtitZDr8w==
|
integrity sha512-YE+KY08IjEEmXo6XKKpeSGW4j9LfcXw+5JVixLLUO3fWQ3M95joWJ40VtGzx0w0zQSzoCNk8NgfvwWBGSbIaTA==
|
||||||
|
|
||||||
"@xstate/inspect@^0.5.1":
|
"@xstate/inspect@^0.7.0":
|
||||||
version "0.5.2"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/@xstate/inspect/-/inspect-0.5.2.tgz#83d58c96f704ceaab6f7849e578d8e6ce212038c"
|
resolved "https://registry.yarnpkg.com/@xstate/inspect/-/inspect-0.7.0.tgz#0e3011d0fb8eca6d68f06a7c384ab1390801e176"
|
||||||
integrity sha512-DdqUPiKaHW6VpnVZcm8YMD8LBeS3B9bB3+VT/6VEyilgvf2MgYzho2dKOOkeZM0iDEadSmzGdDpz0jh7DSpMXQ==
|
integrity sha512-3wrTf8TfBYprH1gBFdxmOQUBDpBazlICWvGdFzr8IHFL4MbiexEZdAsL2QC/WAmW9BqNYTWTwgfbvKHKg+FrlA==
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-safe-stringify "^2.0.7"
|
fast-safe-stringify "^2.1.1"
|
||||||
|
|
||||||
"@xstate/react@^1.6.3":
|
"@xstate/react@^3.0.1":
|
||||||
version "1.6.3"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-1.6.3.tgz#706f3beb7bc5879a78088985c8fd43b9dab7f725"
|
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.0.1.tgz#937eeb5d5d61734ab756ca40146f84a6fe977095"
|
||||||
integrity sha512-NCUReRHPGvvCvj2yLZUTfR0qVp6+apc8G83oXSjN4rl89ZjyujiKrTff55bze/HrsvCsP/sUJASf2n0nzMF1KQ==
|
integrity sha512-/tq/gg92P9ke8J+yDNDBv5/PAxBvXJf2cYyGDByzgtl5wKaxKxzDT82Gj3eWlCJXkrBg4J5/V47//gRJuVH2fA==
|
||||||
dependencies:
|
dependencies:
|
||||||
use-isomorphic-layout-effect "^1.0.0"
|
use-isomorphic-layout-effect "^1.0.0"
|
||||||
use-subscription "^1.3.0"
|
use-sync-external-store "^1.0.0"
|
||||||
|
|
||||||
"@xtuc/ieee754@^1.2.0":
|
"@xtuc/ieee754@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
@ -11640,7 +11640,7 @@ fast-redact@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
|
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
|
||||||
integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
|
integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
|
||||||
|
|
||||||
fast-safe-stringify@2.1.1, fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8:
|
fast-safe-stringify@2.1.1, fast-safe-stringify@^2.0.8, fast-safe-stringify@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||||
@ -22179,14 +22179,7 @@ use-isomorphic-layout-effect@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
||||||
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
||||||
|
|
||||||
use-subscription@^1.3.0:
|
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
|
||||||
version "1.8.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.8.0.tgz#f118938c29d263c2bce12fc5585d3fe694d4dbce"
|
|
||||||
integrity sha512-LISuG0/TmmoDoCRmV5XAqYkd3UCBNM0ML3gGBndze65WITcsExCD3DTvXXTLyNcOC0heFQZzluW88bN/oC1DQQ==
|
|
||||||
dependencies:
|
|
||||||
use-sync-external-store "^1.2.0"
|
|
||||||
|
|
||||||
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
@ -23088,10 +23081,10 @@ xmlhttprequest-ssl@~1.6.2:
|
|||||||
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
|
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
|
||||||
integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
|
integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
|
||||||
|
|
||||||
xstate@^4.25.0:
|
xstate@^4.34.0:
|
||||||
version "4.33.6"
|
version "4.34.0"
|
||||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.33.6.tgz#9e23f78879af106f1de853aba7acb2bc3b1eb950"
|
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.34.0.tgz#401901c478f0b2a7f07576c020b6e6f750b5bd10"
|
||||||
integrity sha512-A5R4fsVKADWogK2a43ssu8Fz1AF077SfrKP1ZNyDBD8lNa/l4zfR//Luofp5GSWehOQr36Jp0k2z7b+sH2ivyg==
|
integrity sha512-MFnYz7cJrWuXSZ8IPkcCyLB1a2T3C71kzMeShXKmNaEjBR/JQebKZPHTtxHKZpymESaWO31rA3IQ30TC6LW+sw==
|
||||||
|
|
||||||
xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
|
xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user