feat(dep-graph): launch path tracing (#9919)

This commit is contained in:
Philip Fulcher 2022-04-21 02:30:13 -04:00 committed by GitHub
parent 5f12ce0f12
commit 3dc818f631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 229 additions and 67 deletions

View File

@ -1,6 +1,7 @@
import {
getCheckedProjectItems,
getDeselectAllButton,
getFocusButtonForProject,
getGroupByFolderCheckbox,
getImageDownloadButton,
getIncludeProjectsInPathButton,
@ -146,7 +147,7 @@ describe('dep-graph-client', () => {
describe('focusing projects in sidebar', () => {
it('should select appropriate projects', () => {
cy.contains('nx-dev').scrollIntoView().should('be.visible');
cy.get('[data-project="nx-dev"]').prev('button').click({ force: true });
getFocusButtonForProject('nx-dev').click({ force: true });
getCheckedProjectItems().should('have.length', 11);
});
@ -154,7 +155,7 @@ describe('dep-graph-client', () => {
describe('unfocus button', () => {
it('should uncheck all project items', () => {
cy.get('[data-project="nx-dev"]').prev('button').click({ force: true });
getFocusButtonForProject('nx-dev').click({ force: true });
getUnfocusProjectButton().click();
getUncheckedProjectItems().should('have.length', 62);
@ -182,12 +183,12 @@ describe('dep-graph-client', () => {
});
it('should be shown when a project is selected', () => {
cy.get('[data-project="nx-dev"]').prev('button').click({ force: true });
cy.get('[data-project="nx-dev"]').click({ force: true });
getImageDownloadButton().should('not.have.class', 'opacity-0');
});
it('should be hidden when no more projects are selected', () => {
cy.get('[data-project="nx-dev"]').prev('button').click({ force: true });
cy.get('[data-project="nx-dev"]').click({ force: true });
getDeselectAllButton().click();
getImageDownloadButton().should('have.class', 'opacity-0');
});
@ -196,7 +197,7 @@ describe('dep-graph-client', () => {
describe('setting url params', () => {
it('should set focused project', () => {
cy.contains('nx-dev').scrollIntoView().should('be.visible');
cy.get('[data-project="nx-dev"]').prev('button').click({ force: true });
getFocusButtonForProject('nx-dev').click({ force: true });
cy.url().should('contain', 'focus=nx-dev');
});

View File

@ -29,3 +29,6 @@ export const getIncludeProjectsInPathButton = () =>
export const getImageDownloadButton = () =>
cy.get('[data-cy=downloadImageButton]');
export const getFocusButtonForProject = (projectName: string) =>
cy.get(`[data-cy="focus-button-${projectName}"]`);

View File

@ -23,8 +23,8 @@ describe('dep-graph-client in watch mode', () => {
// TODO: This test is getting flaky but was fixed by increasing the tick time between checks
// Figure out a better way to test this
it('should retain selected projects as new libs are created', () => {
cy.contains('existing-app-1').siblings('button').click();
cy.contains('existing-lib-1').siblings('button').click();
cy.get('[data-project="existing-app-1"]').click();
cy.get('[data-project="existing-lib-1"]').click();
cy.tick(6000);

View File

@ -40,6 +40,7 @@ export const initialContext: DepGraphContext = {
tracing: {
start: null,
end: null,
algorithm: 'shortest',
},
};
@ -211,6 +212,15 @@ export const depGraphMachine = Machine<
setSearchDepth: {
actions: ['setSearchDepth', 'notifyRouteSearchDepth'],
},
setTracingAlgorithm: {
actions: [
assign((ctx, event) => {
ctx.tracing.algorithm = event.algorithm;
}),
'notifyRouteTracing',
'notifyGraphTracing',
],
},
filterByText: {
target: 'textFiltered',
},
@ -280,6 +290,7 @@ export const depGraphMachine = Machine<
type: 'notifyGraphTracing',
start: ctx.tracing.start,
end: ctx.tracing.end,
algorithm: ctx.tracing.algorithm,
};
},
{
@ -385,6 +396,7 @@ export const depGraphMachine = Machine<
type: 'notifyRouteTracing',
start: ctx.tracing.start,
end: ctx.tracing.end,
algorithm: ctx.tracing.algorithm,
};
},
{

View File

@ -110,8 +110,11 @@ export class GraphService {
case 'notifyGraphTracing':
if (event.start && event.end) {
if (event.algorithm === 'shortest') {
this.traceProjects(event.start, event.end);
// this.traceAllProjects(event.start, event.end);
} else {
this.traceAllProjects(event.start, event.end);
}
}
break;
}
@ -390,13 +393,6 @@ export class GraphService {
if (iterations >= 1000) {
console.log('failsafe triggered!');
}
paths.forEach((currentPath) => {
console.log(
currentPath
.map((path) => path.map((element) => element.id()))
.join(' => ')
);
});
let finalCollection = this.traversalGraph.collection();
@ -413,11 +409,10 @@ export class GraphService {
}
});
console.log(finalCollection.length);
finalCollection.union(finalCollection.ancestors());
console.log(finalCollection.map((element) => element.id()));
this.transferToRenderGraph(finalCollection);
this.transferToRenderGraph(
finalCollection.union(finalCollection.ancestors())
);
}
private transferToRenderGraph(elements: cy.Collection) {

View File

@ -22,6 +22,8 @@ export interface GraphPerfReport {
numNodes: number;
numEdges: number;
}
export type TracingAlgorithmType = 'shortest' | 'all';
// The events that the machine handles
export type DepGraphUIEvents =
@ -40,6 +42,7 @@ export type DepGraphUIEvents =
| { type: 'setTracingEnd'; projectName: string }
| { type: 'clearTraceStart' }
| { type: 'clearTraceEnd' }
| { type: 'setTracingAlgorithm'; algorithm: TracingAlgorithmType }
| { type: 'setCollapseEdges'; collapseEdges: boolean }
| { type: 'setIncludeProjectsByPath'; includeProjectsByPath: boolean }
| { type: 'incrementSearchDepth' }
@ -126,6 +129,7 @@ export type GraphRenderEvents =
type: 'notifyGraphTracing';
start: string;
end: string;
algorithm: TracingAlgorithmType;
};
export type RouteEvents =
@ -156,7 +160,12 @@ export type RouteEvents =
type: 'notifyRouteSelectAffected';
}
| { type: 'notifyRouteClearSelect' }
| { type: 'notifyRouteTracing'; start: string; end: string };
| {
type: 'notifyRouteTracing';
start: string;
end: string;
algorithm: TracingAlgorithmType;
};
export type AllEvents = DepGraphUIEvents | GraphRenderEvents | RouteEvents;
@ -184,6 +193,7 @@ export interface DepGraphContext {
tracing: {
start: string;
end: string;
algorithm: TracingAlgorithmType;
};
}

View File

@ -39,6 +39,12 @@ function parseSearchParamsToEvents(searchParams: string): DepGraphUIEvents[] {
});
}
break;
case 'traceAlgorithm':
if (value === 'shortest' || value === 'all') {
// this needs to go before other tracing options or else the default of 'shortest' gets used
events.unshift({ type: 'setTracingAlgorithm', algorithm: value });
}
break;
case 'traceStart':
events.push({
type: 'setTracingStart',

View File

@ -10,7 +10,8 @@ type ParamKeys =
| 'select'
| 'collapseEdges'
| 'traceStart'
| 'traceEnd';
| 'traceEnd'
| 'traceAlgorithm';
type ParamRecord = Record<ParamKeys, string | null>;
function reduceParamRecordToQueryString(params: ParamRecord): string {
@ -37,6 +38,7 @@ export const createRouteMachine = () => {
select: params.get('select'),
traceStart: params.get('traceStart'),
traceEnd: params.get('traceEnd'),
traceAlgorithm: params.get('traceAlgorithm'),
};
const initialContext = {
@ -61,6 +63,7 @@ export const createRouteMachine = () => {
collapseEdges: null,
traceStart: null,
traceEnd: null,
traceAlgorithm: null,
},
},
always: {
@ -129,15 +132,17 @@ export const createRouteMachine = () => {
},
notifyRouteTracing: {
actions: assign((ctx, event) => {
if (event.start !== null && event.end !== null) {
if (event.start !== null && event.end !== null && event.algorithm) {
ctx.params.traceStart = event.start;
ctx.params.traceEnd = event.end;
ctx.params.traceAlgorithm = event.algorithm;
ctx.params.focus = null;
ctx.params.select = null;
} else {
ctx.params.traceStart = null;
ctx.params.traceEnd = null;
ctx.params.traceAlgorithm = null;
}
}),
},

View File

@ -2,7 +2,7 @@
import type { ProjectGraphProjectNode } from '@nrwl/devkit';
import { DepGraphSelector } from '../hooks/use-dep-graph-selector';
import { WorkspaceLayout } from '../interfaces';
import { GraphPerfReport } from './interfaces';
import { GraphPerfReport, TracingAlgorithmType } from './interfaces';
export const allProjectsSelector: DepGraphSelector<
ProjectGraphProjectNode[]
@ -52,4 +52,5 @@ export const hasAffectedProjectsSelector: DepGraphSelector<boolean> = (state) =>
export const getTracingInfo: DepGraphSelector<{
start: string;
end: string;
algorithm: TracingAlgorithmType;
}> = (state) => state.context.tracing;

View File

@ -13,6 +13,15 @@ export const tracingStateConfig: DepGraphStateNodeConfig = {
'notifyRouteTracing',
'notifyGraphTracing',
],
exit: [
assign((ctx, event) => {
if (event.type !== 'setTracingStart' && event.type !== 'setTracingEnd') {
ctx.tracing.start = null;
ctx.tracing.end = null;
}
}),
'notifyRouteTracing',
],
on: {
clearTraceStart: {
actions: [

View File

@ -1,4 +1,3 @@
import ExperimentalFeature from './experimental-feature';
import { getDepGraphService } from './machines/dep-graph.service';
export interface ProjectNodeToolTipProps {
@ -53,7 +52,6 @@ function ProjectNodeToolTip({ type, id, tags }: ProjectNodeToolTipProps) {
<div className="flex">
<button onClick={onFocus}>Focus</button>
<button onClick={onExclude}>Exclude</button>
<ExperimentalFeature>
<button className="flex flex-row items-center" onClick={onStartTrace}>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -84,7 +82,6 @@ function ProjectNodeToolTip({ type, id, tags }: ProjectNodeToolTipProps) {
</svg>
End
</button>
</ExperimentalFeature>
</div>
</div>
);

View File

@ -1,14 +1,21 @@
import { DocumentSearchIcon } from '@heroicons/react/solid';
import {
DocumentSearchIcon,
FlagIcon,
LocationMarkerIcon,
} from '@heroicons/react/solid';
// nx-ignore-next-line
import type { ProjectGraphNode } from '@nrwl/devkit';
import { useDepGraphService } from '../hooks/use-dep-graph';
import { useDepGraphSelector } from '../hooks/use-dep-graph-selector';
import {
allProjectsSelector,
getTracingInfo,
selectedProjectNamesSelector,
workspaceLayoutSelector,
} from '../machines/selectors';
import { parseParentDirectoriesFromFilePath } from '../util';
import { TracingAlgorithmType } from '../machines/interfaces';
import ExperimentalFeature from '../experimental-feature';
function getProjectsByType(type: string, projects: ProjectGraphNode[]) {
return projects
@ -23,6 +30,12 @@ interface SidebarProject {
type DirectoryProjectRecord = Record<string, SidebarProject[]>;
interface TracingInfo {
start: string;
end: string;
algorithm: TracingAlgorithmType;
}
function groupProjectsByDirectory(
projects: ProjectGraphNode[],
selectedProjects: string[],
@ -58,17 +71,24 @@ function ProjectListItem({
project,
toggleProject,
focusProject,
startTrace,
endTrace,
tracingInfo,
}: {
project: SidebarProject;
toggleProject: (projectId: string, currentlySelected: boolean) => void;
focusProject: (projectId: string) => void;
startTrace: (projectId: string) => void;
endTrace: (projectId: string) => void;
tracingInfo: TracingInfo;
}) {
return (
<li className="relative block cursor-default select-none py-1 pl-3 pr-9 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">
<div className="flex items-center">
<button
data-cy={`focus-button-${project.projectGraphNode.name}`}
type="button"
className="flex rounded-md"
className="mr-1 flex rounded-md"
title="Focus on this library"
onClick={() => focusProject(project.projectGraphNode.name)}
>
@ -76,9 +96,47 @@ function ProjectListItem({
<DocumentSearchIcon className="h-5 w-5" />
</span>
</button>
<ExperimentalFeature>
<button
type="button"
className="mr-1 flex rounded-md"
title="Start Trace"
onClick={() => startTrace(project.projectGraphNode.name)}
>
<span
className={`${
tracingInfo.start === project.projectGraphNode.name
? 'ring-blue-nx-base'
: 'ring-slate-200 dark:ring-slate-600'
} flex items-center rounded-md bg-white p-1 font-medium shadow-sm ring-1 transition hover:bg-slate-50 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-600 hover:dark:bg-slate-700`}
>
<LocationMarkerIcon className="h-5 w-5" />
</span>
</button>
<button
type="button"
className="flex rounded-md"
title="End Trace"
onClick={() => endTrace(project.projectGraphNode.name)}
>
<span
className={`${
tracingInfo.end === project.projectGraphNode.name
? 'ring-blue-nx-base'
: 'ring-slate-200 dark:ring-slate-600'
} flex items-center rounded-md bg-white p-1 font-medium shadow-sm ring-1 transition hover:bg-slate-50 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-600 hover:dark:bg-slate-700`}
>
<FlagIcon className="h-5 w-5" />
</span>
</button>
</ExperimentalFeature>
<label
className="ml-3 block w-full cursor-pointer truncate rounded-md p-2 font-mono font-normal transition hover:bg-slate-50 hover:dark:bg-slate-700"
className="ml-2 block w-full cursor-pointer truncate rounded-md p-2 font-mono font-normal transition hover:bg-slate-50 hover:dark:bg-slate-700"
data-project={project.projectGraphNode.name}
title={project.projectGraphNode.name}
data-active={project.isSelected.toString()}
onClick={() =>
toggleProject(project.projectGraphNode.name, project.isSelected)
@ -128,12 +186,18 @@ function SubProjectList({
selectProject,
deselectProject,
focusProject,
startTrace,
endTrace,
tracingInfo,
}: {
headerText: string;
projects: SidebarProject[];
selectProject: (projectName: string) => void;
deselectProject: (projectName: string) => void;
focusProject: (projectName: string) => void;
startTrace: (projectId: string) => void;
endTrace: (projectId: string) => void;
tracingInfo: TracingInfo;
}) {
let sortedProjects = [...projects];
sortedProjects.sort((a, b) => {
@ -163,6 +227,9 @@ function SubProjectList({
project={project}
toggleProject={toggleProject}
focusProject={focusProject}
startTrace={startTrace}
endTrace={endTrace}
tracingInfo={tracingInfo}
></ProjectListItem>
);
})}
@ -173,6 +240,7 @@ function SubProjectList({
export function ProjectList() {
const depGraphService = useDepGraphService();
const tracingInfo = useDepGraphSelector(getTracingInfo);
function deselectProject(projectName: string) {
depGraphService.send({ type: 'deselectProject', projectName });
@ -186,6 +254,14 @@ export function ProjectList() {
depGraphService.send({ type: 'focusProject', projectName });
}
function startTrace(projectName: string) {
depGraphService.send({ type: 'setTracingStart', projectName });
}
function endTrace(projectName: string) {
depGraphService.send({ type: 'setTracingEnd', projectName });
}
const projects = useDepGraphSelector(allProjectsSelector);
const workspaceLayout = useDepGraphSelector(workspaceLayoutSelector);
const selectedProjects = useDepGraphSelector(selectedProjectNamesSelector);
@ -229,6 +305,9 @@ export function ProjectList() {
deselectProject={deselectProject}
selectProject={selectProject}
focusProject={focusProject}
startTrace={startTrace}
endTrace={endTrace}
tracingInfo={tracingInfo}
></SubProjectList>
);
})}
@ -246,6 +325,9 @@ export function ProjectList() {
deselectProject={deselectProject}
selectProject={selectProject}
focusProject={focusProject}
startTrace={startTrace}
endTrace={endTrace}
tracingInfo={tracingInfo}
></SubProjectList>
);
})}
@ -263,6 +345,9 @@ export function ProjectList() {
deselectProject={deselectProject}
selectProject={selectProject}
focusProject={focusProject}
startTrace={startTrace}
endTrace={endTrace}
tracingInfo={tracingInfo}
></SubProjectList>
);
})}

View File

@ -23,6 +23,7 @@ import ShowHideProjects from './show-hide-projects';
import TextFilterPanel from './text-filter-panel';
import ThemePanel from './theme-panel';
import TracingPanel from './tracing-panel';
import { TracingAlgorithmType } from '../machines/interfaces';
export function Sidebar() {
const depGraphService = useDepGraphService();
@ -33,9 +34,8 @@ export function Sidebar() {
const hasAffectedProjects = useDepGraphSelector(hasAffectedProjectsSelector);
const groupByFolder = useDepGraphSelector(groupByFolderSelector);
const collapseEdges = useDepGraphSelector(collapseEdgesSelector);
const environment = useEnvironmentConfig();
const { showExperimentalFeatures } = environment.appConfig;
const isTracing = depGraphService.state.matches('tracing');
// const isTracing = depGraphService.state.matches('tracing');
const tracingInfo = useDepGraphSelector(getTracingInfo);
@ -98,6 +98,10 @@ export function Sidebar() {
depGraphService.send({ type: 'clearTraceEnd' });
}
function setAlgorithm(algorithm: TracingAlgorithmType) {
depGraphService.send({ type: 'setTracingAlgorithm', algorithm: algorithm });
}
const updateTextFilter = useCallback(
(textFilter: string) => {
depGraphService.send({ type: 'filterByText', search: textFilter });
@ -153,15 +157,16 @@ export function Sidebar() {
resetFocus={resetFocus}
></FocusedProjectPanel>
) : null}
<ExperimentalFeature>
{isTracing ? (
<TracingPanel
start={tracingInfo.start}
end={tracingInfo.end}
algorithm={tracingInfo.algorithm}
setAlgorithm={setAlgorithm}
resetStart={resetTraceStart}
resetEnd={resetTraceEnd}
></TracingPanel>
</ExperimentalFeature>
) : null}
<TextFilterPanel
includePath={includePath}

View File

@ -5,22 +5,55 @@ import {
XCircleIcon,
} from '@heroicons/react/solid';
import { memo } from 'react';
import { TracingAlgorithmType } from '../machines/interfaces';
export interface TracingPanelProps {
start: string;
end: string;
algorithm: TracingAlgorithmType;
resetStart: () => void;
resetEnd: () => void;
setAlgorithm: (algorithm: TracingAlgorithmType) => void;
}
export const TracingPanel = memo(
({ start, end, resetStart, resetEnd }: TracingPanelProps) => {
({
start,
end,
algorithm,
setAlgorithm,
resetStart,
resetEnd,
}: TracingPanelProps) => {
return (
<div className="mt-10 px-4">
<div className="transition duration-200 ease-in-out group-hover:opacity-60">
<h3 className="cursor-text pb-2 text-sm font-semibold uppercase tracking-wide text-slate-800 dark:text-slate-200 lg:text-xs">
Tracing Path
</h3>
<div className="mb-3 flex cursor-pointer flex-row rounded-md border text-center text-xs dark:border-slate-600">
<button
onClick={() => setAlgorithm('shortest')}
className={`${
algorithm === 'shortest'
? 'border-blue-nx-base dark:border-slate-200'
: 'border-gray-300 dark:border-slate-600'
} flex-1 rounded-l-md border bg-slate-50 py-2 px-4 text-slate-500 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700`}
>
<span>Shortest</span>
</button>
<button
onClick={() => setAlgorithm('all')}
className={`${
algorithm === 'all'
? 'border-blue-nx-base dark:border-slate-200'
: 'border-gray-300 dark:border-slate-600'
} flex-1 rounded-r-md border bg-slate-50 py-2 px-4 text-slate-500 hover:bg-slate-100 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700`}
>
<span>All</span>
</button>
</div>
<div className="flex flex-row items-center truncate ">
<LocationMarkerIcon className="mr-2 h-5 w-5 text-slate-500 dark:text-slate-400" />
{start ? (