chore(graph): add group by project option to task graph (#13239)

This commit is contained in:
Philip Fulcher 2022-11-17 15:09:36 -07:00 committed by GitHub
parent 558b99c3c6
commit e42da40438
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 337 additions and 185 deletions

View File

@ -14,7 +14,7 @@ export const getCheckedProjectItems = () => cy.get('[data-active="true"]');
export const getUncheckedProjectItems = () => cy.get('[data-active="false"]');
export const getGroupByFolderCheckbox = () =>
cy.get('input[name=displayOptions][value=groupByFolder]');
cy.get('input[name=groupByFolder]');
export const getSearchDepthCheckbox = () =>
cy.get('input[name=depthFilter][value=depthFilterActivated]');

View File

@ -8,6 +8,12 @@ export class ExternalApi {
}
enableExperimentalFeatures() {
localStorage.setItem('showExperimentalFeatures', 'true');
window.appConfig.showExperimentalFeatures = true;
}
disableExperimentalFeatures() {
localStorage.setItem('showExperimentalFeatures', 'false');
window.appConfig.showExperimentalFeatures = false;
}
}

View File

@ -1,17 +0,0 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { GroupByFolderPanel } from './group-by-folder-panel';
export default {
component: GroupByFolderPanel,
title: 'Project Graph/GroupByFolderPanel',
argTypes: { groupByFolderChanged: { action: 'groupByFolderChanged' } },
} as ComponentMeta<typeof GroupByFolderPanel>;
const Template: ComponentStory<typeof GroupByFolderPanel> = (args) => (
<GroupByFolderPanel {...args} />
);
export const Primary = Template.bind({});
Primary.args = {
groupByFolder: false,
};

View File

