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:
Philip Fulcher 2022-04-02 16:06:24 -06:00 committed by GitHub
parent 05a9544806
commit dbe942c9c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 17983 additions and 453 deletions

View File

@ -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);
});
});

View 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} &rarr; {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;

View 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;

View 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;

View 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;

View File

@ -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',

View File

@ -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;

View File

@ -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<

View File

@ -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;

View File

@ -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;
}
}),
},
},
},
{

View File

@ -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;

View 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',
],
},
},
};

View File

@ -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: [],
},
};
}

View File

@ -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;
}
}

View 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;

View File

@ -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}

View 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;

View File

@ -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();

View File

@ -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 ?? '';

View File

@ -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

View File

@ -28,6 +28,10 @@ declare module 'cytoscape' {
pannable: () => boolean;
}
interface EdgeSingular {
popperRef: Function;
}
namespace Css {
interface EdgeLine {
'edge-text-rotation'?: string;

View File

@ -89,7 +89,6 @@ canvas {
color: #fff;
font-weight: 600;
letter-spacing: 0.025em;
margin-bottom: 0.75rem;
margin-right: 0.75rem;
}

View File

@ -490,7 +490,7 @@ async function createDepGraphClientResponse(
data: {
tags: project.data.tags,
root: project.data.root,
files: [],
files: project.data.files,
},
} as ProjectGraphProjectNode)
);