chore(graph): add group by project option to task graph (#13239)
This commit is contained in:
parent
558b99c3c6
commit
e42da40438
@ -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]');
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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) => {
|
||||
export const GroupByFolderPanel = ({
|
||||
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)}
|
||||
<CheckboxPanel
|
||||
checked={groupByFolder}
|
||||
checkChanged={groupByFolderChanged}
|
||||
name={'groupByFolder'}
|
||||
label={'Group by folder'}
|
||||
description={'Visually arrange libraries by folders.'}
|
||||
/>
|
||||
</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 default GroupByFolderPanel;
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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.',
|
||||
};
|
||||
42
graph/client/src/app/ui-components/checkbox-panel.tsx
Normal file
42
graph/client/src/app/ui-components/checkbox-panel.tsx
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -216,6 +216,12 @@ export class GraphService {
|
||||
break;
|
||||
case 'notifyTaskGraphDeselectTask':
|
||||
elementsToSendToRender = this.taskTraversalGraph.deselectTask();
|
||||
break;
|
||||
case 'setGroupByProject':
|
||||
elementsToSendToRender = this.taskTraversalGraph.setGroupByProject(
|
||||
event.groupByProject
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@ -124,4 +124,8 @@ export type TaskGraphRenderEvents =
|
||||
}
|
||||
| {
|
||||
type: 'notifyTaskGraphDeselectTask';
|
||||
}
|
||||
| {
|
||||
type: 'setGroupByProject';
|
||||
groupByProject: boolean;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
];
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 taskElements = [];
|
||||
|
||||
const parents: Record<
|
||||
string,
|
||||
{ id: string; parentId: string; label: string }
|
||||
> = {};
|
||||
|
||||
for (let taskName in taskGraph.tasks) {
|
||||
const task = taskGraph.tasks[taskName];
|
||||
const project = this.projects.find(
|
||||
(project) => project.name === projectName
|
||||
(project) => project.name === task.target.project
|
||||
);
|
||||
|
||||
if (project === undefined) {
|
||||
throw new Error(`Could not find project ${projectName}`);
|
||||
throw new Error(`Could not find project ${project.name}`);
|
||||
}
|
||||
|
||||
const taskElements = [];
|
||||
taskElements.push(new TaskNode(taskGraph.tasks[taskName], project));
|
||||
|
||||
for (let taskName in taskGraph.tasks) {
|
||||
taskElements.push(
|
||||
new TaskNode(
|
||||
taskGraph.tasks[taskName],
|
||||
this.projects[taskGraph.tasks[taskName].target.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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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}',
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user