@ -1,41 +1,24 @@
import { memo } from 'react';
import CheckboxPanel from '../../ui-components/checkbox-panel';
export interface DisplayOptionsPanelProps {
groupByFolder: boolean;
groupByFolderChanged: (checked: boolean) => void;
}
export const GroupByFolderPanel = memo(
({ groupByFolder, groupByFolderChanged }: DisplayOptionsPanelProps) => {
return (
<div className="mt-8 px-4">
<div className="flex items-start">
<div className="flex h-5 items-center">
<input
id="displayOptions"
name="displayOptions"
value="groupByFolder"
type="checkbox"
className="h-4 w-4 accent-blue-500 dark:accent-sky-500"
onChange={(event) => groupByFolderChanged(event.target.checked)}
checked={groupByFolder}
/>
</div>
<div className="ml-3 text-sm">
<label
htmlFor="displayOptions"
className="cursor-pointer font-medium text-slate-600 dark:text-slate-400"
>
Group by folder
</label>
<p className="text-slate-400 dark:text-slate-500">
Visually arrange libraries by folders with different colors.
</p>
</div>
</div>
</div>
);
}
);
export const GroupByFolderPanel = ({
groupByFolder,
groupByFolderChanged,
}: DisplayOptionsPanelProps) => {
return (
<CheckboxPanel
checked={groupByFolder}
checkChanged={groupByFolderChanged}
name={'groupByFolder'}
label={'Group by folder'}
description={'Visually arrange libraries by folders.'}
/>
);
};
export default GroupByFolderPanel;

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import ExperimentalFeature from '../ui-components/experimental-feature';
import { useProjectGraphSelector } from './hooks/use-project-graph-selector';
import {
@ -22,6 +22,11 @@ import TracingPanel from './panels/tracing-panel';
import { useEnvironmentConfig } from '../hooks/use-environment-config';
import { TracingAlgorithmType } from './machines/interfaces';
import { getProjectGraphService } from '../machines/get-services';
import { useIntervalWhen } from '../hooks/use-interval-when';
// nx-ignore-next-line
import { ProjectGraphClientResponse } from 'nx/src/command-line/dep-graph';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import { getProjectGraphDataService } from '../hooks/get-project-graph-data-service';
export function ProjectsSidebar(): JSX.Element {
const environmentConfig = useEnvironmentConfig();
@ -38,6 +43,12 @@ export function ProjectsSidebar(): JSX.Element {
const isTracing = projectGraphService.state.matches('tracing');
const tracingInfo = useProjectGraphSelector(getTracingInfo);
const projectGraphDataService = getProjectGraphDataService();
const selectedProjectRouteData = useRouteLoaderData(
'selectedWorkspace'
) as ProjectGraphClientResponse;
const params = useParams();
function resetFocus() {
projectGraphService.send({ type: 'unfocusProject' });
@ -110,6 +121,45 @@ export function ProjectsSidebar(): JSX.Element {
});
}
useEffect(() => {
projectGraphService.send({
type: 'setProjects',
projects: selectedProjectRouteData.projects,
dependencies: selectedProjectRouteData.dependencies,
affectedProjects: selectedProjectRouteData.affected,
workspaceLayout: selectedProjectRouteData.layout,
});
}, [selectedProjectRouteData]);
useIntervalWhen(
() => {
const selectedWorkspaceId =
params.selectedProjectId ??
environmentConfig.appConfig.defaultWorkspaceId;
const projectInfo = environmentConfig.appConfig.workspaces.find(
(graph) => graph.id === selectedWorkspaceId
);
const fetchProjectGraph = async () => {
const project: ProjectGraphClientResponse =
await projectGraphDataService.getProjectGraph(
projectInfo.projectGraphUrl
);
projectGraphService.send({
type: 'updateGraph',
projects: project.projects,
dependencies: project.dependencies,
});
};
fetchProjectGraph();
},
5000,
environmentConfig.watch
);
const updateTextFilter = useCallback(
(textFilter: string) => {
projectGraphService.send({ type: 'filterByText', search: textFilter });

View File

@ -1,5 +1,10 @@
import TaskList from './task-list';
import { useNavigate, useParams, useRouteLoaderData } from 'react-router-dom';
import {
useNavigate,
useParams,
useRouteLoaderData,
useSearchParams,
} from 'react-router-dom';
// nx-ignore-next-line
import type {
ProjectGraphClientResponse,
@ -8,24 +13,35 @@ import type {
import { getGraphService } from '../machines/graph.service';
import { useEffect } from 'react';
import FocusedPanel from '../ui-components/focused-panel';
import CheckboxPanel from '../ui-components/checkbox-panel';
export function TasksSidebar() {
const graphService = getGraphService();
const navigate = useNavigate();
const params = useParams();
const [searchParams, setSearchParams] = useSearchParams();
const selectedProjectRouteData = useRouteLoaderData(
'SelectedProject'
'selectedWorkspace'
) as ProjectGraphClientResponse;
const projects = selectedProjectRouteData.projects;
const workspaceLayout = selectedProjectRouteData.layout;
const routeData = useRouteLoaderData(
'selectedTask'
) as TaskGraphClientResponse;
const { taskGraphs } = routeData;
const selectedTask = params['selectedTask'];
const projects = selectedProjectRouteData.projects;
// const projects = selectedProjectRouteData.projects.filter((project) => {
// return (
// Object.keys(project.data.targets).filter((target) => {
// const taskName = `${project.name}:${target}`;
// return (
// taskGraphs[taskName]?.dependencies[taskName]?.length > 0
// );
// }).length > 0
// );
// });
const selectedTask = params['selectedTaskId'];
useEffect(() => {
graphService.handleTaskEvent({
@ -48,6 +64,22 @@ export function TasksSidebar() {
}
}, [params]);
const groupByProject = searchParams.get('groupByProject') === 'true';
useEffect(() => {
if (groupByProject) {
graphService.handleTaskEvent({
type: 'setGroupByProject',
groupByProject: true,
});
} else {
graphService.handleTaskEvent({
type: 'setGroupByProject',
groupByProject: false,
});
}
}, [searchParams]);
function selectTask(taskId: string) {
if (selectedTask) {
navigate(`../${taskId}`);
@ -60,6 +92,18 @@ export function TasksSidebar() {
navigate('..');
}
function groupByProjectChanged(checked) {
setSearchParams((currentSearchParams) => {
if (checked) {
currentSearchParams.set('groupByProject', 'true');
} else {
currentSearchParams.delete('groupByProject');
}
return currentSearchParams;
});
}
return (
<>
{selectedTask ? (
@ -68,6 +112,15 @@ export function TasksSidebar() {
resetFocus={resetFocus}
></FocusedPanel>
) : null}
<CheckboxPanel
checked={groupByProject}
checkChanged={groupByProjectChanged}
name={'groupByProject'}
label={'Group by project'}
description={'Visually arrange tasks by project.'}
/>
<TaskList
projects={projects}
workspaceLayout={workspaceLayout}

View File

@ -24,7 +24,13 @@ export function getEnvironmentConfig() {
localMode: window.localMode,
projectGraphResponse: window.projectGraphResponse,
environment: window.environment,
appConfig: window.appConfig,
appConfig: {
...window.appConfig,
showExperimentalFeatures:
localStorage.getItem('showExperimentalFeatures') === 'true'
? true
: window.appConfig.showExperimentalFeatures,
},
useXstateInspect: window.useXstateInspect,
};
}

View File

@ -4,7 +4,7 @@ import type {
TaskGraphClientResponse,
} from 'nx/src/command-line/dep-graph';
export interface GraphListItem {
export interface WorkspaceData {
id: string;
label: string;
projectGraphUrl: string;
@ -29,8 +29,8 @@ export interface Environment {
export interface AppConfig {
showDebugger: boolean;
showExperimentalFeatures: boolean;
projects: GraphListItem[];
defaultProject: string;
workspaces: WorkspaceData[];
defaultWorkspaceId: string;
}
export interface GraphPerfReport {

View File

@ -6,7 +6,8 @@ let projectGraphService = interpret(projectGraphMachine, {
});
export function getProjectGraphService() {
if (projectGraphService.status === InterpreterStatus.Stopped) {
if (projectGraphService.status === InterpreterStatus.NotStarted) {
projectGraphService.start();
}
return projectGraphService;

View File

@ -6,7 +6,6 @@ import { getEnvironmentConfig } from './hooks/use-environment-config';
// nx-ignore-next-line
import { ProjectGraphClientResponse } from 'nx/src/command-line/dep-graph';
import { getProjectGraphDataService } from './hooks/get-project-graph-data-service';
import { getProjectGraphService } from './machines/get-services';
const { appConfig } = getEnvironmentConfig();
const projectGraphDataService = getProjectGraphDataService();
@ -19,19 +18,21 @@ export function getRoutesForEnvironment() {
}
}
const projectDataLoader = async (selectedProjectId: string) => {
const projectInfo = appConfig.projects.find(
(graph) => graph.id === selectedProjectId
const workspaceDataLoader = async (selectedWorkspaceId: string) => {
const workspaceInfo = appConfig.workspaces.find(
(graph) => graph.id === selectedWorkspaceId
);
const projectGraph: ProjectGraphClientResponse =
await projectGraphDataService.getProjectGraph(projectInfo.projectGraphUrl);
await projectGraphDataService.getProjectGraph(
workspaceInfo.projectGraphUrl
);
return projectGraph;
};
const taskDataLoader = async (selectedProjectId: string) => {
const projectInfo = appConfig.projects.find(
const projectInfo = appConfig.workspaces.find(
(graph) => graph.id === selectedProjectId
);
@ -41,9 +42,7 @@ const taskDataLoader = async (selectedProjectId: string) => {
const childRoutes: RouteObject[] = [
{
path: 'projects',
loader: () => {
getProjectGraphService().start();
},
loader: () => {},
element: <ProjectsSidebar />,
},
{
@ -54,10 +53,8 @@ const childRoutes: RouteObject[] = [
return redirect(`/projects`);
}
getProjectGraphService().stop();
const selectedProjectId =
params.selectedProjectId ?? appConfig.defaultProject;
params.selectedProjectId ?? appConfig.defaultWorkspaceId;
return taskDataLoader(selectedProjectId);
},
path: 'tasks',
@ -68,7 +65,7 @@ const childRoutes: RouteObject[] = [
element: <TasksSidebar />,
},
{
path: ':selectedTask',
path: ':selectedTaskId',
element: <TasksSidebar />,
},
],
@ -84,17 +81,17 @@ export const devRoutes: RouteObject[] = [
loader: async ({ request, params }) => {
const { search } = new URL(request.url);
return redirect(`/${appConfig.defaultProject}/projects${search}`);
return redirect(`/${appConfig.defaultWorkspaceId}/projects${search}`);
},
},
{
path: ':selectedProjectId',
id: 'SelectedProject',
id: 'selectedWorkspace',
element: <Shell />,
loader: async ({ request, params }) => {
const selectedProjectId =
params.selectedProjectId ?? appConfig.defaultProject;
return projectDataLoader(selectedProjectId);
params.selectedProjectId ?? appConfig.defaultWorkspaceId;
return workspaceDataLoader(selectedProjectId);
},
children: childRoutes,
},
@ -105,18 +102,19 @@ export const devRoutes: RouteObject[] = [
export const releaseRoutes: RouteObject[] = [
{
path: '/',
id: 'SelectedProject',
id: 'selectedWorkspace',
loader: async ({ request, params }) => {
const selectedProjectId =
params.selectedProjectId ?? appConfig.defaultProject;
return projectDataLoader(selectedProjectId);
const selectedWorkspaceId = appConfig.defaultWorkspaceId;
return workspaceDataLoader(selectedWorkspaceId);
},
element: <Shell />,
children: [
{
index: true,
loader: () => {
return redirect(`/projects/`);
loader: ({ request }) => {
const { search } = new URL(request.url);
return redirect(`/projects${search}`);
},
},
...childRoutes,

View File

@ -20,7 +20,12 @@ import {
projectIsSelectedSelector,
} from './feature-projects/machines/selectors';
import { selectValueByThemeStatic } from './theme-resolver';
import { Outlet, useLoaderData, useNavigate } from 'react-router-dom';
import {
Outlet,
useLoaderData,
useNavigate,
useParams,
} from 'react-router-dom';
import ThemePanel from './feature-projects/panels/theme-panel';
import Dropdown from './ui-components/dropdown';
import { useCurrentPath } from './hooks/use-current-path';
@ -32,20 +37,15 @@ import TooltipDisplay from './ui-tooltips/graph-tooltip-display';
export function Shell(): JSX.Element {
const projectGraphService = getProjectGraphService();
const projectGraphDataService = getProjectGraphDataService();
const environment = useEnvironmentConfig();
const lastPerfReport = useProjectGraphSelector(lastPerfReportSelector);
const projectIsSelected = useProjectGraphSelector(projectIsSelectedSelector);
const taskIsSelected = true;
const environmentConfig = useEnvironmentConfig();
const [selectedProjectId, setSelectedProjectId] = useState<string>(
environment.appConfig.defaultProject
);
const navigate = useNavigate();
const currentPath = useCurrentPath();
const { selectedProjectId, selectedTaskId } = useParams();
const taskIsSelected = !!selectedTaskId;
const currentRoute = currentPath.currentPath;
const topLevelRoute = currentRoute.startsWith('/tasks')
@ -67,40 +67,6 @@ export function Shell(): JSX.Element {
}
const routeData = useLoaderData() as ProjectGraphClientResponse;
useEffect(() => {
projectGraphService.send({
type: 'setProjects',
projects: routeData.projects,
dependencies: routeData.dependencies,
affectedProjects: routeData.affected,
workspaceLayout: routeData.layout,
});
}, [routeData]);
useIntervalWhen(
() => {
const projectInfo = environment.appConfig.projects.find(
(graph) => graph.id === selectedProjectId
);
const fetchProjectGraph = async () => {
const project: ProjectGraphClientResponse =
await projectGraphDataService.getProjectGraph(
projectInfo.projectGraphUrl
);
projectGraphService.send({
type: 'updateGraph',
projects: project.projects,
dependencies: project.dependencies,
});
};
fetchProjectGraph();
},
5000,
environment.watch
);
function downloadImage() {
const graph = getGraphService();
@ -202,7 +168,7 @@ export function Shell(): JSX.Element {
>
{environment.appConfig.showDebugger ? (
<DebuggerPanel
projects={environment.appConfig.projects}
projects={environment.appConfig.workspaces}
selectedProject={selectedProjectId}
lastPerfReport={lastPerfReport}
selectedProjectChange={projectChange}

View File

@ -0,0 +1,20 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
import CheckboxPanel from './checkbox-panel';
export default {
component: CheckboxPanel,
title: 'Shared/CheckboxPanel',
argTypes: { checkChanged: { action: 'checkChanged' } },
} as ComponentMeta<typeof CheckboxPanel>;
const Template: ComponentStory<typeof CheckboxPanel> = (args) => (
<CheckboxPanel {...args} />
);
export const Primary = Template.bind({});
Primary.args = {
checked: false,
name: 'option-to-check',
label: 'Option to check',
description: 'You can check this option.',
};

View File

@ -0,0 +1,42 @@
import { memo } from 'react';
export interface CheckboxPanelProps {
checked: boolean;
checkChanged: (checked: boolean) => void;
name: string;
label: string;
description: string;
}
export const CheckboxPanel = memo(
({ checked, checkChanged, label, description, name }: CheckboxPanelProps) => {
return (
<div className="mt-8 px-4">
<div className="flex items-start">
<div className="flex h-5 items-center">
<input
id={name}
name={name}
value={name}
type="checkbox"
className="h-4 w-4 accent-blue-500 dark:accent-sky-500"
onChange={(event) => checkChanged(event.target.checked)}
checked={checked}
/>
</div>
<div className="ml-3 text-sm">
<label
htmlFor={name}
className="cursor-pointer font-medium text-slate-600 dark:text-slate-400"
>
{label}
</label>
<p className="text-slate-400 dark:text-slate-500">{description}</p>
</div>
</div>
</div>
);
}
);
export default CheckboxPanel;

View File

@ -1,9 +1,9 @@
import { memo } from 'react';
import { GraphListItem, GraphPerfReport } from '../interfaces';
import { WorkspaceData, GraphPerfReport } from '../interfaces';
import Dropdown from './dropdown';
export interface DebuggerPanelProps {
projects: GraphListItem[];
projects: WorkspaceData[];
selectedProject: string;
selectedProjectChange: (projectName: string) => void;
lastPerfReport: GraphPerfReport;

View File

@ -6,7 +6,7 @@ window.useXstateInspect = false;
window.appConfig = {
showDebugger: true,
showExperimentalFeatures: true,
projects: [
workspaces: [
{
id: 'e2e',
label: 'e2e',
@ -20,5 +20,5 @@ window.appConfig = {
taskGraphUrl: 'assets/task-graphs/affected.json',
},
],
defaultProject: 'e2e',
defaultWorkspaceId: 'e2e',
};

View File

@ -6,7 +6,7 @@ window.useXstateInspect = false;
window.appConfig = {
showDebugger: false,
showExperimentalFeatures: false,
projects: [
workspaces: [
{
id: 'local',
label: 'local',
@ -14,5 +14,5 @@ window.appConfig = {
taskGraphUrl: 'assets/task-graphs/e2e.json',
},
],
defaultProject: 'local',
defaultWorkspaceId: 'local',
};

View File

@ -7,7 +7,7 @@ window.localMode = 'build';
window.appConfig = {
showDebugger: false,
showExperimentalFeatures: false,
projects: [
workspaces: [
{
id: 'local',
label: 'local',
@ -15,7 +15,7 @@ window.appConfig = {
taskGraphUrl: 'assets/task-graphs/e2e.json',
},
],
defaultProject: 'local',
defaultWorkspaceId: 'local',
};
window.projectGraphResponse = {

View File

@ -6,7 +6,7 @@ window.useXstateInspect = false;
window.appConfig = {
showDebugger: false,
showExperimentalFeatures: false,
projects: [
workspaces: [
{
id: 'local',
label: 'local',
@ -14,5 +14,5 @@ window.appConfig = {
taskGraphUrl: 'assets/task-graphs/e2e.json',
},
],
defaultProject: 'local',
defaultWorkspaceId: 'local',
};

View File

@ -6,7 +6,7 @@ window.useXstateInspect = false;
window.appConfig = {
showDebugger: false,
showExperimentalFeatures: true,
projects: [
workspaces: [
{
id: 'local',
label: 'local',
@ -14,5 +14,5 @@ window.appConfig = {
taskGraphUrl: 'assets/task-graphs/e2e.json',
},
],
defaultProject: 'local',
defaultWorkspaceId: 'local',
};

View File

@ -216,6 +216,12 @@ export class GraphService {
break;
case 'notifyTaskGraphDeselectTask':
elementsToSendToRender = this.taskTraversalGraph.deselectTask();
break;
case 'setGroupByProject':
elementsToSendToRender = this.taskTraversalGraph.setGroupByProject(
event.groupByProject
);
break;
}

View File

@ -124,4 +124,8 @@ export type TaskGraphRenderEvents =
}
| {
type: 'notifyTaskGraphDeselectTask';
}
| {
type: 'setGroupByProject';
groupByProject: boolean;
};

View File

@ -91,6 +91,13 @@ const highlightedNodes: Stylesheet = {
},
};
const taskNodes: Stylesheet = {
selector: 'node.task',
style: {
label: 'data(label)',
},
};
const transparentProjectNodes: Stylesheet = {
selector: 'node.transparent:childless',
style: { opacity: 0.5 },
@ -113,4 +120,5 @@ export const nodeStyles = [
highlightedNodes,
transparentProjectNodes,
transparentParentNodes,
taskNodes,
];

View File

@ -46,29 +46,12 @@ export class RenderGraph {
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);
}
this.render();
}
set rankDir(rankDir: 'LR' | 'TB') {
this._rankDir = rankDir;
if (this.cy) {
const elements = this.cy.elements();
elements
.layout({
...cytoscapeDagreConfig,
...{ rankDir: rankDir },
} as CytoscapeDagreConfig)
.run();
}
this.render();
}
get activeContainer() {

View File

@ -4,26 +4,33 @@ import * as cy from 'cytoscape';
export interface TaskNodeDataDefinition extends cy.NodeDataDefinition {
id: string;
label: string;
executor: string;
}
export class TaskNode {
constructor(private task: Task, private project: ProjectGraphProjectNode) {}
getCytoscapeNodeDef(): cy.NodeDefinition {
getCytoscapeNodeDef(groupByProject: boolean): cy.NodeDefinition {
return {
group: 'nodes',
data: this.getData(),
classes: 'task',
data: this.getData(groupByProject),
selectable: false,
grabbable: false,
pannable: true,
};
}
private getData(): TaskNodeDataDefinition {
private getData(groupByProject: boolean): TaskNodeDataDefinition {
const label = groupByProject
? this.task.id.split(':').slice(1).join(':')
: this.task.id;
return {
id: this.task.id,
executor: 'placeholder',
label,
executor: this.project.data.targets[this.task.target.target].executor,
parent: groupByProject ? this.task.target.project : null,
};
}
}

View File

@ -4,22 +4,44 @@ import { TaskGraphRecord } from '../interfaces';
import { TaskNode } from './task-node';
import { TaskEdge } from './task-edge';
import cytoscape, { Core } from 'cytoscape';
import { ParentNode } from './parent-node';
export class TaskTraversalGraph {
private projects: ProjectGraphProjectNode[] = [];
private taskGraphs: TaskGraphRecord = {};
private cy: Core;
private selectedTask: string;
private groupByProject: boolean = false;
setProjects(
projects: ProjectGraphProjectNode[],
taskGraphs: TaskGraphRecord
) {
this.selectedTask = null;
this.projects = projects;
this.taskGraphs = taskGraphs;
}
selectTask(taskId: string) {
this.createElements(taskId);
this.selectedTask = taskId;
this.createElements(taskId, this.groupByProject);
return this.cy.elements();
}
setGroupByProject(groupByProject: boolean) {
this.groupByProject = groupByProject;
if (this.selectedTask) {
this.createElements(this.selectedTask, groupByProject);
} else {
this.cy = cytoscape({
headless: true,
elements: [],
});
return this.cy.elements();
}
return this.cy.elements();
}
@ -33,31 +55,43 @@ export class TaskTraversalGraph {
return this.cy.elements();
}
private createElements(taskId: string) {
const [projectName, target, configuration] = taskId.split(':');
private createElements(taskId: string, groupByFolder: boolean) {
const taskGraph = this.taskGraphs[taskId];
if (taskGraph === undefined) {
throw new Error(`Could not find task graph for ${taskId}`);
}
const project = this.projects.find(
(project) => project.name === projectName
);
if (project === undefined) {
throw new Error(`Could not find project ${projectName}`);
}
const taskElements = [];
const parents: Record<
string,
{ id: string; parentId: string; label: string }
> = {};
for (let taskName in taskGraph.tasks) {
taskElements.push(
new TaskNode(
taskGraph.tasks[taskName],
this.projects[taskGraph.tasks[taskName].target.project]
)
const task = taskGraph.tasks[taskName];
const project = this.projects.find(
(project) => project.name === task.target.project
);
if (project === undefined) {
throw new Error(`Could not find project ${project.name}`);
}
taskElements.push(new TaskNode(taskGraph.tasks[taskName], project));
if (groupByFolder) {
parents[project.name] = {
id: project.name,
parentId: null,
label: project.name,
};
}
}
for (let parent in parents) {
taskElements.push(new ParentNode(parents[parent]));
}
for (let topDep in taskGraph.dependencies) {
@ -68,7 +102,9 @@ export class TaskTraversalGraph {
this.cy = cytoscape({
headless: true,
elements: taskElements.map((element) => element.getCytoscapeNodeDef()),
elements: taskElements.map((element) =>
element.getCytoscapeNodeDef(groupByFolder)
),
boxSelectionEnabled: false,
});
}

View File

@ -73,7 +73,7 @@ function buildEnvironmentJs(
window.appConfig = {
showDebugger: false,
showExperimentalFeatures: false,
projects: [
workspaces: [
{
id: 'local',
label: 'local',
@ -81,7 +81,7 @@ function buildEnvironmentJs(
taskGraphUrl: 'task-graph.json'
}
],
defaultProject: 'local',
defaultWorkspaceId: 'local',
};
`;
@ -408,7 +408,7 @@ async function startServer(
params.append('groupByFolder', 'true');
}
open(`${url}?${params.toString()}`);
open(`${url}/projects?${params.toString()}`);
}
}

View File

@ -4,7 +4,7 @@ import { readdirSync, writeFileSync } from 'fs';
import { execSync } from 'child_process';
function generateFileContent(
projects: { id: string; label: string; url: string }[]
workspaces: { id: string; label: string; url: string }[]
) {
return `
window.exclude = [];
@ -15,8 +15,8 @@ function generateFileContent(
window.appConfig = {
showDebugger: true,
showExperimentalFeatures: true,
projects: ${JSON.stringify(projects)},
defaultProject: '${projects[0].id}',
workspaces: ${JSON.stringify(workspaces)},
defaultWorkspaceId: '${workspaces[0].id}',
};
`;
}