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 (