feat(core): refactor graph implementation details (#27267)
Co-authored-by: nartc <nartc7789@gmail.com>
This commit is contained in:
parent
320d9f223f
commit
4fd639b170
@ -1,4 +1,4 @@
|
|||||||
import { themeInit } from '@nx/graph/ui-theme';
|
import { themeInit } from '@nx/graph-internal/ui-theme';
|
||||||
import { rankDirInit } from './rankdir-resolver';
|
import { rankDirInit } from './rankdir-resolver';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import { getRouter } from './get-router';
|
import { getRouter } from './get-router';
|
||||||
|
|||||||
@ -0,0 +1,155 @@
|
|||||||
|
import { ProjectGraphStateNodeConfig } from './interfaces';
|
||||||
|
import { assign } from '@xstate/immer';
|
||||||
|
import { send } from 'xstate';
|
||||||
|
|
||||||
|
export const compositeGraphStateConfig: ProjectGraphStateNodeConfig = {
|
||||||
|
entry: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
if (event.type !== 'enableCompositeGraph') return;
|
||||||
|
ctx.compositeGraph.enabled = true;
|
||||||
|
ctx.compositeGraph.context = event.context || undefined;
|
||||||
|
}),
|
||||||
|
send(
|
||||||
|
(ctx) => ({
|
||||||
|
type: 'notifyGraphUpdateGraph',
|
||||||
|
projects: ctx.projects,
|
||||||
|
dependencies: ctx.dependencies,
|
||||||
|
fileMap: ctx.fileMap,
|
||||||
|
affectedProjects: ctx.affectedProjects,
|
||||||
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
|
groupByFolder: ctx.groupByFolder,
|
||||||
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
|
}),
|
||||||
|
{ to: (ctx) => ctx.graphActor }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
exit: [
|
||||||
|
send(() => ({ type: 'notifyGraphDisableCompositeGraph' }), {
|
||||||
|
to: (ctx) => ctx.graphActor,
|
||||||
|
}),
|
||||||
|
assign((ctx) => {
|
||||||
|
ctx.compositeGraph.enabled = false;
|
||||||
|
ctx.compositeGraph.context = undefined;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
on: {
|
||||||
|
focusProject: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
if (event.type !== 'focusProject') return;
|
||||||
|
ctx.focusedProject = event.projectName;
|
||||||
|
}),
|
||||||
|
send(
|
||||||
|
(context, event) => ({
|
||||||
|
type: 'notifyGraphFocusProject',
|
||||||
|
projectName: context.focusedProject,
|
||||||
|
searchDepth: context.searchDepthEnabled ? context.searchDepth : -1,
|
||||||
|
}),
|
||||||
|
{ to: (context) => context.graphActor }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
unfocusProject: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
if (event.type !== 'unfocusProject') return;
|
||||||
|
ctx.focusedProject = null;
|
||||||
|
}),
|
||||||
|
send((ctx) => ({
|
||||||
|
type: 'enableCompositeGraph',
|
||||||
|
context: ctx.compositeGraph.context,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
deselectProject: {
|
||||||
|
actions: [
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyGraphHideProjects',
|
||||||
|
projectNames: [event.projectName],
|
||||||
|
}),
|
||||||
|
{ to: (ctx) => ctx.graphActor }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
selectProject: {
|
||||||
|
actions: [
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyGraphShowProjects',
|
||||||
|
projectNames: [event.projectName],
|
||||||
|
}),
|
||||||
|
{ to: (ctx) => ctx.graphActor }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
expandCompositeNode: {
|
||||||
|
actions: [
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyGraphExpandCompositeNode',
|
||||||
|
id: event.id,
|
||||||
|
}),
|
||||||
|
{ to: (ctx) => ctx.graphActor }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
collapseCompositeNode: {
|
||||||
|
actions: [
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyGraphCollapseCompositeNode',
|
||||||
|
id: event.id,
|
||||||
|
}),
|
||||||
|
{ to: (ctx) => ctx.graphActor }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
enableCompositeGraph: {
|
||||||
|
actions: [
|
||||||
|
assign((ctx, event) => {
|
||||||
|
if (event.type !== 'enableCompositeGraph') return;
|
||||||
|
ctx.compositeGraph.enabled = true;
|
||||||
|
ctx.compositeGraph.context = event.context || undefined;
|
||||||
|
}),
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyGraphUpdateGraph',
|
||||||
|
projects: ctx.projects,
|
||||||
|
dependencies: ctx.dependencies,
|
||||||
|
fileMap: ctx.fileMap,
|
||||||
|
affectedProjects: ctx.affectedProjects,
|
||||||
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
|
groupByFolder: ctx.groupByFolder,
|
||||||
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
|
}),
|
||||||
|
{ to: (ctx) => ctx.graphActor }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
disableCompositeGraph: {
|
||||||
|
target: 'unselected',
|
||||||
|
},
|
||||||
|
updateGraph: {
|
||||||
|
actions: [
|
||||||
|
'setGraph',
|
||||||
|
send(
|
||||||
|
(ctx, event) => ({
|
||||||
|
type: 'notifyGraphUpdateGraph',
|
||||||
|
projects: ctx.projects,
|
||||||
|
dependencies: ctx.dependencies,
|
||||||
|
fileMap: ctx.fileMap,
|
||||||
|
affectedProjects: ctx.affectedProjects,
|
||||||
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
|
groupByFolder: ctx.groupByFolder,
|
||||||
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
|
}),
|
||||||
|
{ to: (ctx) => ctx.graphActor }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -31,6 +31,7 @@ export const customSelectedStateConfig: ProjectGraphStateNodeConfig = {
|
|||||||
workspaceLayout: ctx.workspaceLayout,
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
groupByFolder: ctx.groupByFolder,
|
groupByFolder: ctx.groupByFolder,
|
||||||
selectedProjects: ctx.selectedProjects,
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
to: (context) => context.graphActor,
|
to: (context) => context.graphActor,
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export const focusedStateConfig: ProjectGraphStateNodeConfig = {
|
|||||||
workspaceLayout: ctx.workspaceLayout,
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
groupByFolder: ctx.groupByFolder,
|
groupByFolder: ctx.groupByFolder,
|
||||||
selectedProjects: ctx.selectedProjects,
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
to: (context) => context.graphActor,
|
to: (context) => context.graphActor,
|
||||||
|
|||||||
@ -4,12 +4,13 @@ export const graphActor = (callback, receive) => {
|
|||||||
const graphService = getGraphService();
|
const graphService = getGraphService();
|
||||||
|
|
||||||
receive((e) => {
|
receive((e) => {
|
||||||
const { selectedProjectNames, perfReport } =
|
const { selectedProjectNames, perfReport, compositeNodes } =
|
||||||
graphService.handleProjectEvent(e);
|
graphService.handleProjectEvent(e);
|
||||||
callback({
|
callback({
|
||||||
type: 'setSelectedProjectsFromGraph',
|
type: 'setSelectedProjectsFromGraph',
|
||||||
selectedProjectNames,
|
selectedProjectNames,
|
||||||
perfReport,
|
perfReport,
|
||||||
|
compositeNodes,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { GraphPerfReport } from '../../interfaces';
|
import { CompositeNode, GraphPerfReport } from '../../interfaces';
|
||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import {
|
import {
|
||||||
@ -30,6 +30,7 @@ export type ProjectGraphMachineEvents =
|
|||||||
type: 'setSelectedProjectsFromGraph';
|
type: 'setSelectedProjectsFromGraph';
|
||||||
selectedProjectNames: string[];
|
selectedProjectNames: string[];
|
||||||
perfReport: GraphPerfReport;
|
perfReport: GraphPerfReport;
|
||||||
|
compositeNodes: Array<CompositeNode>;
|
||||||
}
|
}
|
||||||
| { type: 'selectProject'; projectName: string }
|
| { type: 'selectProject'; projectName: string }
|
||||||
| { type: 'deselectProject'; projectName: string }
|
| { type: 'deselectProject'; projectName: string }
|
||||||
@ -70,7 +71,12 @@ export type ProjectGraphMachineEvents =
|
|||||||
projects: ProjectGraphProjectNode[];
|
projects: ProjectGraphProjectNode[];
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
fileMap: ProjectFileMap;
|
fileMap: ProjectFileMap;
|
||||||
};
|
}
|
||||||
|
| { type: 'enableCompositeGraph'; context: string | null }
|
||||||
|
| { type: 'setCompositeContext'; context: string | null }
|
||||||
|
| { type: 'expandCompositeNode'; id: string }
|
||||||
|
| { type: 'collapseCompositeNode'; id: string }
|
||||||
|
| { type: 'disableCompositeGraph' };
|
||||||
|
|
||||||
// The context (extended state) of the machine
|
// The context (extended state) of the machine
|
||||||
export interface ProjectGraphContext {
|
export interface ProjectGraphContext {
|
||||||
@ -97,6 +103,7 @@ export interface ProjectGraphContext {
|
|||||||
end: string;
|
end: string;
|
||||||
algorithm: TracingAlgorithmType;
|
algorithm: TracingAlgorithmType;
|
||||||
};
|
};
|
||||||
|
compositeGraph: CompositeGraph;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectGraphStateNodeConfig = StateNodeConfig<
|
export type ProjectGraphStateNodeConfig = StateNodeConfig<
|
||||||
@ -115,3 +122,9 @@ export type ProjectGraphState = State<
|
|||||||
context: ProjectGraphContext;
|
context: ProjectGraphContext;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export interface CompositeGraph {
|
||||||
|
enabled: boolean;
|
||||||
|
context?: string;
|
||||||
|
nodes: CompositeNode[];
|
||||||
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { textFilteredStateConfig } from './text-filtered.state';
|
|||||||
import { tracingStateConfig } from './tracing.state';
|
import { tracingStateConfig } from './tracing.state';
|
||||||
import { unselectedStateConfig } from './unselected.state';
|
import { unselectedStateConfig } from './unselected.state';
|
||||||
import { ProjectGraphContext, ProjectGraphMachineEvents } from './interfaces';
|
import { ProjectGraphContext, ProjectGraphMachineEvents } from './interfaces';
|
||||||
|
import { compositeGraphStateConfig } from './composite-graph.state';
|
||||||
|
|
||||||
export const initialContext: ProjectGraphContext = {
|
export const initialContext: ProjectGraphContext = {
|
||||||
projects: [],
|
projects: [],
|
||||||
@ -36,6 +37,10 @@ export const initialContext: ProjectGraphContext = {
|
|||||||
end: null,
|
end: null,
|
||||||
algorithm: 'shortest',
|
algorithm: 'shortest',
|
||||||
},
|
},
|
||||||
|
compositeGraph: {
|
||||||
|
enabled: false,
|
||||||
|
nodes: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const projectGraphMachine = createMachine<
|
export const projectGraphMachine = createMachine<
|
||||||
@ -54,6 +59,7 @@ export const projectGraphMachine = createMachine<
|
|||||||
focused: focusedStateConfig,
|
focused: focusedStateConfig,
|
||||||
textFiltered: textFilteredStateConfig,
|
textFiltered: textFilteredStateConfig,
|
||||||
tracing: tracingStateConfig,
|
tracing: tracingStateConfig,
|
||||||
|
composite: compositeGraphStateConfig,
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
setProjects: {
|
setProjects: {
|
||||||
@ -70,6 +76,7 @@ export const projectGraphMachine = createMachine<
|
|||||||
workspaceLayout: ctx.workspaceLayout,
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
groupByFolder: ctx.groupByFolder,
|
groupByFolder: ctx.groupByFolder,
|
||||||
collapseEdges: ctx.collapseEdges,
|
collapseEdges: ctx.collapseEdges,
|
||||||
|
composite: ctx.compositeGraph.enabled,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
to: (context) => context.graphActor,
|
to: (context) => context.graphActor,
|
||||||
@ -82,6 +89,7 @@ export const projectGraphMachine = createMachine<
|
|||||||
assign((ctx, event) => {
|
assign((ctx, event) => {
|
||||||
ctx.selectedProjects = event.selectedProjectNames;
|
ctx.selectedProjects = event.selectedProjectNames;
|
||||||
ctx.lastPerfReport = event.perfReport;
|
ctx.lastPerfReport = event.perfReport;
|
||||||
|
ctx.compositeGraph.nodes = event.compositeNodes;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -147,6 +155,7 @@ export const projectGraphMachine = createMachine<
|
|||||||
groupByFolder: ctx.groupByFolder,
|
groupByFolder: ctx.groupByFolder,
|
||||||
collapseEdges: ctx.collapseEdges,
|
collapseEdges: ctx.collapseEdges,
|
||||||
selectedProjects: ctx.selectedProjects,
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
to: (context) => context.graphActor,
|
to: (context) => context.graphActor,
|
||||||
@ -168,6 +177,7 @@ export const projectGraphMachine = createMachine<
|
|||||||
groupByFolder: ctx.groupByFolder,
|
groupByFolder: ctx.groupByFolder,
|
||||||
collapseEdges: ctx.collapseEdges,
|
collapseEdges: ctx.collapseEdges,
|
||||||
selectedProjects: ctx.selectedProjects,
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
to: (context) => context.graphActor,
|
to: (context) => context.graphActor,
|
||||||
@ -205,6 +215,9 @@ export const projectGraphMachine = createMachine<
|
|||||||
filterByText: {
|
filterByText: {
|
||||||
target: 'textFiltered',
|
target: 'textFiltered',
|
||||||
},
|
},
|
||||||
|
enableCompositeGraph: {
|
||||||
|
target: 'composite',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -212,6 +225,9 @@ export const projectGraphMachine = createMachine<
|
|||||||
deselectLastProject: (ctx) => {
|
deselectLastProject: (ctx) => {
|
||||||
return ctx.selectedProjects.length <= 1;
|
return ctx.selectedProjects.length <= 1;
|
||||||
},
|
},
|
||||||
|
isCompositeGraphEnabled: (ctx) => {
|
||||||
|
return ctx.compositeGraph.enabled;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setGroupByFolder: assign((ctx, event) => {
|
setGroupByFolder: assign((ctx, event) => {
|
||||||
@ -356,7 +372,6 @@ export const projectGraphMachine = createMachine<
|
|||||||
to: (context) => context.graphActor,
|
to: (context) => context.graphActor,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
notifyGraphFilterProjectsByText: send(
|
notifyGraphFilterProjectsByText: send(
|
||||||
(context, event) => ({
|
(context, event) => ({
|
||||||
type: 'notifyGraphFilterProjectsByText',
|
type: 'notifyGraphFilterProjectsByText',
|
||||||
|
|||||||
@ -106,6 +106,8 @@ const mockAppConfig: AppConfig = {
|
|||||||
label: 'local',
|
label: 'local',
|
||||||
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
||||||
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
||||||
|
taskInputsUrl: '',
|
||||||
|
sourceMapsUrl: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultWorkspaceId: 'local',
|
defaultWorkspaceId: 'local',
|
||||||
|
|||||||
@ -3,7 +3,11 @@
|
|||||||
import type { ProjectGraphProjectNode } from '@nx/devkit';
|
import type { ProjectGraphProjectNode } from '@nx/devkit';
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
import { ProjectGraphSelector } from '../hooks/use-project-graph-selector';
|
import { ProjectGraphSelector } from '../hooks/use-project-graph-selector';
|
||||||
import { GraphPerfReport, WorkspaceLayout } from '../../interfaces';
|
import {
|
||||||
|
CompositeNode,
|
||||||
|
GraphPerfReport,
|
||||||
|
WorkspaceLayout,
|
||||||
|
} from '../../interfaces';
|
||||||
import { TracingAlgorithmType } from './interfaces';
|
import { TracingAlgorithmType } from './interfaces';
|
||||||
|
|
||||||
export const allProjectsSelector: ProjectGraphSelector<
|
export const allProjectsSelector: ProjectGraphSelector<
|
||||||
@ -46,6 +50,15 @@ export const groupByFolderSelector: ProjectGraphSelector<boolean> = (state) =>
|
|||||||
|
|
||||||
export const collapseEdgesSelector: ProjectGraphSelector<boolean> = (state) =>
|
export const collapseEdgesSelector: ProjectGraphSelector<boolean> = (state) =>
|
||||||
state.context.collapseEdges;
|
state.context.collapseEdges;
|
||||||
|
export const compositeGraphEnabledSelector: ProjectGraphSelector<boolean> = (
|
||||||
|
state
|
||||||
|
) => state.context.compositeGraph.enabled;
|
||||||
|
export const compositeContextSelector: ProjectGraphSelector<string | null> = (
|
||||||
|
state
|
||||||
|
) => state.context.compositeGraph.context;
|
||||||
|
export const compositeNodesSelector: ProjectGraphSelector<CompositeNode[]> = (
|
||||||
|
state
|
||||||
|
) => state.context.compositeGraph.nodes;
|
||||||
|
|
||||||
export const textFilterSelector: ProjectGraphSelector<string> = (state) =>
|
export const textFilterSelector: ProjectGraphSelector<string> = (state) =>
|
||||||
state.context.textFilter;
|
state.context.textFilter;
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export const textFilteredStateConfig: ProjectGraphStateNodeConfig = {
|
|||||||
workspaceLayout: ctx.workspaceLayout,
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
groupByFolder: ctx.groupByFolder,
|
groupByFolder: ctx.groupByFolder,
|
||||||
selectedProjects: ctx.selectedProjects,
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
to: (context) => context.graphActor,
|
to: (context) => context.graphActor,
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export const unselectedStateConfig: ProjectGraphStateNodeConfig = {
|
|||||||
workspaceLayout: ctx.workspaceLayout,
|
workspaceLayout: ctx.workspaceLayout,
|
||||||
groupByFolder: ctx.groupByFolder,
|
groupByFolder: ctx.groupByFolder,
|
||||||
selectedProjects: ctx.selectedProjects,
|
selectedProjects: ctx.selectedProjects,
|
||||||
|
composite: ctx.compositeGraph,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
to: (context) => context.graphActor,
|
to: (context) => context.graphActor,
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export interface CompositeGraphPanelProps {
|
||||||
|
compositeEnabled: boolean;
|
||||||
|
compositeEnabledChanged: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CompositeGraphPanel = memo(
|
||||||
|
({ compositeEnabled, compositeEnabledChanged }: CompositeGraphPanelProps) => {
|
||||||
|
return (
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex h-5 items-center">
|
||||||
|
<input
|
||||||
|
id="composite"
|
||||||
|
name="composite"
|
||||||
|
value="composite"
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 accent-purple-500"
|
||||||
|
onChange={(event) =>
|
||||||
|
compositeEnabledChanged(event.target.checked)
|
||||||
|
}
|
||||||
|
checked={compositeEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label
|
||||||
|
htmlFor="composite"
|
||||||
|
className="cursor-pointer font-medium text-slate-600 dark:text-slate-400"
|
||||||
|
>
|
||||||
|
Composite Graph
|
||||||
|
</label>
|
||||||
|
<p className="text-slate-400 dark:text-slate-500">
|
||||||
|
Enables experimental composite graph with composite nodes and
|
||||||
|
edges
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
ArrowsPointingInIcon,
|
||||||
|
ArrowsPointingOutIcon,
|
||||||
DocumentMagnifyingGlassIcon,
|
DocumentMagnifyingGlassIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FlagIcon,
|
FlagIcon,
|
||||||
@ -11,6 +13,9 @@ import type { ProjectGraphProjectNode } from '@nx/devkit';
|
|||||||
import { useProjectGraphSelector } from './hooks/use-project-graph-selector';
|
import { useProjectGraphSelector } from './hooks/use-project-graph-selector';
|
||||||
import {
|
import {
|
||||||
allProjectsSelector,
|
allProjectsSelector,
|
||||||
|
compositeContextSelector,
|
||||||
|
compositeGraphEnabledSelector,
|
||||||
|
compositeNodesSelector,
|
||||||
getTracingInfo,
|
getTracingInfo,
|
||||||
selectedProjectNamesSelector,
|
selectedProjectNamesSelector,
|
||||||
workspaceLayoutSelector,
|
workspaceLayoutSelector,
|
||||||
@ -19,8 +24,9 @@ import { getProjectsByType, parseParentDirectoriesFromFilePath } from '../util';
|
|||||||
import { ExperimentalFeature } from '../ui-components/experimental-feature';
|
import { ExperimentalFeature } from '../ui-components/experimental-feature';
|
||||||
import { TracingAlgorithmType } from './machines/interfaces';
|
import { TracingAlgorithmType } from './machines/interfaces';
|
||||||
import { getProjectGraphService } from '../machines/get-services';
|
import { getProjectGraphService } from '../machines/get-services';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate, useNavigation } from 'react-router-dom';
|
||||||
import { useRouteConstructor } from '@nx/graph/shared';
|
import { useRouteConstructor } from '@nx/graph/shared';
|
||||||
|
import { CompositeNode } from '../interfaces';
|
||||||
|
|
||||||
interface SidebarProject {
|
interface SidebarProject {
|
||||||
projectGraphNode: ProjectGraphProjectNode;
|
projectGraphNode: ProjectGraphProjectNode;
|
||||||
@ -234,6 +240,120 @@ function SubProjectList({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CompositeNodeListItem({
|
||||||
|
compositeNode,
|
||||||
|
}: {
|
||||||
|
compositeNode: CompositeNode;
|
||||||
|
}) {
|
||||||
|
const projectGraphService = getProjectGraphService();
|
||||||
|
const routeConstructor = useRouteConstructor();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function toggleProject() {
|
||||||
|
if (compositeNode.state !== 'hidden') {
|
||||||
|
projectGraphService.send({
|
||||||
|
type: 'deselectProject',
|
||||||
|
projectName: compositeNode.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
projectGraphService.send({
|
||||||
|
type: 'selectProject',
|
||||||
|
projectName: compositeNode.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
navigate(routeConstructor('/projects', true));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpansion() {
|
||||||
|
if (compositeNode.state === 'expanded') {
|
||||||
|
projectGraphService.send({
|
||||||
|
type: 'collapseCompositeNode',
|
||||||
|
id: compositeNode.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
projectGraphService.send({
|
||||||
|
type: 'expandCompositeNode',
|
||||||
|
id: compositeNode.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<Link
|
||||||
|
to={routeConstructor(
|
||||||
|
{
|
||||||
|
pathname: `/projects`,
|
||||||
|
search: `?composite=true&compositeContext=${encodeURIComponent(
|
||||||
|
compositeNode.id
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
className="mr-1 flex items-center rounded-md border-slate-300 bg-white p-1 font-medium text-slate-500 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-600 hover:dark:bg-slate-700"
|
||||||
|
title="Focus on this node"
|
||||||
|
data-cy={`focus-button-${compositeNode.id}`}
|
||||||
|
>
|
||||||
|
<DocumentMagnifyingGlassIcon className="h-5 w-5" />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="mr-1 flex items-center rounded-md border-slate-300 bg-white p-1 font-medium text-slate-500 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-600 hover:dark:bg-slate-700"
|
||||||
|
onClick={toggleExpansion}
|
||||||
|
title={compositeNode.state === 'expanded' ? 'Collapse' : 'Expand'}
|
||||||
|
>
|
||||||
|
{compositeNode.state === 'expanded' ? (
|
||||||
|
<ArrowsPointingInIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<ArrowsPointingOutIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<label
|
||||||
|
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={compositeNode.id}
|
||||||
|
title={compositeNode.label}
|
||||||
|
data-active={compositeNode.state !== 'hidden'}
|
||||||
|
onClick={toggleProject}
|
||||||
|
>
|
||||||
|
{compositeNode.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{compositeNode.state !== 'hidden' ? (
|
||||||
|
<span
|
||||||
|
title="This node is visible"
|
||||||
|
className="absolute inset-y-0 right-0 flex cursor-pointer items-center text-blue-500 dark:text-sky-500"
|
||||||
|
onClick={toggleProject}
|
||||||
|
>
|
||||||
|
<EyeIcon className="h-5 w-5"></EyeIcon>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompositeNodeList({
|
||||||
|
compositeNodes,
|
||||||
|
}: {
|
||||||
|
compositeNodes: CompositeNode[];
|
||||||
|
}) {
|
||||||
|
const projectGraphService = getProjectGraphService();
|
||||||
|
|
||||||
|
if (compositeNodes.length === 0) {
|
||||||
|
return <p>No composite nodes</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="-ml-3 mt-2">
|
||||||
|
{compositeNodes.map((node) => {
|
||||||
|
return <CompositeNodeListItem key={node.id} compositeNode={node} />;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ProjectList() {
|
export function ProjectList() {
|
||||||
const tracingInfo = useProjectGraphSelector(getTracingInfo);
|
const tracingInfo = useProjectGraphSelector(getTracingInfo);
|
||||||
|
|
||||||
@ -242,6 +362,11 @@ export function ProjectList() {
|
|||||||
const selectedProjects = useProjectGraphSelector(
|
const selectedProjects = useProjectGraphSelector(
|
||||||
selectedProjectNamesSelector
|
selectedProjectNamesSelector
|
||||||
);
|
);
|
||||||
|
const compositeGraphEnabled = useProjectGraphSelector(
|
||||||
|
compositeGraphEnabledSelector
|
||||||
|
);
|
||||||
|
const compositeContext = useProjectGraphSelector(compositeContextSelector);
|
||||||
|
const compositeNodes = useProjectGraphSelector(compositeNodesSelector);
|
||||||
|
|
||||||
const appProjects = getProjectsByType('app', projects);
|
const appProjects = getProjectsByType('app', projects);
|
||||||
const libProjects = getProjectsByType('lib', projects);
|
const libProjects = getProjectsByType('lib', projects);
|
||||||
@ -269,6 +394,15 @@ export function ProjectList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="project-lists" className="mt-8 border-t border-slate-400/10 px-4">
|
<div id="project-lists" className="mt-8 border-t border-slate-400/10 px-4">
|
||||||
|
{compositeGraphEnabled && !compositeContext ? (
|
||||||
|
<>
|
||||||
|
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light text-slate-400 dark:text-slate-500">
|
||||||
|
composite nodes
|
||||||
|
</h2>
|
||||||
|
<CompositeNodeList compositeNodes={compositeNodes} />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light text-slate-400 dark:text-slate-500">
|
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light text-slate-400 dark:text-slate-500">
|
||||||
app projects
|
app projects
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@ -68,24 +68,24 @@ export function ProjectsSidebar(): JSX.Element {
|
|||||||
const [lastHash, setLastHash] = useState(selectedProjectRouteData.hash);
|
const [lastHash, setLastHash] = useState(selectedProjectRouteData.hash);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const routeContructor = useRouteConstructor();
|
const routeConstructor = useRouteConstructor();
|
||||||
|
|
||||||
function resetFocus() {
|
function resetFocus() {
|
||||||
projectGraphService.send({ type: 'unfocusProject' });
|
projectGraphService.send({ type: 'unfocusProject' });
|
||||||
navigate(routeContructor('/projects', true));
|
navigate(routeConstructor('/projects', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAllProjects() {
|
function showAllProjects() {
|
||||||
navigate(routeContructor('/projects/all', true));
|
navigate(routeConstructor('/projects/all', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideAllProjects() {
|
function hideAllProjects() {
|
||||||
projectGraphService.send({ type: 'deselectAll' });
|
projectGraphService.send({ type: 'deselectAll' });
|
||||||
navigate(routeContructor('/projects', true));
|
navigate(routeConstructor('/projects', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAffectedProjects() {
|
function showAffectedProjects() {
|
||||||
navigate(routeContructor('/projects/affected', true));
|
navigate(routeConstructor('/projects/affected', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchDepthFilterEnabledChange(checked: boolean) {
|
function searchDepthFilterEnabledChange(checked: boolean) {
|
||||||
@ -166,12 +166,12 @@ export function ProjectsSidebar(): JSX.Element {
|
|||||||
|
|
||||||
function resetTraceStart() {
|
function resetTraceStart() {
|
||||||
projectGraphService.send({ type: 'clearTraceStart' });
|
projectGraphService.send({ type: 'clearTraceStart' });
|
||||||
navigate(routeContructor('/projects', true));
|
navigate(routeConstructor('/projects', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetTraceEnd() {
|
function resetTraceEnd() {
|
||||||
projectGraphService.send({ type: 'clearTraceEnd' });
|
projectGraphService.send({ type: 'clearTraceEnd' });
|
||||||
navigate(routeContructor('/projects', true));
|
navigate(routeConstructor('/projects', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAlgorithm(algorithm: TracingAlgorithmType) {
|
function setAlgorithm(algorithm: TracingAlgorithmType) {
|
||||||
@ -320,7 +320,7 @@ export function ProjectsSidebar(): JSX.Element {
|
|||||||
const updateTextFilter = useCallback(
|
const updateTextFilter = useCallback(
|
||||||
(textFilter: string) => {
|
(textFilter: string) => {
|
||||||
projectGraphService.send({ type: 'filterByText', search: textFilter });
|
projectGraphService.send({ type: 'filterByText', search: textFilter });
|
||||||
navigate(routeContructor('/projects', true));
|
navigate(routeConstructor('/projects', true));
|
||||||
},
|
},
|
||||||
[projectGraphService]
|
[projectGraphService]
|
||||||
);
|
);
|
||||||
@ -335,6 +335,7 @@ export function ProjectsSidebar(): JSX.Element {
|
|||||||
resetFocus={resetFocus}
|
resetFocus={resetFocus}
|
||||||
></FocusedPanel>
|
></FocusedPanel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isTracing ? (
|
{isTracing ? (
|
||||||
<TracingPanel
|
<TracingPanel
|
||||||
start={tracingInfo.start}
|
start={tracingInfo.start}
|
||||||
@ -377,7 +378,7 @@ export function ProjectsSidebar(): JSX.Element {
|
|||||||
></SearchDepth>
|
></SearchDepth>
|
||||||
|
|
||||||
<ExperimentalFeature>
|
<ExperimentalFeature>
|
||||||
<div className="mx-4 mt-4 rounded-lg border-2 border-dashed border-purple-500 p-4 shadow-lg dark:border-purple-600 dark:bg-[#0B1221]">
|
<div className="mx-4 mt-8 rounded-lg border-2 border-dashed border-purple-500 p-4 shadow-lg dark:border-purple-600 dark:bg-[#0B1221]">
|
||||||
<h3 className="cursor-text px-4 py-2 text-sm font-semibold uppercase tracking-wide text-slate-800 lg:text-xs dark:text-slate-200">
|
<h3 className="cursor-text px-4 py-2 text-sm font-semibold uppercase tracking-wide text-slate-800 lg:text-xs dark:text-slate-200">
|
||||||
Experimental Features
|
Experimental Features
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@ -31,3 +31,9 @@ export interface GraphPerfReport {
|
|||||||
numNodes: number;
|
numNodes: number;
|
||||||
numEdges: number;
|
numEdges: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CompositeNode {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
state: 'expanded' | 'collapsed' | 'hidden';
|
||||||
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
getEnvironmentConfig,
|
getEnvironmentConfig,
|
||||||
getProjectGraphDataService,
|
getProjectGraphDataService,
|
||||||
} from '@nx/graph/shared';
|
} from '@nx/graph/shared';
|
||||||
import { selectValueByThemeStatic } from '@nx/graph/ui-theme';
|
import { selectValueByThemeStatic } from '@nx/graph-internal/ui-theme';
|
||||||
|
|
||||||
let graphService: GraphService;
|
let graphService: GraphService;
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export type GraphRenderEvents =
|
|||||||
};
|
};
|
||||||
groupByFolder: boolean;
|
groupByFolder: boolean;
|
||||||
collapseEdges: boolean;
|
collapseEdges: boolean;
|
||||||
|
composite: { enabled: boolean; context: string | null };
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'notifyGraphUpdateGraph';
|
type: 'notifyGraphUpdateGraph';
|
||||||
@ -36,6 +37,7 @@ export type GraphRenderEvents =
|
|||||||
groupByFolder: boolean;
|
groupByFolder: boolean;
|
||||||
collapseEdges: boolean;
|
collapseEdges: boolean;
|
||||||
selectedProjects: string[];
|
selectedProjects: string[];
|
||||||
|
composite: { enabled: boolean; context: string | null };
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'notifyGraphFocusProject';
|
type: 'notifyGraphFocusProject';
|
||||||
@ -70,4 +72,5 @@ export type GraphRenderEvents =
|
|||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
algorithm: TracingAlgorithmType;
|
algorithm: TracingAlgorithmType;
|
||||||
};
|
}
|
||||||
|
| { type: 'notifyGraphDisableCompositeGraph' };
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
getProjectGraphDataService,
|
getProjectGraphDataService,
|
||||||
} from '@nx/graph/shared';
|
} from '@nx/graph/shared';
|
||||||
import { TasksSidebarErrorBoundary } from './feature-tasks/tasks-sidebar-error-boundary';
|
import { TasksSidebarErrorBoundary } from './feature-tasks/tasks-sidebar-error-boundary';
|
||||||
import { ProjectDetailsPage } from '@nx/graph/project-details';
|
import { ProjectDetailsPage } from '@nx/graph-internal/project-details';
|
||||||
import { ErrorBoundary } from './ui-components/error-boundary';
|
import { ErrorBoundary } from './ui-components/error-boundary';
|
||||||
|
|
||||||
const { appConfig } = getEnvironmentConfig();
|
const { appConfig } = getEnvironmentConfig();
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
ArrowDownTrayIcon,
|
ArrowDownTrayIcon,
|
||||||
ArrowLeftCircleIcon,
|
ArrowLeftCircleIcon,
|
||||||
InformationCircleIcon,
|
InformationCircleIcon,
|
||||||
|
ViewfinderCircleIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import {
|
import {
|
||||||
ErrorToast,
|
ErrorToast,
|
||||||
@ -19,7 +20,7 @@ import {
|
|||||||
usePoll,
|
usePoll,
|
||||||
} from '@nx/graph/shared';
|
} from '@nx/graph/shared';
|
||||||
import { Dropdown, Spinner } from '@nx/graph/ui-components';
|
import { Dropdown, Spinner } from '@nx/graph/ui-components';
|
||||||
import { getSystemTheme, Theme, ThemePanel } from '@nx/graph/ui-theme';
|
import { getSystemTheme, Theme, ThemePanel } from '@nx/graph-internal/ui-theme';
|
||||||
import { Tooltip } from '@nx/graph/ui-tooltips';
|
import { Tooltip } from '@nx/graph/ui-tooltips';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useLayoutEffect, useState } from 'react';
|
import { useLayoutEffect, useState } from 'react';
|
||||||
@ -118,6 +119,11 @@ export function Shell(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetLayout() {
|
||||||
|
const graph = getGraphService();
|
||||||
|
graph.resetLayout();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen">
|
<div className="flex h-screen w-screen">
|
||||||
<div
|
<div
|
||||||
@ -246,7 +252,7 @@ export function Shell(): JSX.Element {
|
|||||||
type="button"
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
!nodesVisible ? 'invisible opacity-0' : '',
|
!nodesVisible ? 'invisible opacity-0' : '',
|
||||||
'fixed bottom-4 right-4 z-50 block h-16 w-16 transform rounded-full bg-blue-500 text-white shadow-sm transition duration-300 dark:bg-sky-500'
|
'fixed bottom-4 right-4 z-50 block h-12 w-12 transform rounded-full bg-blue-500 text-white shadow-sm transition duration-300 dark:bg-sky-500'
|
||||||
)}
|
)}
|
||||||
data-cy="downloadImageButton"
|
data-cy="downloadImageButton"
|
||||||
onClick={downloadImage}
|
onClick={downloadImage}
|
||||||
@ -254,6 +260,18 @@ export function Shell(): JSX.Element {
|
|||||||
<ArrowDownTrayIcon className="absolute left-1/2 top-1/2 -ml-3 -mt-3 h-6 w-6" />
|
<ArrowDownTrayIcon className="absolute left-1/2 top-1/2 -ml-3 -mt-3 h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(
|
||||||
|
!nodesVisible ? 'invisible opacity-0' : '',
|
||||||
|
'fixed bottom-20 right-4 z-50 block h-12 w-12 transform rounded-full bg-blue-500 text-white shadow-sm transition duration-300 dark:bg-sky-500'
|
||||||
|
)}
|
||||||
|
data-cy="resetLayoutButton"
|
||||||
|
onClick={resetLayout}
|
||||||
|
>
|
||||||
|
<ViewfinderCircleIcon className="absolute left-1/2 top-1/2 -ml-3 -mt-3 h-6 w-6" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ErrorToast errors={errors} />
|
<ErrorToast errors={errors} />
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { ArrowRightCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export interface CompositeContextPanelProps {
|
||||||
|
compositeContext: string;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CompositeContextPanel = memo(
|
||||||
|
({ compositeContext, reset }: CompositeContextPanelProps) => {
|
||||||
|
return (
|
||||||
|
<div className="mt-10 px-4">
|
||||||
|
<div
|
||||||
|
className="group relative flex cursor-pointer items-center overflow-hidden rounded-md border border-slate-200 bg-blue-500 p-2 text-slate-50 shadow-sm dark:border-slate-700 dark:bg-sky-500"
|
||||||
|
data-cy="resetCompositeContextButton"
|
||||||
|
onClick={() => reset()}
|
||||||
|
>
|
||||||
|
<p className="truncate transition duration-200 ease-in-out group-hover:opacity-60">
|
||||||
|
<ArrowRightCircleIcon className="-mt-1 mr-1 inline h-6 w-6" />
|
||||||
|
<span id="focused-project-name">Focused on {compositeContext}</span>
|
||||||
|
</p>
|
||||||
|
<div className="absolute right-2 flex translate-x-32 items-center rounded-md bg-white pl-2 text-sm font-medium text-slate-700 shadow-sm ring-1 ring-slate-500 transition-all duration-200 ease-in-out group-hover:translate-x-0 dark:bg-slate-800 dark:text-slate-300">
|
||||||
|
Reset
|
||||||
|
<span className="rounded-md p-1">
|
||||||
|
<XCircleIcon className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { ProjectDetailsHeader } from '@nx/graph/project-details';
|
import { ProjectDetailsHeader } from '@nx/graph-internal/project-details';
|
||||||
import {
|
import {
|
||||||
fetchProjectGraph,
|
fetchProjectGraph,
|
||||||
getProjectGraphDataService,
|
getProjectGraphDataService,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
|
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import { ProjectDetailsWrapper } from '@nx/graph/project-details';
|
import { ProjectDetailsWrapper } from '@nx/graph-internal/project-details';
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
import { useFloating } from '@floating-ui/react';
|
import { useFloating } from '@floating-ui/react';
|
||||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
||||||
import { getTooltipService } from '../machines/get-services';
|
|
||||||
import {
|
import {
|
||||||
|
getProjectGraphService,
|
||||||
|
getTooltipService,
|
||||||
|
} from '../machines/get-services';
|
||||||
|
import {
|
||||||
|
CompositeNodeTooltip,
|
||||||
|
CompositeNodeTooltipActions,
|
||||||
|
NodeTooltipAction,
|
||||||
ProjectEdgeNodeTooltip,
|
ProjectEdgeNodeTooltip,
|
||||||
ProjectNodeToolTip,
|
ProjectNodeToolTip,
|
||||||
|
ProjectNodeTooltipActions,
|
||||||
TaskNodeTooltip,
|
TaskNodeTooltip,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@nx/graph/ui-tooltips';
|
} from '@nx/graph/ui-tooltips';
|
||||||
import { ProjectNodeActions } from './project-node-actions';
|
|
||||||
import { TaskNodeActions } from './task-node-actions';
|
import { TaskNodeActions } from './task-node-actions';
|
||||||
import { getExternalApiService, useRouteConstructor } from '@nx/graph/shared';
|
import { getExternalApiService, useRouteConstructor } from '@nx/graph/shared';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
const tooltipService = getTooltipService();
|
const tooltipService = getTooltipService();
|
||||||
|
|
||||||
@ -17,12 +24,73 @@ export function TooltipDisplay() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const routeConstructor = useRouteConstructor();
|
const routeConstructor = useRouteConstructor();
|
||||||
const externalApiService = getExternalApiService();
|
const externalApiService = getExternalApiService();
|
||||||
|
const projectGraphService = getProjectGraphService();
|
||||||
|
|
||||||
const currentTooltip = useSyncExternalStore(
|
const currentTooltip = useSyncExternalStore(
|
||||||
(callback) => tooltipService.subscribe(callback),
|
(callback) => tooltipService.subscribe(callback),
|
||||||
() => tooltipService.currentTooltip
|
() => tooltipService.currentTooltip
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onAction = useCallback(
|
||||||
|
(action: NodeTooltipAction) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'expand-node':
|
||||||
|
projectGraphService.send({
|
||||||
|
type: 'expandCompositeNode',
|
||||||
|
id: action.id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'focus-node': {
|
||||||
|
const to =
|
||||||
|
action.tooltipNodeType === 'compositeNode'
|
||||||
|
? routeConstructor(
|
||||||
|
{
|
||||||
|
pathname: `/projects`,
|
||||||
|
search: `?composite=true&compositeContext=${action.id}`,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
: routeConstructor(`/projects/${action.id}`, true);
|
||||||
|
navigate(to);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'collapse-node':
|
||||||
|
projectGraphService.send({
|
||||||
|
type: 'collapseCompositeNode',
|
||||||
|
id: action.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'exclude-node':
|
||||||
|
projectGraphService.send({
|
||||||
|
type: 'deselectProject',
|
||||||
|
projectName:
|
||||||
|
action.tooltipNodeType === 'projectNode'
|
||||||
|
? action.rawId
|
||||||
|
: action.id,
|
||||||
|
});
|
||||||
|
if (action.tooltipNodeType === 'projectNode') {
|
||||||
|
navigate(routeConstructor('/projects', true));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'start-trace':
|
||||||
|
navigate(routeConstructor(`/projects/trace/${action.id}`, true));
|
||||||
|
break;
|
||||||
|
case 'end-trace': {
|
||||||
|
const { start } = projectGraphService.getSnapshot().context.tracing;
|
||||||
|
navigate(
|
||||||
|
routeConstructor(
|
||||||
|
`/projects/trace/${encodeURIComponent(start)}/${action.id}`,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[projectGraphService, navigate, routeConstructor]
|
||||||
|
);
|
||||||
|
|
||||||
let tooltipToRender;
|
let tooltipToRender;
|
||||||
if (currentTooltip) {
|
if (currentTooltip) {
|
||||||
if (currentTooltip.type === 'projectNode') {
|
if (currentTooltip.type === 'projectNode') {
|
||||||
@ -59,9 +127,21 @@ export function TooltipDisplay() {
|
|||||||
{...currentTooltip.props}
|
{...currentTooltip.props}
|
||||||
openConfigCallback={onConfigClick}
|
openConfigCallback={onConfigClick}
|
||||||
>
|
>
|
||||||
<ProjectNodeActions {...currentTooltip.props} />
|
<ProjectNodeTooltipActions
|
||||||
|
onAction={onAction}
|
||||||
|
{...currentTooltip.props}
|
||||||
|
/>
|
||||||
</ProjectNodeToolTip>
|
</ProjectNodeToolTip>
|
||||||
);
|
);
|
||||||
|
} else if (currentTooltip.type === 'compositeNode') {
|
||||||
|
tooltipToRender = (
|
||||||
|
<CompositeNodeTooltip {...currentTooltip.props}>
|
||||||
|
<CompositeNodeTooltipActions
|
||||||
|
onAction={onAction}
|
||||||
|
{...currentTooltip.props}
|
||||||
|
/>
|
||||||
|
</CompositeNodeTooltip>
|
||||||
|
);
|
||||||
} else if (currentTooltip.type === 'projectEdge') {
|
} else if (currentTooltip.type === 'projectEdge') {
|
||||||
const onFileClick =
|
const onFileClick =
|
||||||
currentTooltip.props.renderMode === 'nx-console'
|
currentTooltip.props.renderMode === 'nx-console'
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
import { ProjectNodeToolTipProps } from '@nx/graph/ui-tooltips';
|
|
||||||
import { getProjectGraphService } from '../machines/get-services';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
import { TooltipButton, TooltipLinkButton } from '@nx/graph/ui-tooltips';
|
|
||||||
import { FlagIcon, MapPinIcon } from '@heroicons/react/24/solid';
|
|
||||||
import { useRouteConstructor } from '@nx/graph/shared';
|
|
||||||
|
|
||||||
export function ProjectNodeActions({ id }: ProjectNodeToolTipProps) {
|
|
||||||
const projectGraphService = getProjectGraphService();
|
|
||||||
const { start, end, algorithm } =
|
|
||||||
projectGraphService.getSnapshot().context.tracing;
|
|
||||||
const routeConstructor = useRouteConstructor();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const encodedId = encodeURIComponent(id);
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
|
|
||||||
function onProjectDetails() {
|
|
||||||
setSearchParams({ projectDetails: id });
|
|
||||||
}
|
|
||||||
function onExclude() {
|
|
||||||
projectGraphService.send({
|
|
||||||
type: 'deselectProject',
|
|
||||||
projectName: id,
|
|
||||||
});
|
|
||||||
navigate(routeConstructor('/projects', true));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onStartTrace() {
|
|
||||||
navigate(routeConstructor(`/projects/trace/${encodedId}`, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEndTrace() {
|
|
||||||
navigate(
|
|
||||||
routeConstructor(
|
|
||||||
`/projects/trace/${encodeURIComponent(start)}/${encodedId}`,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
{/* <TooltipButton onClick={onProjectDetails}>Project Details</TooltipButton> */}
|
|
||||||
<TooltipLinkButton to={routeConstructor(`/projects/${encodedId}`, true)}>
|
|
||||||
Focus
|
|
||||||
</TooltipLinkButton>
|
|
||||||
<TooltipButton onClick={onExclude}>Exclude</TooltipButton>
|
|
||||||
{!start ? (
|
|
||||||
<TooltipButton
|
|
||||||
className="flex flex-row items-center"
|
|
||||||
onClick={onStartTrace}
|
|
||||||
>
|
|
||||||
<MapPinIcon className="mr-2 h-5 w-5 text-slate-500"></MapPinIcon>
|
|
||||||
Start
|
|
||||||
</TooltipButton>
|
|
||||||
) : (
|
|
||||||
<TooltipButton
|
|
||||||
className="flex flex-row items-center"
|
|
||||||
onClick={onEndTrace}
|
|
||||||
>
|
|
||||||
<FlagIcon className="mr-2 h-5 w-5 text-slate-500"></FlagIcon>
|
|
||||||
End
|
|
||||||
</TooltipButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
2
graph/client/src/globals.d.ts
vendored
2
graph/client/src/globals.d.ts
vendored
@ -5,7 +5,7 @@ import type {
|
|||||||
ProjectGraphClientResponse,
|
ProjectGraphClientResponse,
|
||||||
TaskGraphClientResponse,
|
TaskGraphClientResponse,
|
||||||
} from 'nx/src/command-line/graph/graph';
|
} from 'nx/src/command-line/graph/graph';
|
||||||
import { AppConfig, ExternalApi } from '@nx/graph/shared';
|
import type { AppConfig, ExternalApi } from '@nx/graph/shared';
|
||||||
|
|
||||||
export declare global {
|
export declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
const path = require('path');
|
// @ts-check
|
||||||
|
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
// Ignore these nx related dependencies since they are the installed versions not the ones in the workspace
|
||||||
|
// nx-ignore-next-line
|
||||||
|
const { workspaceRoot } = require('@nx/devkit');
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
||||||
|
|
||||||
@ -7,6 +12,12 @@ module.exports = {
|
|||||||
content: [
|
content: [
|
||||||
path.join(__dirname, 'src/**/*.{js,ts,jsx,tsx,html}'),
|
path.join(__dirname, 'src/**/*.{js,ts,jsx,tsx,html}'),
|
||||||
...createGlobPatternsForDependencies(__dirname),
|
...createGlobPatternsForDependencies(__dirname),
|
||||||
|
// Resolve the classes used in @nx/graph components
|
||||||
|
// TODO: make a decision on whether this is really the best approach, or if precompiling and deduplicating the classes would be better
|
||||||
|
path.join(
|
||||||
|
workspaceRoot,
|
||||||
|
'node_modules/@nx/graph/**/*.{js,ts,jsx,tsx,html}'
|
||||||
|
),
|
||||||
],
|
],
|
||||||
darkMode: 'class', // or 'media' or 'class'
|
darkMode: 'class', // or 'media' or 'class'
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useRouteConstructor } from '@nx/graph/shared';
|
import { useRouteConstructor } from '@nx/graph/shared';
|
||||||
import { ThemePanel } from '@nx/graph/ui-theme';
|
import { ThemePanel } from '@nx/graph-internal/ui-theme';
|
||||||
|
|
||||||
export function ProjectDetailsHeader() {
|
export function ProjectDetailsHeader() {
|
||||||
const routeConstructor = useRouteConstructor();
|
const routeConstructor = useRouteConstructor();
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from '@nx/graph/shared';
|
} from '@nx/graph/shared';
|
||||||
import { Spinner } from '@nx/graph/ui-components';
|
import { Spinner } from '@nx/graph/ui-components';
|
||||||
|
|
||||||
import { ProjectDetails } from '@nx/graph/ui-project-details';
|
import { ProjectDetails } from '@nx/graph-internal/ui-project-details';
|
||||||
import { useCallback, useContext, useEffect } from 'react';
|
import { useCallback, useContext, useEffect } from 'react';
|
||||||
|
|
||||||
interface ProjectDetailsProps {
|
interface ProjectDetailsProps {
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"@nx/react/babel",
|
|
||||||
{
|
|
||||||
"runtime": "automatic",
|
|
||||||
"useBuiltIns": "usage"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": []
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
|
||||||
"ignorePatterns": ["!**/*"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx"],
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
# graph-shared
|
|
||||||
|
|
||||||
This library was generated with [Nx](https://nx.dev).
|
|
||||||
|
|
||||||
## Running unit tests
|
|
||||||
|
|
||||||
Run `nx test graph-shared` to execute the unit tests via [Jest](https://jestjs.io).
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
export default {
|
|
||||||
displayName: 'graph-shared',
|
|
||||||
preset: '../../jest.preset.js',
|
|
||||||
transform: {
|
|
||||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
|
||||||
},
|
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
||||||
};
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "graph-shared",
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"sourceRoot": "graph/shared/src",
|
|
||||||
"projectType": "library",
|
|
||||||
"tags": [],
|
|
||||||
"targets": {}
|
|
||||||
}
|
|
||||||
25
graph/shared/src/globals.d.ts
vendored
25
graph/shared/src/globals.d.ts
vendored
@ -1,25 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ExpandedTaskInputsReponse,
|
|
||||||
ProjectGraphClientResponse,
|
|
||||||
TaskGraphClientResponse,
|
|
||||||
} from 'nx/src/command-line/graph/graph';
|
|
||||||
import { AppConfig } from './lib/app-config';
|
|
||||||
import { ExternalApi } from './lib/external-api';
|
|
||||||
|
|
||||||
export declare global {
|
|
||||||
interface Window {
|
|
||||||
exclude: string[];
|
|
||||||
watch: boolean;
|
|
||||||
localMode: 'serve' | 'build';
|
|
||||||
projectGraphResponse?: ProjectGraphClientResponse;
|
|
||||||
taskGraphResponse?: TaskGraphClientResponse;
|
|
||||||
expandedTaskInputsResponse?: ExpandedTaskInputsReponse;
|
|
||||||
sourceMapsResponse?: Record<string, Record<string, string[]>>;
|
|
||||||
environment: 'dev' | 'watch' | 'release' | 'nx-console';
|
|
||||||
appConfig: AppConfig;
|
|
||||||
useXstateInspect: boolean;
|
|
||||||
externalApi?: ExternalApi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
export * from './lib/external-api';
|
|
||||||
export * from './lib/external-api-service';
|
|
||||||
export * from './lib/use-environment-config';
|
|
||||||
export * from './lib/app-config';
|
|
||||||
export * from './lib/use-route-constructor';
|
|
||||||
export * from './lib/use-poll';
|
|
||||||
export * from './lib/project-graph-data-service/get-project-graph-data-service';
|
|
||||||
export * from './lib/fetch-project-graph';
|
|
||||||
export * from './lib/error-toast';
|
|
||||||
export * from './lib/expanded-targets-provider';
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
export interface AppConfig {
|
|
||||||
showDebugger: boolean;
|
|
||||||
showExperimentalFeatures: boolean;
|
|
||||||
workspaces: WorkspaceData[];
|
|
||||||
defaultWorkspaceId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceData {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
projectGraphUrl: string;
|
|
||||||
taskGraphUrl: string;
|
|
||||||
taskInputsUrl: string;
|
|
||||||
sourceMapsUrl: string;
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import { GraphError } from 'nx/src/command-line/graph/graph';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
|
|
||||||
import {
|
|
||||||
createRef,
|
|
||||||
ForwardedRef,
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useImperativeHandle,
|
|
||||||
useLayoutEffect,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { Transition } from '@headlessui/react';
|
|
||||||
import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
|
|
||||||
import { SetURLSearchParams, useSearchParams } from 'react-router-dom';
|
|
||||||
import { ErrorRenderer, Modal, ModalHandle } from '@nx/graph/ui-components';
|
|
||||||
|
|
||||||
export interface ErrorToastImperativeHandle {
|
|
||||||
closeModal: () => void;
|
|
||||||
openModal: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ErrorToastProps {
|
|
||||||
errors?: GraphError[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorToast = forwardRef(
|
|
||||||
(
|
|
||||||
{ errors }: ErrorToastProps,
|
|
||||||
ref: ForwardedRef<ErrorToastImperativeHandle>
|
|
||||||
) => {
|
|
||||||
const inputsModalRef = createRef<ModalHandle>();
|
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
openModal: () => {
|
|
||||||
inputsModalRef?.current?.openModal();
|
|
||||||
},
|
|
||||||
closeModal: () => {
|
|
||||||
inputsModalRef?.current?.closeModal();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const handleModalOpen = useCallback(() => {
|
|
||||||
if (searchParams.get('show-error') === 'true') return;
|
|
||||||
setSearchParams(
|
|
||||||
(currentSearchParams) => {
|
|
||||||
currentSearchParams.set('show-error', 'true');
|
|
||||||
return currentSearchParams;
|
|
||||||
},
|
|
||||||
{ replace: true, preventScrollReset: true }
|
|
||||||
);
|
|
||||||
}, [setSearchParams, searchParams]);
|
|
||||||
|
|
||||||
const handleModalClose = useCallback(() => {
|
|
||||||
if (!searchParams.get('show-error')) return;
|
|
||||||
setSearchParams(
|
|
||||||
(currentSearchParams) => {
|
|
||||||
currentSearchParams.delete('show-error');
|
|
||||||
return currentSearchParams;
|
|
||||||
},
|
|
||||||
{ replace: true, preventScrollReset: true }
|
|
||||||
);
|
|
||||||
}, [setSearchParams, searchParams]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (searchParams.get('show-error') === 'true') {
|
|
||||||
if (errors && errors.length > 0) {
|
|
||||||
inputsModalRef.current?.openModal();
|
|
||||||
} else {
|
|
||||||
setSearchParams(
|
|
||||||
(currentSearchParams) => {
|
|
||||||
currentSearchParams.delete('show-error');
|
|
||||||
return currentSearchParams;
|
|
||||||
},
|
|
||||||
{ replace: true, preventScrollReset: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [searchParams, inputsModalRef, errors, setSearchParams]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition
|
|
||||||
show={!!errors && errors.length > 0}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-x-0 bottom-0 px-4 py-2 text-center">
|
|
||||||
<div
|
|
||||||
onClick={() => inputsModalRef.current?.openModal()}
|
|
||||||
className="z-50 mx-auto flex w-fit max-w-[75%] cursor-pointer items-center rounded-md bg-red-600 p-4 text-slate-200 shadow-lg"
|
|
||||||
>
|
|
||||||
<ExclamationCircleIcon className="mr-2 inline-block h-6 w-6" />
|
|
||||||
Some project information might be missing. Click to see errors.
|
|
||||||
</div>
|
|
||||||
{errors?.length > 0 && (
|
|
||||||
<Modal
|
|
||||||
title={`Project Graph Errors`}
|
|
||||||
ref={inputsModalRef}
|
|
||||||
onOpen={handleModalOpen}
|
|
||||||
onClose={handleModalClose}
|
|
||||||
>
|
|
||||||
<ErrorRenderer errors={errors ?? []} />
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useRouterHandleModalOpen = (
|
|
||||||
searchParams: URLSearchParams,
|
|
||||||
setSearchParams: SetURLSearchParams
|
|
||||||
) =>
|
|
||||||
useCallback(() => {
|
|
||||||
if (searchParams.get('show-error') === 'true') return;
|
|
||||||
setSearchParams(
|
|
||||||
(currentSearchParams) => {
|
|
||||||
currentSearchParams.set('show-error', 'true');
|
|
||||||
return currentSearchParams;
|
|
||||||
},
|
|
||||||
{ replace: true, preventScrollReset: true }
|
|
||||||
);
|
|
||||||
}, [setSearchParams, searchParams]);
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { createContext, useState } from 'react';
|
|
||||||
|
|
||||||
export const ExpandedTargetsContext = createContext<{
|
|
||||||
expandedTargets?: string[];
|
|
||||||
setExpandedTargets?: (expandedTargets: string[]) => void;
|
|
||||||
toggleTarget?: (targetName: string) => void;
|
|
||||||
collapseAllTargets?: () => void;
|
|
||||||
}>({});
|
|
||||||
|
|
||||||
export const ExpandedTargetsProvider = ({
|
|
||||||
children,
|
|
||||||
initialExpanededTargets = [],
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
initialExpanededTargets?: string[];
|
|
||||||
}) => {
|
|
||||||
const [expandedTargets, setExpandedTargets] = useState<string[]>(
|
|
||||||
initialExpanededTargets
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleTarget = (targetName: string) => {
|
|
||||||
setExpandedTargets((prevExpandedTargets) => {
|
|
||||||
if (prevExpandedTargets.includes(targetName)) {
|
|
||||||
return prevExpandedTargets.filter((name) => name !== targetName);
|
|
||||||
}
|
|
||||||
return [...prevExpandedTargets, targetName];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const collapseAllTargets = () => {
|
|
||||||
setExpandedTargets([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ExpandedTargetsContext.Provider
|
|
||||||
value={{
|
|
||||||
expandedTargets,
|
|
||||||
setExpandedTargets,
|
|
||||||
toggleTarget,
|
|
||||||
collapseAllTargets,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ExpandedTargetsContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
let externalApiService: ExternalApiService | null = null;
|
|
||||||
|
|
||||||
export function getExternalApiService() {
|
|
||||||
if (!externalApiService) {
|
|
||||||
externalApiService = new ExternalApiService();
|
|
||||||
}
|
|
||||||
|
|
||||||
return externalApiService;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExternalApiService {
|
|
||||||
private subscribers: Set<(event: { type: string; payload?: any }) => void> =
|
|
||||||
new Set();
|
|
||||||
|
|
||||||
postEvent(event: { type: string; payload?: any }) {
|
|
||||||
this.subscribers.forEach((subscriber) => {
|
|
||||||
subscriber(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(callback: (event: { type: string; payload: any }) => void) {
|
|
||||||
this.subscribers.add(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ProjectGraphClientResponse,
|
|
||||||
TaskGraphClientResponse,
|
|
||||||
} from 'nx/src/command-line/graph/graph';
|
|
||||||
|
|
||||||
export abstract class ExternalApi {
|
|
||||||
abstract openProjectDetails(projectName: string, targetName?: string): void;
|
|
||||||
|
|
||||||
abstract focusProject(projectName: string): void;
|
|
||||||
|
|
||||||
abstract toggleSelectProject(projectName: string): void;
|
|
||||||
|
|
||||||
abstract selectAllProjects(): void;
|
|
||||||
|
|
||||||
abstract showAffectedProjects(): void;
|
|
||||||
|
|
||||||
abstract focusTarget(projectName: string, targetName: string): void;
|
|
||||||
|
|
||||||
abstract selectAllTargetsByName(targetName: string): void;
|
|
||||||
|
|
||||||
abstract enableExperimentalFeatures(): void;
|
|
||||||
|
|
||||||
abstract disableExperimentalFeatures(): void;
|
|
||||||
|
|
||||||
loadProjectGraph:
|
|
||||||
| ((url: string) => Promise<ProjectGraphClientResponse>)
|
|
||||||
| null = null;
|
|
||||||
loadTaskGraph: ((url: string) => Promise<TaskGraphClientResponse>) | null =
|
|
||||||
null;
|
|
||||||
loadExpandedTaskInputs:
|
|
||||||
| ((taskId: string) => Promise<Record<string, Record<string, string[]>>>)
|
|
||||||
| null = null;
|
|
||||||
loadSourceMaps:
|
|
||||||
| ((url: string) => Promise<Record<string, Record<string, string[]>>>)
|
|
||||||
| null = null;
|
|
||||||
|
|
||||||
graphInteractionEventListener:
|
|
||||||
| ((event: { type: string; payload: any }) => void | undefined)
|
|
||||||
| null = null;
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { Params } from 'react-router-dom';
|
|
||||||
import { ProjectGraphService } from './project-graph-data-service/get-project-graph-data-service';
|
|
||||||
import { AppConfig } from './app-config';
|
|
||||||
|
|
||||||
export async function fetchProjectGraph(
|
|
||||||
projectGraphService: ProjectGraphService,
|
|
||||||
params: Readonly<Params<string>>,
|
|
||||||
appConfig: AppConfig
|
|
||||||
) {
|
|
||||||
const selectedWorkspaceId =
|
|
||||||
params.selectedWorkspaceId ?? appConfig.defaultWorkspaceId;
|
|
||||||
|
|
||||||
const projectInfo = appConfig.workspaces.find(
|
|
||||||
(graph) => graph.id === selectedWorkspaceId
|
|
||||||
);
|
|
||||||
|
|
||||||
return await projectGraphService.getProjectGraph(projectInfo.projectGraphUrl);
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ProjectGraphClientResponse,
|
|
||||||
TaskGraphClientResponse,
|
|
||||||
} from 'nx/src/command-line/graph/graph';
|
|
||||||
import { ProjectGraphService } from './get-project-graph-data-service';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
|
|
||||||
export class FetchProjectGraphService implements ProjectGraphService {
|
|
||||||
private taskInputsUrl: string;
|
|
||||||
|
|
||||||
async getHash(): Promise<string> {
|
|
||||||
const request = new Request('currentHash', { mode: 'no-cors' });
|
|
||||||
|
|
||||||
const response = await fetch(request);
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectGraph(url: string): Promise<ProjectGraphClientResponse> {
|
|
||||||
const request = new Request(url, { mode: 'no-cors' });
|
|
||||||
|
|
||||||
const response = await fetch(request);
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
|
||||||
const request = new Request(url, { mode: 'no-cors' });
|
|
||||||
|
|
||||||
const response = await fetch(request);
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSourceMaps(
|
|
||||||
url: string
|
|
||||||
): Promise<Record<string, Record<string, string[]>>> {
|
|
||||||
const request = new Request(url, { mode: 'no-cors' });
|
|
||||||
|
|
||||||
const response = await fetch(request);
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTaskInputsUrl(url: string) {
|
|
||||||
this.taskInputsUrl = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getExpandedTaskInputs(
|
|
||||||
taskId: string
|
|
||||||
): Promise<Record<string, string[]>> {
|
|
||||||
if (!this.taskInputsUrl) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const request = new Request(`${this.taskInputsUrl}?taskId=${taskId}`, {
|
|
||||||
mode: 'no-cors',
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(request);
|
|
||||||
return (await response.json())[taskId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { FetchProjectGraphService } from './fetch-project-graph-service';
|
|
||||||
import { LocalProjectGraphService } from './local-project-graph-service';
|
|
||||||
import { MockProjectGraphService } from './mock-project-graph-service';
|
|
||||||
import { NxConsoleProjectGraphService } from './nx-console-project-graph-service';
|
|
||||||
|
|
||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ProjectGraphClientResponse,
|
|
||||||
TaskGraphClientResponse,
|
|
||||||
} from 'nx/src/command-line/graph/graph';
|
|
||||||
|
|
||||||
let projectGraphService: ProjectGraphService;
|
|
||||||
|
|
||||||
export interface ProjectGraphService {
|
|
||||||
getHash: () => Promise<string>;
|
|
||||||
getProjectGraph: (url: string) => Promise<ProjectGraphClientResponse>;
|
|
||||||
getTaskGraph: (url: string) => Promise<TaskGraphClientResponse>;
|
|
||||||
setTaskInputsUrl?: (url: string) => void;
|
|
||||||
getExpandedTaskInputs?: (taskId: string) => Promise<Record<string, string[]>>;
|
|
||||||
getSourceMaps?: (
|
|
||||||
url: string
|
|
||||||
) => Promise<Record<string, Record<string, string[]>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProjectGraphDataService() {
|
|
||||||
if (projectGraphService === undefined) {
|
|
||||||
if (window.environment === 'dev') {
|
|
||||||
projectGraphService = new FetchProjectGraphService();
|
|
||||||
} else if (window.environment === 'watch') {
|
|
||||||
projectGraphService = new MockProjectGraphService();
|
|
||||||
} else if (window.environment === 'nx-console') {
|
|
||||||
projectGraphService = new NxConsoleProjectGraphService();
|
|
||||||
} else if (window.environment === 'release') {
|
|
||||||
if (window.localMode === 'build') {
|
|
||||||
projectGraphService = new LocalProjectGraphService();
|
|
||||||
} else {
|
|
||||||
projectGraphService = new FetchProjectGraphService();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return projectGraphService;
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ProjectGraphClientResponse,
|
|
||||||
TaskGraphClientResponse,
|
|
||||||
} from 'nx/src/command-line/graph/graph';
|
|
||||||
import { ProjectGraphService } from './get-project-graph-data-service';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
|
|
||||||
export class LocalProjectGraphService implements ProjectGraphService {
|
|
||||||
async getHash(): Promise<string> {
|
|
||||||
return new Promise((resolve) => resolve('some-hash'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectGraph(url: string): Promise<ProjectGraphClientResponse> {
|
|
||||||
return new Promise((resolve) => resolve(window.projectGraphResponse));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
|
||||||
return new Promise((resolve) => resolve(window.taskGraphResponse));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getExpandedTaskInputs(
|
|
||||||
taskId: string
|
|
||||||
): Promise<Record<string, string[]>> {
|
|
||||||
return new Promise((resolve) =>
|
|
||||||
resolve(window.expandedTaskInputsResponse[taskId])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSourceMaps(
|
|
||||||
url: string
|
|
||||||
): Promise<Record<string, Record<string, string[]>>> {
|
|
||||||
return new Promise((resolve) => resolve(window.sourceMapsResponse));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ProjectGraphDependency,
|
|
||||||
ProjectGraphProjectNode,
|
|
||||||
} from '@nx/devkit';
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ProjectGraphClientResponse,
|
|
||||||
TaskGraphClientResponse,
|
|
||||||
} from 'nx/src/command-line/graph/graph';
|
|
||||||
import { ProjectGraphService } from './get-project-graph-data-service';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
|
|
||||||
export class MockProjectGraphService implements ProjectGraphService {
|
|
||||||
private projectGraphsResponse: ProjectGraphClientResponse = {
|
|
||||||
hash: '79054025255fb1a26e4bc422aef54eb4',
|
|
||||||
layout: {
|
|
||||||
appsDir: 'apps',
|
|
||||||
libsDir: 'libs',
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'existing-app-1',
|
|
||||||
type: 'app',
|
|
||||||
data: {
|
|
||||||
root: 'apps/app1',
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'existing-lib-1',
|
|
||||||
type: 'lib',
|
|
||||||
data: {
|
|
||||||
root: 'libs/lib1',
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dependencies: {
|
|
||||||
'existing-app-1': [
|
|
||||||
{
|
|
||||||
source: 'existing-app-1',
|
|
||||||
target: 'existing-lib-1',
|
|
||||||
type: 'static',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'existing-lib-1': [],
|
|
||||||
},
|
|
||||||
fileMap: {
|
|
||||||
'existing-app-1': [
|
|
||||||
{
|
|
||||||
file: 'some/file.ts',
|
|
||||||
hash: 'ecccd8481d2e5eae0e59928be1bc4c2d071729d7',
|
|
||||||
deps: ['existing-lib-1'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'exiting-lib-1': [],
|
|
||||||
},
|
|
||||||
affected: [],
|
|
||||||
focus: null,
|
|
||||||
exclude: [],
|
|
||||||
groupByFolder: false,
|
|
||||||
isPartial: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
private taskGraphsResponse: TaskGraphClientResponse = {
|
|
||||||
taskGraphs: {},
|
|
||||||
errors: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(updateFrequency: number = 5000) {
|
|
||||||
setInterval(() => this.updateResponse(), updateFrequency);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getHash(): Promise<string> {
|
|
||||||
return new Promise((resolve) => resolve(this.projectGraphsResponse.hash));
|
|
||||||
}
|
|
||||||
|
|
||||||
getProjectGraph(url: string): Promise<ProjectGraphClientResponse> {
|
|
||||||
return new Promise((resolve) => resolve(this.projectGraphsResponse));
|
|
||||||
}
|
|
||||||
|
|
||||||
getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
|
||||||
return new Promise((resolve) => resolve(this.taskGraphsResponse));
|
|
||||||
}
|
|
||||||
|
|
||||||
getSourceMaps(
|
|
||||||
url: string
|
|
||||||
): Promise<Record<string, Record<string, string[]>>> {
|
|
||||||
return new Promise((resolve) => resolve({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private createNewProject(): ProjectGraphProjectNode {
|
|
||||||
const type = Math.random() > 0.25 ? 'lib' : 'app';
|
|
||||||
const name = `${type}-${this.projectGraphsResponse.projects.length + 1}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
data: {
|
|
||||||
root: type === 'app' ? `apps/${name}` : `libs/${name}`,
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateResponse() {
|
|
||||||
const newProject = this.createNewProject();
|
|
||||||
const libProjects = this.projectGraphsResponse.projects.filter(
|
|
||||||
(project) => project.type === 'lib'
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetDependency =
|
|
||||||
libProjects[Math.floor(Math.random() * libProjects.length)];
|
|
||||||
const newDependency: ProjectGraphDependency[] = [
|
|
||||||
{
|
|
||||||
source: newProject.name,
|
|
||||||
target: targetDependency.name,
|
|
||||||
type: 'static',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
this.projectGraphsResponse = {
|
|
||||||
...this.projectGraphsResponse,
|
|
||||||
projects: [...this.projectGraphsResponse.projects, newProject],
|
|
||||||
dependencies: {
|
|
||||||
...this.projectGraphsResponse.dependencies,
|
|
||||||
[newProject.name]: newDependency,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ProjectGraphClientResponse,
|
|
||||||
TaskGraphClientResponse,
|
|
||||||
} from 'nx/src/command-line/graph/graph';
|
|
||||||
import { ProjectGraphService } from './get-project-graph-data-service';
|
|
||||||
|
|
||||||
export class NxConsoleProjectGraphService implements ProjectGraphService {
|
|
||||||
async getHash(): Promise<string> {
|
|
||||||
return new Promise((resolve) => resolve('some-hash'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectGraph(url: string): Promise<ProjectGraphClientResponse> {
|
|
||||||
return await window.externalApi.loadProjectGraph?.(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTaskGraph(url: string): Promise<TaskGraphClientResponse> {
|
|
||||||
return await window.externalApi.loadTaskGraph?.(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getExpandedTaskInputs(
|
|
||||||
taskId: string
|
|
||||||
): Promise<Record<string, string[]>> {
|
|
||||||
const res = await window.externalApi.loadExpandedTaskInputs?.(taskId);
|
|
||||||
return res ? res[taskId] : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSourceMaps(
|
|
||||||
url: string
|
|
||||||
): Promise<Record<string, Record<string, string[]>>> {
|
|
||||||
return await window.externalApi.loadSourceMaps?.(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
import { useRef } from 'react';
|
|
||||||
import { AppConfig } from './app-config';
|
|
||||||
|
|
||||||
export function useEnvironmentConfig(): {
|
|
||||||
exclude: string[];
|
|
||||||
watch: boolean;
|
|
||||||
localMode: 'serve' | 'build';
|
|
||||||
projectGraphResponse?: ProjectGraphClientResponse;
|
|
||||||
environment: 'dev' | 'watch' | 'release' | 'nx-console' | 'docs';
|
|
||||||
appConfig: AppConfig;
|
|
||||||
useXstateInspect: boolean;
|
|
||||||
} {
|
|
||||||
const environmentConfig = useRef(getEnvironmentConfig());
|
|
||||||
|
|
||||||
return environmentConfig.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEnvironmentConfig() {
|
|
||||||
return {
|
|
||||||
exclude: window.exclude,
|
|
||||||
watch: window.watch,
|
|
||||||
localMode: window.localMode,
|
|
||||||
projectGraphResponse: window.projectGraphResponse,
|
|
||||||
// If this was not built into JS or HTML, then it is rendered on docs (nx.dev).
|
|
||||||
environment: window.environment ?? ('docs' as const),
|
|
||||||
appConfig: {
|
|
||||||
...window.appConfig,
|
|
||||||
showExperimentalFeatures:
|
|
||||||
localStorage.getItem('showExperimentalFeatures') === 'true'
|
|
||||||
? true
|
|
||||||
: window.appConfig?.showExperimentalFeatures,
|
|
||||||
},
|
|
||||||
useXstateInspect: window.useXstateInspect,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
export const usePoll = (
|
|
||||||
callback: () => Promise<void>,
|
|
||||||
delay: number,
|
|
||||||
condition: boolean
|
|
||||||
) => {
|
|
||||||
const savedCallback = useRef(() => Promise.resolve());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (condition) {
|
|
||||||
savedCallback.current = callback;
|
|
||||||
}
|
|
||||||
}, [callback, condition]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!condition) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let timeoutId: NodeJS.Timeout;
|
|
||||||
|
|
||||||
async function callTickAfterDelay() {
|
|
||||||
await savedCallback.current();
|
|
||||||
if (delay !== null) {
|
|
||||||
timeoutId = setTimeout(callTickAfterDelay, delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callTickAfterDelay();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [delay, condition]);
|
|
||||||
};
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { To, useParams, useSearchParams } from 'react-router-dom';
|
|
||||||
import { getEnvironmentConfig } from './use-environment-config';
|
|
||||||
|
|
||||||
export const useRouteConstructor = (): ((
|
|
||||||
to: To,
|
|
||||||
retainSearchParams: boolean,
|
|
||||||
searchParamsKeysToOmit?: string[]
|
|
||||||
) => To) => {
|
|
||||||
const { environment } = getEnvironmentConfig();
|
|
||||||
const { selectedWorkspaceId } = useParams();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
return (
|
|
||||||
to: To,
|
|
||||||
retainSearchParams: boolean = true,
|
|
||||||
searchParamsKeysToOmit: string[] = []
|
|
||||||
) => {
|
|
||||||
if (searchParamsKeysToOmit?.length) {
|
|
||||||
searchParamsKeysToOmit.forEach((key) => {
|
|
||||||
searchParams.delete(key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let pathname = '';
|
|
||||||
|
|
||||||
if (typeof to === 'object') {
|
|
||||||
if (environment === 'dev') {
|
|
||||||
pathname = `/${selectedWorkspaceId}${to.pathname}`;
|
|
||||||
} else {
|
|
||||||
pathname = to.pathname;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...to,
|
|
||||||
pathname,
|
|
||||||
search: to.search
|
|
||||||
? to.search.toString()
|
|
||||||
: retainSearchParams
|
|
||||||
? searchParams.toString()
|
|
||||||
: '',
|
|
||||||
};
|
|
||||||
} else if (typeof to === 'string') {
|
|
||||||
if (environment === 'dev') {
|
|
||||||
pathname = `/${selectedWorkspaceId}${to}`;
|
|
||||||
} else {
|
|
||||||
pathname = to;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
pathname,
|
|
||||||
search: retainSearchParams ? searchParams.toString() : '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"allowJs": false,
|
|
||||||
"esModuleInterop": false,
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"files": [],
|
|
||||||
"include": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.lib.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.spec.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extends": "../../tsconfig.base.json"
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"types": [
|
|
||||||
"node",
|
|
||||||
"@nx/react/typings/cssmodule.d.ts",
|
|
||||||
"@nx/react/typings/image.d.ts"
|
|
||||||
],
|
|
||||||
"lib": ["dom"]
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"jest.config.ts",
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.test.ts",
|
|
||||||
"src/**/*.spec.tsx",
|
|
||||||
"src/**/*.test.tsx",
|
|
||||||
"src/**/*.spec.js",
|
|
||||||
"src/**/*.test.js",
|
|
||||||
"src/**/*.spec.jsx",
|
|
||||||
"src/**/*.test.jsx"
|
|
||||||
],
|
|
||||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"module": "commonjs",
|
|
||||||
"types": ["jest", "node"]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"jest.config.ts",
|
|
||||||
"src/**/*.test.ts",
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.test.tsx",
|
|
||||||
"src/**/*.spec.tsx",
|
|
||||||
"src/**/*.test.js",
|
|
||||||
"src/**/*.spec.js",
|
|
||||||
"src/**/*.test.jsx",
|
|
||||||
"src/**/*.spec.jsx",
|
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"@nx/react/babel",
|
|
||||||
{
|
|
||||||
"runtime": "automatic",
|
|
||||||
"useBuiltIns": "usage"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": []
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
|
||||||
"ignorePatterns": ["!**/*"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx"],
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
stories: ['../src/lib/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
|
|
||||||
addons: ['@storybook/addon-essentials', '@nx/react/plugins/storybook'],
|
|
||||||
framework: {
|
|
||||||
name: '@storybook/react-webpack5',
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
docs: {},
|
|
||||||
};
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import 'graph/client/.storybook/tailwind-imports.css';
|
|
||||||
|
|
||||||
export const parameters = {};
|
|
||||||
export const tags = ['autodocs'];
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
# graph-ui-components
|
|
||||||
|
|
||||||
This library was generated with [Nx](https://nx.dev).
|
|
||||||
|
|
||||||
## Running unit tests
|
|
||||||
|
|
||||||
Run `nx test graph-ui-components` to execute the unit tests via [Jest](https://jestjs.io).
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
export default {
|
|
||||||
displayName: 'graph-ui-components',
|
|
||||||
preset: '../../jest.preset.js',
|
|
||||||
transform: {
|
|
||||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
|
||||||
},
|
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
||||||
coverageDirectory: '../../coverage/graph/ui-graph',
|
|
||||||
};
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {
|
|
||||||
config: './graph/ui-components/tailwind.config.js',
|
|
||||||
},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "graph-ui-components",
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"sourceRoot": "graph/ui-components/src",
|
|
||||||
"projectType": "library",
|
|
||||||
"tags": [],
|
|
||||||
"targets": {
|
|
||||||
"storybook": {
|
|
||||||
"executor": "@nx/storybook:storybook",
|
|
||||||
"options": {
|
|
||||||
"port": 4400,
|
|
||||||
"configDir": "graph/ui-components/.storybook"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"quiet": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"build-storybook": {
|
|
||||||
"executor": "@nx/storybook:build",
|
|
||||||
"outputs": ["{options.outputDir}"],
|
|
||||||
"options": {
|
|
||||||
"configDir": "graph/ui-components/.storybook",
|
|
||||||
"outputDir": "dist/storybook/graph-ui-components"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"quiet": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export * from './lib/copy-to-clipboard-button';
|
|
||||||
export * from './lib/debounced-text-input';
|
|
||||||
export * from './lib/tag';
|
|
||||||
export * from './lib/dropdown';
|
|
||||||
export * from './lib/spinner';
|
|
||||||
export * from './lib/error-renderer';
|
|
||||||
export * from './lib/modal';
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import {
|
|
||||||
CopyToClipboardButton,
|
|
||||||
CopyToClipboardButtonProps,
|
|
||||||
} from './copy-to-clipboard-button';
|
|
||||||
|
|
||||||
const meta: Meta<typeof CopyToClipboardButton> = {
|
|
||||||
component: CopyToClipboardButton,
|
|
||||||
title: 'CopyToClipboardButton',
|
|
||||||
};
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof CopyToClipboardButton>;
|
|
||||||
|
|
||||||
export const Simple: Story = {
|
|
||||||
args: {
|
|
||||||
text: 'Hello, world!',
|
|
||||||
tooltipAlignment: 'left',
|
|
||||||
} as CopyToClipboardButtonProps,
|
|
||||||
};
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
// @ts-ignore
|
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
|
||||||
import { JSX, ReactNode, useEffect, useState } from 'react';
|
|
||||||
import {
|
|
||||||
ClipboardDocumentCheckIcon,
|
|
||||||
ClipboardDocumentIcon,
|
|
||||||
} from '@heroicons/react/24/outline';
|
|
||||||
|
|
||||||
export interface CopyToClipboardButtonProps {
|
|
||||||
text: string;
|
|
||||||
tooltipText?: string;
|
|
||||||
tooltipAlignment?: 'left' | 'right';
|
|
||||||
className?: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CopyToClipboardButton({
|
|
||||||
text,
|
|
||||||
tooltipAlignment,
|
|
||||||
tooltipText,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
}: CopyToClipboardButtonProps) {
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!copied) return;
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
setCopied(false);
|
|
||||||
}, 3000);
|
|
||||||
return () => clearTimeout(t);
|
|
||||||
}, [copied]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CopyToClipboard
|
|
||||||
text={text}
|
|
||||||
onCopy={() => {
|
|
||||||
setCopied(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-tooltip={tooltipText ? tooltipText : false}
|
|
||||||
data-tooltip-align-right={tooltipAlignment === 'right'}
|
|
||||||
data-tooltip-align-left={tooltipAlignment === 'left'}
|
|
||||||
className={className}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<ClipboardDocumentCheckIcon className="inline h-5 w-5 text-blue-500 dark:text-sky-500" />
|
|
||||||
) : (
|
|
||||||
<ClipboardDocumentIcon className="inline h-5 w-5" />
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
</CopyToClipboard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { DebouncedTextInput } from './debounced-text-input';
|
|
||||||
|
|
||||||
const meta: Meta<typeof DebouncedTextInput> = {
|
|
||||||
component: DebouncedTextInput,
|
|
||||||
title: 'Shared/DebouncedTextInput',
|
|
||||||
argTypes: {
|
|
||||||
resetTextFilter: {
|
|
||||||
action: 'resetTextFilter',
|
|
||||||
},
|
|
||||||
updateTextFilter: {
|
|
||||||
action: 'updateTextFilter',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof DebouncedTextInput>;
|
|
||||||
|
|
||||||
export const Primary: Story = {
|
|
||||||
args: {
|
|
||||||
initialText: '',
|
|
||||||
placeholderText: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import { KeyboardEvent, useEffect, useState } from 'react';
|
|
||||||
import { useDebounce } from './use-debounce';
|
|
||||||
import { BackspaceIcon, FunnelIcon } from '@heroicons/react/24/outline';
|
|
||||||
|
|
||||||
export interface DebouncedTextInputProps {
|
|
||||||
initialText: string;
|
|
||||||
placeholderText: string;
|
|
||||||
resetTextFilter: () => void;
|
|
||||||
updateTextFilter: (textFilter: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DebouncedTextInput({
|
|
||||||
initialText,
|
|
||||||
placeholderText,
|
|
||||||
resetTextFilter,
|
|
||||||
updateTextFilter,
|
|
||||||
}: DebouncedTextInputProps) {
|
|
||||||
const [currentTextFilter, setCurrentTextFilter] = useState(initialText ?? '');
|
|
||||||
|
|
||||||
const [debouncedValue, setDebouncedValue] = useDebounce(
|
|
||||||
currentTextFilter,
|
|
||||||
500
|
|
||||||
);
|
|
||||||
|
|
||||||
function onTextFilterKeyUp(event: KeyboardEvent<HTMLInputElement>) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
onTextInputChange(event.currentTarget.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTextInputChange(change: string) {
|
|
||||||
if (change === '') {
|
|
||||||
setCurrentTextFilter('');
|
|
||||||
setDebouncedValue('');
|
|
||||||
|
|
||||||
resetTextFilter();
|
|
||||||
} else {
|
|
||||||
setCurrentTextFilter(change);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetClicked() {
|
|
||||||
setCurrentTextFilter('');
|
|
||||||
setDebouncedValue('');
|
|
||||||
|
|
||||||
resetTextFilter();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (debouncedValue !== '') {
|
|
||||||
updateTextFilter(debouncedValue);
|
|
||||||
}
|
|
||||||
}, [debouncedValue, updateTextFilter]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className="group relative flex rounded-md shadow-sm"
|
|
||||||
onSubmit={(event) => event.preventDefault()}
|
|
||||||
>
|
|
||||||
<span className="inline-flex items-center rounded-l-md border border-r-0 border-slate-300 bg-slate-50 p-2 dark:border-slate-900 dark:bg-slate-800">
|
|
||||||
<FunnelIcon className="h-4 w-4"></FunnelIcon>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={`block w-full flex-1 rounded-none rounded-r-md border border-slate-300 bg-white p-1.5 font-light text-slate-400 placeholder:font-light placeholder:text-slate-400 dark:border-slate-900 dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700`}
|
|
||||||
placeholder={placeholderText}
|
|
||||||
data-cy="textFilterInput"
|
|
||||||
name="filter"
|
|
||||||
value={currentTextFilter}
|
|
||||||
onKeyUp={onTextFilterKeyUp}
|
|
||||||
onChange={(event) => onTextInputChange(event.currentTarget.value)}
|
|
||||||
></input>
|
|
||||||
{currentTextFilter.length > 0 ? (
|
|
||||||
<button
|
|
||||||
data-cy="textFilterReset"
|
|
||||||
type="reset"
|
|
||||||
onClick={resetClicked}
|
|
||||||
className="absolute right-1 top-1 inline-block rounded-md bg-slate-50 p-1 text-slate-400 dark:bg-slate-800"
|
|
||||||
>
|
|
||||||
<BackspaceIcon className="h-5 w-5"></BackspaceIcon>
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { Dropdown } from './dropdown';
|
|
||||||
|
|
||||||
const meta: Meta<typeof Dropdown> = {
|
|
||||||
component: Dropdown,
|
|
||||||
title: 'Shared/Dropdown',
|
|
||||||
argTypes: {
|
|
||||||
onChange: { action: 'onChange' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof Dropdown>;
|
|
||||||
|
|
||||||
export const Primary: Story = {
|
|
||||||
render: () => (
|
|
||||||
<Dropdown {...{}}>
|
|
||||||
<option value="Option 1">Option 1</option>
|
|
||||||
<option value="Option 2">Option 2</option>
|
|
||||||
</Dropdown>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
/* eslint-disable-next-line */
|
|
||||||
import React, { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export type DropdownProps = {
|
|
||||||
children: ReactNode[];
|
|
||||||
} & React.HTMLAttributes<HTMLSelectElement>;
|
|
||||||
|
|
||||||
export function Dropdown(props: DropdownProps) {
|
|
||||||
const { className, children, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
className={`form-select flex items-center rounded-md rounded-md border border-slate-300 bg-white py-2 pl-4 pr-8 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300 hover:dark:bg-slate-700 ${className}`}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import { GraphError } from 'nx/src/command-line/graph/graph';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
export function ErrorRenderer({ errors }: { errors: GraphError[] }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{errors.map((error, index) => {
|
|
||||||
const errorHeading =
|
|
||||||
error.pluginName && error.name
|
|
||||||
? `${error.name} - ${error.pluginName}`
|
|
||||||
: error.name ?? error.message;
|
|
||||||
const fileSpecifier =
|
|
||||||
isCauseWithLocation(error.cause) && error.cause.errors.length === 1
|
|
||||||
? `${error.fileName}:${error.cause.errors[0].location.line}:${error.cause.errors[0].location.column}`
|
|
||||||
: error.fileName;
|
|
||||||
return (
|
|
||||||
<div className="overflow-hidden pb-4">
|
|
||||||
<span className="inline-flex max-w-full flex-col break-words font-bold font-normal text-gray-900 md:inline dark:text-slate-200">
|
|
||||||
<span>{errorHeading}</span>
|
|
||||||
{fileSpecifier && (
|
|
||||||
<span className="hidden px-1 md:inline">-</span>
|
|
||||||
)}
|
|
||||||
<span>{fileSpecifier}</span>
|
|
||||||
</span>
|
|
||||||
<pre className="overflow-x-scroll pl-4 pt-3">
|
|
||||||
{isCauseWithErrors(error.cause) &&
|
|
||||||
error.cause.errors.length === 1 ? (
|
|
||||||
<div>
|
|
||||||
{error.message} <br />
|
|
||||||
{error.cause.errors[0].text}{' '}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>{error.stack}</div>
|
|
||||||
)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCauseWithLocation(cause: unknown): cause is {
|
|
||||||
errors: {
|
|
||||||
location: {
|
|
||||||
column: number;
|
|
||||||
line: number;
|
|
||||||
};
|
|
||||||
text: string;
|
|
||||||
}[];
|
|
||||||
} {
|
|
||||||
return (
|
|
||||||
isCauseWithErrors(cause) &&
|
|
||||||
(cause as any).errors[0].location &&
|
|
||||||
(cause as any).errors[0].location.column &&
|
|
||||||
(cause as any).errors[0].location.line
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCauseWithErrors(
|
|
||||||
cause: unknown
|
|
||||||
): cause is { errors: { text: string }[] } {
|
|
||||||
return cause && (cause as any).errors && (cause as any).errors[0].text;
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
// component from https://tailwindui.com/components/application-ui/overlays/dialogs
|
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
|
||||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
|
||||||
import {
|
|
||||||
ForwardedRef,
|
|
||||||
Fragment,
|
|
||||||
ReactNode,
|
|
||||||
forwardRef,
|
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
export interface ModalProps {
|
|
||||||
children: ReactNode;
|
|
||||||
title: string;
|
|
||||||
onOpen?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModalHandle {
|
|
||||||
openModal: () => void;
|
|
||||||
closeModal: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Modal = forwardRef(
|
|
||||||
(
|
|
||||||
{ children, title, onOpen, onClose }: ModalProps,
|
|
||||||
ref: ForwardedRef<ModalHandle>
|
|
||||||
) => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
onOpen?.();
|
|
||||||
} else {
|
|
||||||
onClose?.();
|
|
||||||
}
|
|
||||||
}, [open, onOpen, onClose]);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
closeModal: () => {
|
|
||||||
setOpen(false);
|
|
||||||
},
|
|
||||||
openModal: () => {
|
|
||||||
setOpen(true);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={open} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={setOpen}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
|
||||||
<div className="flex min-h-full items-end items-center justify-center p-4 text-center sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel
|
|
||||||
className={`relative mx-4 my-8 w-full transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all md:max-w-5xl xl:max-w-7xl dark:bg-slate-900
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between rounded-t border-b bg-white p-2 pb-1 md:p-4 md:pb-2 dark:border-gray-600 dark:bg-slate-900">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-gray-900 dark:text-slate-200"
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Dialog.Title>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white"
|
|
||||||
data-modal-hide="default-modal"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<XMarkIcon />
|
|
||||||
<span className="sr-only">Close modal</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-[90vh] overflow-y-auto bg-white p-2 md:p-4 dark:bg-slate-900">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { Spinner } from './spinner';
|
|
||||||
|
|
||||||
const meta: Meta<typeof Spinner> = {
|
|
||||||
component: Spinner,
|
|
||||||
title: 'Shared/Spinner',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof Spinner>;
|
|
||||||
|
|
||||||
export const Primary: Story = {
|
|
||||||
args: {
|
|
||||||
className: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* Spinner component from https://tailwindcss.com/docs/animation#spin
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export type SpinnerProps = React.SVGProps<SVGSVGElement>;
|
|
||||||
|
|
||||||
export function Spinner({ className, ...rest }: SpinnerProps) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
className={`${className} h-8 w-8 animate-spin`}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
className="opacity-10"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-90"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { Tag } from './tag';
|
|
||||||
|
|
||||||
const meta: Meta<typeof Tag> = {
|
|
||||||
component: Tag,
|
|
||||||
title: 'Shared/Tag',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof Tag>;
|
|
||||||
|
|
||||||
export const Primary: Story = {
|
|
||||||
args: {
|
|
||||||
content: 'tag',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/* eslint-disable-next-line */
|
|
||||||
import React, { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export type TagProps = Partial<{
|
|
||||||
className: string;
|
|
||||||
children: ReactNode | ReactNode[];
|
|
||||||
}> &
|
|
||||||
React.HTMLAttributes<HTMLSpanElement>;
|
|
||||||
|
|
||||||
export function Tag({ className, children, ...rest }: TagProps) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`${className} inline-block rounded-md bg-slate-300 p-2 font-sans text-xs font-semibold uppercase leading-4 tracking-wide text-slate-700`}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import type { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export function useDebounce(
|
|
||||||
value: string,
|
|
||||||
delay: number
|
|
||||||
): [string, Dispatch<SetStateAction<string>>] {
|
|
||||||
// State and setters for debounced value
|
|
||||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
// Update debounced value after delay
|
|
||||||
const handler = setTimeout(() => {
|
|
||||||
setDebouncedValue(value);
|
|
||||||
}, delay);
|
|
||||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
|
||||||
// This is how we prevent debounced value from updating if value is changed ...
|
|
||||||
// .. within the delay period. Timeout gets cleared and restarted.
|
|
||||||
return () => {
|
|
||||||
clearTimeout(handler);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[value, delay] // Only re-call effect if value or delay changes
|
|
||||||
);
|
|
||||||
return [debouncedValue, setDebouncedValue];
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
// nx-ignore-next-line
|
|
||||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
path.join(__dirname, 'src/**/*.{js,ts,jsx,tsx,html}'),
|
|
||||||
...createGlobPatternsForDependencies(__dirname),
|
|
||||||
],
|
|
||||||
darkMode: 'class', // or 'media' or 'class'
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
typography: {
|
|
||||||
DEFAULT: {
|
|
||||||
css: {
|
|
||||||
'code::before': {
|
|
||||||
content: '',
|
|
||||||
},
|
|
||||||
'code::after': {
|
|
||||||
content: '',
|
|
||||||
},
|
|
||||||
'blockquote p:first-of-type::before': {
|
|
||||||
content: '',
|
|
||||||
},
|
|
||||||
'blockquote p:last-of-type::after': {
|
|
||||||
content: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
extend: {
|
|
||||||
translate: ['group-hover'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require('@tailwindcss/typography')],
|
|
||||||
};
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"allowJs": false,
|
|
||||||
"esModuleInterop": false,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true
|
|
||||||
},
|
|
||||||
"files": [],
|
|
||||||
"include": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.lib.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.spec.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.storybook.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extends": "../../tsconfig.base.json"
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"types": ["node"],
|
|
||||||
"lib": ["dom"]
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
|
|
||||||
"../../node_modules/@nx/react/typings/image.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"jest.config.ts",
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.test.ts",
|
|
||||||
"src/**/*.spec.tsx",
|
|
||||||
"src/**/*.test.tsx",
|
|
||||||
"src/**/*.spec.js",
|
|
||||||
"src/**/*.test.js",
|
|
||||||
"src/**/*.spec.jsx",
|
|
||||||
"src/**/*.test.jsx",
|
|
||||||
"**/*.stories.ts",
|
|
||||||
"**/*.stories.js",
|
|
||||||
"**/*.stories.jsx",
|
|
||||||
"**/*.stories.tsx"
|
|
||||||
],
|
|
||||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"module": "commonjs",
|
|
||||||
"types": ["jest", "node"]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"jest.config.ts",
|
|
||||||
"src/**/*.test.ts",
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.test.tsx",
|
|
||||||
"src/**/*.spec.tsx",
|
|
||||||
"src/**/*.test.js",
|
|
||||||
"src/**/*.spec.js",
|
|
||||||
"src/**/*.test.jsx",
|
|
||||||
"src/**/*.spec.jsx",
|
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"outDir": ""
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"../../node_modules/@nx/react/typings/styled-jsx.d.ts",
|
|
||||||
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
|
|
||||||
"../../node_modules/@nx/react/typings/image.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.spec.js",
|
|
||||||
"src/**/*.spec.tsx",
|
|
||||||
"src/**/*.spec.jsx"
|
|
||||||
],
|
|
||||||
"include": [
|
|
||||||
"src/**/*.stories.ts",
|
|
||||||
"src/**/*.stories.js",
|
|
||||||
"src/**/*.stories.jsx",
|
|
||||||
"src/**/*.stories.tsx",
|
|
||||||
"src/**/*.stories.mdx",
|
|
||||||
".storybook/*.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"@nx/react/babel",
|
|
||||||
{
|
|
||||||
"runtime": "automatic",
|
|
||||||
"useBuiltIns": "usage"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": []
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
|
||||||
"ignorePatterns": ["!**/*"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx"],
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
stories: ['../src/lib/**/*.stories.@(mdx|js|jsx|ts|tsx)'],
|
|
||||||
addons: ['@storybook/addon-essentials', '@nx/react/plugins/storybook'],
|
|
||||||
framework: {
|
|
||||||
name: '@storybook/react-webpack5',
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
docs: {},
|
|
||||||
};
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<script>
|
|
||||||
window.exclude = [];
|
|
||||||
window.watch = false;
|
|
||||||
window.environment = 'dev';
|
|
||||||
window.useXstateInspect = false;
|
|
||||||
|
|
||||||
window.appConfig = {
|
|
||||||
showDebugger: true,
|
|
||||||
showExperimentalFeatures: true,
|
|
||||||
workspaces: [
|
|
||||||
{
|
|
||||||
id: 'e2e',
|
|
||||||
label: 'e2e',
|
|
||||||
projectGraphUrl: 'assets/project-graphs/e2e.json',
|
|
||||||
taskGraphUrl: 'assets/task-graphs/e2e.json',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'affected',
|
|
||||||
label: 'affected',
|
|
||||||
projectGraphUrl: 'assets/project-graphs/affected.json',
|
|
||||||
taskGraphUrl: 'assets/task-graphs/affected.json',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultWorkspaceId: 'e2e',
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import './tailwind-imports.css';
|
|
||||||
|
|
||||||
export const parameters = {};
|
|
||||||
export const tags = ['autodocs'];
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
# graph-ui-graph
|
|
||||||
|
|
||||||
This library was generated with [Nx](https://nx.dev).
|
|
||||||
|
|
||||||
## Running unit tests
|
|
||||||
|
|
||||||
Run `nx test graph-ui-graph` to execute the unit tests via [Jest](https://jestjs.io).
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
export default {
|
|
||||||
displayName: 'graph-ui-graph',
|
|
||||||
preset: '../../jest.preset.js',
|
|
||||||
transform: {
|
|
||||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
|
||||||
},
|
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
||||||
coverageDirectory: '../../coverage/graph/ui-graph',
|
|
||||||
};
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {
|
|
||||||
config: path.join(__dirname, 'tailwind.config.js'),
|
|
||||||
},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "graph-ui-graph",
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"sourceRoot": "graph/ui-graph/src",
|
|
||||||
"projectType": "library",
|
|
||||||
"tags": [],
|
|
||||||
"targets": {
|
|
||||||
"storybook": {
|
|
||||||
"executor": "@nx/storybook:storybook",
|
|
||||||
"options": {
|
|
||||||
"port": 4400,
|
|
||||||
"configDir": "graph/ui-graph/.storybook"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"quiet": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"build-storybook": {
|
|
||||||
"executor": "@nx/storybook:build",
|
|
||||||
"outputs": ["{options.outputDir}"],
|
|
||||||
"options": {
|
|
||||||
"configDir": "graph/ui-graph/.storybook",
|
|
||||||
"outputDir": "dist/storybook/graph-ui-graph"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"ci": {
|
|
||||||
"quiet": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from './lib/nx-project-graph-viz';
|
|
||||||
export * from './lib/nx-task-graph-viz';
|
|
||||||
export * from './lib/graph';
|
|
||||||
export * from './lib/tooltip-service';
|
|
||||||
export * from './lib/graph-interaction-events';
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { VirtualElement } from '@floating-ui/react';
|
|
||||||
import { ProjectNodeDataDefinition } from './util-cytoscape/project-node';
|
|
||||||
import { TaskNodeDataDefinition } from './util-cytoscape/task-node';
|
|
||||||
import { ProjectEdgeDataDefinition } from './util-cytoscape';
|
|
||||||
|
|
||||||
interface ProjectNodeClickEvent {
|
|
||||||
type: 'ProjectNodeClick';
|
|
||||||
ref: VirtualElement;
|
|
||||||
id: string;
|
|
||||||
data: ProjectNodeDataDefinition;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskNodeClickEvent {
|
|
||||||
type: 'TaskNodeClick';
|
|
||||||
ref: VirtualElement;
|
|
||||||
id: string;
|
|
||||||
data: TaskNodeDataDefinition;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EdgeClickEvent {
|
|
||||||
type: 'EdgeClick';
|
|
||||||
ref: VirtualElement;
|
|
||||||
id: string;
|
|
||||||
data: ProjectEdgeDataDefinition;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GraphRegeneratedEvent {
|
|
||||||
type: 'GraphRegenerated';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BackgroundClickEvent {
|
|
||||||
type: 'BackgroundClick';
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GraphInteractionEvents =
|
|
||||||
| ProjectNodeClickEvent
|
|
||||||
| EdgeClickEvent
|
|
||||||
| GraphRegeneratedEvent
|
|
||||||
| TaskNodeClickEvent
|
|
||||||
| BackgroundClickEvent;
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
// nx-ignore-next-line
|
|
||||||
import { CollectionReturnValue, use } from 'cytoscape';
|
|
||||||
import cytoscapeDagre from 'cytoscape-dagre';
|
|
||||||
import popper from 'cytoscape-popper';
|
|
||||||
import {
|
|
||||||
GraphPerfReport,
|
|
||||||
ProjectGraphRenderEvents,
|
|
||||||
TaskGraphRenderEvents,
|
|
||||||
} from './interfaces';
|
|
||||||
import { GraphInteractionEvents } from './graph-interaction-events';
|
|
||||||
import { RenderGraph } from './util-cytoscape/render-graph';
|
|
||||||
import { ProjectTraversalGraph } from './util-cytoscape/project-traversal-graph';
|
|
||||||
import { TaskTraversalGraph } from './util-cytoscape/task-traversal.graph';
|
|
||||||
|
|
||||||
export class GraphService {
|
|
||||||
private projectTraversalGraph: ProjectTraversalGraph;
|
|
||||||
private taskTraversalGraph: TaskTraversalGraph;
|
|
||||||
private renderGraph: RenderGraph;
|
|
||||||
|
|
||||||
lastPerformanceReport: GraphPerfReport = {
|
|
||||||
numEdges: 0,
|
|
||||||
numNodes: 0,
|
|
||||||
renderTime: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
private listeners = new Map<
|
|
||||||
number,
|
|
||||||
(event: GraphInteractionEvents) => void
|
|
||||||
>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
container: string | HTMLElement,
|
|
||||||
theme: 'light' | 'dark',
|
|
||||||
public renderMode?: 'nx-console' | 'nx-docs',
|
|
||||||
rankDir: 'TB' | 'LR' = 'TB',
|
|
||||||
public getTaskInputs: (
|
|
||||||
taskId: string
|
|
||||||
) => Promise<Record<string, string[]>> = undefined
|
|
||||||
) {
|
|
||||||
use(cytoscapeDagre);
|
|
||||||
use(popper);
|
|
||||||
|
|
||||||
this.renderGraph = new RenderGraph(container, theme, renderMode, rankDir);
|
|
||||||
|
|
||||||
this.renderGraph.listen((event) => this.broadcast(event));
|
|
||||||
this.projectTraversalGraph = new ProjectTraversalGraph();
|
|
||||||
this.taskTraversalGraph = new TaskTraversalGraph();
|
|
||||||
}
|
|
||||||
|
|
||||||
set theme(theme: 'light' | 'dark') {
|
|
||||||
this.renderGraph.theme = theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
set rankDir(rankDir: 'TB' | 'LR') {
|
|
||||||
this.renderGraph.rankDir = rankDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
listen(callback: (event: GraphInteractionEvents) => void) {
|
|
||||||
const listenerId = this.listeners.size + 1;
|
|
||||||
this.listeners.set(listenerId, callback);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.listeners.delete(listenerId);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcast(event: GraphInteractionEvents) {
|
|
||||||
this.listeners.forEach((callback) => callback(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleProjectEvent(event: ProjectGraphRenderEvents): {
|
|
||||||
selectedProjectNames: string[];
|
|
||||||
perfReport: GraphPerfReport;
|
|
||||||
} {
|
|
||||||
const time = Date.now();
|
|
||||||
|
|
||||||
if (event.type !== 'notifyGraphUpdateGraph') {
|
|
||||||
this.renderGraph.clearFocussedElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
let elementsToSendToRender: CollectionReturnValue;
|
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
case 'notifyGraphInitGraph':
|
|
||||||
this.renderGraph.collapseEdges = event.collapseEdges;
|
|
||||||
this.broadcast({ type: 'GraphRegenerated' });
|
|
||||||
this.projectTraversalGraph.initGraph(
|
|
||||||
event.fileMap,
|
|
||||||
event.projects,
|
|
||||||
event.groupByFolder,
|
|
||||||
event.workspaceLayout,
|
|
||||||
event.dependencies,
|
|
||||||
event.affectedProjects,
|
|
||||||
event.collapseEdges
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphUpdateGraph':
|
|
||||||
this.renderGraph.collapseEdges = event.collapseEdges;
|
|
||||||
this.broadcast({ type: 'GraphRegenerated' });
|
|
||||||
this.projectTraversalGraph.initGraph(
|
|
||||||
event.fileMap,
|
|
||||||
event.projects,
|
|
||||||
event.groupByFolder,
|
|
||||||
event.workspaceLayout,
|
|
||||||
event.dependencies,
|
|
||||||
event.affectedProjects,
|
|
||||||
event.collapseEdges
|
|
||||||
);
|
|
||||||
elementsToSendToRender = this.projectTraversalGraph.setShownProjects(
|
|
||||||
event.selectedProjects.length > 0
|
|
||||||
? event.selectedProjects
|
|
||||||
: this.renderGraph.getCurrentlyShownProjectIds()
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphFocusProject':
|
|
||||||
elementsToSendToRender = this.projectTraversalGraph.focusProject(
|
|
||||||
event.projectName,
|
|
||||||
event.searchDepth
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphFilterProjectsByText':
|
|
||||||
elementsToSendToRender =
|
|
||||||
this.projectTraversalGraph.filterProjectsByText(
|
|
||||||
event.search,
|
|
||||||
event.includeProjectsByPath,
|
|
||||||
event.searchDepth
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphShowProjects':
|
|
||||||
elementsToSendToRender = this.projectTraversalGraph.showProjects(
|
|
||||||
event.projectNames,
|
|
||||||
this.renderGraph.getCurrentlyShownProjectIds()
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphHideProjects':
|
|
||||||
elementsToSendToRender = this.projectTraversalGraph.hideProjects(
|
|
||||||
event.projectNames,
|
|
||||||
this.renderGraph.getCurrentlyShownProjectIds()
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphShowAllProjects':
|
|
||||||
elementsToSendToRender = this.projectTraversalGraph.showAllProjects();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphHideAllProjects':
|
|
||||||
elementsToSendToRender = this.projectTraversalGraph.hideAllProjects();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphShowAffectedProjects':
|
|
||||||
elementsToSendToRender =
|
|
||||||
this.projectTraversalGraph.showAffectedProjects();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'notifyGraphTracing':
|
|
||||||
if (event.start && event.end) {
|
|
||||||
if (event.algorithm === 'shortest') {
|
|
||||||
elementsToSendToRender = this.projectTraversalGraph.traceProjects(
|
|
||||||
event.start,
|
|
||||||
event.end
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
elementsToSendToRender =
|
|
||||||
this.projectTraversalGraph.traceAllProjects(
|
|
||||||
event.start,
|
|
||||||
event.end
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedProjectNames: string[] = [];
|
|
||||||
let perfReport: GraphPerfReport = {
|
|
||||||
numEdges: 0,
|
|
||||||
numNodes: 0,
|
|
||||||
renderTime: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.renderGraph) {
|
|
||||||
if (elementsToSendToRender) {
|
|
||||||
this.renderGraph.setElements(elementsToSendToRender);
|
|
||||||
|
|
||||||
if (event.type === 'notifyGraphFocusProject') {
|
|
||||||
this.renderGraph.setFocussedElement(event.projectName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { numEdges, numNodes } = this.renderGraph.render();
|
|
||||||
|
|
||||||
selectedProjectNames = (
|
|
||||||
elementsToSendToRender.nodes('[type!="dir"]') ?? []
|
|
||||||
).map((node) => node.id());
|
|
||||||
|
|
||||||
const renderTime = Date.now() - time;
|
|
||||||
|
|
||||||
perfReport = {
|
|
||||||
renderTime,
|
|
||||||
numNodes,
|
|
||||||
numEdges,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const { numEdges, numNodes } = this.renderGraph.render();
|
|
||||||
|
|
||||||
this.renderGraph.getCurrentlyShownProjectIds();
|
|
||||||
|
|
||||||
const renderTime = Date.now() - time;
|
|
||||||
|
|
||||||
perfReport = {
|
|
||||||
renderTime,
|
|
||||||
numNodes,
|
|
||||||
numEdges,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastPerformanceReport = perfReport;
|
|
||||||
this.broadcast({ type: 'GraphRegenerated' });
|
|
||||||
|
|
||||||
return { selectedProjectNames, perfReport };
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTaskEvent(event: TaskGraphRenderEvents) {
|
|
||||||
const time = Date.now();
|
|
||||||
|
|
||||||
this.broadcast({ type: 'GraphRegenerated' });
|
|
||||||
|
|
||||||
let elementsToSendToRender: CollectionReturnValue;
|
|
||||||
switch (event.type) {
|
|
||||||
case 'notifyTaskGraphSetProjects':
|
|
||||||
this.taskTraversalGraph.setProjects(event.projects, event.taskGraphs);
|
|
||||||
break;
|
|
||||||
case 'notifyTaskGraphSetTasks':
|
|
||||||
elementsToSendToRender = this.taskTraversalGraph.setTasks(
|
|
||||||
event.taskIds
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'notifyTaskGraphTasksSelected':
|
|
||||||
elementsToSendToRender = this.taskTraversalGraph.selectTask(
|
|
||||||
event.taskIds
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'notifyTaskGraphTasksDeselected':
|
|
||||||
elementsToSendToRender = this.taskTraversalGraph.deselectTask(
|
|
||||||
event.taskIds
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'setGroupByProject':
|
|
||||||
elementsToSendToRender = this.taskTraversalGraph.setGroupByProject(
|
|
||||||
event.groupByProject
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedProjectNames: string[] = [];
|
|
||||||
let perfReport: GraphPerfReport = {
|
|
||||||
numEdges: 0,
|
|
||||||
numNodes: 0,
|
|
||||||
renderTime: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.renderGraph && elementsToSendToRender) {
|
|
||||||
this.renderGraph.setElements(elementsToSendToRender);
|
|
||||||
|
|
||||||
const { numEdges, numNodes } = this.renderGraph.render();
|
|
||||||
|
|
||||||
selectedProjectNames = (
|
|
||||||
elementsToSendToRender.nodes('[type!="dir"]') ?? []
|
|
||||||
).map((node) => node.id());
|
|
||||||
|
|
||||||
const renderTime = Date.now() - time;
|
|
||||||
|
|
||||||
perfReport = {
|
|
||||||
renderTime,
|
|
||||||
numNodes,
|
|
||||||
numEdges,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastPerformanceReport = perfReport;
|
|
||||||
this.broadcast({ type: 'GraphRegenerated' });
|
|
||||||
|
|
||||||
return { selectedProjectNames, perfReport };
|
|
||||||
}
|
|
||||||
|
|
||||||
getImage() {
|
|
||||||
return this.renderGraph.getImage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
// nx-ignore-next-line
|
|
||||||
import type {
|
|
||||||
ProjectFileMap,
|
|
||||||
ProjectGraphDependency,
|
|
||||||
ProjectGraphProjectNode,
|
|
||||||
TaskGraph,
|
|
||||||
} from '@nx/devkit';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
import { VirtualElement } from '@floating-ui/react';
|
|
||||||
import {
|
|
||||||
ProjectEdgeNodeTooltipProps,
|
|
||||||
ProjectNodeToolTipProps,
|
|
||||||
TaskNodeTooltipProps,
|
|
||||||
} from '@nx/graph/ui-tooltips';
|
|
||||||
|
|
||||||
export interface GraphPerfReport {
|
|
||||||
renderTime: number;
|
|
||||||
numNodes: number;
|
|
||||||
numEdges: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TracingAlgorithmType = 'shortest' | 'all';
|
|
||||||
|
|
||||||
// The events that the graph actor handles
|
|
||||||
|
|
||||||
export type ProjectGraphRenderEvents =
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphInitGraph';
|
|
||||||
projects: ProjectGraphProjectNode[];
|
|
||||||
fileMap: ProjectFileMap;
|
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
|
||||||
affectedProjects: string[];
|
|
||||||
workspaceLayout: {
|
|
||||||
libsDir: string;
|
|
||||||
appsDir: string;
|
|
||||||
};
|
|
||||||
groupByFolder: boolean;
|
|
||||||
collapseEdges: boolean;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphUpdateGraph';
|
|
||||||
projects: ProjectGraphProjectNode[];
|
|
||||||
fileMap: ProjectFileMap;
|
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
|
||||||
affectedProjects: string[];
|
|
||||||
workspaceLayout: {
|
|
||||||
libsDir: string;
|
|
||||||
appsDir: string;
|
|
||||||
};
|
|
||||||
groupByFolder: boolean;
|
|
||||||
collapseEdges: boolean;
|
|
||||||
selectedProjects: string[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphFocusProject';
|
|
||||||
projectName: string;
|
|
||||||
searchDepth: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphShowProjects';
|
|
||||||
projectNames: string[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphHideProjects';
|
|
||||||
projectNames: string[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphShowAllProjects';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphHideAllProjects';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphShowAffectedProjects';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphFilterProjectsByText';
|
|
||||||
search: string;
|
|
||||||
includeProjectsByPath: boolean;
|
|
||||||
searchDepth: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyGraphTracing';
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
algorithm: TracingAlgorithmType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TaskGraphRecord = Record<string, TaskGraph>;
|
|
||||||
export type TaskGraphRenderEvents =
|
|
||||||
| {
|
|
||||||
type: 'notifyTaskGraphSetProjects';
|
|
||||||
projects: ProjectGraphProjectNode[];
|
|
||||||
taskGraphs: TaskGraphRecord;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyTaskGraphTasksSelected';
|
|
||||||
taskIds: string[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'notifyTaskGraphTasksDeselected';
|
|
||||||
taskIds: string[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'setGroupByProject';
|
|
||||||
groupByProject: boolean;
|
|
||||||
}
|
|
||||||
| { type: 'notifyTaskGraphSetTasks'; taskIds: string[] };
|
|
||||||
|
|
||||||
export type TooltipEvent =
|
|
||||||
| {
|
|
||||||
ref: VirtualElement;
|
|
||||||
type: 'projectNode';
|
|
||||||
props: ProjectNodeToolTipProps;
|
|
||||||
}
|
|
||||||
| { ref: VirtualElement; type: 'taskNode'; props: TaskNodeTooltipProps }
|
|
||||||
| {
|
|
||||||
ref: VirtualElement;
|
|
||||||
type: 'projectEdge';
|
|
||||||
props: ProjectEdgeNodeTooltipProps;
|
|
||||||
};
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { NxProjectGraphViz } from './nx-project-graph-viz';
|
|
||||||
|
|
||||||
const meta: Meta<typeof NxProjectGraphViz> = {
|
|
||||||
component: NxProjectGraphViz,
|
|
||||||
title: 'NxProjectGraphViz',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof NxProjectGraphViz>;
|
|
||||||
|
|
||||||
export const Primary: Story = {
|
|
||||||
args: {
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
type: 'app',
|
|
||||||
name: 'app',
|
|
||||||
data: {
|
|
||||||
tags: ['scope:cart'],
|
|
||||||
description: 'This is your top-level app',
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
file: 'whatever.ts',
|
|
||||||
deps: ['lib'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
{
|
|
||||||
type: 'lib',
|
|
||||||
name: 'lib',
|
|
||||||
data: {
|
|
||||||
tags: ['scope:cart'],
|
|
||||||
description: 'This lib implements some type of feature for your app.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'lib',
|
|
||||||
name: 'lib2',
|
|
||||||
data: {
|
|
||||||
root: 'libs/nested-scope/lib2',
|
|
||||||
tags: ['scope:cart'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'lib',
|
|
||||||
name: 'lib3',
|
|
||||||
data: {
|
|
||||||
root: 'libs/nested-scope/lib3',
|
|
||||||
tags: ['scope:cart'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
groupByFolder: true,
|
|
||||||
workspaceLayout: { appsDir: 'apps', libsDir: 'libs' },
|
|
||||||
dependencies: {
|
|
||||||
app: [{ target: 'lib', source: 'app', type: 'direct' }],
|
|
||||||
lib: [
|
|
||||||
{ target: 'lib2', source: 'lib', type: 'implicit' },
|
|
||||||
{ target: 'lib3', source: 'lib', type: 'direct' },
|
|
||||||
],
|
|
||||||
lib2: [],
|
|
||||||
lib3: [],
|
|
||||||
},
|
|
||||||
affectedProjectIds: [],
|
|
||||||
theme: 'light',
|
|
||||||
height: '450px',
|
|
||||||
enableTooltips: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
'use client';
|
|
||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
/* nx-ignore-next-line */
|
|
||||||
import type {
|
|
||||||
ProjectGraphProjectNode,
|
|
||||||
ProjectGraphDependency,
|
|
||||||
ProjectFileMap,
|
|
||||||
} from 'nx/src/config/project-graph';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { GraphService } from './graph';
|
|
||||||
import {
|
|
||||||
ProjectEdgeNodeTooltip,
|
|
||||||
ProjectNodeToolTip,
|
|
||||||
TaskNodeTooltip,
|
|
||||||
Tooltip,
|
|
||||||
} from '@nx/graph/ui-tooltips';
|
|
||||||
import { GraphTooltipService } from './tooltip-service';
|
|
||||||
import { TooltipEvent } from './interfaces';
|
|
||||||
|
|
||||||
type Theme = 'light' | 'dark' | 'system';
|
|
||||||
|
|
||||||
export interface GraphUiGraphProps {
|
|
||||||
projects: ProjectGraphProjectNode[];
|
|
||||||
fileMap: ProjectFileMap;
|
|
||||||
groupByFolder: boolean;
|
|
||||||
workspaceLayout: { appsDir: string; libsDir: string };
|
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
|
||||||
affectedProjectIds: string[];
|
|
||||||
theme: Theme;
|
|
||||||
height: string;
|
|
||||||
enableTooltips: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTheme(theme: Theme): 'dark' | 'light' {
|
|
||||||
if (theme !== 'system') {
|
|
||||||
return theme;
|
|
||||||
} else {
|
|
||||||
const darkMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
return darkMedia.matches ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NxProjectGraphViz({
|
|
||||||
projects,
|
|
||||||
fileMap,
|
|
||||||
groupByFolder,
|
|
||||||
workspaceLayout,
|
|
||||||
dependencies,
|
|
||||||
affectedProjectIds,
|
|
||||||
theme,
|
|
||||||
height,
|
|
||||||
enableTooltips,
|
|
||||||
}: GraphUiGraphProps) {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [graph, setGraph] = useState<GraphService>(null);
|
|
||||||
const [currentTooltip, setCurrenTooltip] = useState<TooltipEvent>(null);
|
|
||||||
|
|
||||||
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>();
|
|
||||||
|
|
||||||
const newlyResolvedTheme = resolveTheme(theme);
|
|
||||||
|
|
||||||
if (newlyResolvedTheme !== resolvedTheme) {
|
|
||||||
setResolvedTheme(newlyResolvedTheme);
|
|
||||||
|
|
||||||
if (graph) {
|
|
||||||
graph.theme = newlyResolvedTheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (containerRef.current !== null) {
|
|
||||||
import('./graph')
|
|
||||||
.then((module) => module.GraphService)
|
|
||||||
.then((GraphService) => {
|
|
||||||
const graph = new GraphService(
|
|
||||||
containerRef.current,
|
|
||||||
resolvedTheme,
|
|
||||||
'nx-docs',
|
|
||||||
'TB'
|
|
||||||
);
|
|
||||||
graph.handleProjectEvent({
|
|
||||||
type: 'notifyGraphInitGraph',
|
|
||||||
fileMap,
|
|
||||||
projects,
|
|
||||||
groupByFolder,
|
|
||||||
workspaceLayout,
|
|
||||||
dependencies,
|
|
||||||
affectedProjects: affectedProjectIds,
|
|
||||||
collapseEdges: false,
|
|
||||||
});
|
|
||||||
graph.handleProjectEvent({ type: 'notifyGraphShowAllProjects' });
|
|
||||||
|
|
||||||
if (enableTooltips) {
|
|
||||||
const tooltipService = new GraphTooltipService(graph);
|
|
||||||
tooltipService.subscribe((tooltip) => {
|
|
||||||
setCurrenTooltip(tooltip);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let tooltipToRender;
|
|
||||||
if (currentTooltip) {
|
|
||||||
switch (currentTooltip.type) {
|
|
||||||
case 'projectNode':
|
|
||||||
tooltipToRender = <ProjectNodeToolTip {...currentTooltip.props} />;
|
|
||||||
break;
|
|
||||||
case 'projectEdge':
|
|
||||||
tooltipToRender = <ProjectEdgeNodeTooltip {...currentTooltip.props} />;
|
|
||||||
break;
|
|
||||||
case 'taskNode':
|
|
||||||
tooltipToRender = <TaskNodeTooltip {...currentTooltip.props} />;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="not-prose">
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
style={{ width: '100%', height }}
|
|
||||||
></div>
|
|
||||||
{tooltipToRender ? (
|
|
||||||
<Tooltip
|
|
||||||
content={tooltipToRender}
|
|
||||||
open={true}
|
|
||||||
reference={currentTooltip.ref}
|
|
||||||
placement="top"
|
|
||||||
openAction="manual"
|
|
||||||
></Tooltip>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { NxTaskGraphViz } from './nx-task-graph-viz';
|
|
||||||
|
|
||||||
const meta: Meta<typeof NxTaskGraphViz> = {
|
|
||||||
component: NxTaskGraphViz,
|
|
||||||
title: 'NxTaskGraphViz',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof NxTaskGraphViz>;
|
|
||||||
|
|
||||||
export const Primary: Story = {
|
|
||||||
args: {
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
type: 'app',
|
|
||||||
name: 'app',
|
|
||||||
data: {
|
|
||||||
tags: ['scope:cart'],
|
|
||||||
targets: {
|
|
||||||
build: {
|
|
||||||
executor: '@nrwl/js:tsc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: 'The app uses this task to build itself.',
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
{
|
|
||||||
type: 'lib',
|
|
||||||
name: 'lib1',
|
|
||||||
data: {
|
|
||||||
tags: ['scope:cart'],
|
|
||||||
targets: {
|
|
||||||
build: {
|
|
||||||
executor: '@nrwl/js:tsc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: 'The lib uses this task to build itself.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'lib',
|
|
||||||
name: 'lib2',
|
|
||||||
data: {
|
|
||||||
root: 'libs/nested-scope/lib2',
|
|
||||||
tags: ['scope:cart'],
|
|
||||||
targets: {
|
|
||||||
build: {
|
|
||||||
executor: '@nrwl/js:tsc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'lib',
|
|
||||||
name: 'lib3',
|
|
||||||
data: {
|
|
||||||
root: 'libs/nested-scope/lib3',
|
|
||||||
tags: ['scope:cart'],
|
|
||||||
targets: {
|
|
||||||
build: {
|
|
||||||
executor: '@nrwl/js:tsc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
taskGraphs: {
|
|
||||||
'app:build': {
|
|
||||||
tasks: {
|
|
||||||
'app:build': {
|
|
||||||
id: 'app:build',
|
|
||||||
target: {
|
|
||||||
project: 'app',
|
|
||||||
target: 'build',
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
'lib1:build': {
|
|
||||||
id: 'lib1:build',
|
|
||||||
target: {
|
|
||||||
project: 'lib1',
|
|
||||||
target: 'build',
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
'lib2:build': {
|
|
||||||
id: 'lib2:build',
|
|
||||||
target: {
|
|
||||||
project: 'lib2',
|
|
||||||
target: 'build',
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
'lib3:build': {
|
|
||||||
id: 'lib3:build',
|
|
||||||
target: {
|
|
||||||
project: 'lib3',
|
|
||||||
target: 'build',
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
dependencies: {
|
|
||||||
'app:build': ['lib1:build', 'lib2:build', 'lib3:build'],
|
|
||||||
'lib1:build': [],
|
|
||||||
'lib2:build': [],
|
|
||||||
'lib3:build': [],
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
taskId: 'app:build',
|
|
||||||
height: '450px',
|
|
||||||
enableTooltips: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
'use client';
|
|
||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
|
||||||
/* nx-ignore-next-line */
|
|
||||||
import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
|
|
||||||
/* eslint-enable @nx/enforce-module-boundaries */
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import { GraphService } from './graph';
|
|
||||||
import { TaskGraphRecord, TooltipEvent } from './interfaces';
|
|
||||||
import { TaskNodeTooltip, Tooltip } from '@nx/graph/ui-tooltips';
|
|
||||||
import { GraphTooltipService } from './tooltip-service';
|
|
||||||
|
|
||||||
type Theme = 'light' | 'dark' | 'system';
|
|
||||||
|
|
||||||
export interface TaskGraphUiGraphProps {
|
|
||||||
projects: ProjectGraphProjectNode[];
|
|
||||||
taskGraphs: TaskGraphRecord;
|
|
||||||
taskId: string;
|
|
||||||
theme: Theme;
|
|
||||||
height: string;
|
|
||||||
enableTooltips: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTheme(theme: Theme): 'dark' | 'light' {
|
|
||||||
if (theme !== 'system') {
|
|
||||||
return theme;
|
|
||||||
} else {
|
|
||||||
const darkMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
return darkMedia.matches ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NxTaskGraphViz({
|
|
||||||
projects,
|
|
||||||
taskId,
|
|
||||||
taskGraphs,
|
|
||||||
theme,
|
|
||||||
height,
|
|
||||||
enableTooltips,
|
|
||||||
}: TaskGraphUiGraphProps) {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [graph, setGraph] = useState<GraphService>(null);
|
|
||||||
const [currentTooltip, setCurrenTooltip] = useState<TooltipEvent>(null);
|
|
||||||
|
|
||||||
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>();
|
|
||||||
|
|
||||||
const newlyResolvedTheme = resolveTheme(theme);
|
|
||||||
|
|
||||||
if (newlyResolvedTheme !== resolvedTheme) {
|
|
||||||
setResolvedTheme(newlyResolvedTheme);
|
|
||||||
|
|
||||||
if (graph) {
|
|
||||||
graph.theme = newlyResolvedTheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
useEffect(() => {
|
|
||||||
if (containerRef.current !== null) {
|
|
||||||
import('./graph')
|
|
||||||
.then((module) => module.GraphService)
|
|
||||||
.then((GraphService) => {
|
|
||||||
const graph = new GraphService(
|
|
||||||
containerRef.current,
|
|
||||||
resolvedTheme,
|
|
||||||
'nx-docs',
|
|
||||||
'TB'
|
|
||||||
);
|
|
||||||
graph.handleTaskEvent({
|
|
||||||
type: 'notifyTaskGraphSetProjects',
|
|
||||||
projects,
|
|
||||||
taskGraphs,
|
|
||||||
});
|
|
||||||
graph.handleTaskEvent({
|
|
||||||
type: 'notifyTaskGraphSetTasks',
|
|
||||||
taskIds: [taskId],
|
|
||||||
});
|
|
||||||
setGraph(graph);
|
|
||||||
|
|
||||||
if (enableTooltips) {
|
|
||||||
const tooltipService = new GraphTooltipService(graph);
|
|
||||||
tooltipService.subscribe((tooltip) => {
|
|
||||||
setCurrenTooltip(tooltip);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
let tooltipToRender;
|
|
||||||
if (currentTooltip) {
|
|
||||||
switch (currentTooltip.type) {
|
|
||||||
case 'taskNode':
|
|
||||||
tooltipToRender = <TaskNodeTooltip {...currentTooltip.props} />;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="not-prose">
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
style={{ width: '100%', height }}
|
|
||||||
></div>
|
|
||||||
{tooltipToRender ? (
|
|
||||||
<Tooltip
|
|
||||||
content={tooltipToRender}
|
|
||||||
open={true}
|
|
||||||
reference={currentTooltip.ref}
|
|
||||||
placement="top"
|
|
||||||
openAction="manual"
|
|
||||||
></Tooltip>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { SingularData, Core } from 'cytoscape';
|
|
||||||
|
|
||||||
export const darkModeScratchKey = 'NX_GRAPH_DARK_MODE';
|
|
||||||
|
|
||||||
export function scratchHasDarkMode(element: SingularData | Core) {
|
|
||||||
return element.scratch(darkModeScratchKey) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function switchValueByDarkMode<T>(
|
|
||||||
element: SingularData | Core,
|
|
||||||
dark: T,
|
|
||||||
light: T
|
|
||||||
) {
|
|
||||||
return scratchHasDarkMode(element) ? dark : light;
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
import { EdgeSingular, Stylesheet } from 'cytoscape';
|
|
||||||
import { NrwlPalette } from './palette';
|
|
||||||
import { switchValueByDarkMode } from './dark-mode';
|
|
||||||
|
|
||||||
const allEdges: Stylesheet = {
|
|
||||||
selector: 'edge',
|
|
||||||
style: {
|
|
||||||
width: '1px',
|
|
||||||
'line-color': (node) =>
|
|
||||||
switchValueByDarkMode(node, NrwlPalette.slate_400, NrwlPalette.slate_500),
|
|
||||||
'text-outline-color': (node: EdgeSingular) =>
|
|
||||||
switchValueByDarkMode(node, NrwlPalette.slate_400, NrwlPalette.slate_500),
|
|
||||||
'text-outline-width': '0px',
|
|
||||||
color: (node: EdgeSingular) =>
|
|
||||||
switchValueByDarkMode(node, NrwlPalette.slate_400, NrwlPalette.slate_500),
|
|
||||||
'curve-style': 'unbundled-bezier',
|
|
||||||
'target-arrow-shape': 'triangle',
|
|
||||||
'target-arrow-fill': 'filled',
|
|
||||||
'target-arrow-color': (node) =>
|
|
||||||
switchValueByDarkMode(node, NrwlPalette.slate_400, NrwlPalette.slate_500),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const affectedEdges: Stylesheet = {
|
|
||||||
selector: 'edge.affected',
|
|
||||||
style: {
|
|
||||||
'line-color': (node) =>
|
|
||||||
switchValueByDarkMode(
|
|
||||||
node,
|
|
||||||
NrwlPalette.fuchsia_500,
|
|
||||||
NrwlPalette.pink_500
|
|
||||||
),
|
|
||||||
'target-arrow-color': (node) =>
|
|
||||||
switchValueByDarkMode(
|
|
||||||
node,
|
|
||||||
NrwlPalette.fuchsia_500,
|
|
||||||
NrwlPalette.pink_500
|
|
||||||
),
|
|
||||||
'curve-style': 'unbundled-bezier',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const implicitEdges: Stylesheet = {
|
|
||||||
selector: 'edge.implicit',
|
|
||||||
style: {
|
|
||||||
label: 'implicit',
|
|
||||||
'font-size': '16px',
|
|
||||||
'curve-style': 'unbundled-bezier',
|
|
||||||
'text-rotation': 'autorotate',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const transparentEdges: Stylesheet = {
|
|
||||||
selector: 'edge.transparent',
|
|
||||||
style: { opacity: 0.2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const dynamicEdges: Stylesheet = {
|
|
||||||
selector: 'edge.dynamic',
|
|
||||||
style: {
|
|
||||||
'line-dash-pattern': [5, 5],
|
|
||||||
'line-style': 'dashed',
|
|
||||||
'curve-style': 'unbundled-bezier',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const edgeStyles: Stylesheet[] = [
|
|
||||||
allEdges,
|
|
||||||
affectedEdges,
|
|
||||||
implicitEdges,
|
|
||||||
dynamicEdges,
|
|
||||||
transparentEdges,
|
|
||||||
];
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user