diff --git a/graph/client/src/app/console-project-details/project-details.app.tsx b/graph/client/src/app/console-project-details/project-details.app.tsx
new file mode 100644
index 0000000000000..13c20e92f1939
--- /dev/null
+++ b/graph/client/src/app/console-project-details/project-details.app.tsx
@@ -0,0 +1,94 @@
+import { useCallback } from 'react';
+import {
+ ErrorToastUI,
+ ExpandedTargetsProvider,
+ getExternalApiService,
+} from '@nx/graph/shared';
+import { useMachine, useSelector } from '@xstate/react';
+import { ProjectDetails } from '@nx/graph-internal/ui-project-details';
+import {
+ ProjectDetailsEvents,
+ ProjectDetailsState,
+} from './project-details.machine';
+import { Interpreter } from 'xstate';
+
+export function ProjectDetailsApp({
+ service,
+}: {
+ service: Interpreter;
+}) {
+ 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 handleViewInProjectGraph = useCallback(
+ (data: { projectName: string }) => {
+ externalApiService.postEvent({
+ type: 'open-project-graph',
+ payload: {
+ projectName: data.projectName,
+ },
+ });
+ },
+ [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 (
+ <>
+
+
+
+
+ >
+ );
+ } else {
+ return null;
+ }
+}
diff --git a/graph/client/src/app/console-project-details/project-details.machine.spec.ts b/graph/client/src/app/console-project-details/project-details.machine.spec.ts
new file mode 100644
index 0000000000000..f1405c0efeacb
--- /dev/null
+++ b/graph/client/src/app/console-project-details/project-details.machine.spec.ts
@@ -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);
+ });
+});
diff --git a/graph/client/src/app/console-project-details/project-details.machine.ts b/graph/client/src/app/console-project-details/project-details.machine.ts
new file mode 100644
index 0000000000000..c778ac4f56825
--- /dev/null
+++ b/graph/client/src/app/console-project-details/project-details.machine.ts
@@ -0,0 +1,58 @@
+/* eslint-disable @nx/enforce-module-boundaries */
+// nx-ignore-next-line
+import type { ProjectGraphProjectNode } from '@nx/devkit';
+// nx-ignore-next-line
+import { GraphError } from 'nx/src/command-line/graph/graph';
+/* eslint-enable @nx/enforce-module-boundaries */
+import { createMachine } from 'xstate';
+import { assign } from '@xstate/immer';
+
+export interface ProjectDetailsState {
+ project: null | ProjectGraphProjectNode;
+ sourceMap: null | Record;
+ errors?: GraphError[];
+ connectedToCloud?: boolean;
+}
+
+export type ProjectDetailsEvents = {
+ type: 'loadData';
+ project: ProjectGraphProjectNode;
+ sourceMap: Record;
+ connectedToCloud: boolean;
+ errors?: GraphError[];
+};
+
+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;
+ }),
+ ],
+ },
+ ],
+ },
+});
diff --git a/graph/client/src/app/ui-components/error-boundary.tsx b/graph/client/src/app/ui-components/error-boundary.tsx
index e4993ca9a47e8..1c9522cc31585 100644
--- a/graph/client/src/app/ui-components/error-boundary.tsx
+++ b/graph/client/src/app/ui-components/error-boundary.tsx
@@ -5,12 +5,12 @@ import {
useEnvironmentConfig,
usePoll,
} from '@nx/graph/shared';
-import { ErrorRenderer } from '@nx/graph/ui-components';
import {
isRouteErrorResponse,
useParams,
useRouteError,
} from 'react-router-dom';
+import { ErrorPage } from './error-page';
export function ErrorBoundary() {
let error = useRouteError();
@@ -63,38 +63,11 @@ export function ErrorBoundary() {
return (
{environment !== 'nx-console' &&
}
-
-
Error
-
-
-
- {hasErrorData && (
-
-
- Nx encountered the following issues while processing the project
- graph:{' '}
-
-
-
-
-
- )}
-
-
- );
-}
-
-function ErrorWithStack({
- message,
- stack,
-}: {
- message: string | JSX.Element;
- stack?: string;
-}) {
- return (
-
-
{message}
- {stack &&
Error message: {stack}
}
+
);
}
diff --git a/graph/client/src/app/ui-components/error-page.tsx b/graph/client/src/app/ui-components/error-page.tsx
new file mode 100644
index 0000000000000..841733d5e55f2
--- /dev/null
+++ b/graph/client/src/app/ui-components/error-page.tsx
@@ -0,0 +1,49 @@
+/* eslint-disable @nx/enforce-module-boundaries */
+// nx-ignore-next-line
+import { ErrorRenderer } from '@nx/graph/ui-components';
+import { GraphError } from 'nx/src/command-line/graph/graph';
+/* eslint-enable @nx/enforce-module-boundaries */
+import type { JSX } from 'react';
+
+export type ErrorPageProps = {
+ message: string | JSX.Element;
+ stack?: string;
+ errors: GraphError[];
+};
+
+export function ErrorPage({ message, stack, errors }: ErrorPageProps) {
+ return (
+
+
Error
+
+
+
+ {errors && (
+
+
+ Nx encountered the following issues while processing the project
+ graph:{' '}
+
+
+
+
+
+ )}
+
+ );
+}
+
+function ErrorWithStack({
+ message,
+ stack,
+}: {
+ message: string | JSX.Element;
+ stack?: string;
+}) {
+ return (
+
+
{message}
+ {stack &&
Error message: {stack}
}
+
+ );
+}
diff --git a/graph/client/src/globals.d.ts b/graph/client/src/globals.d.ts
index 844bea3c3d1b3..cee748a801fbc 100644
--- a/graph/client/src/globals.d.ts
+++ b/graph/client/src/globals.d.ts
@@ -6,6 +6,12 @@ import type {
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 {
@@ -20,6 +26,13 @@ export declare global {
appConfig: AppConfig;
useXstateInspect: boolean;
externalApi?: ExternalApi;
+
+ // using bundled graph components directly from outside the graph app
+ __NX_RENDER_GRAPH__?: boolean;
+ renderPDV?: (
+ data: any
+ ) => Interpreter;
+ renderError?: (data: any) => void;
}
}
declare module 'cytoscape' {
diff --git a/graph/client/src/main.tsx b/graph/client/src/main.tsx
index 7ad6c5b98f45e..6849ff6588467 100644
--- a/graph/client/src/main.tsx
+++ b/graph/client/src/main.tsx
@@ -4,35 +4,85 @@ if (process.env.NODE_ENV === 'development') {
require('preact/debug');
}
+import { projectDetailsMachine } from './app/console-project-details/project-details.machine';
+/* eslint-disable @nx/enforce-module-boundaries */
+// nx-ignore-next-line
+import type { ProjectGraphProjectNode } from '@nx/devkit';
+// nx-ignore-next-line
+import type { GraphError } from 'nx/src/command-line/graph/graph';
+/* eslint-enable @nx/enforce-module-boundaries */
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';
-if (window.useXstateInspect === true) {
- inspect({
- url: 'https://stately.ai/viz?inspect',
- iframe: false, // open in new window
- });
-}
+if (window.__NX_RENDER_GRAPH__ === false) {
+ window.externalApi = new ExternalApiImpl();
+
+ window.renderPDV = (data: {
+ project: ProjectGraphProjectNode;
+ sourceMap: Record;
+ connectedToCloud: boolean;
+ errors?: GraphError[];
+ }) => {
+ const service = interpret(projectDetailsMachine).start();
+
+ service.send({
+ type: 'loadData',
+ ...data,
+ });
+
+ render(
+
+
+ ,
+ document.getElementById('app')
+ );
-window.externalApi = new ExternalApiImpl();
-const container = document.getElementById('app');
-
-if (!window.appConfig) {
- render(
-
- No environment could be found. Please run{' '}
-
npx nx run graph-client:generate-dev-environment-js
.
-
,
- container
- );
+ return service;
+ };
+
+ window.renderError = (data: {
+ message: string;
+ stack?: string;
+ errors: GraphError[];
+ }) => {
+ render(
+
+
+ ,
+ document.getElementById('app')
+ );
+ };
} else {
- render(
-
-
- ,
- container
- );
+ 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(
+
+ No environment could be found. Please run{' '}
+
npx nx run graph-client:generate-dev-environment-js
.
+ ,
+ container
+ );
+ } else {
+ render(
+
+
+ ,
+ container
+ );
+ }
}
diff --git a/graph/ui-project-details/src/lib/project-details/project-details.tsx b/graph/ui-project-details/src/lib/project-details/project-details.tsx
index c5bbdc8a5011b..87e5165207841 100644
--- a/graph/ui-project-details/src/lib/project-details/project-details.tsx
+++ b/graph/ui-project-details/src/lib/project-details/project-details.tsx
@@ -90,7 +90,7 @@ export const ProjectDetails = ({
{onViewInProjectGraph && viewInProjectGraphPosition === 'top' && (
+ onClick={() =>
onViewInProjectGraph({ projectName: project.name })
}
/>
@@ -125,7 +125,7 @@ export const ProjectDetails = ({
{onViewInProjectGraph &&
viewInProjectGraphPosition === 'bottom' && (
+ onClick={() =>
onViewInProjectGraph({ projectName: project.name })
}
/>
@@ -162,11 +162,11 @@ export const ProjectDetails = ({
export default ProjectDetails;
-function ViewInProjectGraphButton({ callback }: { callback: () => void }) {
+function ViewInProjectGraphButton({ onClick }: { onClick: () => void }) {
return (