Skip to content

Commit

Permalink
feat(graph): expose functions to render pdv & error page (#27833)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
MaxKless and jaysoo authored Sep 13, 2024
1 parent 61b3503 commit dc821ab
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 82 deletions.
Original file line number Diff line number Diff line change
@@ -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<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 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 (
<>
<ExpandedTargetsProvider>
<ProjectDetails
project={project}
sourceMap={sourceMap}
onViewInProjectGraph={handleViewInProjectGraph}
onViewInTaskGraph={handleViewInTaskGraph}
onRunTarget={handleRunTarget}
viewInProjectGraphPosition="bottom"
connectedToCloud={connectedToCloud}
onNxConnect={handleNxConnect}
/>
</ExpandedTargetsProvider>
<ErrorToastUI errors={errors} />
</>
);
} else {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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<string, string[]>;
errors?: GraphError[];
connectedToCloud?: boolean;
}

export type ProjectDetailsEvents = {
type: 'loadData';
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
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;
}),
],
},
],
},
});
39 changes: 6 additions & 33 deletions graph/client/src/app/ui-components/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -63,38 +63,11 @@ export function ErrorBoundary() {
return (
<div className="flex h-screen w-full flex-col items-center">
{environment !== 'nx-console' && <ProjectDetailsHeader />}
<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>
{hasErrorData && (
<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={error.data.errors} />
</div>
</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>}
<ErrorPage
message={message}
stack={stack}
errors={hasErrorData ? error.data.errors : undefined}
/>
</div>
);
}
49 changes: 49 additions & 0 deletions graph/client/src/app/ui-components/error-page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<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>
);
}
13 changes: 13 additions & 0 deletions graph/client/src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<ProjectDetailsState, any, ProjectDetailsEvents>;
renderError?: (data: any) => void;
}
}
declare module 'cytoscape' {
Expand Down
Loading

0 comments on commit dc821ab

Please sign in to comment.