From dc821abd5249bb2769c58e3d9f466bd67bb516fa Mon Sep 17 00:00:00 2001
From: MaxKless <34165455+MaxKless@users.noreply.github.com>
Date: Fri, 13 Sep 2024 20:48:30 +0200
Subject: [PATCH] feat(graph): expose functions to render pdv & error page
(#27833)
we have fully isolated graph & error components with a good api but no
way to access them directly from the outside (in console).
This PR adds two functions to the window object so that we can render
the PDV directly instead of needing the entire app with routing and
everything.
---------
Co-authored-by: Jack Hsu
---
.../project-details.app.tsx | 94 ++++++++++++++++++
.../project-details.machine.spec.ts | 49 ++++++++++
.../project-details.machine.ts | 58 +++++++++++
.../src/app/ui-components/error-boundary.tsx | 39 ++------
.../src/app/ui-components/error-page.tsx | 49 ++++++++++
graph/client/src/globals.d.ts | 13 +++
graph/client/src/main.tsx | 96 ++++++++++++++-----
.../lib/project-details/project-details.tsx | 8 +-
.../src/lib/utils/group-targets.ts | 4 +-
package.json | 2 +-
pnpm-lock.yaml | 38 ++++----
11 files changed, 368 insertions(+), 82 deletions(-)
create mode 100644 graph/client/src/app/console-project-details/project-details.app.tsx
create mode 100644 graph/client/src/app/console-project-details/project-details.machine.spec.ts
create mode 100644 graph/client/src/app/console-project-details/project-details.machine.ts
create mode 100644 graph/client/src/app/ui-components/error-page.tsx
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 (