chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:36 +08:00
commit 8e2a6eb840
10194 changed files with 1593658 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic"
}
]
],
"plugins": []
}
+16
View File
@@ -0,0 +1,16 @@
# This file is used by:
# 1. autoprefixer to adjust CSS to support the below specified browsers
# 2. babel preset-env to adjust included polyfills
#
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# If you need to support different browsers in production, you may tweak the list below.
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.
+28
View File
@@ -0,0 +1,28 @@
import { baseConfig, reactHooksV7Off } from '../../eslint.config.mjs';
import nx from '@nx/eslint-plugin';
export default [
...baseConfig,
...nx.configs['flat/react'],
...reactHooksV7Off,
{
rules: {
'@typescript-eslint/no-implicit-any': 'off',
},
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
rules: {
'no-restricted-imports': [
'error',
{
paths: ['rxjs'],
patterns: ['rxjs/*'],
},
],
},
},
{
ignores: ['src/assets/generated-*'],
},
];
+19
View File
@@ -0,0 +1,19 @@
// nx-ignore-next-line
const nxPreset = require('@nx/jest/preset').default;
module.exports = {
...nxPreset,
displayName: 'graph-client',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/graph/client',
// The mock for widnow.matchMedia has to be in a separete file and imported before the components to test
// for more info check : // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
modulePathIgnorePatterns: [
...(nxPreset.modulePathIgnorePatterns ?? []),
'/graph/client/src/app/machines/match-media-mock.spec.ts',
],
};
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@nx/graph-client",
"version": "0.0.1",
"type": "commonjs",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"devDependencies": {
"jest": "catalog:jest"
},
"dependencies": {
"@nx/graph-internal-ui-render-config": "workspace:",
"@nx/graph-internal-project-details": "workspace:",
"@nx/graph-internal-ui-project-details": "workspace:",
"@nx/graph-migrate": "workspace:",
"@nx/graph-shared": "workspace:",
"@nx/graph-ui-common": "workspace:",
"@nx/graph-ui-code-block": "workspace:"
}
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
plugins: [require('@tailwindcss/postcss'), require('autoprefixer')],
};
+99
View File
@@ -0,0 +1,99 @@
{
"name": "graph-client",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "graph/client/src",
"projectType": "application",
"targets": {
"lint": {
"options": {
"max-warnings": 15
}
},
"generate-dev-environment-js": {
"executor": "nx:run-commands",
"options": {
"commands": ["tsx ./scripts/generate-graph-environment.ts"]
}
},
"generate-graph-base": {
"executor": "nx:run-commands",
"options": {
"commands": ["tsx ./scripts/generate-graph.ts"]
}
},
"generate-graph": {
"executor": "nx:run-commands",
"options": {
"commands": [
"npx nx run graph-client:generate-graph-base",
"npx nx run graph-client:generate-dev-environment-js"
],
"parallel": false
}
},
"build-client": {
"dependsOn": ["nx:build-native"],
"configurations": {
"dev": {},
"nx-console": {},
"release": {},
"watch": {},
"release-static": {},
"dev-e2e": {}
},
"defaultConfiguration": "release"
},
"typecheck": {
"dependsOn": ["^typecheck", "nx:build-native"],
"inputs": [
"...",
"{workspaceRoot}/packages/devkit/tsconfig.lib.json",
"{workspaceRoot}/packages/nx/tsconfig.lib.json"
]
},
"serve-base": {
"configurations": {
"dev": {},
"nx-console": {},
"release": {},
"watch": {},
"release-static": {},
"dev-e2e": {}
},
"defaultConfiguration": "dev"
},
"serve": {
"executor": "nx:run-commands",
"configurations": {
"dev": {
"commands": [
"npx nx run graph-client:generate-dev-environment-js",
"npx nx serve-base graph-client"
],
"parallel": false
},
"dev-e2e": {
"commands": ["npx nx serve-base graph-client --configuration dev-e2e"]
},
"release": {
"commands": ["npx nx serve-base graph-client --configuration release"]
},
"release-static": {
"commands": [
"npx nx serve-base graph-client --configuration release-static"
]
},
"watch": {
"commands": ["npx nx serve-base graph-client --configuration watch"]
},
"nx-console": {
"commands": [
"npx nx serve-base graph-client --configuration nx-console"
]
}
},
"defaultConfiguration": "dev"
}
},
"tags": []
}
+16
View File
@@ -0,0 +1,16 @@
import { RouterProvider } from 'react-router-dom';
import { getRouter } from './get-router';
import {
ThemeProvider,
RankDirProvider,
} from '@nx/graph-internal-ui-render-config';
export function App() {
return (
<ThemeProvider>
<RankDirProvider>
<RouterProvider router={getRouter()} />
</RankDirProvider>
</ThemeProvider>
);
}
@@ -0,0 +1,146 @@
// nx-ignore-next-line
import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces';
// nx-ignore-next-line
import { type FileChange } from 'nx/src/devkit-exports';
import { MigrateUI } from '@nx/graph-migrate';
import { getExternalApiService } from '@nx/graph-shared';
import { useSelector } from '@xstate/react';
import { Interpreter } from 'xstate';
import { MigrateEvents, MigrateState } from './migrate.machine';
export function MigrateApp({
service,
}: {
service: Interpreter<MigrateState, any, MigrateEvents>;
}) {
const externalApiService = getExternalApiService();
const onRunMigration = (
migration: MigrationDetailsWithId,
configuration: {
createCommits: boolean;
}
) => {
externalApiService.postEvent({
type: 'run-migration',
payload: {
migration,
configuration,
},
});
};
const onRunMany = (
migrations: MigrationDetailsWithId[],
configuration: {
createCommits: boolean;
}
) => {
externalApiService.postEvent({
type: 'run-many',
payload: {
migrations,
configuration,
},
});
};
const onCancel = () => {
externalApiService.postEvent({
type: 'cancel',
});
};
const onFinish = (squashCommits: boolean) => {
externalApiService.postEvent({
type: 'finish',
payload: {
squashCommits,
},
});
};
const migrations = useSelector(service, (state) => state.context.migrations);
const nxConsoleMetadata = useSelector(
service,
(state) => state.context.nxConsoleMetadata
);
const onFileClick = (migration: MigrationDetailsWithId, file: FileChange) => {
externalApiService.postEvent({
type: 'file-click',
payload: {
path: file.path,
migration: migration,
},
});
};
const onSkipMigration = (migration: MigrationDetailsWithId) => {
externalApiService.postEvent({
type: 'skip-migration',
payload: { migration },
});
};
const onUndoMigration = (migration: MigrationDetailsWithId) => {
externalApiService.postEvent({
type: 'undo-migration',
payload: { migration },
});
};
const onViewImplementation = (migration: MigrationDetailsWithId) => {
externalApiService.postEvent({
type: 'view-implementation',
payload: { migration },
});
};
const onViewDocumentation = (migration: MigrationDetailsWithId) => {
externalApiService.postEvent({
type: 'view-documentation',
payload: { migration },
});
};
const onStopMigration = (migration: MigrationDetailsWithId) => {
externalApiService.postEvent({
type: 'stop-migration',
payload: { migration },
});
};
const onAcknowledgePrompt = (migration: MigrationDetailsWithId) => {
externalApiService.postEvent({
type: 'acknowledge-prompt',
payload: { migration },
});
};
const onViewPrompt = (migration: MigrationDetailsWithId) => {
externalApiService.postEvent({
type: 'view-prompt',
payload: { migration },
});
};
return (
<MigrateUI
migrations={migrations}
nxConsoleMetadata={nxConsoleMetadata}
onRunMigration={onRunMigration}
onRunMany={onRunMany}
onCancel={onCancel}
onFinish={onFinish}
onFileClick={onFileClick}
onSkipMigration={onSkipMigration}
onUndoMigration={onUndoMigration}
onAcknowledgePrompt={onAcknowledgePrompt}
onViewImplementation={onViewImplementation}
onViewDocumentation={onViewDocumentation}
onViewPrompt={onViewPrompt}
onStopMigration={onStopMigration}
></MigrateUI>
);
}
@@ -0,0 +1,62 @@
import { assign } from '@xstate/immer';
// nx-ignore-next-line
import type {
GeneratedMigrationDetails,
MigrationDetailsWithId,
} from 'nx/src/config/misc-interfaces';
// nx-ignore-next-line
import { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
import { createMachine } from 'xstate';
export interface MigrateState {
migrations: MigrationDetailsWithId[];
nxConsoleMetadata: MigrationsJsonMetadata;
}
export type MigrateEvents = {
type: 'loadData';
migrations: GeneratedMigrationDetails[];
'nx-console': MigrationsJsonMetadata;
};
const initialContext: MigrateState = {
migrations: [],
nxConsoleMetadata: {},
};
export const migrateMachine = createMachine<MigrateState, MigrateEvents>({
/** @xstate-layout N4IgpgJg5mDOIC5QFsCWUBOBDALmAxADYD2WEAIrlgNoAMAuoqAA7Gyo6rEB2TIAHogAsAJgA0IAJ6IAjAGYAnADoAHDJFCAbAFYVtGTNpHtAXzMTuxCHD5pMuMH1btOPPoIQBaTROlfNSkZBwSEA7OYgdth4SqgQhI5IIM4cXLxJHpoySiIqcira2gqitEJBPlKIakpyIiIKMrql6kIqKhFRDkokZJBObKluGbKaynU6oXKKIjKaU+KVCLMiSqG02uvF2jqaeiJmZkA */
predictableActionArguments: true,
preserveActionOrder: true,
id: 'migrate',
initial: 'idle',
context: initialContext,
states: {
idle: {},
loaded: {},
},
on: {
loadData: [
{
target: 'loaded',
actions: [
assign((ctx, event) => {
ctx.migrations = event.migrations.map((migration, index) => {
const duplicateCount = event.migrations
.slice(0, index)
.filter((m) => m.name === migration.name).length;
return {
...migration,
id:
duplicateCount === 0
? migration.name
: `${migration.name}-${duplicateCount}`,
};
});
ctx.nxConsoleMetadata = event['nx-console'];
}),
],
},
],
},
});
@@ -0,0 +1,107 @@
import { ProjectDetails } from '@nx/graph-internal-ui-project-details';
import { getExternalApiService } from '@nx/graph-shared';
import { ErrorToastUI } from '@nx/graph-ui-common';
import { ExpandedTargetsProvider } from '@nx/graph-internal-ui-project-details';
import { useSelector } from '@xstate/react';
import { useCallback } from 'react';
import { Interpreter } from 'xstate';
import {
ProjectDetailsEvents,
ProjectDetailsState,
} from './project-details.machine';
import { GraphStateSerializer } from '@nx/graph';
import { ProjectElement } from '@nx/graph/projects';
export function ProjectDetailsApp({
service,
}: {
service: Interpreter<ProjectDetailsState, any, ProjectDetailsEvents>;
}) {
const externalApiService = getExternalApiService();
const project = useSelector(service, (state) => state.context.project);
const sourceMap = useSelector(service, (state) => state.context.sourceMap);
const errors = useSelector(service, (state) => state.context.errors);
const connectedToCloud = useSelector(
service,
(state) => state.context.connectedToCloud
);
const disabledTaskSyncGenerators = useSelector(
service,
(state) => state.context.disabledTaskSyncGenerators
);
const handleViewInProjectGraph = useCallback(
(data: { projectName: string }) => {
const serializedState = GraphStateSerializer.serialize({
c: {},
s: {
type: 'focused',
nodeId: ProjectElement.makeId('project', data.projectName),
},
});
externalApiService.postEvent({
type: 'open-project-graph',
payload: {
projectName: data.projectName,
serializedProjectGraphState: serializedState,
},
});
},
[externalApiService]
);
const handleViewInTaskGraph = useCallback(
(data: { projectName: string; targetName: string }) => {
externalApiService.postEvent({
type: 'open-task-graph',
payload: {
projectName: data.projectName,
targetName: data.targetName,
},
});
},
[externalApiService]
);
const handleRunTarget = useCallback(
(data: { projectName: string; targetName: string }) => {
externalApiService.postEvent({
type: 'run-task',
payload: { taskId: `${data.projectName}:${data.targetName}` },
});
},
[externalApiService]
);
const handleNxConnect = useCallback(
() =>
externalApiService.postEvent({
type: 'nx-connect',
}),
[externalApiService]
);
if (project && sourceMap) {
return (
<>
<ExpandedTargetsProvider>
<ProjectDetails
project={project as any}
sourceMap={sourceMap}
onViewInProjectGraph={handleViewInProjectGraph}
onViewInTaskGraph={handleViewInTaskGraph}
onRunTarget={handleRunTarget}
viewInProjectGraphPosition="bottom"
connectedToCloud={connectedToCloud}
onNxConnect={handleNxConnect}
disabledTaskSyncGenerators={disabledTaskSyncGenerators}
/>
</ExpandedTargetsProvider>
<ErrorToastUI errors={errors} />
</>
);
} else {
return null;
}
}
@@ -0,0 +1,49 @@
import { interpret } from 'xstate';
import { projectDetailsMachine } from './project-details.machine';
describe('graphMachine', () => {
let service;
beforeEach(() => {
service = interpret(projectDetailsMachine).start();
});
afterEach(() => {
service.stop();
});
it('should have initial idle state', () => {
expect(service.state.value).toEqual('idle');
expect(service.state.context.project).toEqual(null);
expect(service.state.context.errors).toBeUndefined();
});
it('should handle setting data', () => {
service.send({
type: 'loadData',
project: {
type: 'app',
name: 'proj',
data: {},
},
sourceMap: {
root: ['project.json', 'nx-core-build-project-json-nodes'],
},
errors: [{ name: 'ERROR' }],
connectedToCloud: true,
});
expect(service.state.value).toEqual('loaded');
expect(service.state.context.project).toEqual({
type: 'app',
name: 'proj',
data: {},
});
expect(service.state.context.sourceMap).toEqual({
root: ['project.json', 'nx-core-build-project-json-nodes'],
});
expect(service.state.context.errors).toEqual([{ name: 'ERROR' }]);
expect(service.state.context.connectedToCloud).toEqual(true);
});
});
@@ -0,0 +1,60 @@
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
// nx-ignore-next-line
import { GraphError } from 'nx/src/command-line/graph/graph';
import { createMachine } from 'xstate';
import { assign } from '@xstate/immer';
export interface ProjectDetailsState {
project: null | ProjectGraphProjectNode;
sourceMap: null | Record<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
}
export type ProjectDetailsEvents = {
type: 'loadData';
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
connectedToCloud: boolean;
errors?: GraphError[];
disabledTaskSyncGenerators?: string[];
};
const initialContext: ProjectDetailsState = {
project: null,
sourceMap: null,
};
export const projectDetailsMachine = createMachine<
ProjectDetailsState,
ProjectDetailsEvents
>({
predictableActionArguments: true,
preserveActionOrder: true,
id: 'project-view',
initial: 'idle',
context: initialContext,
states: {
idle: {},
loaded: {},
},
on: {
loadData: [
{
target: 'loaded',
actions: [
assign((ctx, event) => {
ctx.project = event.project;
ctx.sourceMap = event.sourceMap;
ctx.connectedToCloud = event.connectedToCloud;
ctx.errors = event.errors;
ctx.disabledTaskSyncGenerators = event.disabledTaskSyncGenerators;
}),
],
},
],
},
});
+117
View File
@@ -0,0 +1,117 @@
import { ExternalApi, getExternalApiService } from '@nx/graph-shared';
import { getRouter } from './get-router';
import { GraphStateSerializer } from '@nx/graph';
import { ProjectElement } from '@nx/graph/projects';
export class ExternalApiImpl extends ExternalApi {
router = getRouter();
externalApiService = getExternalApiService();
constructor() {
super();
this.externalApiService.subscribe(
({ type, payload }: { type: string; payload: any }) => {
if (!this.graphInteractionEventListener) {
console.log('graphInteractionEventListener not registered.');
return;
}
this.graphInteractionEventListener({
type,
payload,
});
}
);
// make sure properties set before are taken into account again
if (window.externalApi?.loadProjectGraph) {
this.loadProjectGraph = window.externalApi.loadProjectGraph;
}
if (window.externalApi?.loadTaskGraph) {
this.loadTaskGraph = window.externalApi.loadTaskGraph;
}
if (window.externalApi?.loadExpandedTaskInputs) {
this.loadExpandedTaskInputs = window.externalApi.loadExpandedTaskInputs;
}
if (window.externalApi?.loadSourceMaps) {
this.loadSourceMaps = window.externalApi.loadSourceMaps;
}
if (window.externalApi?.graphInteractionEventListener) {
this.graphInteractionEventListener =
window.externalApi.graphInteractionEventListener;
}
}
async openProjectDetails(projectName: string, targetName?: string) {
await this.router.navigate(
`/project-details/${encodeURIComponent(projectName)}`
);
if (targetName) {
this.focusTargetInProjectDetails(targetName);
}
}
focusTargetInProjectDetails(targetName: string) {
const currentLocation = this.router.state.location;
const searchParams = new URLSearchParams(currentLocation.search);
searchParams.set('expanded', targetName);
const newUrl = `${currentLocation.pathname}?${searchParams.toString()}`;
this.router.navigate(newUrl);
}
focusProject(projectName: string) {
const serializedState = GraphStateSerializer.serialize({
c: {},
s: {
type: 'focused',
nodeId: ProjectElement.makeId('project', projectName),
},
});
const searchParams = new URLSearchParams();
searchParams.set('graph', serializedState);
this.router.navigate(`/projects?${searchParams.toString()}`);
}
selectAllProjects() {
const serializedState = GraphStateSerializer.serialize({
c: { showMode: 'all' },
});
const searchParams = new URLSearchParams();
searchParams.set('graph', serializedState);
this.router.navigate(`/projects?${searchParams.toString()}`);
}
showAffectedProjects() {
const serializedState = GraphStateSerializer.serialize({
c: { showMode: 'affected' },
});
const searchParams = new URLSearchParams();
searchParams.set('graph', serializedState);
this.router.navigate(`/projects?${searchParams.toString()}`);
}
focusTarget(projectName: string, targetName: string) {
this.router.navigate(
`/tasks?targets=${encodeURIComponent(
targetName
)}&projects=${encodeURIComponent(projectName)}`
);
}
selectAllTargetsByName(targetName: string) {
this.router.navigate(
`/tasks/all?targets=${encodeURIComponent(targetName)}`
);
}
enableExperimentalFeatures() {
localStorage.setItem('showExperimentalFeatures', 'true');
window.appConfig.showExperimentalFeatures = true;
}
disableExperimentalFeatures() {
localStorage.setItem('showExperimentalFeatures', 'false');
window.appConfig.showExperimentalFeatures = false;
}
}
@@ -0,0 +1,572 @@
// nx-ignore-next-line
import {
GraphError,
ProjectGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import {
fetchProjectGraph,
getExternalApiService,
getProjectGraphDataService,
useEnvironmentConfig,
usePoll,
useRouteConstructor,
} from '@nx/graph-shared';
import { useRankDir, useTheme } from '@nx/graph-internal-ui-render-config';
import {
Link,
useNavigate,
useParams,
useRouteLoaderData,
useSearchParams,
} from 'react-router-dom';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import {
NxGraphProjectGraphProvider,
ProjectGraphHandleEventResult,
useProjectGraphContext,
} from '@nx/graph/projects';
import {
NxGraphEmpty,
NxGraphDebuggerPanel,
useElementPanel,
NxGraphToolbar,
NxGraphToolbarItemGroup,
NxGraphThemeTool,
NxGraphRankDirTool,
NxGraphResetLayoutTool,
NxGraphResetGraphTool,
NxGraphShareTool,
NxGraphProjectModeTool,
NxGraphShowModeTool,
NxGraphGroupByFolderTool,
NxGraphCollapseEdgesTool,
NxGraphElementPanel,
NxGraphProjectNodePanelContent,
NxGraphProjectNodePanelHeader,
NxGraphPanel,
NxGraphPanelItemGroup,
NxGraphDependencyDistanceControl,
NxGraphProjectListControl,
NxGraphCompositeProjectNodePanelHeader,
NxGraphProjectEdgePanelHeader,
NxGraphCompositeProjectEdgePanelHeader,
NxGraphCompositeProjectNodePanelContent,
NxGraphCompositeProjectEdgePanelContent,
NxGraphProjectEdgePanelContent,
} from '@nx/graph/ui';
import { ElementData, useRendererEvents } from '@nx/graph';
import { ArrowLeftCircleIcon } from '@heroicons/react/24/outline';
import { ErrorToast } from '@nx/graph-ui-common';
import classNames from 'classnames';
import { Tab, TabGroup, TabList } from '@headlessui/react';
import { useCurrentPath } from '../hooks/use-current-path';
export function ProjectsShell() {
const environmentConfig = useEnvironmentConfig();
const { errors: routerErrors } = useRouteLoaderData(
'selectedWorkspace'
) as ProjectGraphClientResponse;
const [errors, setErrors] = useState<GraphError[] | undefined>(routerErrors);
const renderPlatform = useMemo(
() =>
environmentConfig.environment === 'nx-console' ? 'nx-console' : 'nx',
[environmentConfig.environment]
);
useEffect(() => {
setErrors(routerErrors);
}, [routerErrors]);
return (
<>
<NxGraphProjectGraphProvider renderPlatform={renderPlatform}>
<ProjectsShellInner />
</NxGraphProjectGraphProvider>
<ErrorToast errors={errors} />
</>
);
}
function ProjectsShellInner() {
const projectGraphDataService = getProjectGraphDataService();
const externalApiService = getExternalApiService();
const graphContext = useProjectGraphContext();
const {
containerRef,
send,
orchestrator,
sendRendererConfigEvent,
restoreGraphState,
serializedGraphState,
handleEventResult,
} = graphContext;
const navigate = useNavigate();
const routeConstructor = useRouteConstructor();
const { currentPath } = useCurrentPath();
const { resolvedTheme, theme, setTheme } = useTheme();
const { rankDir, setRankDir } = useRankDir();
const environmentConfig = useEnvironmentConfig();
const params = useParams();
const selectedWorkspaceLoaderData = useRouteLoaderData(
'selectedWorkspace'
) as ProjectGraphClientResponse;
const [lastHash, setLastHash] = useState(selectedWorkspaceLoaderData.hash);
const [searchParams, setSearchParams] = useSearchParams();
const graphState = searchParams.get('graph');
useRendererEvents(orchestrator, {
compositeProjectNodeDoubleTap: ({ data }) => {
if (data.expanded) {
send({ type: 'collapseCompositeNode', compositeNodeId: data.id });
} else {
send({
type: 'selectNode',
nodeId: data.id,
pendingAction: { type: 'expand' },
});
}
},
});
const [element] = useElementPanel<
| ElementData.ProjectNode
| ElementData.CompositeProjectNode
| ElementData.SelectedCompositeProjectEdge
| ElementData.SelectedProjectEdge
>(orchestrator);
useEffect(() => {
if (!orchestrator) return;
send(
{
type: 'initGraph',
projects: selectedWorkspaceLoaderData.projects,
dependencies: selectedWorkspaceLoaderData.dependencies,
fileMap: selectedWorkspaceLoaderData.fileMap,
affectedProjects: selectedWorkspaceLoaderData.affected,
},
{
type: 'updateRendererConfig',
updater: () => ({
autoExpand: 15,
rankDir,
theme: resolvedTheme,
showMode: 'all',
}),
},
{ type: 'showAll' }
);
}, [orchestrator]);
useEffect(() => {
sendRendererConfigEvent({ type: 'themeChange', theme: resolvedTheme });
}, [resolvedTheme]);
useEffect(() => {
sendRendererConfigEvent({ type: 'rankDirChange', rankDir });
}, [rankDir]);
useEffect(() => {
if (!graphState || !orchestrator || graphState === serializedGraphState) {
return;
}
const result = restoreGraphState(graphState);
if (!result) return;
if (result.rendererConfig.rankDir !== rankDir) {
setRankDir(result.rendererConfig.rankDir);
}
if (
result.rendererConfig.platform !== 'nx-console' &&
result.rendererConfig.theme !== resolvedTheme
) {
setTheme(result.rendererConfig.theme);
}
}, [graphState, orchestrator]);
useEffect(() => {
setSearchParams(
(params) => {
const currentGraphParams = params.get('graph');
// this means serialization went wrong
if (serializedGraphState === null && currentGraphParams) {
params.delete('graph');
return params;
}
if (
serializedGraphState &&
currentGraphParams !== serializedGraphState
) {
params.set('graph', serializedGraphState);
}
return params;
},
{ preventScrollReset: true }
);
}, [serializedGraphState]);
usePoll(
async () => {
const response: ProjectGraphClientResponse = await fetchProjectGraph(
projectGraphDataService,
params,
environmentConfig.appConfig
);
if (response.hash === lastHash) {
return;
}
send({
type: 'updateGraph',
projects: response.projects,
dependencies: response.dependencies,
fileMap: response.fileMap,
affectedProjects: response.affected,
});
setLastHash(response.hash);
},
5000,
environmentConfig.watch
);
const onViewProjectDetailsClick = (project: ElementData.ProjectNode) => {
if (handleEventResult.rendererConfig.platform === 'nx-dev') return;
if (handleEventResult.rendererConfig.platform === 'nx-console') {
return externalApiService.postEvent({
type: 'open-project-config',
payload: { projectName: project.name, projectId: project.id },
});
}
return navigate(
routeConstructor(
{ pathname: `/project-details/${encodeURIComponent(project.name)}` },
() => {
const searchParams = new URLSearchParams();
searchParams.set('projectId', project.id);
return searchParams;
}
)
);
};
return (
<div className="flex h-full w-full flex-col">
{environmentConfig.appConfig.showDebugger ? (
<NxGraphDebuggerPanel
position="top"
containerClassName="static bg-white text-slate-700 dark:bg-slate-900 dark:text-slate-300 border-slate-300 dark:border-slate-800"
expandButtonClassName="text-slate-600 dark:text-slate-200 hover:text-slate-900 dark:hover:text-slate-100 bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600"
copyButtonClassName="text-slate-600 dark:text-slate-200 hover:text-slate-900 dark:hover:text-slate-100 bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600"
textareaClassName="bg-slate-50 dark:bg-slate-800"
statsClassName="text-slate-600 dark:text-slate-200"
textareaLabelClassName="text-slate-600 dark:text-slate-200"
/>
) : null}
<div className="relative h-full w-full bg-white dark:bg-slate-900">
<div
ref={containerRef}
className="h-full w-full bg-white dark:bg-slate-900"
id="cytoscape-graph"
></div>
<NxGraphToolbar toolbarClassName="border-slate-300 dark:border-slate-700 divide-slate-300 dark:divide-slate-700 z-50 divide-x">
<NxGraphToolbarItemGroup className="pr-0">
{handleEventResult.rendererConfig.platform !== 'nx-console' ? (
<NxGraphThemeTool
theme={theme}
setTheme={setTheme}
toolPopoverButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolPopoverPanelClassName="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 z-50"
toolPopoverPanelItemClassName="hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-slate-200 dark:text-slate-300"
toolPopoverPanelItemActiveClassName="text-slate-200 bg-sky-500 dark:bg-sky-600"
/>
) : null}
<NxGraphRankDirTool
onRankDirChange={setRankDir}
toolPopoverButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolPopoverPanelClassName="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 z-50"
toolPopoverPanelItemClassName="hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-slate-200 dark:text-slate-300"
toolPopoverPanelItemActiveClassName="text-slate-200 bg-sky-500 dark:bg-sky-600"
/>
</NxGraphToolbarItemGroup>
<NxGraphToolbarItemGroup className="pl-2 pr-0">
<NxGraphResetLayoutTool toolClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600" />
<NxGraphResetGraphTool toolClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600" />
<NxGraphShareTool
toolPopoverButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolPopoverPanelClassName="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 z-50"
toolPopoverPanelItemClassName="hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-slate-200 dark:text-slate-300"
/>
</NxGraphToolbarItemGroup>
<NxGraphToolbarItemGroup className="pl-2 pr-0">
<NxGraphProjectModeTool
toolPopoverButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolPopoverPanelClassName="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 z-50"
toolPopoverPanelItemClassName="hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-slate-200 dark:text-slate-300"
toolPopoverPanelItemActiveClassName="text-slate-200 bg-sky-500 dark:bg-sky-600"
/>
{handleEventResult.rendererConfig.mode === 'individual' && (
<>
<NxGraphGroupByFolderTool
toolClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolActiveClassName="bg-sky-500 dark:bg-sky-600 text-white dark:text-white"
/>
<NxGraphCollapseEdgesTool
toolClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolActiveClassName="bg-sky-500 dark:bg-sky-600 text-white dark:text-white"
toolDisabledClassName="opacity-50 cursor-not-allowed hover:bg-transparent hover:text-slate-400 dark:hover:text-slate-600 border-slate-300 dark:border-slate-600"
/>
</>
)}
</NxGraphToolbarItemGroup>
<NxGraphToolbarItemGroup last className="pl-2">
<NxGraphShowModeTool
toolPopoverButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolPopoverPanelClassName="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 z-50"
toolPopoverPanelItemClassName="hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-slate-200 dark:text-slate-300"
toolPopoverPanelItemActiveClassName="text-slate-200 bg-sky-500 dark:bg-sky-600"
/>
</NxGraphToolbarItemGroup>
</NxGraphToolbar>
<NxGraphEmpty className="z-40 flex items-center gap-2 bg-white text-slate-700 dark:bg-slate-900 dark:text-slate-300">
<ArrowLeftCircleIcon className="h-6 w-6 text-slate-600 dark:text-slate-400" />
<h4 className="text-slate-700 dark:text-slate-300">
No projects found for the current graph configuration.
</h4>
</NxGraphEmpty>
{handleEventResult.rendererConfig.platform === 'nx-console' ? (
<ProjectGraphControlsPanel handleEventResult={handleEventResult} />
) : (
<div className="absolute bottom-0 left-0 top-4 flex flex-col">
<div className="mb-2 ml-4 min-w-96 max-w-96">
<TabGroup>
<TabList className="flex rounded-lg bg-slate-100 p-1 dark:bg-slate-800">
<Tab
className={classNames(
'text-md flex-1 rounded-md px-3 py-1.5 font-medium transition-colors',
currentPath === '/projects'
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-slate-100'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-100'
)}
>
Projects
</Tab>
<Tab
className={classNames(
'text-md flex-1 rounded-md px-3 py-1.5 font-medium transition-colors',
currentPath === '/tasks'
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-slate-100'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-100'
)}
onClick={() => navigate('../tasks')}
>
Tasks
</Tab>
</TabList>
</TabGroup>
</div>
<div className="relative flex-1">
<ProjectGraphControlsPanel
handleEventResult={handleEventResult}
closable={false}
/>
</div>
</div>
)}
<NxGraphElementPanel
element={element}
panelContainerClassName="border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900"
panelHeaderClassName="border-slate-300 dark:border-slate-700"
panelContentContainerClassName="divide-slate-300 dark:divide-slate-700"
header={{
project: (element, { open, close }) => (
<NxGraphProjectNodePanelHeader
element={element}
open={open}
close={close}
badgeClassName="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600 uppercase"
elementNameClassName="text-slate-900 dark:text-slate-100"
closeButtonClassName="hover:bg-slate-100 dark:hover:bg-slate-700"
/>
),
'composite-project': (element, { open, close }) => (
<NxGraphCompositeProjectNodePanelHeader
element={element}
open={open}
close={close}
badgeClassName="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600 uppercase"
elementNameClassName="text-slate-900 dark:text-slate-100"
closeButtonClassName="hover:bg-slate-100 dark:hover:bg-slate-700"
/>
),
'project-edge': (element, { open, close }) => (
<NxGraphProjectEdgePanelHeader
element={element}
open={open}
close={close}
badgeClassName="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600 uppercase"
closeButtonClassName="hover:bg-slate-100 dark:hover:bg-slate-700"
/>
),
'composite-project-edge': (element, { open, close }) => (
<NxGraphCompositeProjectEdgePanelHeader
element={element}
open={open}
close={close}
badgeClassName="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600 uppercase"
closeButtonClassName="hover:bg-slate-100 dark:hover:bg-slate-700"
/>
),
}}
>
{{
project: (element) => (
<NxGraphProjectNodePanelContent
element={element}
sectionHeadingClassName="text-slate-900 dark:text-slate-100"
sectionTextClassName="text-slate-700 dark:text-slate-300"
tagBadgeClassName="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600"
actionButtonClassName="bg-slate-100/60 dark:bg-slate-700/60 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-slate-100 border-slate-300 dark:border-slate-600"
cancelActionButtonClassName="bg-red-500 dark:bg-red-600 text-white hover:bg-red-600 dark:hover:bg-red-700 border-red-500 dark:border-red-600"
dependencyItemClassName="text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700"
dependentItemClassName="text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700"
emptyItemListClassName="text-slate-600 dark:text-slate-400"
traceAlgorithmButtonClassName="bg-slate-100/60 dark:bg-slate-700/60 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-slate-100 border-slate-300 dark:border-slate-600"
traceAlgorithmActiveButtonClassName="bg-sky-500 dark:bg-sky-600 text-white hover:bg-sky-600 dark:hover:bg-sky-700 border-sky-500 dark:border-sky-600"
traceableProjectItemClassName="text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 justify-between"
traceableProjectSelectedItemClassName="bg-sky-500/10 dark:bg-sky-600/10 text-slate-900 dark:text-slate-100"
traceEndFilterInputClassName="bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:border-sky-500 dark:focus:border-sky-400"
onViewProjectDetailsClick={() =>
onViewProjectDetailsClick(element)
}
/>
),
'project-edge': (element) => (
<NxGraphProjectEdgePanelContent
element={element}
sectionHeadingClassName="text-slate-900 dark:text-slate-100"
sectionTextClassName="text-slate-700 dark:text-slate-300"
projectNameClassName="text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700"
sectionListContainerClassName="border-slate-200 dark:border-slate-700"
sectionListSectionClassName="bg-slate-50 dark:bg-slate-800"
sectionListHeaderClassName="text-slate-700 dark:text-slate-300 bg-slate-200 dark:bg-slate-800"
sectionListHeaderLabelClassName="text-slate-600 dark:text-slate-400"
sectionListItemsClassName="divide-slate-200 dark:divide-slate-700"
sectionListItemClassName="bg-slate-50 dark:bg-slate-700"
sectionListItemLabelClassName="text-slate-900 dark:text-slate-100"
/>
),
'composite-project': (element) => (
<NxGraphCompositeProjectNodePanelContent
element={element}
sectionHeadingClassName="text-slate-900 dark:text-slate-100"
sectionTextClassName="text-slate-700 dark:text-slate-300"
actionButtonClassName="bg-slate-100/60 dark:bg-slate-700/60 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-slate-100 border-slate-300 dark:border-slate-600"
cancelActionButtonClassName="bg-red-500 dark:bg-red-600 text-white hover:bg-red-600 dark:hover:bg-red-700 border-red-500 dark:border-red-600"
confirmActionButtonClassName="bg-sky-500 dark:bg-sky-600 text-white hover:bg-sky-600 dark:hover:bg-sky-700 border-sky-500 dark:border-sky-600"
multiselectContainerClassName="text-slate-700 dark:text-slate-300"
multiselectFilterInputClassName="bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 focus:border-sky-500 dark:focus:border-sky-400"
multiselectEmptyStateClassName="text-slate-600 dark:text-slate-400"
multiselectListItemClassName="hover:bg-slate-100 dark:hover:bg-slate-700"
multiselectCheckboxClassName="border-slate-300 dark:border-slate-600 accent-sky-500"
multiselectLabelClassName="text-slate-700 dark:text-slate-300"
multiselectSectionHeaderClassName="text-slate-900 dark:text-slate-100 border-slate-300 dark:border-slate-700"
/>
),
'composite-project-edge': (element) => (
<NxGraphCompositeProjectEdgePanelContent
element={element}
sectionHeadingClassName="text-slate-900 dark:text-slate-100"
sectionTextClassName="text-slate-700 dark:text-slate-300"
projectNameClassName="text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700"
sectionListContainerClassName="border-slate-200 dark:border-slate-700"
sectionListSectionClassName="bg-slate-50 dark:bg-slate-800"
sectionListHeaderClassName="text-slate-700 dark:text-slate-300 bg-slate-200 dark:bg-slate-800"
sectionListHeaderLabelClassName="text-slate-600 dark:text-slate-400"
sectionListItemsClassName="divide-slate-200 dark:divide-slate-700"
sectionListItemClassName="bg-slate-50 dark:bg-slate-700"
sectionListItemLabelClassName="text-slate-900 dark:text-slate-100"
/>
),
}}
</NxGraphElementPanel>
</div>
</div>
);
}
function ProjectGraphControlsPanel({
handleEventResult,
closable = true,
}: {
handleEventResult: ProjectGraphHandleEventResult;
closable?: boolean;
}) {
return (
<NxGraphPanel
anchor="left"
closable={closable}
initialOpen
panelContainerClassName="border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900"
panelHeaderClassName="border-slate-300 dark:border-slate-700"
panelCloseButtonClassName="hover:bg-slate-100 dark:hover:bg-slate-700"
panelContentContainerClassName="divide-slate-300 dark:divide-slate-700"
panelTriggerButtonClassName="border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700"
>
{handleEventResult.state.type === 'focused' ? (
<NxGraphPanelItemGroup>
<NxGraphDependencyDistanceControl
controlLabelClassName="text-slate-900 dark:text-slate-100"
controlDescriptionClassName="text-slate-600 dark:text-slate-400"
controlInputClassName="border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:border-sky-500 dark:focus:border-sky-400 focus:ring-sky-500/20 dark:focus:ring-sky-400/20"
removeFocusButtonClassName="bg-red-500 dark:bg-red-600 text-white"
/>
</NxGraphPanelItemGroup>
) : null}
<NxGraphPanelItemGroup last>
<NxGraphProjectListControl
projectItemClassName={({ rendered }) =>
rendered
? 'text-slate-900 dark:text-slate-50 hover:bg-slate-100 dark:hover:bg-slate-700'
: 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'
}
projectItemSelectIconClassName={() =>
'hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-sky-500 dark:hover:text-sky-400'
}
searchInputClassName="bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 border-slate-300 dark:border-slate-600 focus:border-transparent focus:ring-2 focus:ring-sky-500/20 dark:focus:ring-sky-400/20 focus:outline-none data-[focus]:border-sky-500 dark:data-[focus]:border-sky-400 data-[focus]:ring-2 data-[focus]:ring-sky-500/20 dark:data-[focus]:ring-sky-400/20"
searchInputClearButtonClassName="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-white border-slate-300 dark:border-slate-600"
projectListEmptyClassName="text-slate-600 dark:text-slate-400"
projectSectionHeaderClassName="text-slate-900 dark:text-slate-100"
projectItemCheckboxClassName={({ rendered }) =>
classNames(
'border-slate-300 dark:border-slate-600',
rendered ? 'bg-sky-500' : ''
)
}
projectItemTextClassName={({ rendered }) =>
rendered
? 'text-slate-900 dark:text-slate-100'
: 'text-slate-400 dark:text-slate-400'
}
/>
</NxGraphPanelItemGroup>
</NxGraphPanel>
);
}
@@ -0,0 +1,668 @@
// nx-ignore-next-line
import type {
GraphError,
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import type { ProjectGraphProjectNode } from '@nx/devkit';
import { useTaskGraphContext, NxGraphTaskGraphProvider } from '@nx/graph/tasks';
import {
getExternalApiService,
getProjectGraphDataService,
useEnvironmentConfig,
useRouteConstructor,
} from '@nx/graph-shared';
import { useRankDir, useTheme } from '@nx/graph-internal-ui-render-config';
import {
useNavigate,
useRouteLoaderData,
useSearchParams,
} from 'react-router-dom';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { ErrorToast } from '@nx/graph-ui-common';
import {
NxGraphDebuggerPanel,
NxGraphEmpty,
NxGraphProjectListControl,
NxGraphCustomProjectItem,
NxGraphDropdownMultiselect,
NxGraphTaskNodePanelContent,
useElementPanel,
NxGraphToolbar,
NxGraphToolbarItemGroup,
NxGraphThemeTool,
NxGraphRankDirTool,
NxGraphResetLayoutTool,
NxGraphResetGraphTool,
NxGraphShareTool,
NxGraphGroupByProjectTool,
NxGraphPanel,
NxGraphElementPanel,
NxGraphTaskNodePanelHeader,
} from '@nx/graph/ui';
import {
ArrowLeftCircleIcon,
DocumentMagnifyingGlassIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
import { useCurrentPath } from '../hooks/use-current-path';
import { createTaskName } from '../util';
import classNames from 'classnames';
import { ElementData, RenderPlatform } from '@nx/graph';
import { Tab, TabGroup, TabList } from '@headlessui/react';
export function TasksShell() {
const environmentConfig = useEnvironmentConfig();
const [errors, setErrors] = useState<GraphError[] | undefined>(undefined);
const renderPlatform = useMemo(
() =>
environmentConfig.environment === 'nx-console' ? 'nx-console' : 'nx',
[environmentConfig.environment]
);
const { errors: routerErrors } = useRouteLoaderData(
'selectedWorkspace'
) as ProjectGraphClientResponse;
const tasksRouteData = useRouteLoaderData('tasks') as TaskGraphClientResponse;
const allTasksRouteData = useRouteLoaderData(
'tasksAll'
) as TaskGraphClientResponse;
const taskGraphError = useMemo(
() => allTasksRouteData?.error || tasksRouteData?.error,
[allTasksRouteData, tasksRouteData]
);
useLayoutEffect(() => {
const allErrors: GraphError[] = [];
if (routerErrors) {
allErrors.push(...routerErrors);
}
if (taskGraphError) {
allErrors.push({
message: taskGraphError,
pluginName: 'graph',
cause: 'TaskGraphClientResponse error',
name: 'graph',
stack: '',
});
}
if (allErrors.length === 0) return;
setErrors(allErrors);
}, [routerErrors, taskGraphError]);
return (
<>
<NxGraphTaskGraphProvider renderPlatform={renderPlatform}>
<TasksShellInner />
</NxGraphTaskGraphProvider>
<ErrorToast errors={errors} />
</>
);
}
function TasksShellInner() {
const graphContext = useTaskGraphContext();
const {
containerRef,
send,
orchestrator,
sendRendererConfigEvent,
restoreGraphState,
serializedGraphState,
handleEventResult,
} = graphContext;
const navigate = useNavigate();
const { currentPath } = useCurrentPath();
const { resolvedTheme, theme, setTheme } = useTheme();
const { rankDir, setRankDir } = useRankDir();
const environmentConfig = useEnvironmentConfig();
const [searchParams, setSearchParams] = useSearchParams();
const createRoute = useRouteConstructor();
const isAllRoute = useMemo(() => currentPath === '/tasks/all', [currentPath]);
const selectedWorkspaceLoaderData = useRouteLoaderData(
'selectedWorkspace'
) as ProjectGraphClientResponse & { targets: string[] };
const tasksRouteData = useRouteLoaderData('tasks') as TaskGraphClientResponse;
const allTasksRouteData = useRouteLoaderData(
'tasksAll'
) as TaskGraphClientResponse;
const tasksData = useMemo(() => {
return allTasksRouteData || tasksRouteData;
}, [allTasksRouteData, tasksRouteData]);
const selectedTargets = useMemo(() => {
const targetsParam = searchParams.get('targets');
return targetsParam ? targetsParam.split(' ').filter(Boolean) : [];
}, [searchParams]);
const allProjectsWithTargetsAndNoErrors = useMemo(
() =>
selectedWorkspaceLoaderData.projects.filter(
(project) =>
selectedTargets.length > 0 &&
selectedTargets.some((target) =>
project.data.targets?.hasOwnProperty(target)
) &&
!tasksData.error // If there's a global error, exclude all projects
),
[selectedWorkspaceLoaderData.projects, selectedTargets, tasksData.error]
);
const selectedProjects = useMemo(
() =>
isAllRoute
? allProjectsWithTargetsAndNoErrors
: selectedWorkspaceLoaderData.projects.filter((project) =>
(searchParams.get('projects') ?? '')
.split(' ')
.includes(project.name)
),
[allProjectsWithTargetsAndNoErrors, searchParams, isAllRoute]
);
const selectedProjectNames = useMemo(
() => selectedProjects.map((project) => project.name),
[selectedProjects]
);
const graphState = searchParams.get('graph');
const [element] = useElementPanel<
ElementData.TaskNode | ElementData.CompositeTaskNode
>(orchestrator);
useEffect(() => {
if (!orchestrator) return;
send({
type: 'initGraph',
projects: selectedWorkspaceLoaderData.projects,
taskGraph: tasksData.taskGraph,
});
}, [orchestrator]);
useEffect(() => {
if (!orchestrator) return;
send({
type: 'mergeGraph',
projects: selectedWorkspaceLoaderData.projects,
taskGraph: tasksData.taskGraph,
});
}, [orchestrator, tasksData.taskGraph]);
useEffect(() => {
sendRendererConfigEvent({ type: 'themeChange', theme: resolvedTheme });
}, [resolvedTheme]);
useEffect(() => {
sendRendererConfigEvent({ type: 'rankDirChange', rankDir });
}, [rankDir]);
useEffect(() => {
if (!orchestrator) return;
const taskIds = selectedProjects.flatMap((project) =>
selectedTargets.map((target) => {
const resolvedConfiguration =
project.data.targets?.[target]?.defaultConfiguration;
return createTaskName(project.name, target, resolvedConfiguration);
})
);
send({ type: 'show', taskIds });
}, [selectedProjects, selectedTargets, send, orchestrator]);
useEffect(() => {
if (!graphState || !orchestrator || graphState === serializedGraphState) {
return;
}
const result = restoreGraphState(graphState);
if (!result) return;
if (result.rendererConfig.rankDir !== rankDir) {
setRankDir(result.rendererConfig.rankDir);
}
if (result.rendererConfig.theme !== resolvedTheme) {
setTheme(result.rendererConfig.theme);
}
}, [graphState, orchestrator]);
useEffect(() => {
setSearchParams(
(params) => {
const currentGraphParams = params.get('graph');
// this means serialization went wrong
if (serializedGraphState === null && currentGraphParams) {
params.delete('graph');
return params;
}
if (
serializedGraphState &&
currentGraphParams !== serializedGraphState
) {
params.set('graph', serializedGraphState);
}
return params;
},
{ preventScrollReset: true }
);
}, [serializedGraphState]);
const onTargetsSelectionChange = (selectedIds: string[]) => {
const newSearchParams = new URLSearchParams(searchParams);
// remove selected projects when targets change
newSearchParams.delete('projects');
if (selectedIds.length > 0) {
newSearchParams.set('targets', selectedIds.join(' '));
} else {
newSearchParams.delete('targets');
}
setSearchParams((prev) => {
prev.delete('projects');
if (selectedIds.length > 0) {
prev.set('targets', selectedIds.join(' '));
} else {
prev.delete('targets');
}
return prev;
});
};
const onToggleProject = (projectName: string, isSelected: boolean) => {
if (isSelected) {
const newSelectedProjects = selectedProjectNames.filter(
(selectedProject) => selectedProject !== projectName
);
const newParams = new URLSearchParams(searchParams);
if (newSelectedProjects.length === 0) {
newParams.delete('projects');
} else {
newParams.set('projects', newSelectedProjects.join(' '));
}
navigate(
createRoute(
{ pathname: '/tasks', search: newParams.toString() },
() => newParams
)
);
return;
}
const newSelectedProjects = [...selectedProjectNames, projectName];
const allProjectsSelected =
newSelectedProjects.length === allProjectsWithTargetsAndNoErrors.length;
const newParams = new URLSearchParams(searchParams);
if (allProjectsSelected) {
newParams.delete('projects');
} else {
newParams.set('projects', newSelectedProjects.join(' '));
}
navigate(
createRoute(
{
pathname: allProjectsSelected ? '/tasks/all' : '/tasks',
search: newParams.toString(),
},
() => newParams
)
);
};
return (
<div className="flex h-full w-full flex-col">
{environmentConfig.appConfig.showDebugger ? (
<NxGraphDebuggerPanel
position="top"
containerClassName="static bg-white text-slate-700 dark:bg-slate-900 dark:text-slate-300 border-slate-300 dark:border-slate-800"
expandButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600"
copyButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600"
textareaClassName="bg-slate-50 dark:bg-slate-800"
/>
) : null}
<div className="relative h-full w-full bg-white dark:bg-slate-900">
<div
ref={containerRef}
className="h-full w-full bg-white dark:bg-slate-900"
id="cytoscape-graph"
></div>
<NxGraphToolbar toolbarClassName="border-slate-300 dark:border-slate-700 divide-slate-300 dark:divide-slate-700 z-50 divide-x">
<NxGraphToolbarItemGroup className="pr-0">
<NxGraphThemeTool
theme={theme}
setTheme={setTheme}
toolPopoverButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolPopoverPanelClassName="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 z-50"
toolPopoverPanelItemClassName="hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-slate-200 dark:text-slate-300"
toolPopoverPanelItemActiveClassName="text-slate-200 bg-sky-500 dark:bg-sky-600"
/>
<NxGraphRankDirTool
onRankDirChange={setRankDir}
toolPopoverButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolPopoverPanelClassName="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 z-50"
toolPopoverPanelItemClassName="hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-slate-200 dark:text-slate-300"
toolPopoverPanelItemActiveClassName="text-slate-200 bg-sky-500 dark:bg-sky-600"
/>
</NxGraphToolbarItemGroup>
<NxGraphToolbarItemGroup className="pl-2 pr-0">
<NxGraphGroupByProjectTool
toolClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolActiveClassName="bg-sky-500 dark:bg-sky-600 text-white dark:text-white"
/>
</NxGraphToolbarItemGroup>
<NxGraphToolbarItemGroup last className="pl-2">
<NxGraphResetLayoutTool toolClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600" />
<NxGraphResetGraphTool toolClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600" />
<NxGraphShareTool
toolPopoverButtonClassName="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 border-slate-300 dark:border-slate-600"
toolPopoverPanelClassName="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 z-50"
toolPopoverPanelItemClassName="hover:bg-sky-500 dark:hover:bg-sky-600 hover:text-slate-200 dark:text-slate-300"
/>
</NxGraphToolbarItemGroup>
</NxGraphToolbar>
<NxGraphEmpty className="z-30 flex items-center gap-2 bg-white text-slate-700 dark:bg-slate-900 dark:text-slate-300">
<ArrowLeftCircleIcon className="h-6 w-6 text-slate-600 dark:text-slate-400" />
<h4 className="text-slate-700 dark:text-slate-300">
No tasks found for the current graph configuration.
</h4>
</NxGraphEmpty>
{handleEventResult.rendererConfig.platform === 'nx-console' ? (
<TaskGraphControlsPanel
targets={selectedWorkspaceLoaderData.targets}
selectedTargets={selectedTargets}
selectedProjects={selectedProjectNames}
allProjects={allProjectsWithTargetsAndNoErrors}
error={tasksData.error}
onTargetsSelectionChange={onTargetsSelectionChange}
onToggleProject={onToggleProject}
/>
) : (
<div className="absolute bottom-0 left-0 top-4 z-50 flex flex-col">
<TabGroup className="mb-2 ml-4 min-w-96 max-w-96">
<TabList className="flex rounded-lg bg-slate-100 p-1 dark:bg-slate-800">
<Tab
className={classNames(
'text-md flex-1 rounded-md px-3 py-1.5 font-medium transition-colors',
currentPath === '/projects'
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-slate-100'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-100'
)}
onClick={() => navigate('../projects')}
>
Projects
</Tab>
<Tab
className={classNames(
'text-md flex-1 rounded-md px-3 py-1.5 font-medium transition-colors',
currentPath === '/tasks'
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-slate-100'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-100'
)}
>
Tasks
</Tab>
</TabList>
</TabGroup>
<div className="relative flex-1">
<TaskGraphControlsPanel
closable={false}
targets={selectedWorkspaceLoaderData.targets}
selectedTargets={selectedTargets}
selectedProjects={selectedProjectNames}
allProjects={allProjectsWithTargetsAndNoErrors}
error={tasksData.error}
onTargetsSelectionChange={onTargetsSelectionChange}
onToggleProject={onToggleProject}
/>
</div>
</div>
)}
<NxGraphElementPanel
element={element}
panelContainerClassName="border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900"
panelHeaderClassName="border-slate-300 dark:border-slate-700"
panelContentContainerClassName="divide-slate-300 dark:divide-slate-700"
header={{
task: (element, { open, close }) => (
<NxGraphTaskNodePanelHeader
element={element}
open={open}
close={close}
elementNameClassName="text-slate-900 dark:text-slate-100"
closeButtonClassName="hover:bg-slate-100 dark:hover:bg-slate-700"
taskFlagBadgeClassName="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600"
/>
),
}}
>
{{
task: (element) => (
<TaskNodeDetails
element={element}
platform={handleEventResult.rendererConfig.platform}
/>
),
}}
</NxGraphElementPanel>
</div>
</div>
);
}
interface TaskNodeDetailsProps {
platform: RenderPlatform;
element: ElementData.TaskNode;
}
function TaskNodeDetails({ element, platform }: TaskNodeDetailsProps) {
const projectGraphDataService = getProjectGraphDataService();
const externalApiService = getExternalApiService();
const routeConstructor = useRouteConstructor();
const navigate = useNavigate();
const [inputs, setInputs] = useState<Record<string, string[]> | null>(null);
const [isLoadingInputs, setIsLoadingInputs] = useState(true);
useEffect(() => {
// Only fetch if the service has the method
if (!projectGraphDataService.getExpandedTaskInputs) return;
projectGraphDataService
.getExpandedTaskInputs(element.name)
.then((expandedInputs) => {
setInputs(expandedInputs);
})
.catch((error) => {
console.error('Failed to fetch task inputs:', error);
})
.finally(() => {
setIsLoadingInputs(false);
});
}, [element.name, projectGraphDataService]);
const onViewProjectDetailsClick = () => {
if (platform === 'nx-dev') return;
const projectName = element.project.name;
const targetName = element.task.target.target;
if (platform === 'nx-console') {
return externalApiService.postEvent({
type: 'open-project-config',
payload: { projectName, targetName },
});
}
navigate(
routeConstructor(
{
pathname: `/project-details/${encodeURIComponent(projectName)}`,
search: `expanded=${targetName}`,
},
false
)
);
};
const onRunTaskClick = () => {
if (platform !== 'nx-console') return;
externalApiService.postEvent({
type: 'run-task',
payload: { taskId: element.id, takeName: element.name },
});
};
return (
<NxGraphTaskNodePanelContent
element={element}
inputs={inputs}
isLoadingInputs={isLoadingInputs}
sectionHeadingClassName="text-slate-900 dark:text-slate-100"
sectionTextClassName="text-slate-700 dark:text-slate-300"
actionButtonClassName="bg-slate-100/60 dark:bg-slate-700/60 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-slate-100 border-slate-300 dark:border-slate-600"
sectionListContainerClassName="border-slate-200 dark:border-slate-700"
sectionListSectionClassName="bg-slate-50 dark:bg-slate-800"
sectionListHeaderClassName="text-slate-700 dark:text-slate-300 bg-slate-200 dark:bg-slate-800"
sectionListHeaderLabelClassName="text-slate-600 dark:text-slate-400"
sectionListItemsClassName="divide-slate-200 dark:divide-slate-600"
sectionListItemClassName="bg-slate-50 dark:bg-slate-700"
sectionListItemLabelClassName="text-slate-900 dark:text-slate-100"
loadingSkeletonHeaderClassName="bg-slate-200 dark:bg-slate-600"
loadingSkeletonItemClassName="bg-slate-100 dark:bg-slate-700"
loadingSkeletonListClassName="bg-slate-50 dark:bg-slate-800"
>
{({ NxGraphActionButton }) => (
<>
<NxGraphActionButton onClick={onViewProjectDetailsClick}>
<DocumentMagnifyingGlassIcon className="h-4 w-4" />
<span className="text-sm">View in Project Details</span>
</NxGraphActionButton>
{platform === 'nx-console' ? (
<NxGraphActionButton onClick={onRunTaskClick}>
<PlayIcon className="h-4 w-4" />
<span className="text-sm">Run task</span>
</NxGraphActionButton>
) : null}
</>
)}
</NxGraphTaskNodePanelContent>
);
}
interface TaskGraphControlsPanelProps {
targets: string[];
selectedTargets: string[];
selectedProjects: string[];
allProjects: ProjectGraphProjectNode[];
closable?: boolean;
error?: string;
onTargetsSelectionChange: (selectedIds: string[]) => void;
onToggleProject: (projectName: string, isSelected: boolean) => void;
}
function TaskGraphControlsPanel({
targets,
selectedTargets,
selectedProjects,
closable = true,
allProjects,
error = '',
onTargetsSelectionChange,
onToggleProject,
}: TaskGraphControlsPanelProps) {
return (
<NxGraphPanel
anchor="left"
closable={closable}
initialOpen
panelContainerClassName="border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900"
panelHeaderClassName="border-slate-300 dark:border-slate-700"
panelCloseButtonClassName="hover:bg-slate-100 dark:hover:bg-slate-700"
panelContentContainerClassName="divide-slate-300 dark:divide-slate-700"
panelTriggerButtonClassName="border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/80 text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700"
>
<NxGraphDropdownMultiselect
items={targets.map((target) => ({ id: target, name: target }))}
selectedIds={selectedTargets}
onSelectionChange={onTargetsSelectionChange}
containerClassName="bg-white dark:bg-slate-800"
triggerClassName="bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-600"
triggerPlaceholderClassName="text-slate-500 dark:text-slate-400"
triggerChevronClassName="text-slate-500 dark:text-slate-400"
selectedChipClassName="bg-sky-100 dark:bg-sky-800 text-sky-900 dark:text-sky-100 border-sky-200 dark:border-sky-700"
selectedCountClassName="text-slate-700 dark:text-slate-300"
chipRemoveButtonClassName="text-sky-700 dark:text-sky-300 hover:text-sky-900 dark:hover:text-sky-100"
dropdownClassName="bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600"
dropdownContentClassName="bg-white dark:bg-slate-800"
multiselectContainerClassName="bg-white dark:bg-slate-800 border-slate-300 dark:border-slate-600"
multiselectFilterInputClassName="bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 placeholder:text-slate-500 dark:placeholder:text-slate-400"
multiselectEmptyStateClassName="text-slate-500 dark:text-slate-400"
multiselectListClassName="bg-white dark:bg-slate-800"
multiselectListItemClassName="hover:bg-slate-50 dark:hover:bg-slate-700"
multiselectCheckboxClassName="text-sky-600 dark:text-sky-400"
multiselectLabelClassName="text-slate-900 dark:text-slate-100"
/>
<NxGraphProjectListControl
projects={allProjects.map((p) => ({
type: p.type,
id: p.data.name,
name: p.data.name,
selected: selectedProjects.includes(p.name),
rendered: false,
affected: false,
}))}
projectListEmptyClassName="text-slate-500 dark:text-slate-400"
projectSectionHeaderClassName="text-slate-700 dark:text-slate-300"
searchInputClassName="bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-900 dark:text-slate-100 placeholder:text-slate-500 dark:placeholder:text-slate-400"
>
{(props) => (
<NxGraphCustomProjectItem
project={props.project}
error={error}
onToggle={() => {
onToggleProject(props.project.name, props.project.selected);
}}
projectItemClassName={({ selected }) =>
classNames(
'hover:bg-slate-100 dark:hover:bg-slate-700',
selected ? 'opacity-100' : 'opacity-70'
)
}
projectNameClassName="text-slate-900 dark:text-slate-100 dark:hover:bg-slate-700"
/>
)}
</NxGraphProjectListControl>
</NxGraphPanel>
);
}
@@ -0,0 +1,14 @@
import { useRouteError } from 'react-router-dom';
export function TasksSidebarErrorBoundary() {
let error = useRouteError();
console.error(error);
return (
<div className="mt-8 px-4">
<h2 className="mt-8 border-b border-solid border-slate-200/10 text-lg font-light text-slate-400 dark:text-slate-500">
Error
</h2>
<p>There was a problem loading your task graph.</p>
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { createBrowserRouter, createHashRouter } from 'react-router-dom';
import { getRoutesForEnvironment } from './routes';
import { getEnvironmentConfig } from '@nx/graph-shared';
let router;
export function getRouter() {
if (!router) {
const environmentConfig = getEnvironmentConfig();
let routerCreate = createBrowserRouter;
if (
environmentConfig.localMode === 'build' ||
environmentConfig.environment === 'nx-console'
) {
routerCreate = createHashRouter;
}
router = routerCreate(getRoutesForEnvironment());
}
return router;
}
@@ -0,0 +1,37 @@
import { matchRoutes, useLocation } from 'react-router-dom';
import { getRoutesForEnvironment } from '../routes';
import { useState } from 'react';
import { getEnvironmentConfig } from '@nx/graph-shared';
export const useCurrentPath = () => {
const [lastLocation, setLastLocation] = useState<string>();
const [lastPath, setLastPath] = useState<{
workspace: string;
currentPath: string;
}>();
const location = useLocation();
if (location.pathname === lastLocation) {
return lastPath;
}
setLastLocation(location.pathname);
const route = matchRoutes(getRoutesForEnvironment(), location).at(-1);
const { environment } = getEnvironmentConfig();
let currentPath: { workspace: string; currentPath: string };
// if using dev routes, remove first segment for workspace
if (environment === 'dev') {
currentPath = {
workspace: route.pathname.split('/')[1],
currentPath: `/${route.pathname.split('/').slice(2).join('/')}`,
};
} else {
currentPath = { workspace: 'local', currentPath: route.pathname };
}
setLastPath(currentPath);
return currentPath;
};
+20
View File
@@ -0,0 +1,20 @@
// nx-ignore-next-line
import type {
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
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 interface Environment {
environment: 'dev' | 'watch' | 'release';
}
+349
View File
@@ -0,0 +1,349 @@
import { redirect, RouteObject, json, LoaderFunction } from 'react-router-dom';
// nx-ignore-next-line
import type {
GraphError,
ProjectGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
import {
getEnvironmentConfig,
getProjectGraphDataService,
} from '@nx/graph-shared';
import { TasksSidebarErrorBoundary } from './feature-tasks/tasks-sidebar-error-boundary';
import { ProjectDetailsPage } from '@nx/graph-internal-project-details';
import { ErrorBoundary } from './ui-components/error-boundary';
import { taskGraphClientCache } from './task-graph-client-cache';
import { ProjectsShell } from './feature-projects/projects-shell';
import { TasksShell } from './feature-tasks/tasks-shell';
import { GraphSerializableState, GraphStateSerializer } from '@nx/graph';
const { appConfig } = getEnvironmentConfig();
const projectGraphDataService = getProjectGraphDataService();
export function getRoutesForEnvironment() {
if (getEnvironmentConfig().environment === 'dev') {
return devRoutes;
} else {
return releaseRoutes;
}
}
const workspaceDataLoader = async (
selectedWorkspaceId: string,
requestFull = false
) => {
const workspaceInfo = appConfig.workspaces.find(
(graph) => graph.id === selectedWorkspaceId
);
if (!workspaceInfo) {
throw new Error(`Workspace ${selectedWorkspaceId} not found`);
}
projectGraphDataService.setTaskInputsUrl?.(workspaceInfo.taskInputsUrl);
const projectGraph: ProjectGraphClientResponse =
await projectGraphDataService.getProjectGraph(
workspaceInfo.projectGraphUrl,
requestFull
);
const targetsSet = new Set<string>();
projectGraph.projects.forEach((project) => {
Object.keys(project.data.targets ?? {}).forEach((targetName) => {
targetsSet.add(targetName);
});
});
const targets = Array.from(targetsSet).sort((a, b) => a.localeCompare(b));
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
return { ...projectGraph, targets, sourceMaps };
};
const sourceMapsLoader = async (selectedWorkspaceId: string) => {
const workspaceInfo = appConfig.workspaces.find(
(graph) => graph.id === selectedWorkspaceId
);
if (!workspaceInfo) {
throw new Error(`Workspace ${selectedWorkspaceId} not found`);
}
return await projectGraphDataService.getSourceMaps(
workspaceInfo.sourceMapsUrl
);
};
const tasksLoader: LoaderFunction = async ({ params, request }) => {
const selectedWorkspaceId =
params.selectedWorkspaceId ?? appConfig.defaultWorkspaceId;
const workspaceInfo = appConfig.workspaces.find(
(graph) => graph.id === selectedWorkspaceId
);
if (!workspaceInfo) {
throw new Error(`Workspace ${selectedWorkspaceId} not found`);
}
// bust the task graph cache if workspaceId changes
taskGraphClientCache.setWorkspace(selectedWorkspaceId);
const requestUrl = new URL(request.url);
const targetsParam = requestUrl.searchParams.get('targets');
const projectsParam = requestUrl.searchParams.get('projects');
const selectedTargets = targetsParam
? targetsParam.split(' ').filter(Boolean)
: [];
const requestedProjects = projectsParam
? projectsParam.split(' ').filter(Boolean)
: undefined;
// if no targets are selected, empty taskGraph response
if (selectedTargets.length === 0) {
return {
taskGraph: {
tasks: {},
dependencies: {},
continuousDependencies: {},
roots: [],
},
error: null,
};
}
const isAllRoute = requestUrl.pathname.endsWith('/all');
// if we don't request any projects and we're not on the "all" route,
// we return empty taskGraph response. consumers need to select projects
if (!requestedProjects && !isAllRoute) {
return {
taskGraph: {
tasks: {},
dependencies: {},
continuousDependencies: {},
roots: [],
},
error: null,
};
}
// Check if we have cached data for this exact combination
const { cached, missingTargets, missingProjects } =
taskGraphClientCache.getCached(selectedTargets, requestedProjects);
if (cached) return cached;
const response = await projectGraphDataService.getSpecificTaskGraph(
workspaceInfo.taskGraphUrl,
missingProjects,
missingTargets
);
if (response.error) return response;
// only merge if we don't have error
return taskGraphClientCache.mergeTaskGraph(
response,
missingTargets,
missingProjects
);
};
const projectDetailsLoader = async (
selectedWorkspaceId: string,
projectName: string
): Promise<{
hash: string;
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
disabledTaskSyncGenerators?: string[];
}> => {
const workspaceData = await workspaceDataLoader(selectedWorkspaceId);
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
const project = workspaceData.projects.find(
(project) => project.name === projectName
);
if (!project) {
throw json({
id: 'project-not-found',
projectName,
errors: workspaceData.errors,
});
}
return {
hash: workspaceData.hash,
project,
sourceMap: sourceMaps[project.data.root],
errors: workspaceData.errors,
connectedToCloud: workspaceData.connectedToCloud,
disabledTaskSyncGenerators: workspaceData.disabledTaskSyncGenerators,
};
};
const tasksRouteObject: RouteObject = {
element: <TasksShell />,
errorElement: <TasksSidebarErrorBoundary />,
loader: tasksLoader,
shouldRevalidate: ({ currentParams, nextParams, currentUrl, nextUrl }) => {
// Always revalidate if workspace changes
if (
!currentParams.selectedWorkspaceId ||
currentParams.selectedWorkspaceId !== nextParams.selectedWorkspaceId
) {
return true;
}
// Revalidate if query parameters change (targets or projects)
const currentSearchParams = new URLSearchParams(currentUrl.search);
const nextSearchParams = new URLSearchParams(nextUrl.search);
const currentTargets = currentSearchParams.get('targets');
const nextTargets = nextSearchParams.get('targets');
const currentProjects = currentSearchParams.get('projects');
const nextProjects = nextSearchParams.get('projects');
return currentTargets !== nextTargets || currentProjects !== nextProjects;
},
};
const childRoutes: RouteObject[] = [
{
path: 'projects',
element: <ProjectsShell />,
loader: ({ request }) => {
const url = new URL(request.url);
if (!url.searchParams.has('rawGraph')) {
return null;
}
handleRawGraphQueryParam(url.searchParams);
return redirect(`${url.pathname}?${url.searchParams.toString()}`);
},
},
{
path: 'tasks',
id: 'tasks',
...tasksRouteObject,
},
{
path: 'tasks/all',
id: 'tasksAll',
...tasksRouteObject,
},
];
function handleRawGraphQueryParam(searchParams: URLSearchParams) {
const rawGraph = searchParams.get('rawGraph');
// nothing to do here, bail
if (!rawGraph) return;
try {
const rawGraphJson: {
config: Record<string, unknown>;
state?: Record<string, unknown>;
} = JSON.parse(rawGraph);
searchParams.delete('rawGraph');
searchParams.set(
'graph',
GraphStateSerializer.serialize({
c: rawGraphJson.config,
s: rawGraphJson.state,
} as Omit<GraphSerializableState<any>, 'v'>)
);
} catch (err) {
console.error('Graph Client: error during rawGraph handling', err);
return;
}
}
export const devRoutes: RouteObject[] = [
{
path: '/',
errorElement: <ErrorBoundary />,
children: [
{
index: true,
loader: ({ request }) => {
const { searchParams } = new URL(request.url);
handleRawGraphQueryParam(searchParams);
return redirect(
`/${
appConfig.defaultWorkspaceId
}/projects?${searchParams.toString()}`
);
},
},
{
path: ':selectedWorkspaceId',
id: 'selectedWorkspace',
shouldRevalidate: ({ currentParams, nextParams }) => {
return (
currentParams.selectedWorkspaceId !== nextParams.selectedWorkspaceId
);
},
loader: async ({ params }) => {
const selectedWorkspaceId =
params.selectedWorkspaceId ?? appConfig.defaultWorkspaceId;
return workspaceDataLoader(selectedWorkspaceId, true);
},
children: childRoutes,
},
{
path: ':selectedWorkspaceId/project-details/:projectName',
id: 'selectedProjectDetails',
element: <ProjectDetailsPage />,
loader: async ({ params }) => {
const projectName = params.projectName;
return projectDetailsLoader(params.selectedWorkspaceId, projectName);
},
},
],
},
];
export const releaseRoutes: RouteObject[] = [
{
path: '/',
id: 'selectedWorkspace',
loader: async () => {
const selectedWorkspaceId = appConfig.defaultWorkspaceId;
return workspaceDataLoader(selectedWorkspaceId, true);
},
shouldRevalidate: () => {
return false;
},
children: [
{
index: true,
loader: ({ request }) => {
const { searchParams } = new URL(request.url);
handleRawGraphQueryParam(searchParams);
return redirect(`/projects?${searchParams.toString()}`);
},
},
...childRoutes,
],
errorElement: <ErrorBoundary />,
},
{
path: 'project-details/:projectName',
id: 'selectedProjectDetails',
element: <ProjectDetailsPage />,
errorElement: <ErrorBoundary />,
loader: async ({ params }) => {
const projectName = params.projectName;
return projectDetailsLoader(appConfig.defaultWorkspaceId, projectName);
},
},
];
@@ -0,0 +1,153 @@
// nx-ignore-next-line
import type { TaskGraphClientResponse } from 'nx/src/command-line/graph/graph';
class TaskGraphClientCache {
private cached: TaskGraphClientResponse | null = null;
private currentWorkspaceId: string | null = null;
private fetchedSet = new Set<string>();
setWorkspace(workspaceId: string) {
if (this.currentWorkspaceId === workspaceId) return;
this.currentWorkspaceId = workspaceId;
this.clear();
}
getCached(targets: string[], requestedProjects?: string[]) {
// if we don't have cached result yet, we'll always fetch
if (!this.cached) {
return {
cached: null,
missingTargets: targets,
missingProjects: requestedProjects,
};
}
// this means we're fetching "all" for all targets
if (!requestedProjects || requestedProjects.length === 0) {
const missingTargets: string[] = [];
for (const target of targets) {
const targetWithAllKey = `${target}:all`;
// if we already fetch for this target, skip
if (this.fetchedSet.has(targetWithAllKey)) continue;
missingTargets.push(target);
}
// no missing targets needed, we return the cached
return {
cached: missingTargets.length === 0 ? this.cached : null,
missingTargets,
};
}
const missingTargetsSet = new Set<string>();
const missingProjectsSet = new Set<string>();
for (const target of targets) {
for (const project of requestedProjects) {
const key = `${target}:${project}`;
if (this.fetchedSet.has(key)) continue;
missingTargetsSet.add(target);
missingProjectsSet.add(project);
}
}
if (missingTargetsSet.size === 0 && missingProjectsSet.size === 0) {
return { cached: this.cached, missingTargets: [] };
}
return {
cached: null,
missingTargets: Array.from(missingTargetsSet),
missingProjects: Array.from(missingProjectsSet),
};
}
mergeTaskGraph(
response: TaskGraphClientResponse,
fetchedTargets: string[],
fetchedProjects?: string[]
) {
// populate the fetchedSet with targets and projects that were used
// in the request
if (!fetchedProjects) {
fetchedTargets.forEach((fetchedTarget) => {
this.fetchedSet.add(`${fetchedTarget}:all`);
});
} else {
for (const fetchedTarget of fetchedTargets) {
for (const fetchedProject of fetchedProjects) {
this.fetchedSet.add(`${fetchedTarget}:${fetchedProject}`);
}
}
}
const mergedTasks = {
...this.cached?.taskGraph.tasks,
...response.taskGraph.tasks,
};
const rootsSet = new Set<string>(this.cached?.taskGraph.roots || []);
for (const root of response.taskGraph.roots) {
rootsSet.add(root);
}
const mergedDependencies = this.mergeStringCollectionRecord(
this.cached?.taskGraph.dependencies,
response.taskGraph.dependencies
);
const mergedContinuousDependencies = this.mergeStringCollectionRecord(
this.cached?.taskGraph.continuousDependencies,
response.taskGraph.continuousDependencies
);
const mergedTaskGraph = {
tasks: mergedTasks,
roots: Array.from(rootsSet),
dependencies: mergedDependencies,
continuousDependencies: mergedContinuousDependencies,
};
const mergedPlans = this.mergeStringCollectionRecord(
this.cached?.plans,
response.plans
);
this.cached = {
taskGraph: mergedTaskGraph,
plans: mergedPlans,
error: response.error || this.cached?.error || null,
};
return this.cached;
}
clear() {
this.cached = null;
this.fetchedSet.clear();
}
private mergeStringCollectionRecord(
cachedRecord?: Record<string, string[]>,
responseRecord?: Record<string, string[]>
) {
const mergedRecord: Record<string, string[]> = {};
for (const taskId in cachedRecord) {
mergedRecord[taskId] = cachedRecord[taskId];
}
for (const taskId in responseRecord) {
const collectionSet = new Set([
...(mergedRecord[taskId] || []),
...(responseRecord[taskId] || []),
]);
mergedRecord[taskId] = Array.from(collectionSet);
}
return mergedRecord;
}
}
export const taskGraphClientCache = new TaskGraphClientCache();
@@ -0,0 +1,73 @@
import { ProjectDetailsHeader } from '@nx/graph-internal-project-details';
import {
fetchProjectGraph,
getProjectGraphDataService,
useEnvironmentConfig,
usePoll,
} from '@nx/graph-shared';
import {
isRouteErrorResponse,
useParams,
useRouteError,
} from 'react-router-dom';
import { ErrorPage } from './error-page';
export function ErrorBoundary() {
let error = useRouteError();
console.error(error);
const { environment, appConfig, watch } = useEnvironmentConfig();
const projectGraphDataService = getProjectGraphDataService();
const params = useParams();
const hasErrorData =
isRouteErrorResponse(error) && error.data.errors?.length > 0;
usePoll(
async () => {
const data = await fetchProjectGraph(
projectGraphDataService,
params,
appConfig
);
if (
isRouteErrorResponse(error) &&
error.data.id === 'project-not-found' &&
data.projects.find((p) => p.name === error.data.projectName)
) {
window.location.reload();
}
},
1000,
watch
);
let message: string | JSX.Element;
let stack: string;
if (isRouteErrorResponse(error) && error.data.id === 'project-not-found') {
message = (
<p>
Project <code>{error.data.projectName}</code> not found.
</p>
);
} else {
message = 'Disconnected from graph server. ';
if (environment === 'nx-console') {
message += 'Please refresh the page.';
} else {
message += 'Please rerun your command and refresh the page.';
}
stack = error.toString();
}
return (
<div className="flex h-screen w-full flex-col items-center">
{environment !== 'nx-console' && <ProjectDetailsHeader />}
<ErrorPage
message={message}
stack={stack}
errors={hasErrorData ? error.data.errors : undefined}
/>
</div>
);
}
@@ -0,0 +1,49 @@
// nx-ignore-next-line
import { ErrorRenderer } from '@nx/graph-ui-common';
// nx-ignore-next-line
import { GraphError } from 'nx/src/command-line/graph/graph';
import type { JSX } from 'react';
export type ErrorPageProps = {
message: string | JSX.Element;
stack?: string;
errors: GraphError[];
};
export function ErrorPage({ message, stack, errors }: ErrorPageProps) {
return (
<div className="mx-auto mb-8 w-full max-w-6xl flex-grow px-8">
<h1 className="mb-4 text-4xl dark:text-slate-100">Error</h1>
<div>
<ErrorWithStack message={message} stack={stack} />
</div>
{errors && (
<div>
<p className="text-md mb-4 dark:text-slate-200">
Nx encountered the following issues while processing the project
graph:{' '}
</p>
<div>
<ErrorRenderer errors={errors} />
</div>
</div>
)}
</div>
);
}
function ErrorWithStack({
message,
stack,
}: {
message: string | JSX.Element;
stack?: string;
}) {
return (
<div>
<p className="mb-4 text-lg dark:text-slate-100">{message}</p>
{stack && <p className="text-sm">Error message: {stack}</p>}
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
// nx-ignore-next-line
import type { FileDataDependency } from 'nx/src/config/project-graph';
export function createTaskName(
project: string,
target: string,
configuration?: string
) {
if (configuration) {
return `task-${project}:${target}:${configuration}`;
} else {
return `task-${project}:${target}`;
}
}
export function extractDependencyTarget(dependency: FileDataDependency) {
if (typeof dependency === 'string') return dependency;
if (dependency.length === 2) return dependency[0];
return dependency[1];
}
View File
@@ -0,0 +1,28 @@
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',
taskInputsUrl: 'assets/task-inputs/e2e.json',
sourceMapsUrl: 'assets/source-maps/e2e.json',
},
{
id: 'affected',
label: 'affected',
projectGraphUrl: 'assets/project-graphs/affected.json',
taskGraphUrl: 'assets/task-graphs/affected.json',
taskInputsUrl: 'assets/task-inputs/affected.json',
sourceMapsUrl: 'assets/source-maps/e2e-affected.json',
},
],
defaultWorkspaceId: 'e2e',
};
@@ -0,0 +1,18 @@
window.exclude = [];
window.watch = false;
window.environment = 'nx-console';
window.useXstateInspect = false;
window.appConfig = {
showDebugger: false,
showExperimentalFeatures: false,
workspaces: [
{
id: 'local',
label: 'local',
projectGraphUrl: 'assets/project-graphs/e2e.json',
taskGraphUrl: 'assets/task-graphs/e2e.json',
},
],
defaultWorkspaceId: 'local',
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
window.exclude = [];
window.watch = false;
window.environment = 'release';
window.useXstateInspect = false;
window.appConfig = {
showDebugger: false,
showExperimentalFeatures: false,
workspaces: [
{
id: 'local',
label: 'local',
projectGraphUrl: 'assets/project-graphs/e2e.json',
taskGraphUrl: 'assets/task-graphs/e2e.json',
taskInputsUrl: 'assets/task-inputs/e2e.json',
sourceMapsUrl: 'assets/source-maps/e2e.json',
},
],
defaultWorkspaceId: 'local',
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,450 @@
{
"taskGraph": {
"tasks": {
"products-product-detail-page:lint": {
"id": "products-product-detail-page:lint",
"target": {
"project": "products-product-detail-page",
"target": "lint"
},
"projectRoot": "libs/products/product-detail-page",
"overrides": {}
},
"products-product-detail-page:test": {
"id": "products-product-detail-page:test",
"target": {
"project": "products-product-detail-page",
"target": "test"
},
"projectRoot": "libs/products/product-detail-page",
"overrides": {}
},
"shared-product-state:lint": {
"id": "shared-product-state:lint",
"target": {
"project": "shared-product-state",
"target": "lint"
},
"projectRoot": "libs/shared/product/state",
"overrides": {}
},
"shared-product-state:test": {
"id": "shared-product-state:test",
"target": {
"project": "shared-product-state",
"target": "test"
},
"projectRoot": "libs/shared/product/state",
"overrides": {}
},
"shared-product-types:lint": {
"id": "shared-product-types:lint",
"target": {
"project": "shared-product-types",
"target": "lint"
},
"projectRoot": "libs/shared/product/types",
"overrides": {}
},
"shared-product-data:lint": {
"id": "shared-product-data:lint",
"target": {
"project": "shared-product-data",
"target": "lint"
},
"projectRoot": "libs/shared/product/data",
"overrides": {}
},
"products-home-page:lint": {
"id": "products-home-page:lint",
"target": {
"project": "products-home-page",
"target": "lint"
},
"projectRoot": "libs/products/home-page",
"overrides": {}
},
"products-home-page:test": {
"id": "products-home-page:test",
"target": {
"project": "products-home-page",
"target": "test"
},
"projectRoot": "libs/products/home-page",
"overrides": {}
},
"shared-cart-state:lint": {
"id": "shared-cart-state:lint",
"target": {
"project": "shared-cart-state",
"target": "lint"
},
"projectRoot": "libs/shared/cart/state",
"overrides": {}
},
"shared-cart-state:test": {
"id": "shared-cart-state:test",
"target": {
"project": "shared-cart-state",
"target": "test"
},
"projectRoot": "libs/shared/cart/state",
"overrides": {}
},
"shared-product-ui:lint": {
"id": "shared-product-ui:lint",
"target": {
"project": "shared-product-ui",
"target": "lint"
},
"projectRoot": "libs/shared/product/ui",
"overrides": {}
},
"shared-product-ui:test": {
"id": "shared-product-ui:test",
"target": {
"project": "shared-product-ui",
"target": "test"
},
"projectRoot": "libs/shared/product/ui",
"overrides": {}
},
"shared-e2e-utils:lint": {
"id": "shared-e2e-utils:lint",
"target": {
"project": "shared-e2e-utils",
"target": "lint"
},
"projectRoot": "libs/shared/e2e-utils",
"overrides": {}
},
"cart-cart-page:lint": {
"id": "cart-cart-page:lint",
"target": {
"project": "cart-cart-page",
"target": "lint"
},
"projectRoot": "libs/cart/cart-page",
"overrides": {}
},
"cart-cart-page:test": {
"id": "cart-cart-page:test",
"target": {
"project": "cart-cart-page",
"target": "test"
},
"projectRoot": "libs/cart/cart-page",
"overrides": {}
},
"shared-header:lint": {
"id": "shared-header:lint",
"target": {
"project": "shared-header",
"target": "lint"
},
"projectRoot": "libs/shared/header",
"overrides": {}
},
"shared-header:test": {
"id": "shared-header:test",
"target": {
"project": "shared-header",
"target": "test"
},
"projectRoot": "libs/shared/header",
"overrides": {}
},
"shared-jsxify:lint": {
"id": "shared-jsxify:lint",
"target": {
"project": "shared-jsxify",
"target": "lint"
},
"projectRoot": "libs/shared/jsxify",
"overrides": {}
},
"products-e2e:e2e": {
"id": "products-e2e:e2e",
"target": {
"project": "products-e2e",
"target": "e2e"
},
"projectRoot": "apps/products-e2e",
"overrides": {}
},
"products-e2e:e2e:production": {
"id": "products-e2e:e2e:production",
"target": {
"project": "products-e2e",
"target": "e2e",
"configuration": "production"
},
"projectRoot": "apps/products-e2e",
"overrides": {}
},
"products-e2e:lint": {
"id": "products-e2e:lint",
"target": {
"project": "products-e2e",
"target": "lint"
},
"projectRoot": "apps/products-e2e",
"overrides": {}
},
"cart-e2e:e2e": {
"id": "cart-e2e:e2e",
"target": {
"project": "cart-e2e",
"target": "e2e"
},
"projectRoot": "apps/cart-e2e",
"overrides": {}
},
"cart-e2e:e2e:production": {
"id": "cart-e2e:e2e:production",
"target": {
"project": "cart-e2e",
"target": "e2e",
"configuration": "production"
},
"projectRoot": "apps/cart-e2e",
"overrides": {}
},
"cart-e2e:lint": {
"id": "cart-e2e:lint",
"target": {
"project": "cart-e2e",
"target": "lint"
},
"projectRoot": "apps/cart-e2e",
"overrides": {}
},
"products:build": {
"id": "products:build",
"target": {
"project": "products",
"target": "build"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:build:production": {
"id": "products:build:production",
"target": {
"project": "products",
"target": "build",
"configuration": "production"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:serve": {
"id": "products:serve",
"target": {
"project": "products",
"target": "serve"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:serve:production": {
"id": "products:serve:production",
"target": {
"project": "products",
"target": "serve",
"configuration": "production"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:extract-i18n": {
"id": "products:extract-i18n",
"target": {
"project": "products",
"target": "extract-i18n"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:lint": {
"id": "products:lint",
"target": {
"project": "products",
"target": "lint"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:test": {
"id": "products:test",
"target": {
"project": "products",
"target": "test"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:deploy": {
"id": "products:deploy",
"target": {
"project": "products",
"target": "deploy"
},
"projectRoot": "apps/products",
"overrides": {}
},
"cart:build:production": {
"id": "cart:build:production",
"target": {
"project": "cart",
"target": "build",
"configuration": "production"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:build:development": {
"id": "cart:build:development",
"target": {
"project": "cart",
"target": "build",
"configuration": "development"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:serve:development": {
"id": "cart:serve:development",
"target": {
"project": "cart",
"target": "serve",
"configuration": "development"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:serve:production": {
"id": "cart:serve:production",
"target": {
"project": "cart",
"target": "serve",
"configuration": "production"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:lint": {
"id": "cart:lint",
"target": {
"project": "cart",
"target": "lint"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:test": {
"id": "cart:test",
"target": {
"project": "cart",
"target": "test"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:deploy": {
"id": "cart:deploy",
"target": {
"project": "cart",
"target": "deploy"
},
"projectRoot": "apps/cart",
"overrides": {}
}
},
"dependencies": {
"products-product-detail-page:lint": [],
"products-product-detail-page:test": [],
"shared-product-state:lint": [],
"shared-product-state:test": [],
"shared-product-types:lint": [],
"shared-product-data:lint": [],
"products-home-page:lint": [],
"products-home-page:test": [],
"shared-cart-state:lint": [],
"shared-cart-state:test": [],
"shared-product-ui:lint": [],
"shared-product-ui:test": [],
"shared-e2e-utils:lint": [],
"cart-cart-page:lint": [],
"cart-cart-page:test": [],
"shared-header:lint": [],
"shared-header:test": [],
"shared-jsxify:lint": [],
"products-e2e:e2e": [],
"products-e2e:e2e:production": [],
"products-e2e:lint": [],
"cart-e2e:e2e": [],
"cart-e2e:e2e:production": [],
"cart-e2e:lint": [],
"products:build": [],
"products:build:production": [],
"products:serve": [],
"products:serve:production": [],
"products:extract-i18n": [],
"products:lint": [],
"products:test": [],
"products:deploy": [],
"cart:build:production": [],
"cart:build:development": [],
"cart:serve:development": [],
"cart:serve:production": [],
"cart:lint": [],
"cart:test": [],
"cart:deploy": []
},
"continuousDependencies": {},
"roots": [
"products-product-detail-page:lint",
"products-product-detail-page:test",
"shared-product-state:lint",
"shared-product-state:test",
"shared-product-types:lint",
"shared-product-data:lint",
"products-home-page:lint",
"products-home-page:test",
"shared-cart-state:lint",
"shared-cart-state:test",
"shared-product-ui:lint",
"shared-product-ui:test",
"shared-e2e-utils:lint",
"cart-cart-page:lint",
"cart-cart-page:test",
"shared-header:lint",
"shared-header:test",
"shared-jsxify:lint",
"products-e2e:e2e",
"products-e2e:e2e:production",
"products-e2e:lint",
"cart-e2e:e2e",
"cart-e2e:e2e:production",
"cart-e2e:lint",
"products:build",
"products:build:production",
"products:serve",
"products:serve:production",
"products:extract-i18n",
"products:lint",
"products:test",
"products:deploy",
"cart:build:production",
"cart:build:development",
"cart:serve:development",
"cart:serve:production",
"cart:lint",
"cart:test",
"cart:deploy"
]
},
"plans": {},
"error": null
}
@@ -0,0 +1,450 @@
{
"taskGraph": {
"tasks": {
"products-product-detail-page:lint": {
"id": "products-product-detail-page:lint",
"target": {
"project": "products-product-detail-page",
"target": "lint"
},
"projectRoot": "libs/products/product-detail-page",
"overrides": {}
},
"products-product-detail-page:test": {
"id": "products-product-detail-page:test",
"target": {
"project": "products-product-detail-page",
"target": "test"
},
"projectRoot": "libs/products/product-detail-page",
"overrides": {}
},
"shared-product-state:lint": {
"id": "shared-product-state:lint",
"target": {
"project": "shared-product-state",
"target": "lint"
},
"projectRoot": "libs/shared/product/state",
"overrides": {}
},
"shared-product-state:test": {
"id": "shared-product-state:test",
"target": {
"project": "shared-product-state",
"target": "test"
},
"projectRoot": "libs/shared/product/state",
"overrides": {}
},
"shared-product-types:lint": {
"id": "shared-product-types:lint",
"target": {
"project": "shared-product-types",
"target": "lint"
},
"projectRoot": "libs/shared/product/types",
"overrides": {}
},
"shared-product-data:lint": {
"id": "shared-product-data:lint",
"target": {
"project": "shared-product-data",
"target": "lint"
},
"projectRoot": "libs/shared/product/data",
"overrides": {}
},
"products-home-page:lint": {
"id": "products-home-page:lint",
"target": {
"project": "products-home-page",
"target": "lint"
},
"projectRoot": "libs/products/home-page",
"overrides": {}
},
"products-home-page:test": {
"id": "products-home-page:test",
"target": {
"project": "products-home-page",
"target": "test"
},
"projectRoot": "libs/products/home-page",
"overrides": {}
},
"shared-cart-state:lint": {
"id": "shared-cart-state:lint",
"target": {
"project": "shared-cart-state",
"target": "lint"
},
"projectRoot": "libs/shared/cart/state",
"overrides": {}
},
"shared-cart-state:test": {
"id": "shared-cart-state:test",
"target": {
"project": "shared-cart-state",
"target": "test"
},
"projectRoot": "libs/shared/cart/state",
"overrides": {}
},
"shared-product-ui:lint": {
"id": "shared-product-ui:lint",
"target": {
"project": "shared-product-ui",
"target": "lint"
},
"projectRoot": "libs/shared/product/ui",
"overrides": {}
},
"shared-product-ui:test": {
"id": "shared-product-ui:test",
"target": {
"project": "shared-product-ui",
"target": "test"
},
"projectRoot": "libs/shared/product/ui",
"overrides": {}
},
"shared-e2e-utils:lint": {
"id": "shared-e2e-utils:lint",
"target": {
"project": "shared-e2e-utils",
"target": "lint"
},
"projectRoot": "libs/shared/e2e-utils",
"overrides": {}
},
"cart-cart-page:lint": {
"id": "cart-cart-page:lint",
"target": {
"project": "cart-cart-page",
"target": "lint"
},
"projectRoot": "libs/cart/cart-page",
"overrides": {}
},
"cart-cart-page:test": {
"id": "cart-cart-page:test",
"target": {
"project": "cart-cart-page",
"target": "test"
},
"projectRoot": "libs/cart/cart-page",
"overrides": {}
},
"shared-header:lint": {
"id": "shared-header:lint",
"target": {
"project": "shared-header",
"target": "lint"
},
"projectRoot": "libs/shared/header",
"overrides": {}
},
"shared-header:test": {
"id": "shared-header:test",
"target": {
"project": "shared-header",
"target": "test"
},
"projectRoot": "libs/shared/header",
"overrides": {}
},
"shared-jsxify:lint": {
"id": "shared-jsxify:lint",
"target": {
"project": "shared-jsxify",
"target": "lint"
},
"projectRoot": "libs/shared/jsxify",
"overrides": {}
},
"products-e2e:e2e": {
"id": "products-e2e:e2e",
"target": {
"project": "products-e2e",
"target": "e2e"
},
"projectRoot": "apps/products-e2e",
"overrides": {}
},
"products-e2e:e2e:production": {
"id": "products-e2e:e2e:production",
"target": {
"project": "products-e2e",
"target": "e2e",
"configuration": "production"
},
"projectRoot": "apps/products-e2e",
"overrides": {}
},
"products-e2e:lint": {
"id": "products-e2e:lint",
"target": {
"project": "products-e2e",
"target": "lint"
},
"projectRoot": "apps/products-e2e",
"overrides": {}
},
"cart-e2e:e2e": {
"id": "cart-e2e:e2e",
"target": {
"project": "cart-e2e",
"target": "e2e"
},
"projectRoot": "apps/cart-e2e",
"overrides": {}
},
"cart-e2e:e2e:production": {
"id": "cart-e2e:e2e:production",
"target": {
"project": "cart-e2e",
"target": "e2e",
"configuration": "production"
},
"projectRoot": "apps/cart-e2e",
"overrides": {}
},
"cart-e2e:lint": {
"id": "cart-e2e:lint",
"target": {
"project": "cart-e2e",
"target": "lint"
},
"projectRoot": "apps/cart-e2e",
"overrides": {}
},
"products:build": {
"id": "products:build",
"target": {
"project": "products",
"target": "build"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:build:production": {
"id": "products:build:production",
"target": {
"project": "products",
"target": "build",
"configuration": "production"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:serve": {
"id": "products:serve",
"target": {
"project": "products",
"target": "serve"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:serve:production": {
"id": "products:serve:production",
"target": {
"project": "products",
"target": "serve",
"configuration": "production"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:extract-i18n": {
"id": "products:extract-i18n",
"target": {
"project": "products",
"target": "extract-i18n"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:lint": {
"id": "products:lint",
"target": {
"project": "products",
"target": "lint"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:test": {
"id": "products:test",
"target": {
"project": "products",
"target": "test"
},
"projectRoot": "apps/products",
"overrides": {}
},
"products:deploy": {
"id": "products:deploy",
"target": {
"project": "products",
"target": "deploy"
},
"projectRoot": "apps/products",
"overrides": {}
},
"cart:build:production": {
"id": "cart:build:production",
"target": {
"project": "cart",
"target": "build",
"configuration": "production"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:build:development": {
"id": "cart:build:development",
"target": {
"project": "cart",
"target": "build",
"configuration": "development"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:serve:development": {
"id": "cart:serve:development",
"target": {
"project": "cart",
"target": "serve",
"configuration": "development"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:serve:production": {
"id": "cart:serve:production",
"target": {
"project": "cart",
"target": "serve",
"configuration": "production"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:lint": {
"id": "cart:lint",
"target": {
"project": "cart",
"target": "lint"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:test": {
"id": "cart:test",
"target": {
"project": "cart",
"target": "test"
},
"projectRoot": "apps/cart",
"overrides": {}
},
"cart:deploy": {
"id": "cart:deploy",
"target": {
"project": "cart",
"target": "deploy"
},
"projectRoot": "apps/cart",
"overrides": {}
}
},
"dependencies": {
"products-product-detail-page:lint": [],
"products-product-detail-page:test": [],
"shared-product-state:lint": [],
"shared-product-state:test": [],
"shared-product-types:lint": [],
"shared-product-data:lint": [],
"products-home-page:lint": [],
"products-home-page:test": [],
"shared-cart-state:lint": [],
"shared-cart-state:test": [],
"shared-product-ui:lint": [],
"shared-product-ui:test": [],
"shared-e2e-utils:lint": [],
"cart-cart-page:lint": [],
"cart-cart-page:test": [],
"shared-header:lint": [],
"shared-header:test": [],
"shared-jsxify:lint": [],
"products-e2e:e2e": [],
"products-e2e:e2e:production": [],
"products-e2e:lint": [],
"cart-e2e:e2e": [],
"cart-e2e:e2e:production": [],
"cart-e2e:lint": [],
"products:build": [],
"products:build:production": [],
"products:serve": [],
"products:serve:production": [],
"products:extract-i18n": [],
"products:lint": [],
"products:test": [],
"products:deploy": [],
"cart:build:production": [],
"cart:build:development": [],
"cart:serve:development": [],
"cart:serve:production": [],
"cart:lint": [],
"cart:test": [],
"cart:deploy": []
},
"continuousDependencies": {},
"roots": [
"products-product-detail-page:lint",
"products-product-detail-page:test",
"shared-product-state:lint",
"shared-product-state:test",
"shared-product-types:lint",
"shared-product-data:lint",
"products-home-page:lint",
"products-home-page:test",
"shared-cart-state:lint",
"shared-cart-state:test",
"shared-product-ui:lint",
"shared-product-ui:test",
"shared-e2e-utils:lint",
"cart-cart-page:lint",
"cart-cart-page:test",
"shared-header:lint",
"shared-header:test",
"shared-jsxify:lint",
"products-e2e:e2e",
"products-e2e:e2e:production",
"products-e2e:lint",
"cart-e2e:e2e",
"cart-e2e:e2e:production",
"cart-e2e:lint",
"products:build",
"products:build:production",
"products:serve",
"products:serve:production",
"products:extract-i18n",
"products:lint",
"products:test",
"products:deploy",
"cart:build:production",
"cart:build:development",
"cart:serve:development",
"cart:serve:production",
"cart:lint",
"cart:test",
"cart:deploy"
]
},
"plans": {},
"error": null
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
window.exclude = [];
window.watch = true;
window.environment = 'watch';
window.useXstateInspect = false;
window.appConfig = {
showDebugger: false,
showExperimentalFeatures: true,
workspaces: [
{
id: 'local',
label: 'local',
projectGraphUrl: 'assets/project-graphs/e2e.json',
taskGraphUrl: 'assets/task-graphs/e2e.json',
},
],
defaultWorkspaceId: 'local',
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+63
View File
@@ -0,0 +1,63 @@
// nx-ignore-next-line
import type {
ExpandedTaskInputsReponse,
ProjectGraphClientResponse,
TaskGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
import type { AppConfig, ExternalApi } from '@nx/graph/shared';
import {
ProjectDetailsEvents,
projectDetailsMachine,
ProjectDetailsState,
} from './app/console/project-details/project-details.machine';
import { Interpreter } from 'xstate';
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;
// using bundled graph components directly from outside the graph app
__NX_RENDER_GRAPH__?: boolean;
renderPDV?: (
data: any
) => Interpreter<ProjectDetailsState, any, ProjectDetailsEvents>;
renderError?: (data: any) => void;
renderMigrate?: (
data: any
) => Interpreter<MigrateState, any, MigrateEvents>;
}
}
declare module 'cytoscape' {
interface Core {
anywherePanning: Function;
}
interface ElementDefinition {
pannable?: boolean;
}
interface NodeSingular {
popperRef: Function;
pannable: () => boolean;
}
interface EdgeSingular {
popperRef: Function;
}
namespace Css {
interface EdgeLine {
'edge-text-rotation'?: string;
}
}
}
+33
View File
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Nx Workspace Project Graph</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script id="environment" src="environment.js"></script>
<!-- Theming -->
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (
localStorage.getItem('nx-dep-graph-theme') === 'dark' ||
(localStorage.getItem('nx-dep-graph-theme') === null &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
</script>
</head>
<body
class="h-screen w-screen bg-white text-slate-500 dark:bg-slate-900 dark:text-slate-400"
>
<div class="flex h-full w-full p-0" id="app"></div>
</body>
</html>
+122
View File
@@ -0,0 +1,122 @@
/* eslint-disable import/first */
// debug must be first import
if (process.env.NODE_ENV === 'development') {
require('preact/debug');
}
import { projectDetailsMachine } from './app/console-project-details/project-details.machine';
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
// nx-ignore-next-line
import type { GraphError } from 'nx/src/command-line/graph/graph';
// nx-ignore-next-line
import { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api';
// nx-ignore-next-line
import { GeneratedMigrationDetails } from 'nx/src/config/misc-interfaces';
import { StrictMode } from 'react';
import { inspect } from '@xstate/inspect';
import { App } from './app/app';
import { ExternalApiImpl } from './app/external-api-impl';
import { render } from 'preact';
import { ErrorPage } from './app/ui-components/error-page';
import { ProjectDetailsApp } from './app/console-project-details/project-details.app';
import { interpret } from 'xstate';
import { MigrateApp } from './app/console-migrate/migrate.app';
import { migrateMachine } from './app/console-migrate/migrate.machine';
import { ThemeProvider } from '@nx/graph-internal-ui-render-config';
if (window.__NX_RENDER_GRAPH__ === false) {
window.externalApi = new ExternalApiImpl();
window.renderPDV = (data: {
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
connectedToCloud: boolean;
errors?: GraphError[];
}) => {
const service = interpret(projectDetailsMachine).start();
service.send({
type: 'loadData',
...data,
});
render(
<StrictMode>
<ThemeProvider>
<ProjectDetailsApp service={service} />
</ThemeProvider>
</StrictMode>,
document.getElementById('app')
);
return service;
};
window.renderError = (data: {
message: string;
stack?: string;
errors: GraphError[];
}) => {
render(
<StrictMode>
<ThemeProvider>
<ErrorPage {...data} />
</ThemeProvider>
</StrictMode>,
document.getElementById('app')
);
};
window.renderMigrate = (data: {
migrations: GeneratedMigrationDetails[];
'nx-console': MigrationsJsonMetadata;
}) => {
const service = interpret(migrateMachine).start();
service.send({
type: 'loadData',
...data,
});
render(
<StrictMode>
<ThemeProvider>
<MigrateApp service={service} />
</ThemeProvider>
</StrictMode>,
document.getElementById('app')
);
return service;
};
} else {
if (window.useXstateInspect === true) {
inspect({
url: 'https://stately.ai/viz?inspect',
iframe: false, // open in new window
});
}
window.externalApi = new ExternalApiImpl();
const container = document.getElementById('app');
if (!window.appConfig) {
render(
<p>
No environment could be found. Please run{' '}
<pre>npx nx run graph-client:generate-dev-environment-js</pre>.
</p>,
container
);
} else {
render(
<StrictMode>
<App />
</StrictMode>,
container
);
}
}
+97
View File
@@ -0,0 +1,97 @@
/*! purgecss start ignore */
@import './syntax-highlight.css';
/*! purgecss end ignore */
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@plugin '@tailwindcss/forms' {
strategy: class;
}
@custom-variant dark (&:where(.dark, .dark *));
@source '../src';
@source '../../ui-code-block/src';
@source '../../ui-common/src';
@source '../../ui-icons/src';
@source '../../ui-project-details/src';
@source '../../ui-render-config/src';
@source '../../shared/src';
@source '../../migrate/src';
@source '../../../node_modules/@nx/graph';
@source not '../../**/*.{spec,test,stories}.*';
@source not '../../**/eslint.config.*';
/* v3 compat: default border color was gray-200, v4 is currentColor. */
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
/* Strip default quote characters around inline code and blockquote. */
.prose :where(code):not(:where([class~='not-prose'] *))::before,
.prose :where(code):not(:where([class~='not-prose'] *))::after,
.prose
:where(blockquote p:first-of-type):not(
:where([class~='not-prose'] *)
)::before,
.prose
:where(blockquote p:last-of-type):not(:where([class~='not-prose'] *))::after {
content: '';
}
/** Scrollbars **/
.dark {
color-scheme: dark;
}
/**
* Tooltips
*/
[data-tooltip] {
position: relative;
}
[data-tooltip]:before {
background-color: hsla(214, 61%, 11%, 1);
border-radius: 0.375rem;
color: hsla(0, 0%, 95%, 1);
content: attr(data-tooltip);
left: 0;
opacity: 0;
padding: 0.25rem 0.5rem;
position: absolute;
text-align: center;
text-transform: capitalize;
top: 120%;
transition: opacity 150ms ease-out;
visibility: hidden;
z-index: 1;
width: max-content;
}
[data-tooltip][data-tooltip-align-right]:before {
left: auto;
right: 0;
}
[data-tooltip]:hover:before {
opacity: 1;
visibility: visible;
}
/* Dark mode */
html.dark .adaptive-icon {
/* fill: white; */
filter: invert(1);
}
.adaptive-icon {
fill: black;
}
+79
View File
@@ -0,0 +1,79 @@
html {
--interactive-muted: #94a3b8;
}
.dark {
--interactive-muted: #64748b;
}
.hljs .hljs-comment,
.hljs .hljs-quote {
color: var(--interactive-muted);
}
.hljs .hljs-addition,
.hljs .hljs-keyword,
.hljs .hljs-selector-tag {
color: #859900;
}
.hljs .hljs-doctag,
.hljs .hljs-literal,
.hljs .hljs-meta .hljs .hljs-meta-string,
.hljs .hljs-number,
.hljs .hljs-regexp,
.hljs .hljs-string {
color: #2aa198;
}
.hljs .hljs-name,
.hljs .hljs-section,
.hljs .hljs-selector-class,
.hljs .hljs-selector-id,
.hljs .hljs-title {
color: #268bd2;
}
.hljs .hljs-attr,
.hljs .hljs-attribute,
.hljs .hljs-class .hljs .hljs-title,
.hljs .hljs-template-variable,
.hljs .hljs-type,
.hljs .hljs-variable {
color: #b58900;
}
.hljs .hljs-bullet,
.hljs .hljs-link,
.hljs .hljs-meta,
.hljs .hljs-meta .hljs .hljs-keyword,
.hljs .hljs-selector-attr,
.hljs .hljs-selector-pseudo,
.hljs .hljs-subst,
.hljs .hljs-symbol {
color: #cb4b16;
}
.hljs .hljs-built_in,
.hljs .hljs-deletion {
color: #dc322f;
}
.hljs .hljs-formula {
background: #073642;
}
.hljs .hljs-emphasis {
font-style: italic;
}
.hljs .hljs-strong {
font-weight: 700;
}
.linenumber {
border-color: rgb(59 130 246);
background: #e5e7eb;
}
html.dark .linenumber {
background: #e5e7eb33;
}
+55
View File
@@ -0,0 +1,55 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "out-tsc",
"tsBuildInfoFile": "out-tsc/tsconfig.app.tsbuildinfo",
"declaration": true,
"composite": true,
"types": ["node"],
"lib": ["DOM", "es2022"]
},
"files": [
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
],
"exclude": [
"out-tsc",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"jest.config.ts",
"**/*.stories.ts",
"**/*.stories.js",
"**/*.stories.jsx",
"**/*.stories.tsx"
],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"],
"references": [
{
"path": "../ui-code-block/tsconfig.lib.json"
},
{
"path": "../ui-common/tsconfig.lib.json"
},
{
"path": "../shared/tsconfig.lib.json"
},
{
"path": "../migrate/tsconfig.lib.json"
},
{
"path": "../ui-project-details/tsconfig.lib.json"
},
{
"path": "../project-details/tsconfig.lib.json"
},
{
"path": "../ui-render-config/tsconfig.lib.json"
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
}
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "out-tsc/spec",
"customConditions": ["@nx/nx-source"],
"types": ["jest", "node"],
"lib": ["DOM"]
},
"exclude": ["out-tsc"],
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts",
"jest.config.ts"
],
"files": [
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
"../../node_modules/@nx/react/typings/image.d.ts"
]
}
+158
View File
@@ -0,0 +1,158 @@
const { join } = require('node:path');
// nx-ignore-next-line
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
// nx-ignore-next-line
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
module.exports = {
output: {
path: join(__dirname, 'dist'),
},
devServer: {
port: getPort(process.env.NX_TASK_TARGET_CONFIGURATION),
historyApiFallback: {
disableDotRule: true,
},
},
resolve: {
alias: {
react: 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react-dom': 'preact/compat', // Must be below test-utils
'react/jsx-runtime': 'preact/jsx-runtime',
},
},
plugins: [
new NxAppWebpackPlugin({
index: './src/index.html',
compiler: 'babel',
main: './src/main.tsx',
tsConfig: './tsconfig.app.json',
styles: ['./src/styles.css'],
scripts: [],
assets: getAssets(process.env.NX_TASK_TARGET_CONFIGURATION),
webpackConfig: './webpack.config.js',
outputHashing: 'none',
deleteOutputPath: false,
}),
new NxReactWebpackPlugin({
svgr: false,
}),
],
};
function getPort(nxConfiguration) {
switch (nxConfiguration) {
case 'nx-console':
return 4202;
case 'release':
return 4203;
case 'watch':
return 4204;
case 'release-static':
return 4205;
case 'dev-e2e':
return 4206;
default: // dev
return 4201;
}
}
function getAssets(nxConfiguration) {
switch (nxConfiguration) {
case 'nx-console':
return [
'./src/favicon.ico',
{
input: './src/assets/project-graphs',
output: '/assets/project-graphs',
glob: 'e2e.json',
},
{
input: './src/assets/nx-console',
output: '/',
glob: 'environment.js',
},
];
case 'release':
return [
'./src/favicon.ico',
{
input: './src/assets/project-graphs',
output: '/assets/project-graphs',
glob: 'e2e.json',
},
{
input: './src/assets/task-graphs',
output: '/assets/task-graphs',
glob: 'e2e.json',
},
{
input: './src/assets/source-maps',
output: '/assets/source-maps',
glob: 'e2e.json',
},
{
input: './src/assets/release',
output: '/',
glob: 'environment.js',
},
];
case 'watch':
return [
'./src/favicon.ico',
{
input: './src/assets/watch',
output: '/',
glob: 'environment.js',
},
];
case 'release-static':
return [
'./src/favicon.ico',
{
input: './src/assets/project-graphs',
output: '/assets/project-graphs',
glob: 'e2e.json',
},
{
input: './src/assets/task-graphs',
output: '/assets/task-graphs',
glob: 'e2e.json',
},
{
input: './src/assets/release-static',
output: '/',
glob: 'environment.js',
},
];
case 'dev-e2e':
return [
'./src/favicon.ico',
'./src/assets/project-graphs/',
'./src/assets/task-graphs/',
'./src/assets/task-inputs/',
'./src/assets/source-maps/',
{
input: './src/assets/dev-e2e',
output: '/',
glob: 'environment.js',
},
];
default: // dev
return [
'./src/favicon.ico',
'./src/assets/project-graphs/',
'./src/assets/task-graphs/',
'./src/assets/generated-project-graphs/',
'./src/assets/generated-task-graphs/',
'./src/assets/generated-task-inputs/',
'./src/assets/generated-source-maps/',
{
input: './src/assets/dev',
output: '/',
glob: 'environment.js',
},
];
}
}