chore(docs): add docs graph component (#12929)
This commit is contained in:
parent
d103cd9d51
commit
b23d8e911b
@ -150,3 +150,77 @@ src="https://www.youtube.com/embed/rNImFxo9gYs"
|
|||||||
title="Nx Console Run UI Form"
|
title="Nx Console Run UI Form"
|
||||||
width="100%" /%}
|
width="100%" /%}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Graph
|
||||||
|
|
||||||
|
Embed an Nx Graph visualization that can be panned by the user.
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
{% graph height="450px" %}
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"type": "app",
|
||||||
|
"name": "app-changed",
|
||||||
|
"data": {
|
||||||
|
"tags": ["scope:cart"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lib",
|
||||||
|
"name": "lib",
|
||||||
|
"data": {
|
||||||
|
"tags": ["scope:cart"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lib",
|
||||||
|
"name": "lib2",
|
||||||
|
"data": {
|
||||||
|
"tags": ["scope:cart"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lib",
|
||||||
|
"name": "lib3",
|
||||||
|
"data": {
|
||||||
|
"tags": ["scope:cart"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupByFolder": false,
|
||||||
|
"workspaceLayout": {
|
||||||
|
"appsDir": "apps",
|
||||||
|
"libsDir": "libs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"app-changed": [
|
||||||
|
{
|
||||||
|
"target": "lib",
|
||||||
|
"source": "app-changed",
|
||||||
|
"type": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lib": [
|
||||||
|
{
|
||||||
|
"target": "lib2",
|
||||||
|
"source": "lib",
|
||||||
|
"type": "implicit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "lib3",
|
||||||
|
"source": "lib",
|
||||||
|
"type": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lib2": [],
|
||||||
|
"lib3": []
|
||||||
|
},
|
||||||
|
"affectedProjectIds": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{% /graph %}
|
||||||
|
````
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
# Explore the Graph
|
# Explore the Graph
|
||||||
|
|
||||||
For Nx to run tasks quickly and correctly, it creates a graph of the dependencies between all the projects in the repository. Exploring this graph visually can be useful to understand why Nx is behaving in a certain way and to get a high level view of your code architecture.
|
For Nx to run tasks quickly and correctly, it creates a graph of the dependencies between all the projects in the
|
||||||
|
repository. Exploring this graph visually can be useful
|
||||||
|
to understand why Nx is behaving in a certain way and to get a
|
||||||
|
high level view of your code architecture.
|
||||||
|
|
||||||
To launch the project graph visualization run:
|
To launch the project graph visualization run:
|
||||||
|
|
||||||
@ -8,12 +11,16 @@ To launch the project graph visualization run:
|
|||||||
nx graph
|
nx graph
|
||||||
```
|
```
|
||||||
|
|
||||||
This will open a browser window with an interactive representation of the project graph of your current codebase. Viewing the entire graph can be unmanageable even for smaller repositories, so there are several ways to narrow the focus of the visualization down to the most useful part of the graph at the moment.
|
This will open a browser window with an interactive representation of the project graph of your current codebase.
|
||||||
|
Viewing the entire graph can be unmanageable even for smaller repositories, so there are several ways to narrow the
|
||||||
|
focus of the visualization down to the most useful part of the graph at the moment.
|
||||||
|
|
||||||
1. Focus on a specific project and then use the proximity and group by folder controls to modify the graph around that project.
|
1. Focus on a specific project and then use the proximity and group by folder controls to modify the graph around that
|
||||||
|
project.
|
||||||
2. Use the search bar to find all projects with names that contain a certain string.
|
2. Use the search bar to find all projects with names that contain a certain string.
|
||||||
3. Manually hide or show projects in the sidebar
|
3. Manually hide or show projects in the sidebar
|
||||||
|
|
||||||
Once the graph is displayed, you can click on an individual dependency link to find out what specific file(s) created that dependency.
|
Once the graph is displayed, you can click on an individual dependency link to find out what specific file(s) created
|
||||||
|
that dependency.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import Tag from './ui-components/tag';
|
import Tag from './ui-components/tag';
|
||||||
|
|
||||||
export interface EdgeNodeTooltipProps {
|
export interface EdgeNodeTooltipProps {
|
||||||
type: 'static' | 'dynamic' | 'implicit';
|
type: string;
|
||||||
source: string;
|
source: string;
|
||||||
target: string;
|
target: string;
|
||||||
fileDependencies: Array<{ fileName: string }>;
|
fileDependencies: Array<{ fileName: string }>;
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { getTooltipService } from '../tooltip-service';
|
import { GraphService } from '@nrwl/graph/ui-graph';
|
||||||
import { GraphService } from './graph';
|
import { selectValueByThemeStatic } from '../theme-resolver';
|
||||||
|
|
||||||
let graphService: GraphService;
|
let graphService: GraphService;
|
||||||
|
|
||||||
export function getGraphService(): GraphService {
|
export function getGraphService(): GraphService {
|
||||||
if (!graphService) {
|
if (!graphService) {
|
||||||
graphService = new GraphService(getTooltipService(), 'cytoscape-graph');
|
const darkModeEnabled = selectValueByThemeStatic(true, false);
|
||||||
|
graphService = new GraphService(
|
||||||
|
'cytoscape-graph',
|
||||||
|
selectValueByThemeStatic('dark', 'light')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return graphService;
|
return graphService;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export function rankDirInit() {
|
|||||||
export function rankDirResolver(rankDir: RankDir) {
|
export function rankDirResolver(rankDir: RankDir) {
|
||||||
currentRankDir = rankDir;
|
currentRankDir = rankDir;
|
||||||
localStorage.setItem(localStorageRankDirKey, rankDir);
|
localStorage.setItem(localStorageRankDirKey, rankDir);
|
||||||
getGraphService().setRankDir(currentRankDir);
|
getGraphService().rankDir = currentRankDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectValueByRankDirDynamic<T>(
|
export function selectValueByRankDirDynamic<T>(
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
import { NodeSingular } from 'cytoscape';
|
|
||||||
|
|
||||||
export class LabelWidthCalculator {
|
|
||||||
private cache: Map<string, number> = new Map<string, number>();
|
|
||||||
private ctx: CanvasRenderingContext2D;
|
|
||||||
|
|
||||||
calculateWidth(node: NodeSingular) {
|
|
||||||
if (!this.ctx) {
|
|
||||||
this.ctx = document.createElement('canvas').getContext('2d');
|
|
||||||
const fStyle = node.style('font-style');
|
|
||||||
const size = node.style('font-size');
|
|
||||||
const family = node.style('font-family');
|
|
||||||
const weight = node.style('font-weight');
|
|
||||||
|
|
||||||
this.ctx.font = fStyle + ' ' + weight + ' ' + size + ' ' + family;
|
|
||||||
}
|
|
||||||
const label = node.data('id');
|
|
||||||
|
|
||||||
if (this.cache.has(label)) {
|
|
||||||
return this.cache.get(label);
|
|
||||||
} else {
|
|
||||||
const width = this.ctx.measureText(node.data('id')).width;
|
|
||||||
|
|
||||||
this.cache.set(label, width);
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -36,7 +36,8 @@ export function themeResolver(theme: Theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(localStorageThemeKey, theme);
|
localStorage.setItem(localStorageThemeKey, theme);
|
||||||
getGraphService().evaluateStyles();
|
|
||||||
|
getGraphService().theme = currentTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectValueByThemeDynamic<T>(
|
export function selectValueByThemeDynamic<T>(
|
||||||
|
|||||||
@ -1,10 +1,38 @@
|
|||||||
|
import { getGraphService } from './machines/graph.service';
|
||||||
|
|
||||||
import { VirtualElement } from '@popperjs/core';
|
import { VirtualElement } from '@popperjs/core';
|
||||||
import { ProjectNodeToolTipProps } from './project-node-tooltip';
|
import { ProjectNodeToolTipProps } from './project-node-tooltip';
|
||||||
import { EdgeNodeTooltipProps } from './edge-tooltip';
|
import { EdgeNodeTooltipProps } from './edge-tooltip';
|
||||||
|
import { GraphInteractionEvents, GraphService } from '@nrwl/graph/ui-graph';
|
||||||
|
|
||||||
export class GraphTooltipService {
|
export class GraphTooltipService {
|
||||||
private subscribers: Set<Function> = new Set();
|
private subscribers: Set<Function> = new Set();
|
||||||
|
|
||||||
|
constructor(graph: GraphService) {
|
||||||
|
graph.listen((event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'GraphRegenerated':
|
||||||
|
this.hideAll();
|
||||||
|
break;
|
||||||
|
case 'NodeClick':
|
||||||
|
this.openProjectNodeToolTip(event.ref, {
|
||||||
|
id: event.data.id,
|
||||||
|
tags: event.data.tags,
|
||||||
|
type: event.data.type,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'EdgeClick':
|
||||||
|
this.openEdgeToolTip(event.ref, {
|
||||||
|
type: event.data.type,
|
||||||
|
target: event.data.target,
|
||||||
|
source: event.data.source,
|
||||||
|
fileDependencies: event.data.fileDependencies,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
currentTooltip:
|
currentTooltip:
|
||||||
| { ref: VirtualElement; type: 'node'; props: ProjectNodeToolTipProps }
|
| { ref: VirtualElement; type: 'node'; props: ProjectNodeToolTipProps }
|
||||||
| { ref: VirtualElement; type: 'edge'; props: EdgeNodeTooltipProps };
|
| { ref: VirtualElement; type: 'edge'; props: EdgeNodeTooltipProps };
|
||||||
@ -41,7 +69,8 @@ let tooltipService: GraphTooltipService;
|
|||||||
|
|
||||||
export function getTooltipService(): GraphTooltipService {
|
export function getTooltipService(): GraphTooltipService {
|
||||||
if (!tooltipService) {
|
if (!tooltipService) {
|
||||||
tooltipService = new GraphTooltipService();
|
const graph = getGraphService();
|
||||||
|
tooltipService = new GraphTooltipService(graph);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tooltipService;
|
return tooltipService;
|
||||||
|
|||||||
12
graph/ui-graph/.babelrc
Normal file
12
graph/ui-graph/.babelrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@nrwl/react/babel",
|
||||||
|
{
|
||||||
|
"runtime": "automatic",
|
||||||
|
"useBuiltIns": "usage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
18
graph/ui-graph/.eslintrc.json
Normal file
18
graph/ui-graph/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
24
graph/ui-graph/.storybook/main.js
Normal file
24
graph/ui-graph/.storybook/main.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const rootMain = require('../../../.storybook/main');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...rootMain,
|
||||||
|
|
||||||
|
core: { ...rootMain.core, builder: 'webpack5' },
|
||||||
|
|
||||||
|
stories: [
|
||||||
|
...rootMain.stories,
|
||||||
|
'../src/lib/**/*.stories.mdx',
|
||||||
|
'../src/lib/**/*.stories.@(js|jsx|ts|tsx)',
|
||||||
|
],
|
||||||
|
addons: [...rootMain.addons, '@nrwl/react/plugins/storybook'],
|
||||||
|
webpackFinal: async (config, { configType }) => {
|
||||||
|
// apply any global webpack configs that might have been specified in .storybook/main.js
|
||||||
|
if (rootMain.webpackFinal) {
|
||||||
|
config = await rootMain.webpackFinal(config, { configType });
|
||||||
|
}
|
||||||
|
|
||||||
|
// add your own webpack tweaks if needed
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
0
graph/ui-graph/.storybook/preview.js
Normal file
0
graph/ui-graph/.storybook/preview.js
Normal file
27
graph/ui-graph/.storybook/tsconfig.json
Normal file
27
graph/ui-graph/.storybook/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"outDir": ""
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"../../../node_modules/@nrwl/react/typings/styled-jsx.d.ts",
|
||||||
|
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||||
|
"../../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||||
|
],
|
||||||
|
|
||||||
|
"exclude": [
|
||||||
|
"../**/*.spec.ts",
|
||||||
|
"../**/*.spec.js",
|
||||||
|
"../**/*.spec.tsx",
|
||||||
|
"../**/*.spec.jsx"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"../src/**/*.stories.ts",
|
||||||
|
"../src/**/*.stories.js",
|
||||||
|
"../src/**/*.stories.jsx",
|
||||||
|
"../src/**/*.stories.tsx",
|
||||||
|
"../src/**/*.stories.mdx",
|
||||||
|
"*.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
7
graph/ui-graph/README.md
Normal file
7
graph/ui-graph/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# 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).
|
||||||
10
graph/ui-graph/jest.config.ts
Normal file
10
graph/ui-graph/jest.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* 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',
|
||||||
|
};
|
||||||
55
graph/ui-graph/project.json
Normal file
55
graph/ui-graph/project.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "graph-ui-graph",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "graph/ui-graph/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"outputs": ["{options.outputFile}"],
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["graph/ui-graph/**/*.{ts,tsx,js,jsx}"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "graph/ui-graph/jest.config.ts",
|
||||||
|
"passWithNoTests": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storybook": {
|
||||||
|
"executor": "@nrwl/storybook:storybook",
|
||||||
|
"options": {
|
||||||
|
"uiFramework": "@storybook/react",
|
||||||
|
"port": 4400,
|
||||||
|
"config": {
|
||||||
|
"configFolder": "graph/ui-graph/.storybook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"quiet": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build-storybook": {
|
||||||
|
"executor": "@nrwl/storybook:build",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"uiFramework": "@storybook/react",
|
||||||
|
"outputPath": "dist/storybook/graph-ui-graph",
|
||||||
|
"config": {
|
||||||
|
"configFolder": "graph/ui-graph/.storybook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"quiet": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
graph/ui-graph/src/index.ts
Normal file
3
graph/ui-graph/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './lib/nx-graph-viz';
|
||||||
|
export * from './lib/graph';
|
||||||
|
export * from './lib/graph-interaction-events';
|
||||||
31
graph/ui-graph/src/lib/graph-interaction-events.ts
Normal file
31
graph/ui-graph/src/lib/graph-interaction-events.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { VirtualElement } from '@popperjs/core';
|
||||||
|
import { NodeDataDefinition } from './util-cytoscape/project-node';
|
||||||
|
import { EdgeDataDefinition } from './util-cytoscape/edge';
|
||||||
|
|
||||||
|
interface NodeClickEvent {
|
||||||
|
type: 'NodeClick';
|
||||||
|
ref: VirtualElement;
|
||||||
|
id: string;
|
||||||
|
data: NodeDataDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EdgeClickEvent {
|
||||||
|
type: 'EdgeClick';
|
||||||
|
ref: VirtualElement;
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
type: string;
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
fileDependencies: { fileName: string; target: string }[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GraphRegeneratedEvent {
|
||||||
|
type: 'GraphRegenerated';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GraphInteractionEvents =
|
||||||
|
| NodeClickEvent
|
||||||
|
| EdgeClickEvent
|
||||||
|
| GraphRegeneratedEvent;
|
||||||
@ -7,18 +7,19 @@ import type { VirtualElement } from '@popperjs/core';
|
|||||||
import cy from 'cytoscape';
|
import cy from 'cytoscape';
|
||||||
import cytoscapeDagre from 'cytoscape-dagre';
|
import cytoscapeDagre from 'cytoscape-dagre';
|
||||||
import popper from 'cytoscape-popper';
|
import popper from 'cytoscape-popper';
|
||||||
import { edgeStyles, nodeStyles } from '../styles-graph';
|
import { edgeStyles, nodeStyles } from './styles-graph';
|
||||||
import { selectValueByRankDirStatic } from '../rankdir-resolver';
|
|
||||||
import { selectValueByThemeStatic } from '../theme-resolver';
|
|
||||||
import { GraphTooltipService } from '../tooltip-service';
|
|
||||||
import {
|
import {
|
||||||
CytoscapeDagreConfig,
|
CytoscapeDagreConfig,
|
||||||
ParentNode,
|
ParentNode,
|
||||||
ProjectEdge,
|
ProjectEdge,
|
||||||
ProjectNode,
|
ProjectNode,
|
||||||
} from '../util-cytoscape';
|
} from './util-cytoscape';
|
||||||
import { GraphPerfReport, GraphRenderEvents } from './interfaces';
|
import { GraphPerfReport, GraphRenderEvents } from './interfaces';
|
||||||
import { getEnvironmentConfig } from '../hooks/use-environment-config';
|
import {
|
||||||
|
darkModeScratchKey,
|
||||||
|
switchValueByDarkMode,
|
||||||
|
} from './styles-graph/dark-mode';
|
||||||
|
import { GraphInteractionEvents } from './graph-interaction-events';
|
||||||
|
|
||||||
const cytoscapeDagreConfig = {
|
const cytoscapeDagreConfig = {
|
||||||
name: 'dagre',
|
name: 'dagre',
|
||||||
@ -35,12 +36,72 @@ export class GraphService {
|
|||||||
|
|
||||||
private collapseEdges = false;
|
private collapseEdges = false;
|
||||||
|
|
||||||
|
private listeners = new Map<
|
||||||
|
number,
|
||||||
|
(event: GraphInteractionEvents) => void
|
||||||
|
>();
|
||||||
|
|
||||||
|
private _theme: 'light' | 'dark';
|
||||||
|
private _rankDir: 'TB' | 'LR' = 'TB';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private tooltipService: GraphTooltipService,
|
private container: string | HTMLElement,
|
||||||
private containerId: string
|
|
||||||
|
theme: 'light' | 'dark',
|
||||||
|
private renderMode?: 'nx-console' | 'nx-docs',
|
||||||
|
rankDir: 'TB' | 'LR' = 'TB'
|
||||||
) {
|
) {
|
||||||
cy.use(cytoscapeDagre);
|
cy.use(cytoscapeDagre);
|
||||||
cy.use(popper);
|
cy.use(popper);
|
||||||
|
|
||||||
|
this._theme = theme;
|
||||||
|
this._rankDir = rankDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeContainer() {
|
||||||
|
return typeof this.container === 'string'
|
||||||
|
? document.getElementById(this.container)
|
||||||
|
: this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
set theme(theme: 'light' | 'dark') {
|
||||||
|
this._theme = theme;
|
||||||
|
|
||||||
|
if (this.renderGraph) {
|
||||||
|
this.renderGraph.unmount();
|
||||||
|
const useDarkMode = theme === 'dark';
|
||||||
|
|
||||||
|
this.renderGraph.scratch(darkModeScratchKey, useDarkMode);
|
||||||
|
this.renderGraph.elements().scratch(darkModeScratchKey, useDarkMode);
|
||||||
|
|
||||||
|
this.renderGraph.mount(this.activeContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set rankDir(rankDir: 'TB' | 'LR') {
|
||||||
|
this._rankDir = rankDir;
|
||||||
|
if (this.renderGraph) {
|
||||||
|
const elements = this.renderGraph.elements();
|
||||||
|
elements
|
||||||
|
.layout({
|
||||||
|
...cytoscapeDagreConfig,
|
||||||
|
...{ rankDir: rankDir },
|
||||||
|
} as CytoscapeDagreConfig)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvent(event: GraphRenderEvents): {
|
handleEvent(event: GraphRenderEvents): {
|
||||||
@ -51,9 +112,10 @@ export class GraphService {
|
|||||||
|
|
||||||
if (this.renderGraph && event.type !== 'notifyGraphUpdateGraph') {
|
if (this.renderGraph && event.type !== 'notifyGraphUpdateGraph') {
|
||||||
this.renderGraph.nodes('.focused').removeClass('focused');
|
this.renderGraph.nodes('.focused').removeClass('focused');
|
||||||
|
this.renderGraph.unmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tooltipService.hideAll();
|
this.broadcast({ type: 'GraphRegenerated' });
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'notifyGraphInitGraph':
|
case 'notifyGraphInitGraph':
|
||||||
@ -141,7 +203,7 @@ export class GraphService {
|
|||||||
elements
|
elements
|
||||||
.layout({
|
.layout({
|
||||||
...cytoscapeDagreConfig,
|
...cytoscapeDagreConfig,
|
||||||
...{ rankDir: selectValueByRankDirStatic('TB', 'LR') },
|
...{ rankDir: this._rankDir },
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
@ -219,9 +281,7 @@ export class GraphService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const environmentConfig = getEnvironmentConfig();
|
if (this.renderMode === 'nx-console') {
|
||||||
|
|
||||||
if (environmentConfig.environment === 'nx-console') {
|
|
||||||
// when in the nx-console environment, adjust graph width and position to be to right of floating panel
|
// when in the nx-console environment, adjust graph width and position to be to right of floating panel
|
||||||
// 175 is a magic number that represents the width of the floating panels divided in half plus some padding
|
// 175 is a magic number that represents the width of the floating panels divided in half plus some padding
|
||||||
this.renderGraph
|
this.renderGraph
|
||||||
@ -237,6 +297,13 @@ export class GraphService {
|
|||||||
.nodes('[type!="dir"]')
|
.nodes('[type!="dir"]')
|
||||||
.map((node) => node.id());
|
.map((node) => node.id());
|
||||||
|
|
||||||
|
this.renderGraph.scratch(darkModeScratchKey, this._theme === 'dark');
|
||||||
|
this.renderGraph
|
||||||
|
.elements()
|
||||||
|
.scratch(darkModeScratchKey, this._theme === 'dark');
|
||||||
|
|
||||||
|
this.renderGraph.mount(this.activeContainer);
|
||||||
|
|
||||||
const renderTime = Date.now() - time;
|
const renderTime = Date.now() - time;
|
||||||
|
|
||||||
perfReport = {
|
perfReport = {
|
||||||
@ -440,13 +507,14 @@ export class GraphService {
|
|||||||
this.renderGraph.destroy();
|
this.renderGraph.destroy();
|
||||||
delete this.renderGraph;
|
delete this.renderGraph;
|
||||||
}
|
}
|
||||||
const container = document.getElementById(this.containerId);
|
|
||||||
|
|
||||||
this.renderGraph = cy({
|
this.renderGraph = cy({
|
||||||
container: container,
|
headless: this.activeContainer === null,
|
||||||
headless: !container,
|
container: this.activeContainer,
|
||||||
boxSelectionEnabled: false,
|
boxSelectionEnabled: false,
|
||||||
style: [...nodeStyles, ...edgeStyles],
|
style: [...nodeStyles, ...edgeStyles],
|
||||||
|
panningEnabled: true,
|
||||||
|
userZoomingEnabled: this.renderMode !== 'nx-docs',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renderGraph.add(elements);
|
this.renderGraph.add(elements);
|
||||||
@ -456,7 +524,7 @@ export class GraphService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.renderGraph.on('zoom pan', () => {
|
this.renderGraph.on('zoom pan', () => {
|
||||||
this.tooltipService.hideAll();
|
this.broadcast({ type: 'GraphRegenerated' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.listenForProjectNodeClicks();
|
this.listenForProjectNodeClicks();
|
||||||
@ -504,7 +572,7 @@ export class GraphService {
|
|||||||
collapseEdges: boolean
|
collapseEdges: boolean
|
||||||
) {
|
) {
|
||||||
this.collapseEdges = collapseEdges;
|
this.collapseEdges = collapseEdges;
|
||||||
this.tooltipService.hideAll();
|
this.broadcast({ type: 'GraphRegenerated' });
|
||||||
|
|
||||||
this.generateCytoscapeLayout(
|
this.generateCytoscapeLayout(
|
||||||
allProjects,
|
allProjects,
|
||||||
@ -609,10 +677,16 @@ export class GraphService {
|
|||||||
|
|
||||||
let ref: VirtualElement = node.popperRef(); // used only for positioning
|
let ref: VirtualElement = node.popperRef(); // used only for positioning
|
||||||
|
|
||||||
this.tooltipService.openProjectNodeToolTip(ref, {
|
this.broadcast({
|
||||||
|
type: 'NodeClick',
|
||||||
|
ref,
|
||||||
id: node.id(),
|
id: node.id(),
|
||||||
type: node.data('type'),
|
|
||||||
tags: node.data('tags'),
|
data: {
|
||||||
|
id: node.id(),
|
||||||
|
type: node.data('type'),
|
||||||
|
tags: node.data('tags'),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -622,20 +696,31 @@ export class GraphService {
|
|||||||
const edge: cy.EdgeSingular = event.target;
|
const edge: cy.EdgeSingular = event.target;
|
||||||
let ref: VirtualElement = edge.popperRef(); // used only for positioning
|
let ref: VirtualElement = edge.popperRef(); // used only for positioning
|
||||||
|
|
||||||
this.tooltipService.openEdgeToolTip(ref, {
|
this.broadcast({
|
||||||
type: edge.data('type'),
|
type: 'EdgeClick',
|
||||||
source: edge.source().id(),
|
ref,
|
||||||
target: edge.target().id(),
|
id: edge.id(),
|
||||||
fileDependencies: edge
|
|
||||||
.source()
|
data: {
|
||||||
.data('files')
|
type: edge.data('type'),
|
||||||
.filter((file) => file.deps && file.deps.includes(edge.target().id()))
|
source: edge.source().id(),
|
||||||
.map((file) => {
|
target: edge.target().id(),
|
||||||
return {
|
fileDependencies: edge
|
||||||
fileName: file.file.replace(`${edge.source().data('root')}/`, ''),
|
.source()
|
||||||
target: edge.target().id(),
|
.data('files')
|
||||||
};
|
.filter(
|
||||||
}),
|
(file) => file.deps && file.deps.includes(edge.target().id())
|
||||||
|
)
|
||||||
|
.map((file) => {
|
||||||
|
return {
|
||||||
|
fileName: file.file.replace(
|
||||||
|
`${edge.source().data('root')}/`,
|
||||||
|
''
|
||||||
|
),
|
||||||
|
target: edge.target().id(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -671,28 +756,7 @@ export class GraphService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getImage() {
|
getImage() {
|
||||||
const bg = selectValueByThemeStatic('#0F172A', '#FFFFFF');
|
const bg = switchValueByDarkMode(this.renderGraph, '#0F172A', '#FFFFFF');
|
||||||
return this.renderGraph.png({ bg, full: true });
|
return this.renderGraph.png({ bg, full: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluateStyles() {
|
|
||||||
if (this.renderGraph) {
|
|
||||||
const container = this.renderGraph.container();
|
|
||||||
this.renderGraph.unmount();
|
|
||||||
|
|
||||||
this.renderGraph.mount(container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setRankDir(rankDir: 'TB' | 'LR') {
|
|
||||||
if (this.renderGraph) {
|
|
||||||
const elements = this.renderGraph.elements();
|
|
||||||
elements
|
|
||||||
.layout({
|
|
||||||
...cytoscapeDagreConfig,
|
|
||||||
...{ rankDir: rankDir },
|
|
||||||
} as CytoscapeDagreConfig)
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
219
graph/ui-graph/src/lib/interfaces.ts
Normal file
219
graph/ui-graph/src/lib/interfaces.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
// nx-ignore-next-line
|
||||||
|
import type {
|
||||||
|
ProjectGraphDependency,
|
||||||
|
ProjectGraphProjectNode,
|
||||||
|
} from '@nrwl/devkit';
|
||||||
|
import { ActionObject, ActorRef, State, StateNodeConfig } from 'xstate';
|
||||||
|
|
||||||
|
// The hierarchical (recursive) schema for the states
|
||||||
|
export interface DepGraphSchema {
|
||||||
|
states: {
|
||||||
|
idle: {};
|
||||||
|
unselected: {};
|
||||||
|
focused: {};
|
||||||
|
textFiltered: {};
|
||||||
|
customSelected: {};
|
||||||
|
tracing: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphPerfReport {
|
||||||
|
renderTime: number;
|
||||||
|
numNodes: number;
|
||||||
|
numEdges: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TracingAlgorithmType = 'shortest' | 'all';
|
||||||
|
// The events that the machine handles
|
||||||
|
|
||||||
|
export type DepGraphUIEvents =
|
||||||
|
| {
|
||||||
|
type: 'setSelectedProjectsFromGraph';
|
||||||
|
selectedProjectNames: string[];
|
||||||
|
perfReport: GraphPerfReport;
|
||||||
|
}
|
||||||
|
| { type: 'selectProject'; projectName: string }
|
||||||
|
| { type: 'deselectProject'; projectName: string }
|
||||||
|
| { type: 'selectAll' }
|
||||||
|
| { type: 'deselectAll' }
|
||||||
|
| { type: 'selectAffected' }
|
||||||
|
| { type: 'setGroupByFolder'; groupByFolder: boolean }
|
||||||
|
| { type: 'setTracingStart'; projectName: string }
|
||||||
|
| { type: 'setTracingEnd'; projectName: string }
|
||||||
|
| { type: 'clearTraceStart' }
|
||||||
|
| { type: 'clearTraceEnd' }
|
||||||
|
| { type: 'setTracingAlgorithm'; algorithm: TracingAlgorithmType }
|
||||||
|
| { type: 'setCollapseEdges'; collapseEdges: boolean }
|
||||||
|
| { type: 'setIncludeProjectsByPath'; includeProjectsByPath: boolean }
|
||||||
|
| { type: 'incrementSearchDepth' }
|
||||||
|
| { type: 'decrementSearchDepth' }
|
||||||
|
| { type: 'setSearchDepthEnabled'; searchDepthEnabled: boolean }
|
||||||
|
| { type: 'setSearchDepth'; searchDepth: number }
|
||||||
|
| { type: 'focusProject'; projectName: string }
|
||||||
|
| { type: 'unfocusProject' }
|
||||||
|
| { type: 'filterByText'; search: string }
|
||||||
|
| { type: 'clearTextFilter' }
|
||||||
|
| {
|
||||||
|
type: 'initGraph';
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'updateGraph';
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The events that the graph actor handles
|
||||||
|
|
||||||
|
export type GraphRenderEvents =
|
||||||
|
| {
|
||||||
|
type: 'notifyGraphInitGraph';
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
groupByFolder: boolean;
|
||||||
|
collapseEdges: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyGraphUpdateGraph';
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
groupByFolder: boolean;
|
||||||
|
collapseEdges: boolean;
|
||||||
|
selectedProjects: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyGraphFocusProject';
|
||||||
|
projectName: string;
|
||||||
|
searchDepth: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyGraphShowProject';
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyGraphHideProject';
|
||||||
|
projectName: 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 RouteEvents =
|
||||||
|
| {
|
||||||
|
type: 'notifyRouteFocusProject';
|
||||||
|
focusedProject: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyRouteGroupByFolder';
|
||||||
|
groupByFolder: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyRouteCollapseEdges';
|
||||||
|
collapseEdges: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyRouteSearchDepth';
|
||||||
|
searchDepthEnabled: boolean;
|
||||||
|
searchDepth: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyRouteUnfocusProject';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyRouteSelectAll';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'notifyRouteSelectAffected';
|
||||||
|
}
|
||||||
|
| { type: 'notifyRouteClearSelect' }
|
||||||
|
| {
|
||||||
|
type: 'notifyRouteTracing';
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
algorithm: TracingAlgorithmType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AllEvents = DepGraphUIEvents | GraphRenderEvents | RouteEvents;
|
||||||
|
|
||||||
|
// The context (extended state) of the machine
|
||||||
|
export interface DepGraphContext {
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjects: string[];
|
||||||
|
selectedProjects: string[];
|
||||||
|
focusedProject: string | null;
|
||||||
|
textFilter: string;
|
||||||
|
includePath: boolean;
|
||||||
|
searchDepth: number;
|
||||||
|
searchDepthEnabled: boolean;
|
||||||
|
groupByFolder: boolean;
|
||||||
|
collapseEdges: boolean;
|
||||||
|
workspaceLayout: {
|
||||||
|
libsDir: string;
|
||||||
|
appsDir: string;
|
||||||
|
};
|
||||||
|
graphActor: ActorRef<GraphRenderEvents>;
|
||||||
|
routeSetterActor: ActorRef<RouteEvents>;
|
||||||
|
routeListenerActor: ActorRef<DepGraphUIEvents>;
|
||||||
|
lastPerfReport: GraphPerfReport;
|
||||||
|
tracing: {
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
algorithm: TracingAlgorithmType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DepGraphStateNodeConfig = StateNodeConfig<
|
||||||
|
DepGraphContext,
|
||||||
|
{},
|
||||||
|
DepGraphUIEvents,
|
||||||
|
ActionObject<DepGraphContext, DepGraphUIEvents>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DepGraphSend = (
|
||||||
|
event: DepGraphUIEvents | DepGraphUIEvents[]
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type DepGraphState = State<
|
||||||
|
DepGraphContext,
|
||||||
|
DepGraphUIEvents,
|
||||||
|
any,
|
||||||
|
{
|
||||||
|
value: any;
|
||||||
|
context: DepGraphContext;
|
||||||
|
}
|
||||||
|
>;
|
||||||
62
graph/ui-graph/src/lib/nx-graph-viz.stories.tsx
Normal file
62
graph/ui-graph/src/lib/nx-graph-viz.stories.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { NxGraphViz } from './nx-graph-viz';
|
||||||
|
|
||||||
|
const Story: ComponentMeta<typeof NxGraphViz> = {
|
||||||
|
component: NxGraphViz,
|
||||||
|
title: 'NxGraphViz',
|
||||||
|
};
|
||||||
|
export default Story;
|
||||||
|
|
||||||
|
const Template: ComponentStory<typeof NxGraphViz> = (args) => (
|
||||||
|
<NxGraphViz {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Primary = Template.bind({});
|
||||||
|
Primary.args = {
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
type: 'app',
|
||||||
|
name: 'app',
|
||||||
|
data: {
|
||||||
|
tags: ['scope:cart'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'lib',
|
||||||
|
name: 'lib',
|
||||||
|
data: {
|
||||||
|
tags: ['scope:cart'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
};
|
||||||
85
graph/ui-graph/src/lib/nx-graph-viz.tsx
Normal file
85
graph/ui-graph/src/lib/nx-graph-viz.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/* nx-ignore-next-line */
|
||||||
|
import type {
|
||||||
|
ProjectGraphProjectNode,
|
||||||
|
ProjectGraphDependency,
|
||||||
|
} from 'nx/src/config/project-graph';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { GraphService } from './graph';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark' | 'system';
|
||||||
|
export interface GraphUiGraphProps {
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
groupByFolder: boolean;
|
||||||
|
workspaceLayout: { appsDir: string; libsDir: string };
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
|
affectedProjectIds: string[];
|
||||||
|
theme: Theme;
|
||||||
|
height: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 NxGraphViz({
|
||||||
|
projects,
|
||||||
|
groupByFolder,
|
||||||
|
workspaceLayout,
|
||||||
|
dependencies,
|
||||||
|
affectedProjectIds,
|
||||||
|
theme,
|
||||||
|
height,
|
||||||
|
}: GraphUiGraphProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [graph, setGraph] = useState<GraphService>(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.handleEvent({
|
||||||
|
type: 'notifyGraphInitGraph',
|
||||||
|
projects,
|
||||||
|
groupByFolder,
|
||||||
|
workspaceLayout,
|
||||||
|
dependencies,
|
||||||
|
affectedProjects: affectedProjectIds,
|
||||||
|
collapseEdges: false,
|
||||||
|
});
|
||||||
|
graph.handleEvent({ type: 'notifyGraphShowAllProjects' });
|
||||||
|
setGraph(graph);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="w-full"
|
||||||
|
style={{ width: '100%', height }}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NxGraphViz;
|
||||||
15
graph/ui-graph/src/lib/styles-graph/dark-mode.ts
Normal file
15
graph/ui-graph/src/lib/styles-graph/dark-mode.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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,45 +1,41 @@
|
|||||||
import { Stylesheet } from 'cytoscape';
|
import { EdgeSingular, Stylesheet } from 'cytoscape';
|
||||||
import { selectValueByThemeDynamic } from '../theme-resolver';
|
|
||||||
import { NrwlPalette } from './palette';
|
import { NrwlPalette } from './palette';
|
||||||
|
import { switchValueByDarkMode } from './dark-mode';
|
||||||
|
|
||||||
const allEdges: Stylesheet = {
|
const allEdges: Stylesheet = {
|
||||||
selector: 'edge',
|
selector: 'edge',
|
||||||
style: {
|
style: {
|
||||||
width: '1px',
|
width: '1px',
|
||||||
'line-color': selectValueByThemeDynamic(
|
'line-color': (node) =>
|
||||||
NrwlPalette.slate_400,
|
switchValueByDarkMode(node, NrwlPalette.slate_400, NrwlPalette.slate_500),
|
||||||
NrwlPalette.slate_500
|
'text-outline-color': (node: EdgeSingular) =>
|
||||||
),
|
switchValueByDarkMode(node, NrwlPalette.slate_400, NrwlPalette.slate_500),
|
||||||
'text-outline-color': selectValueByThemeDynamic(
|
|
||||||
NrwlPalette.slate_400,
|
|
||||||
NrwlPalette.slate_500
|
|
||||||
),
|
|
||||||
'text-outline-width': '0px',
|
'text-outline-width': '0px',
|
||||||
color: selectValueByThemeDynamic(
|
color: (node: EdgeSingular) =>
|
||||||
NrwlPalette.slate_400,
|
switchValueByDarkMode(node, NrwlPalette.slate_400, NrwlPalette.slate_500),
|
||||||
NrwlPalette.slate_500
|
|
||||||
),
|
|
||||||
'curve-style': 'unbundled-bezier',
|
'curve-style': 'unbundled-bezier',
|
||||||
'target-arrow-shape': 'triangle',
|
'target-arrow-shape': 'triangle',
|
||||||
'target-arrow-fill': 'filled',
|
'target-arrow-fill': 'filled',
|
||||||
'target-arrow-color': selectValueByThemeDynamic(
|
'target-arrow-color': (node) =>
|
||||||
NrwlPalette.slate_400,
|
switchValueByDarkMode(node, NrwlPalette.slate_400, NrwlPalette.slate_500),
|
||||||
NrwlPalette.slate_500
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const affectedEdges: Stylesheet = {
|
const affectedEdges: Stylesheet = {
|
||||||
selector: 'edge.affected',
|
selector: 'edge.affected',
|
||||||
style: {
|
style: {
|
||||||
'line-color': selectValueByThemeDynamic(
|
'line-color': (node) =>
|
||||||
NrwlPalette.fuchsia_500,
|
switchValueByDarkMode(
|
||||||
NrwlPalette.pink_500
|
node,
|
||||||
),
|
NrwlPalette.fuchsia_500,
|
||||||
'target-arrow-color': selectValueByThemeDynamic(
|
NrwlPalette.pink_500
|
||||||
NrwlPalette.fuchsia_500,
|
),
|
||||||
NrwlPalette.pink_500
|
'target-arrow-color': (node) =>
|
||||||
),
|
switchValueByDarkMode(
|
||||||
|
node,
|
||||||
|
NrwlPalette.fuchsia_500,
|
||||||
|
NrwlPalette.pink_500
|
||||||
|
),
|
||||||
'curve-style': 'unbundled-bezier',
|
'curve-style': 'unbundled-bezier',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
34
graph/ui-graph/src/lib/styles-graph/label-width.ts
Normal file
34
graph/ui-graph/src/lib/styles-graph/label-width.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { NodeSingular } from 'cytoscape';
|
||||||
|
|
||||||
|
export class LabelWidthCalculator {
|
||||||
|
private cache = new Map<string, number>();
|
||||||
|
private ctx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
calculateWidth(node: NodeSingular): number {
|
||||||
|
if (!this.ctx) {
|
||||||
|
this.ctx = document
|
||||||
|
.createElement('canvas')
|
||||||
|
.getContext('2d') as CanvasRenderingContext2D;
|
||||||
|
}
|
||||||
|
const label = node.data('id');
|
||||||
|
const fStyle = node.style('font-style');
|
||||||
|
const size = node.style('font-size');
|
||||||
|
const family = node.style('font-family');
|
||||||
|
const weight = node.style('font-weight');
|
||||||
|
|
||||||
|
this.ctx.font = fStyle + ' ' + weight + ' ' + size + ' ' + family;
|
||||||
|
|
||||||
|
const cachedValue = this.cache.get(label);
|
||||||
|
|
||||||
|
if (cachedValue) {
|
||||||
|
return cachedValue;
|
||||||
|
} else {
|
||||||
|
const width = this.ctx.measureText(node.data('id')).width;
|
||||||
|
|
||||||
|
this.cache.set(label, width);
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { Stylesheet } from 'cytoscape';
|
import { NodeSingular, Stylesheet } from 'cytoscape';
|
||||||
import { selectValueByThemeDynamic } from '../theme-resolver';
|
|
||||||
import { FONTS } from './fonts';
|
import { FONTS } from './fonts';
|
||||||
import { NrwlPalette } from './palette';
|
import { NrwlPalette } from './palette';
|
||||||
import { LabelWidthCalculator } from './label-width';
|
import { LabelWidthCalculator } from './label-width';
|
||||||
|
import { switchValueByDarkMode } from './dark-mode';
|
||||||
|
|
||||||
const labelWidthCalculator = new LabelWidthCalculator();
|
const labelWidthCalculator = new LabelWidthCalculator();
|
||||||
|
|
||||||
@ -11,26 +11,19 @@ const allNodes: Stylesheet = {
|
|||||||
style: {
|
style: {
|
||||||
'font-size': '32px',
|
'font-size': '32px',
|
||||||
'font-family': FONTS,
|
'font-family': FONTS,
|
||||||
backgroundColor: selectValueByThemeDynamic(
|
backgroundColor: (node) =>
|
||||||
NrwlPalette.slate_600,
|
switchValueByDarkMode(node, NrwlPalette.slate_600, NrwlPalette.slate_200),
|
||||||
NrwlPalette.slate_200
|
|
||||||
),
|
|
||||||
'border-style': 'solid',
|
'border-style': 'solid',
|
||||||
'border-color': selectValueByThemeDynamic(
|
'border-color': (node) =>
|
||||||
NrwlPalette.slate_700,
|
switchValueByDarkMode(node, NrwlPalette.slate_700, NrwlPalette.slate_300),
|
||||||
NrwlPalette.slate_300
|
|
||||||
),
|
|
||||||
'border-width': '1px',
|
'border-width': '1px',
|
||||||
'text-halign': 'center',
|
'text-halign': 'center',
|
||||||
'text-valign': 'center',
|
'text-valign': 'center',
|
||||||
'padding-left': '16px',
|
'padding-left': '16px',
|
||||||
color: selectValueByThemeDynamic(
|
color: (node: NodeSingular) =>
|
||||||
NrwlPalette.slate_200,
|
switchValueByDarkMode(node, NrwlPalette.slate_200, NrwlPalette.slate_600),
|
||||||
NrwlPalette.slate_600
|
|
||||||
),
|
|
||||||
label: 'data(id)',
|
label: 'data(id)',
|
||||||
// width: (node) => node.data('id').length * 16,
|
width: (node: NodeSingular) => labelWidthCalculator.calculateWidth(node),
|
||||||
width: (node) => labelWidthCalculator.calculateWidth(node),
|
|
||||||
'transition-property':
|
'transition-property':
|
||||||
'background-color, border-color, line-color, target-arrow-color',
|
'background-color, border-color, line-color, target-arrow-color',
|
||||||
'transition-duration': 250,
|
'transition-duration': 250,
|
||||||
@ -43,14 +36,10 @@ const focusedNodes: Stylesheet = {
|
|||||||
selector: 'node.focused',
|
selector: 'node.focused',
|
||||||
style: {
|
style: {
|
||||||
color: NrwlPalette.white,
|
color: NrwlPalette.white,
|
||||||
'border-color': selectValueByThemeDynamic(
|
'border-color': (node) =>
|
||||||
NrwlPalette.slate_700,
|
switchValueByDarkMode(node, NrwlPalette.slate_700, NrwlPalette.slate_200),
|
||||||
NrwlPalette.slate_200
|
backgroundColor: (node) =>
|
||||||
),
|
switchValueByDarkMode(node, NrwlPalette.sky_500, NrwlPalette.blue_500),
|
||||||
backgroundColor: selectValueByThemeDynamic(
|
|
||||||
NrwlPalette.sky_500,
|
|
||||||
NrwlPalette.blue_500
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,29 +47,29 @@ const affectedNodes: Stylesheet = {
|
|||||||
selector: 'node.affected',
|
selector: 'node.affected',
|
||||||
style: {
|
style: {
|
||||||
color: NrwlPalette.white,
|
color: NrwlPalette.white,
|
||||||
'border-color': selectValueByThemeDynamic(
|
'border-color': (node) =>
|
||||||
NrwlPalette.fuchsia_800,
|
switchValueByDarkMode(
|
||||||
NrwlPalette.pink_500
|
node,
|
||||||
),
|
NrwlPalette.fuchsia_800,
|
||||||
backgroundColor: selectValueByThemeDynamic(
|
NrwlPalette.pink_500
|
||||||
NrwlPalette.fuchsia_700,
|
),
|
||||||
NrwlPalette.pink_400
|
backgroundColor: (node) =>
|
||||||
),
|
switchValueByDarkMode(
|
||||||
|
node,
|
||||||
|
NrwlPalette.fuchsia_700,
|
||||||
|
NrwlPalette.pink_400
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const parentNodes: Stylesheet = {
|
const parentNodes: Stylesheet = {
|
||||||
selector: ':parent',
|
selector: ':parent',
|
||||||
style: {
|
style: {
|
||||||
'background-opacity': selectValueByThemeDynamic(0.5, 0.8),
|
'background-opacity': (node) => switchValueByDarkMode(node, 0.5, 0.8),
|
||||||
backgroundColor: selectValueByThemeDynamic(
|
backgroundColor: (node) =>
|
||||||
NrwlPalette.slate_700,
|
switchValueByDarkMode(node, NrwlPalette.slate_700, NrwlPalette.slate_50),
|
||||||
NrwlPalette.slate_50
|
'border-color': (node) =>
|
||||||
),
|
switchValueByDarkMode(node, NrwlPalette.slate_500, NrwlPalette.slate_400),
|
||||||
'border-color': selectValueByThemeDynamic(
|
|
||||||
NrwlPalette.slate_500,
|
|
||||||
NrwlPalette.slate_400
|
|
||||||
),
|
|
||||||
'border-style': 'dashed',
|
'border-style': 'dashed',
|
||||||
'border-width': 2,
|
'border-width': 2,
|
||||||
label: 'data(label)',
|
label: 'data(label)',
|
||||||
@ -95,14 +84,10 @@ const highlightedNodes: Stylesheet = {
|
|||||||
selector: 'node.highlight',
|
selector: 'node.highlight',
|
||||||
style: {
|
style: {
|
||||||
color: NrwlPalette.white,
|
color: NrwlPalette.white,
|
||||||
'border-color': selectValueByThemeDynamic(
|
'border-color': (node) =>
|
||||||
NrwlPalette.sky_600,
|
switchValueByDarkMode(node, NrwlPalette.sky_600, NrwlPalette.blue_600),
|
||||||
NrwlPalette.blue_600
|
backgroundColor: (node) =>
|
||||||
),
|
switchValueByDarkMode(node, NrwlPalette.sky_500, NrwlPalette.blue_500),
|
||||||
backgroundColor: selectValueByThemeDynamic(
|
|
||||||
NrwlPalette.sky_500,
|
|
||||||
NrwlPalette.blue_500
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import * as cy from 'cytoscape';
|
import { EdgeSingular, Position, BaseLayoutOptions } from 'cytoscape';
|
||||||
|
|
||||||
export interface CytoscapeDagreConfig extends cy.BaseLayoutOptions {
|
export interface CytoscapeDagreConfig extends BaseLayoutOptions {
|
||||||
// dagre algo options, uses default value on undefined
|
// dagre algo options, uses default value on undefined
|
||||||
nodeSep: number; // the separation between adjacent nodes in the same rank
|
nodeSep: number; // the separation between adjacent nodes in the same rank
|
||||||
edgeSep: number; // the separation between adjacent edges in the same rank
|
edgeSep: number; // the separation between adjacent edges in the same rank
|
||||||
rankSep: number; // the separation between each rank in the layout
|
rankSep: number; // the separation between each rank in the layout
|
||||||
rankDir: 'TB' | 'LR'; // 'TB' for top to bottom flow, 'LR' for left to right,
|
rankDir: 'TB' | 'LR'; // 'TB' for top to bottom flow, 'LR' for left to right,
|
||||||
ranker: 'network-simplex' | 'tight-tree' | 'longest-path'; // Type of algorithm to assign a rank to each node in the input graph. Possible values: 'network-simplex', 'tight-tree' or 'longest-path'
|
ranker: 'network-simplex' | 'tight-tree' | 'longest-path'; // Type of algorithm to assign a rank to each node in the input graph. Possible values: 'network-simplex', 'tight-tree' or 'longest-path'
|
||||||
minLen: (edge: cy.EdgeSingular) => number; // number of ranks to keep between the source and target of the edge
|
minLen: (edge: EdgeSingular) => number; // number of ranks to keep between the source and target of the edge
|
||||||
edgeWeight: (edge: cy.EdgeSingular) => number; // higher weight edges are generally made shorter and straighter than lower weight edges
|
edgeWeight: (edge: EdgeSingular) => number; // higher weight edges are generally made shorter and straighter than lower weight edges
|
||||||
|
|
||||||
// general layout options
|
// general layout options
|
||||||
fit: boolean; // whether to fit to viewport
|
fit: boolean; // whether to fit to viewport
|
||||||
@ -22,7 +22,7 @@ export interface CytoscapeDagreConfig extends cy.BaseLayoutOptions {
|
|||||||
boundingBox:
|
boundingBox:
|
||||||
| { x1: number; y1: number; x2: number; y2: number }
|
| { x1: number; y1: number; x2: number; y2: number }
|
||||||
| { x1: number; y1: number; w: number; h: number }; // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
| { x1: number; y1: number; w: number; h: number }; // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
||||||
transform: (node, pos) => cy.Position; // a function that applies a transform to the final node position
|
transform: (node, pos) => Position; // a function that applies a transform to the final node position
|
||||||
ready: () => void; // on layoutready
|
ready: () => void; // on layoutready
|
||||||
stop: () => void; // on layoutstop
|
stop: () => void; // on layoutstop
|
||||||
}
|
}
|
||||||
@ -2,6 +2,13 @@
|
|||||||
import type { ProjectGraphDependency } from '@nrwl/devkit';
|
import type { ProjectGraphDependency } from '@nrwl/devkit';
|
||||||
import * as cy from 'cytoscape';
|
import * as cy from 'cytoscape';
|
||||||
|
|
||||||
|
export interface EdgeDataDefinition extends cy.NodeDataDefinition {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
type: 'static' | 'dynamic' | 'implicit';
|
||||||
|
}
|
||||||
|
|
||||||
export class ProjectEdge {
|
export class ProjectEdge {
|
||||||
affected = false;
|
affected = false;
|
||||||
|
|
||||||
@ -3,13 +3,13 @@ import type { ProjectGraphProjectNode } from '@nrwl/devkit';
|
|||||||
import * as cy from 'cytoscape';
|
import * as cy from 'cytoscape';
|
||||||
import { parseParentDirectoriesFromFilePath } from '../util';
|
import { parseParentDirectoriesFromFilePath } from '../util';
|
||||||
|
|
||||||
interface NodeDataDefinition extends cy.NodeDataDefinition {
|
export interface NodeDataDefinition extends cy.NodeDataDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: 'app' | 'lib' | 'e2e';
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Ancestor {
|
export interface Ancestor {
|
||||||
id: string;
|
id: string;
|
||||||
parentId: string;
|
parentId: string;
|
||||||
label: string;
|
label: string;
|
||||||
50
graph/ui-graph/src/lib/util.ts
Normal file
50
graph/ui-graph/src/lib/util.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// nx-ignore-next-line
|
||||||
|
import { ProjectGraphDependency } from '@nrwl/devkit';
|
||||||
|
|
||||||
|
export function trimBackSlash(value: string): string {
|
||||||
|
return value.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseParentDirectoriesFromFilePath(
|
||||||
|
path: string,
|
||||||
|
workspaceRoot: string
|
||||||
|
) {
|
||||||
|
const directories = path
|
||||||
|
.replace(workspaceRoot, '')
|
||||||
|
.split('/')
|
||||||
|
.filter((directory) => directory !== '');
|
||||||
|
// last directory is the project
|
||||||
|
directories.pop();
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasPath(
|
||||||
|
dependencies: Record<string, ProjectGraphDependency[]>,
|
||||||
|
target: string,
|
||||||
|
node: string,
|
||||||
|
visited: string[],
|
||||||
|
currentSearchDepth: number,
|
||||||
|
maxSearchDepth: number = -1 // -1 indicates unlimited search depth
|
||||||
|
) {
|
||||||
|
if (target === node) return true;
|
||||||
|
|
||||||
|
if (maxSearchDepth === -1 || currentSearchDepth <= maxSearchDepth) {
|
||||||
|
for (let d of dependencies[node] || []) {
|
||||||
|
if (visited.indexOf(d.target) > -1) continue;
|
||||||
|
visited.push(d.target);
|
||||||
|
if (
|
||||||
|
hasPath(
|
||||||
|
dependencies,
|
||||||
|
target,
|
||||||
|
d.target,
|
||||||
|
visited,
|
||||||
|
currentSearchDepth + 1,
|
||||||
|
maxSearchDepth
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
20
graph/ui-graph/tsconfig.json
Normal file
20
graph/ui-graph/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.storybook/tsconfig.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
28
graph/ui-graph/tsconfig.lib.json
Normal file
28
graph/ui-graph/tsconfig.lib.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["node"],
|
||||||
|
"lib": ["DOM", "es2019"]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||||
|
"../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"jest.config.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.test.jsx",
|
||||||
|
"**/*.stories.ts",
|
||||||
|
"**/*.stories.js",
|
||||||
|
"**/*.stories.jsx",
|
||||||
|
"**/*.stories.tsx"
|
||||||
|
],
|
||||||
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||||
|
}
|
||||||
20
graph/ui-graph/tsconfig.spec.json
Normal file
20
graph/ui-graph/tsconfig.spec.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"jest.config.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.jsx",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -14,6 +14,8 @@ import { Card, Cards } from './lib/tags/cards.component';
|
|||||||
import { card, cards } from './lib/tags/cards.schema';
|
import { card, cards } from './lib/tags/cards.schema';
|
||||||
import { GithubRepository } from './lib/tags/github-repository.component';
|
import { GithubRepository } from './lib/tags/github-repository.component';
|
||||||
import { githubRepository } from './lib/tags/github-repository.schema';
|
import { githubRepository } from './lib/tags/github-repository.schema';
|
||||||
|
import { Graph } from './lib/tags/graph.component';
|
||||||
|
import { graph } from './lib/tags/graph.schema';
|
||||||
import { Iframe } from './lib/tags/iframe.component';
|
import { Iframe } from './lib/tags/iframe.component';
|
||||||
import { iframe } from './lib/tags/iframe.schema';
|
import { iframe } from './lib/tags/iframe.schema';
|
||||||
import { NxCloudSection } from './lib/tags/nx-cloud-section.component';
|
import { NxCloudSection } from './lib/tags/nx-cloud-section.component';
|
||||||
@ -44,6 +46,7 @@ export const getMarkdocCustomConfig = (
|
|||||||
card,
|
card,
|
||||||
cards,
|
cards,
|
||||||
'github-repository': githubRepository,
|
'github-repository': githubRepository,
|
||||||
|
graph,
|
||||||
iframe,
|
iframe,
|
||||||
'install-nx-console': installNxConsole,
|
'install-nx-console': installNxConsole,
|
||||||
'nx-cloud-section': nxCloudSection,
|
'nx-cloud-section': nxCloudSection,
|
||||||
@ -62,6 +65,7 @@ export const getMarkdocCustomConfig = (
|
|||||||
CustomLink,
|
CustomLink,
|
||||||
Fence,
|
Fence,
|
||||||
GithubRepository,
|
GithubRepository,
|
||||||
|
Graph,
|
||||||
Heading,
|
Heading,
|
||||||
Iframe,
|
Iframe,
|
||||||
InstallNxConsole,
|
InstallNxConsole,
|
||||||
|
|||||||
62
nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx
Normal file
62
nx-dev/ui-markdoc/src/lib/tags/graph.component.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { useTheme } from '@nrwl/nx-dev/ui-theme';
|
||||||
|
|
||||||
|
const wrapperClassNames =
|
||||||
|
'w-full place-content-center rounded-md my-6 ring-1 ring-slate-100 dark:ring-slate-700';
|
||||||
|
|
||||||
|
export function Graph({
|
||||||
|
height,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
height: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [theme] = useTheme();
|
||||||
|
|
||||||
|
if (!children || !children.hasOwnProperty('props')) {
|
||||||
|
return (
|
||||||
|
<div className={`${wrapperClassNames} p-4`}>
|
||||||
|
<p>No JSON provided for graph</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedProps;
|
||||||
|
try {
|
||||||
|
parsedProps = JSON.parse(children?.props.children as any);
|
||||||
|
} catch {
|
||||||
|
return (
|
||||||
|
<div className={`${wrapperClassNames} p-4`}>
|
||||||
|
<p>Could not parse JSON for graph:</p>
|
||||||
|
<pre>{children?.props.children as any}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NxGraphViz = dynamic(
|
||||||
|
() => import('@nrwl/graph/ui-graph').then((module) => module.NxGraphViz),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<div className={wrapperClassNames} style={{ height }}>
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={wrapperClassNames}>
|
||||||
|
<NxGraphViz
|
||||||
|
height={height}
|
||||||
|
groupByFolder={false}
|
||||||
|
theme={theme}
|
||||||
|
projects={parsedProps.projects}
|
||||||
|
workspaceLayout={parsedProps.workspaceLayout}
|
||||||
|
dependencies={parsedProps.dependencies}
|
||||||
|
affectedProjectIds={parsedProps.affectedProjectIds}
|
||||||
|
></NxGraphViz>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
nx-dev/ui-markdoc/src/lib/tags/graph.schema.ts
Normal file
13
nx-dev/ui-markdoc/src/lib/tags/graph.schema.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Schema } from '@markdoc/markdoc';
|
||||||
|
|
||||||
|
export const graph: Schema = {
|
||||||
|
render: 'Graph',
|
||||||
|
children: [],
|
||||||
|
|
||||||
|
attributes: {
|
||||||
|
height: {
|
||||||
|
type: 'String',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -2,7 +2,8 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../../dist/out-tsc",
|
"outDir": "../../dist/out-tsc",
|
||||||
"types": ["node"]
|
"types": ["node"],
|
||||||
|
"lib": ["DOM", "es2019"]
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
"@nrwl/eslint-plugin-nx": ["packages/eslint-plugin-nx/src"],
|
"@nrwl/eslint-plugin-nx": ["packages/eslint-plugin-nx/src"],
|
||||||
"@nrwl/expo": ["packages/expo"],
|
"@nrwl/expo": ["packages/expo"],
|
||||||
"@nrwl/express": ["packages/express"],
|
"@nrwl/express": ["packages/express"],
|
||||||
|
"@nrwl/graph/ui-graph": ["graph/ui-graph/src/index.ts"],
|
||||||
"@nrwl/jest": ["packages/jest"],
|
"@nrwl/jest": ["packages/jest"],
|
||||||
"@nrwl/jest/*": ["packages/jest/*"],
|
"@nrwl/jest/*": ["packages/jest/*"],
|
||||||
"@nrwl/js": ["packages/js/src"],
|
"@nrwl/js": ["packages/js/src"],
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
"expo": "packages/expo",
|
"expo": "packages/expo",
|
||||||
"express": "packages/express",
|
"express": "packages/express",
|
||||||
"graph-client": "graph/client",
|
"graph-client": "graph/client",
|
||||||
|
"graph-ui-graph": "graph/ui-graph",
|
||||||
"jest": "packages/jest",
|
"jest": "packages/jest",
|
||||||
"js": "packages/js",
|
"js": "packages/js",
|
||||||
"linter": "packages/linter",
|
"linter": "packages/linter",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user