feat(dep-graph): add experimental support for finding path between projects (#9643)
* feat(dep-graph): add experimental support for tracing paths between projects * feat(graph): add algorithm for finding all paths between projects * cleanup(dep-graph): clean-up edge tooltip * cleanup(dep-graph): fix watch mode dev environment
This commit is contained in:
parent
05a9544806
commit
dbe942c9c0
@ -55,12 +55,12 @@ describe('dep-graph-client', () => {
|
||||
|
||||
it('should filter projects', () => {
|
||||
getTextFilterInput().type('nx-dev');
|
||||
getCheckedProjectItems().should('have.length', 9);
|
||||
getCheckedProjectItems().should('have.length', 15);
|
||||
});
|
||||
|
||||
it('should clear selection on reset', () => {
|
||||
getTextFilterInput().type('nx-dev');
|
||||
getCheckedProjectItems().should('have.length', 9);
|
||||
getCheckedProjectItems().should('have.length', 15);
|
||||
getTextFilterReset().click();
|
||||
getCheckedProjectItems().should('have.length', 0);
|
||||
});
|
||||
@ -68,7 +68,7 @@ describe('dep-graph-client', () => {
|
||||
|
||||
describe('selecting a different project', () => {
|
||||
it('should change the available projects', () => {
|
||||
getProjectItems().should('have.length', 53);
|
||||
getProjectItems().should('have.length', 62);
|
||||
cy.get('[data-cy=project-select]').select('Ocean', { force: true });
|
||||
getProjectItems().should('have.length', 124);
|
||||
});
|
||||
@ -77,14 +77,14 @@ describe('dep-graph-client', () => {
|
||||
describe('select all button', () => {
|
||||
it('should check all project items', () => {
|
||||
getSelectAllButton().scrollIntoView().click({ force: true });
|
||||
getCheckedProjectItems().should('have.length', 53);
|
||||
getCheckedProjectItems().should('have.length', 62);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deselect all button', () => {
|
||||
it('should uncheck all project items', () => {
|
||||
getDeselectAllButton().click();
|
||||
getUncheckedProjectItems().should('have.length', 53);
|
||||
getUncheckedProjectItems().should('have.length', 62);
|
||||
getSelectProjectsMessage().should('be.visible');
|
||||
});
|
||||
});
|
||||
@ -147,7 +147,7 @@ describe('dep-graph-client', () => {
|
||||
cy.contains('nx-dev').scrollIntoView().should('be.visible');
|
||||
cy.get('[data-project="nx-dev"]').prev('button').click({ force: true });
|
||||
|
||||
getCheckedProjectItems().should('have.length', 10);
|
||||
getCheckedProjectItems().should('have.length', 15);
|
||||
});
|
||||
});
|
||||
|
||||
@ -156,7 +156,7 @@ describe('dep-graph-client', () => {
|
||||
cy.get('[data-project="nx-dev"]').prev('button').click({ force: true });
|
||||
getUnfocusProjectButton().click();
|
||||
|
||||
getUncheckedProjectItems().should('have.length', 53);
|
||||
getUncheckedProjectItems().should('have.length', 62);
|
||||
});
|
||||
});
|
||||
|
||||
@ -164,14 +164,14 @@ describe('dep-graph-client', () => {
|
||||
it('should filter projects by text when pressing enter', () => {
|
||||
getTextFilterInput().type('nx-dev{enter}');
|
||||
|
||||
getCheckedProjectItems().should('have.length', 9);
|
||||
getCheckedProjectItems().should('have.length', 15);
|
||||
});
|
||||
|
||||
it('should include projects in path when option is checked', () => {
|
||||
getTextFilterInput().type('nx-dev');
|
||||
getIncludeProjectsInPathButton().click();
|
||||
|
||||
getCheckedProjectItems().should('have.length', 17);
|
||||
getCheckedProjectItems().should('have.length', 24);
|
||||
});
|
||||
});
|
||||
|
||||
@ -229,7 +229,7 @@ describe('loading dep-graph client with url params', () => {
|
||||
// wait for first graph to finish loading
|
||||
cy.wait('@getGraph');
|
||||
|
||||
getCheckedProjectItems().should('have.length', 10);
|
||||
getCheckedProjectItems().should('have.length', 15);
|
||||
});
|
||||
|
||||
it('should focus projects with search depth', () => {
|
||||
@ -240,7 +240,7 @@ describe('loading dep-graph client with url params', () => {
|
||||
// wait for first graph to finish loading
|
||||
cy.wait('@getGraph');
|
||||
|
||||
getCheckedProjectItems().should('have.length', 8);
|
||||
getCheckedProjectItems().should('have.length', 11);
|
||||
getSearchDepthCheckbox().should('exist');
|
||||
});
|
||||
|
||||
@ -263,7 +263,7 @@ describe('loading dep-graph client with url params', () => {
|
||||
// wait for first graph to finish loading
|
||||
cy.wait('@getGraph');
|
||||
|
||||
getCheckedProjectItems().should('have.length', 53);
|
||||
getCheckedProjectItems().should('have.length', 62);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
39
dep-graph/client/src/app/edge-tooltip.tsx
Normal file
39
dep-graph/client/src/app/edge-tooltip.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
export interface EdgeNodeTooltipProps {
|
||||
type: 'static' | 'dynamic' | 'implicit';
|
||||
source: string;
|
||||
target: string;
|
||||
fileDependencies: Array<{ fileName: string }>;
|
||||
}
|
||||
function EdgeNodeTooltip({
|
||||
type,
|
||||
source,
|
||||
target,
|
||||
fileDependencies,
|
||||
}: EdgeNodeTooltipProps) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className={type !== 'implicit' ? 'mb-3' : ''}>
|
||||
<span className="tag">{type ?? 'unknown'}</span>
|
||||
{source} → {target}
|
||||
</h4>
|
||||
{type !== 'implicit' ? (
|
||||
<div className="rounded-md border border-gray-200">
|
||||
<div className="rounded-t-md bg-gray-50 px-4 py-2 text-xs font-medium uppercase text-gray-500">
|
||||
<span>Files</span>
|
||||
</div>
|
||||
<ul className="max-h-[300px] divide-y divide-gray-200 overflow-auto">
|
||||
{fileDependencies.map((fileDep) => (
|
||||
<li className="dark:text-sidebar-text-dark whitespace-nowrap px-4 py-2 text-sm font-medium text-gray-900">
|
||||
<span className="block truncate font-normal">
|
||||
{fileDep.fileName}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EdgeNodeTooltip;
|
||||
18
dep-graph/client/src/app/icons/flag.tsx
Normal file
18
dep-graph/client/src/app/icons/flag.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
function Flag(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 6a3 3 0 013-3h10a1 1 0 01.8 1.6L14.25 8l2.55 3.4A1 1 0 0116 13H6a1 1 0 00-1 1v3a1 1 0 11-2 0V6z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Flag;
|
||||
14
dep-graph/client/src/app/icons/map-marker.tsx
Normal file
14
dep-graph/client/src/app/icons/map-marker.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
function MapMarker(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapMarker;
|
||||
20
dep-graph/client/src/app/icons/x-circle-outline.tsx
Normal file
20
dep-graph/client/src/app/icons/x-circle-outline.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
function XCircleOutline(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default XCircleOutline;
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from './interfaces';
|
||||
import { createRouteMachine } from './route-setter.machine';
|
||||
import { textFilteredStateConfig } from './text-filtered.state';
|
||||
import { tracingStateConfig } from './tracing.state';
|
||||
import { unselectedStateConfig } from './unselected.state';
|
||||
|
||||
export const initialContext: DepGraphContext = {
|
||||
@ -36,6 +37,10 @@ export const initialContext: DepGraphContext = {
|
||||
numNodes: 0,
|
||||
renderTime: 0,
|
||||
},
|
||||
tracing: {
|
||||
start: null,
|
||||
end: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const depGraphMachine = Machine<
|
||||
@ -53,6 +58,7 @@ export const depGraphMachine = Machine<
|
||||
customSelected: customSelectedStateConfig,
|
||||
focused: focusedStateConfig,
|
||||
textFiltered: textFilteredStateConfig,
|
||||
tracing: tracingStateConfig,
|
||||
},
|
||||
on: {
|
||||
initGraph: {
|
||||
@ -114,6 +120,12 @@ export const depGraphMachine = Machine<
|
||||
focusProject: {
|
||||
target: 'focused',
|
||||
},
|
||||
setTracingStart: {
|
||||
target: 'tracing',
|
||||
},
|
||||
setTracingEnd: {
|
||||
target: 'tracing',
|
||||
},
|
||||
setCollapseEdges: {
|
||||
actions: [
|
||||
'setCollapseEdges',
|
||||
@ -262,6 +274,19 @@ export const depGraphMachine = Machine<
|
||||
ctx.affectedProjects = event.affectedProjects;
|
||||
}
|
||||
}),
|
||||
notifyGraphTracing: send(
|
||||
(ctx, event) => {
|
||||
return {
|
||||
type: 'notifyGraphTracing',
|
||||
start: ctx.tracing.start,
|
||||
end: ctx.tracing.end,
|
||||
};
|
||||
},
|
||||
{
|
||||
to: (context) => context.graphActor,
|
||||
}
|
||||
),
|
||||
|
||||
notifyGraphShowProject: send(
|
||||
(context, event) => {
|
||||
if (event.type !== 'selectProject') return;
|
||||
@ -354,6 +379,18 @@ export const depGraphMachine = Machine<
|
||||
to: (ctx) => ctx.routeSetterActor,
|
||||
}
|
||||
),
|
||||
notifyRouteTracing: send(
|
||||
(ctx) => {
|
||||
return {
|
||||
type: 'notifyRouteTracing',
|
||||
start: ctx.tracing.start,
|
||||
end: ctx.tracing.end,
|
||||
};
|
||||
},
|
||||
{
|
||||
to: (ctx) => ctx.routeSetterActor,
|
||||
}
|
||||
),
|
||||
notifyRouteSearchDepth: send(
|
||||
(ctx, event) => ({
|
||||
type: 'notifyRouteSearchDepth',
|
||||
|
||||
@ -7,8 +7,10 @@ import type { VirtualElement } from '@popperjs/core';
|
||||
import { default as cy } from 'cytoscape';
|
||||
import { default as cytoscapeDagre } from 'cytoscape-dagre';
|
||||
import { default as popper } from 'cytoscape-popper';
|
||||
import path from 'path/posix';
|
||||
import type { Instance } from 'tippy.js';
|
||||
import { ProjectNodeToolTip } from '../project-node-tooltip';
|
||||
import EdgeNodeTooltip from '../edge-tooltip';
|
||||
import ProjectNodeToolTip from '../project-node-tooltip';
|
||||
import { edgeStyles, nodeStyles } from '../styles-graph';
|
||||
import { GraphTooltipService } from '../tooltip-service';
|
||||
import {
|
||||
@ -106,6 +108,13 @@ export class GraphService {
|
||||
case 'notifyGraphShowAffectedProjects':
|
||||
this.showAffectedProjects();
|
||||
break;
|
||||
|
||||
case 'notifyGraphTracing':
|
||||
if (event.start && event.end) {
|
||||
this.traceProjects(event.start, event.end);
|
||||
// this.traceAllProjects(event.start, event.end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let selectedProjectNames: string[] = [];
|
||||
@ -340,6 +349,78 @@ export class GraphService {
|
||||
}
|
||||
}
|
||||
|
||||
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!');
|
||||
}
|
||||
paths.forEach((currentPath) => {
|
||||
console.log(
|
||||
currentPath
|
||||
.map((path) => path.map((element) => element.id()))
|
||||
.join(' => ')
|
||||
);
|
||||
});
|
||||
|
||||
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])
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(finalCollection.length);
|
||||
|
||||
finalCollection.union(finalCollection.ancestors());
|
||||
console.log(finalCollection.map((element) => element.id()));
|
||||
this.transferToRenderGraph(finalCollection);
|
||||
}
|
||||
|
||||
private transferToRenderGraph(elements: cy.Collection) {
|
||||
let currentFocusedProjectName;
|
||||
if (this.renderGraph) {
|
||||
@ -373,6 +454,7 @@ export class GraphService {
|
||||
});
|
||||
|
||||
this.listenForProjectNodeClicks();
|
||||
this.listenForEdgeNodeClicks();
|
||||
this.listenForProjectNodeHovers();
|
||||
}
|
||||
|
||||
@ -521,12 +603,41 @@ export class GraphService {
|
||||
|
||||
let ref: VirtualElement = node.popperRef(); // used only for positioning
|
||||
|
||||
const content = new ProjectNodeToolTip(node).render();
|
||||
const content = ProjectNodeToolTip({
|
||||
id: node.id(),
|
||||
type: node.data('type'),
|
||||
tags: node.data('tags'),
|
||||
});
|
||||
|
||||
this.openTooltip = this.tooltipService.open(ref, content);
|
||||
});
|
||||
}
|
||||
|
||||
listenForEdgeNodeClicks() {
|
||||
this.renderGraph.$('edge').on('click', (event) => {
|
||||
const edge: cy.EdgeSingular = event.target;
|
||||
let ref: VirtualElement = edge.popperRef(); // used only for positioning
|
||||
|
||||
const tooltipContent = EdgeNodeTooltip({
|
||||
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(),
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
this.openTooltip = this.tooltipService.open(ref, tooltipContent);
|
||||
});
|
||||
}
|
||||
|
||||
listenForProjectNodeHovers(): void {
|
||||
this.renderGraph.on('mouseover', (event) => {
|
||||
const node = event.target;
|
||||
|
||||
@ -13,6 +13,7 @@ export interface DepGraphSchema {
|
||||
focused: {};
|
||||
textFiltered: {};
|
||||
customSelected: {};
|
||||
tracing: {};
|
||||
};
|
||||
}
|
||||
|
||||
@ -35,6 +36,10 @@ export type DepGraphUIEvents =
|
||||
| { type: 'deselectAll' }
|
||||
| { type: 'selectAffected' }
|
||||
| { type: 'setGroupByFolder'; groupByFolder: boolean }
|
||||
| { type: 'setTracingStart'; projectName: string }
|
||||
| { type: 'setTracingEnd'; projectName: string }
|
||||
| { type: 'clearTraceStart' }
|
||||
| { type: 'clearTraceEnd' }
|
||||
| { type: 'setCollapseEdges'; collapseEdges: boolean }
|
||||
| { type: 'setIncludeProjectsByPath'; includeProjectsByPath: boolean }
|
||||
| { type: 'incrementSearchDepth' }
|
||||
@ -116,6 +121,11 @@ export type GraphRenderEvents =
|
||||
search: string;
|
||||
includeProjectsByPath: boolean;
|
||||
searchDepth: number;
|
||||
}
|
||||
| {
|
||||
type: 'notifyGraphTracing';
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
export type RouteEvents =
|
||||
@ -145,7 +155,8 @@ export type RouteEvents =
|
||||
| {
|
||||
type: 'notifyRouteSelectAffected';
|
||||
}
|
||||
| { type: 'notifyRouteClearSelect' };
|
||||
| { type: 'notifyRouteClearSelect' }
|
||||
| { type: 'notifyRouteTracing'; start: string; end: string };
|
||||
|
||||
export type AllEvents = DepGraphUIEvents | GraphRenderEvents | RouteEvents;
|
||||
|
||||
@ -170,6 +181,10 @@ export interface DepGraphContext {
|
||||
routeSetterActor: ActorRef<RouteEvents>;
|
||||
routeListenerActor: ActorRef<DepGraphUIEvents>;
|
||||
lastPerfReport: GraphPerfReport;
|
||||
tracing: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type DepGraphStateNodeConfig = StateNodeConfig<
|
||||
|
||||
@ -30,6 +30,15 @@ function parseSearchParamsToEvents(searchParams: string): DepGraphUIEvents[] {
|
||||
searchDepth: parseInt(value),
|
||||
});
|
||||
break;
|
||||
case 'traceStart':
|
||||
events.push({
|
||||
type: 'setTracingStart',
|
||||
projectName: value,
|
||||
});
|
||||
break;
|
||||
case 'traceEnd':
|
||||
events.push({ type: 'setTracingEnd', projectName: value });
|
||||
break;
|
||||
}
|
||||
});
|
||||
return events;
|
||||
|
||||
@ -8,7 +8,9 @@ type ParamKeys =
|
||||
| 'groupByFolder'
|
||||
| 'searchDepth'
|
||||
| 'select'
|
||||
| 'collapseEdges';
|
||||
| 'collapseEdges'
|
||||
| 'traceStart'
|
||||
| 'traceEnd';
|
||||
type ParamRecord = Record<ParamKeys, string | null>;
|
||||
|
||||
function reduceParamRecordToQueryString(params: ParamRecord): string {
|
||||
@ -33,6 +35,8 @@ export const createRouteMachine = () => {
|
||||
collapseEdges: params.get('collapseEdges'),
|
||||
searchDepth: params.get('searchDepth'),
|
||||
select: params.get('select'),
|
||||
traceStart: params.get('traceStart'),
|
||||
traceEnd: params.get('traceEnd'),
|
||||
};
|
||||
|
||||
const initialContext = {
|
||||
@ -55,6 +59,8 @@ export const createRouteMachine = () => {
|
||||
searchDepth: null,
|
||||
select: null,
|
||||
collapseEdges: null,
|
||||
traceStart: null,
|
||||
traceEnd: null,
|
||||
},
|
||||
},
|
||||
always: {
|
||||
@ -117,6 +123,20 @@ export const createRouteMachine = () => {
|
||||
: null;
|
||||
}),
|
||||
},
|
||||
notifyRouteTracing: {
|
||||
actions: assign((ctx, event) => {
|
||||
if (event.start !== null && event.end !== null) {
|
||||
ctx.params.traceStart = event.start;
|
||||
ctx.params.traceEnd = event.end;
|
||||
|
||||
ctx.params.focus = null;
|
||||
ctx.params.select = null;
|
||||
} else {
|
||||
ctx.params.traceStart = null;
|
||||
ctx.params.traceEnd = null;
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -48,3 +48,8 @@ export const textFilterSelector: DepGraphSelector<string> = (state) =>
|
||||
|
||||
export const hasAffectedProjectsSelector: DepGraphSelector<boolean> = (state) =>
|
||||
state.context.affectedProjects.length > 0;
|
||||
|
||||
export const getTracingInfo: DepGraphSelector<{
|
||||
start: string;
|
||||
end: string;
|
||||
}> = (state) => state.context.tracing;
|
||||
|
||||
37
dep-graph/client/src/app/machines/tracing.state.ts
Normal file
37
dep-graph/client/src/app/machines/tracing.state.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { assign } from '@xstate/immer';
|
||||
import { send, actions } from 'xstate';
|
||||
import { DepGraphStateNodeConfig } from './interfaces';
|
||||
|
||||
export const tracingStateConfig: DepGraphStateNodeConfig = {
|
||||
entry: [
|
||||
assign((ctx, event) => {
|
||||
if (event.type === 'setTracingStart') {
|
||||
ctx.tracing.start = event.projectName;
|
||||
} else if (event.type === 'setTracingEnd') {
|
||||
ctx.tracing.end = event.projectName;
|
||||
}
|
||||
}),
|
||||
'notifyRouteTracing',
|
||||
'notifyGraphTracing',
|
||||
],
|
||||
on: {
|
||||
clearTraceStart: {
|
||||
actions: [
|
||||
assign((ctx) => {
|
||||
ctx.tracing.start = null;
|
||||
}),
|
||||
'notifyRouteTracing',
|
||||
'notifyGraphTracing',
|
||||
],
|
||||
},
|
||||
clearTraceEnd: {
|
||||
actions: [
|
||||
assign((ctx) => {
|
||||
ctx.tracing.end = null;
|
||||
}),
|
||||
'notifyRouteTracing',
|
||||
'notifyGraphTracing',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -21,6 +21,13 @@ export class MockProjectGraphService implements ProjectGraphService {
|
||||
data: {
|
||||
root: 'apps/app1',
|
||||
tags: [],
|
||||
files: [
|
||||
{
|
||||
file: 'some/file.ts',
|
||||
hash: 'ecccd8481d2e5eae0e59928be1bc4c2d071729d7',
|
||||
deps: ['existing-lib-1'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -29,6 +36,7 @@ export class MockProjectGraphService implements ProjectGraphService {
|
||||
data: {
|
||||
root: 'libs/lib1',
|
||||
tags: [],
|
||||
files: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -37,7 +45,7 @@ export class MockProjectGraphService implements ProjectGraphService {
|
||||
{
|
||||
source: 'existing-app-1',
|
||||
target: 'existing-lib-1',
|
||||
type: 'statis',
|
||||
type: 'static',
|
||||
},
|
||||
],
|
||||
'existing-lib-1': [],
|
||||
@ -70,6 +78,7 @@ export class MockProjectGraphService implements ProjectGraphService {
|
||||
data: {
|
||||
root: type === 'app' ? `apps/${name}` : `libs/${name}`,
|
||||
tags: [],
|
||||
files: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import * as cy from 'cytoscape';
|
||||
import { getDepGraphService } from './machines/dep-graph.service';
|
||||
|
||||
export class ProjectNodeToolTip {
|
||||
constructor(private node: cy.NodeSingular) {}
|
||||
|
||||
render() {
|
||||
const wrapper = document.createElement('div');
|
||||
const header = this.createHeader();
|
||||
const tags = this.createTags();
|
||||
const buttons = this.createButtons();
|
||||
|
||||
wrapper.appendChild(header);
|
||||
wrapper.appendChild(tags);
|
||||
wrapper.appendChild(buttons);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private createHeader() {
|
||||
const header = document.createElement('h4');
|
||||
const typeLabel = document.createElement('span');
|
||||
const projectName = document.createTextNode(this.node.attr('id'));
|
||||
|
||||
typeLabel.classList.add('tag');
|
||||
typeLabel.innerText = this.node.attr('type');
|
||||
|
||||
header.appendChild(typeLabel);
|
||||
header.appendChild(projectName);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
private createTags() {
|
||||
const wrapper = document.createElement('p');
|
||||
const tagLabel = document.createElement('strong');
|
||||
const tags = document.createTextNode(
|
||||
this.node.attr('tags')?.join(', ') ?? ''
|
||||
);
|
||||
|
||||
tagLabel.innerText = 'tags';
|
||||
|
||||
wrapper.appendChild(tagLabel);
|
||||
wrapper.appendChild(document.createElement('br'));
|
||||
wrapper.appendChild(tags);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private createButtons() {
|
||||
const wrapper = document.createElement('div');
|
||||
const focusButton = document.createElement('button');
|
||||
const excludeButton = document.createElement('button');
|
||||
|
||||
wrapper.classList.add('flex');
|
||||
|
||||
const depGraphService = getDepGraphService();
|
||||
|
||||
focusButton.addEventListener('click', () =>
|
||||
depGraphService.send({
|
||||
type: 'focusProject',
|
||||
projectName: this.node.attr('id'),
|
||||
})
|
||||
);
|
||||
focusButton.innerText = 'Focus';
|
||||
|
||||
excludeButton.addEventListener('click', () => {
|
||||
depGraphService.send({
|
||||
type: 'deselectProject',
|
||||
projectName: this.node.attr('id'),
|
||||
});
|
||||
});
|
||||
|
||||
excludeButton.innerText = 'Exclude';
|
||||
|
||||
wrapper.appendChild(focusButton);
|
||||
wrapper.appendChild(excludeButton);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
93
dep-graph/client/src/app/project-node-tooltip.tsx
Normal file
93
dep-graph/client/src/app/project-node-tooltip.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { getDepGraphService } from './machines/dep-graph.service';
|
||||
import ExperimentalFeature from './experimental-feature';
|
||||
|
||||
export interface ProjectNodeToolTipProps {
|
||||
type: 'app' | 'lib' | 'e2e';
|
||||
id: string;
|
||||
tags: string[];
|
||||
}
|
||||
function ProjectNodeToolTip({ type, id, tags }: ProjectNodeToolTipProps) {
|
||||
const depGraphService = getDepGraphService();
|
||||
|
||||
function onFocus() {
|
||||
depGraphService.send({
|
||||
type: 'focusProject',
|
||||
projectName: id,
|
||||
});
|
||||
}
|
||||
|
||||
function onExclude() {
|
||||
depGraphService.send({
|
||||
type: 'deselectProject',
|
||||
projectName: id,
|
||||
});
|
||||
}
|
||||
|
||||
function onStartTrace() {
|
||||
depGraphService.send({
|
||||
type: 'setTracingStart',
|
||||
projectName: id,
|
||||
});
|
||||
}
|
||||
|
||||
function onEndTrace() {
|
||||
depGraphService.send({
|
||||
type: 'setTracingEnd',
|
||||
projectName: id,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>
|
||||
<span className="tag">{type}</span>
|
||||
{id}
|
||||
</h4>
|
||||
{tags.length > 0 ? (
|
||||
<p>
|
||||
<strong>tags</strong>
|
||||
<br></br>
|
||||
{tags.join(', ')}
|
||||
</p>
|
||||
) : null}
|
||||
<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"
|
||||
className="mr-2 h-5 w-5 text-gray-500"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Start
|
||||
</button>
|
||||
<button className="flex flex-row items-center" onClick={onEndTrace}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2 h-5 w-5 text-gray-500"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 6a3 3 0 013-3h10a1 1 0 01.8 1.6L14.25 8l2.55 3.4A1 1 0 0116 13H6a1 1 0 00-1 1v3a1 1 0 11-2 0V6z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
End
|
||||
</button>
|
||||
</ExperimentalFeature>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectNodeToolTip;
|
||||
@ -5,12 +5,14 @@ import { useDepGraphSelector } from '../hooks/use-dep-graph-selector';
|
||||
import {
|
||||
collapseEdgesSelector,
|
||||
focusedProjectNameSelector,
|
||||
getTracingInfo,
|
||||
groupByFolderSelector,
|
||||
hasAffectedProjectsSelector,
|
||||
includePathSelector,
|
||||
searchDepthSelector,
|
||||
textFilterSelector,
|
||||
} from '../machines/selectors';
|
||||
import { tracingStateConfig } from '../machines/tracing.state';
|
||||
import CollapseEdgesPanel from './collapse-edges-panel';
|
||||
import FocusedProjectPanel from './focused-project-panel';
|
||||
import GroupByFolderPanel from './group-by-folder-panel';
|
||||
@ -18,6 +20,7 @@ import ProjectList from './project-list';
|
||||
import SearchDepth from './search-depth';
|
||||
import ShowHideProjects from './show-hide-projects';
|
||||
import TextFilterPanel from './text-filter-panel';
|
||||
import TracingPanel from './tracing-panel';
|
||||
import ThemePanel from './theme-panel';
|
||||
import { useEnvironmentConfig } from '../hooks/use-environment-config';
|
||||
|
||||
@ -34,6 +37,9 @@ export function Sidebar() {
|
||||
|
||||
const { showExperimentalFeatures } = environment.appConfig;
|
||||
|
||||
// const isTracing = depGraphService.state.matches('tracing');
|
||||
const tracingInfo = useDepGraphSelector(getTracingInfo);
|
||||
|
||||
function resetFocus() {
|
||||
depGraphService.send({ type: 'unfocusProject' });
|
||||
}
|
||||
@ -84,6 +90,14 @@ export function Sidebar() {
|
||||
});
|
||||
}
|
||||
|
||||
function resetTraceStart() {
|
||||
depGraphService.send({ type: 'clearTraceStart' });
|
||||
}
|
||||
|
||||
function resetTraceEnd() {
|
||||
depGraphService.send({ type: 'clearTraceEnd' });
|
||||
}
|
||||
|
||||
const updateTextFilter = useCallback(
|
||||
(textFilter: string) => {
|
||||
depGraphService.send({ type: 'filterByText', search: textFilter });
|
||||
@ -170,6 +184,15 @@ export function Sidebar() {
|
||||
></FocusedProjectPanel>
|
||||
) : null}
|
||||
|
||||
<ExperimentalFeature>
|
||||
<TracingPanel
|
||||
start={tracingInfo.start}
|
||||
end={tracingInfo.end}
|
||||
resetStart={resetTraceStart}
|
||||
resetEnd={resetTraceEnd}
|
||||
></TracingPanel>
|
||||
</ExperimentalFeature>
|
||||
|
||||
<TextFilterPanel
|
||||
includePath={includePath}
|
||||
resetTextFilter={resetTextFilter}
|
||||
|
||||
103
dep-graph/client/src/app/sidebar/tracing-panel.tsx
Normal file
103
dep-graph/client/src/app/sidebar/tracing-panel.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { XCircleIcon } from '@heroicons/react/solid';
|
||||
import { memo } from 'react';
|
||||
import Flag from '../icons/flag';
|
||||
import MapMarker from '../icons/map-marker';
|
||||
|
||||
export interface TracingPanelProps {
|
||||
start: string;
|
||||
end: string;
|
||||
resetStart: () => void;
|
||||
resetEnd: () => void;
|
||||
}
|
||||
|
||||
export const TracingPanel = memo(
|
||||
({ start, end, 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-gray-900 lg:text-xs ">
|
||||
Tracing Path
|
||||
</h3>
|
||||
<div className="flex flex-row items-center truncate ">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2 h-5 w-5 text-gray-500"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{start ? (
|
||||
<div
|
||||
className=" group relative flex-1 cursor-pointer flex-col items-center overflow-hidden "
|
||||
data-cy="resetTraceButton"
|
||||
onClick={resetStart}
|
||||
>
|
||||
<div className="bg-green-nx-base flex-1 truncate rounded-md border border-gray-200 p-2 text-gray-50 shadow-sm transition duration-200 ease-in-out group-hover:opacity-60">
|
||||
<span>{start}</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-2 right-2 flex translate-x-32 items-center rounded-md bg-white pl-2 text-sm font-medium text-gray-700 shadow-sm ring-1 ring-gray-500 transition-all transition duration-200 ease-in-out group-hover:translate-x-0">
|
||||
Reset
|
||||
<span className="rounded-md p-1">
|
||||
<XCircleIcon className="h-5 w-5"></XCircleIcon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Select start project</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<MapMarker className="h-5 w-5 text-gray-500"></MapMarker>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center truncate ">
|
||||
<Flag className="mr-2 h-5 w-5 text-gray-500"></Flag>
|
||||
|
||||
{end ? (
|
||||
<div
|
||||
className=" group relative flex-1 cursor-pointer flex-col items-center overflow-hidden "
|
||||
data-cy="resetTraceButton"
|
||||
onClick={resetEnd}
|
||||
>
|
||||
<div className="bg-green-nx-base flex-1 truncate rounded-md border border-gray-200 p-2 text-gray-50 shadow-sm transition duration-200 ease-in-out group-hover:opacity-60">
|
||||
<span>{end}</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-2 right-2 flex translate-x-32 items-center rounded-md bg-white pl-2 text-sm font-medium text-gray-700 shadow-sm ring-1 ring-gray-500 transition-all transition duration-200 ease-in-out group-hover:translate-x-0">
|
||||
Reset
|
||||
<span className="rounded-md p-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">Select end project</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default TracingPanel;
|
||||
@ -1,16 +1,22 @@
|
||||
import { VirtualElement } from '@popperjs/core';
|
||||
import tippy, { Instance, hideAll } from 'tippy.js';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { selectValueByThemeStatic } from './theme-resolver';
|
||||
|
||||
export class GraphTooltipService {
|
||||
open(ref: VirtualElement, tooltipContent: HTMLElement): Instance {
|
||||
open(ref: VirtualElement, tooltipContent: JSX.Element): Instance {
|
||||
const tempDiv = document.createElement('div');
|
||||
|
||||
ReactDOM.render(tooltipContent, tempDiv);
|
||||
|
||||
let instance = tippy(document.createElement('div'), {
|
||||
trigger: 'manual',
|
||||
theme: selectValueByThemeStatic('dark-nx', 'nx'),
|
||||
interactive: true,
|
||||
appendTo: document.body,
|
||||
content: tooltipContent,
|
||||
content: tempDiv,
|
||||
getReferenceClientRect: ref.getBoundingClientRect,
|
||||
maxWidth: 'none',
|
||||
});
|
||||
|
||||
instance.show();
|
||||
|
||||
@ -15,6 +15,7 @@ export class ProjectEdge {
|
||||
id: `${this.dep.source}|${this.dep.target}`,
|
||||
source: this.dep.source,
|
||||
target: this.dep.target,
|
||||
type: this.dep.type,
|
||||
},
|
||||
};
|
||||
edge.classes = this.dep.type ?? '';
|
||||
|
||||
@ -43,6 +43,8 @@ export class ProjectNode {
|
||||
groupByFolder && this.project.data.hasOwnProperty('root')
|
||||
? this.getParentId()
|
||||
: null,
|
||||
files: this.project.data.files,
|
||||
root: this.project.data.root,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
4
dep-graph/client/src/globals.d.ts
vendored
4
dep-graph/client/src/globals.d.ts
vendored
@ -28,6 +28,10 @@ declare module 'cytoscape' {
|
||||
pannable: () => boolean;
|
||||
}
|
||||
|
||||
interface EdgeSingular {
|
||||
popperRef: Function;
|
||||
}
|
||||
|
||||
namespace Css {
|
||||
interface EdgeLine {
|
||||
'edge-text-rotation'?: string;
|
||||
|
||||
@ -89,7 +89,6 @@ canvas {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
@ -490,7 +490,7 @@ async function createDepGraphClientResponse(
|
||||
data: {
|
||||
tags: project.data.tags,
|
||||
root: project.data.root,
|
||||
files: [],
|
||||
files: project.data.files,
|
||||
},
|
||||
} as ProjectGraphProjectNode)
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user