Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graph): show partial project graph & errors in graph app #22838

Merged
merged 4 commits into from Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion graph/client/src/app/feature-projects/project-list.tsx
Expand Up @@ -219,7 +219,7 @@ function SubProjectList({
</span>
</div>
) : null}
<ul className="mt-2 -ml-3">
<ul className="-ml-3 mt-2">
{sortedProjects.map((project) => {
return (
<ProjectListItem
Expand Down
17 changes: 15 additions & 2 deletions graph/client/src/app/routes.tsx
@@ -1,12 +1,16 @@
import { redirect, RouteObject } from 'react-router-dom';
import { redirect, RouteObject, json } from 'react-router-dom';
import { ProjectsSidebar } from './feature-projects/projects-sidebar';
import { TasksSidebar } from './feature-tasks/tasks-sidebar';
import { Shell } from './shell';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
import type {
GraphError,
ProjectGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
/* eslint-enable @nx/enforce-module-boundaries */
import {
getEnvironmentConfig,
getProjectGraphDataService,
Expand Down Expand Up @@ -78,17 +82,26 @@ const projectDetailsLoader = async (
hash: string;
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
errors?: GraphError[];
}> => {
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,
};
};

Expand Down
66 changes: 53 additions & 13 deletions graph/client/src/app/shell.tsx
@@ -1,30 +1,48 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import {
GraphError,
ProjectGraphClientResponse,
} from 'nx/src/command-line/graph/graph';
/* eslint-enable @nx/enforce-module-boundaries */

import {
ArrowDownTrayIcon,
ArrowLeftCircleIcon,
InformationCircleIcon,
} from '@heroicons/react/24/outline';
import {
ErrorToast,
fetchProjectGraph,
getProjectGraphDataService,
useEnvironmentConfig,
useIntervalWhen,
} from '@nx/graph/shared';
import { Dropdown, Spinner } from '@nx/graph/ui-components';
import { getSystemTheme, Theme, ThemePanel } from '@nx/graph/ui-theme';
import { Tooltip } from '@nx/graph/ui-tooltips';
import classNames from 'classnames';
import { DebuggerPanel } from './ui-components/debugger-panel';
import { getGraphService } from './machines/graph.service';
import { useLayoutEffect, useState } from 'react';
import {
Outlet,
useNavigate,
useNavigation,
useParams,
useRouteLoaderData,
} from 'react-router-dom';
import { getSystemTheme, Theme, ThemePanel } from '@nx/graph/ui-theme';
import { Dropdown, Spinner } from '@nx/graph/ui-components';
import { useCurrentPath } from './hooks/use-current-path';
import { ExperimentalFeature } from './ui-components/experimental-feature';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { RankdirPanel } from './feature-projects/panels/rankdir-panel';
import { useCurrentPath } from './hooks/use-current-path';
import { getProjectGraphService } from './machines/get-services';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { Tooltip } from '@nx/graph/ui-tooltips';
import { getGraphService } from './machines/graph.service';
import { DebuggerPanel } from './ui-components/debugger-panel';
import { ExperimentalFeature } from './ui-components/experimental-feature';
import { TooltipDisplay } from './ui-tooltips/graph-tooltip-display';
import { useEnvironmentConfig } from '@nx/graph/shared';

export function Shell(): JSX.Element {
const projectGraphService = getProjectGraphService();
const projectGraphDataService = getProjectGraphDataService();

const graphService = getGraphService();

const lastPerfReport = useSyncExternalStore(
Expand All @@ -43,9 +61,30 @@ export function Shell(): JSX.Element {
const navigate = useNavigate();
const { state: navigationState } = useNavigation();
const currentPath = useCurrentPath();
const { selectedWorkspaceId } = useParams();
const params = useParams();
const currentRoute = currentPath.currentPath;

const [errors, setErrors] = useState<GraphError[] | undefined>(undefined);
const { errors: routerErrors } = useRouteLoaderData('selectedWorkspace') as {
errors: GraphError[];
};
useLayoutEffect(() => {
setErrors(routerErrors);
}, [routerErrors]);
useIntervalWhen(
() => {
fetchProjectGraph(
projectGraphDataService,
params,
environmentConfig.appConfig
).then((response: ProjectGraphClientResponse) => {
setErrors(response.errors);
});
},
1000,
environmentConfig.watch
);

const topLevelRoute = currentRoute.startsWith('/tasks')
? '/tasks'
: '/projects';
Expand Down Expand Up @@ -84,7 +123,7 @@ export function Shell(): JSX.Element {
<div
className={`${
environmentConfig.environment === 'nx-console'
? 'absolute top-5 left-5 z-50 bg-white'
? 'absolute left-5 top-5 z-50 bg-white'
: 'relative flex h-full overflow-y-scroll'
} w-72 flex-col pb-10 shadow-lg ring-1 ring-slate-900/10 ring-opacity-10 transition-all dark:ring-slate-300/10`}
id="sidebar"
Expand Down Expand Up @@ -165,7 +204,7 @@ export function Shell(): JSX.Element {
{environment.appConfig.showDebugger ? (
<DebuggerPanel
projects={environment.appConfig.workspaces}
selectedProject={selectedWorkspaceId}
selectedProject={params.selectedWorkspaceId}
lastPerfReport={lastPerfReport}
selectedProjectChange={projectChange}
></DebuggerPanel>
Expand Down Expand Up @@ -212,11 +251,12 @@ export function Shell(): JSX.Element {
data-cy="downloadImageButton"
onClick={downloadImage}
>
<ArrowDownTrayIcon className="absolute top-1/2 left-1/2 -mt-3 -ml-3 h-6 w-6" />
<ArrowDownTrayIcon className="absolute left-1/2 top-1/2 -ml-3 -mt-3 h-6 w-6" />
</button>
</Tooltip>
</div>
</div>
<ErrorToast errors={errors} />
</div>
);
}
99 changes: 86 additions & 13 deletions graph/client/src/app/ui-components/error-boundary.tsx
@@ -1,27 +1,100 @@
import { useEnvironmentConfig } from '@nx/graph/shared';
import { ProjectDetailsHeader } from 'graph/project-details/src/lib/project-details-header';
import { useRouteError } from 'react-router-dom';
import { ProjectDetailsHeader } from '@nx/graph/project-details';
import {
fetchProjectGraph,
getProjectGraphDataService,
useEnvironmentConfig,
useIntervalWhen,
} from '@nx/graph/shared';
import { ErrorRenderer } from '@nx/graph/ui-components';
import {
isRouteErrorResponse,
useParams,
useRouteError,
} from 'react-router-dom';

export function ErrorBoundary() {
let error = useRouteError();
console.error(error);
const environment = useEnvironmentConfig()?.environment;

let message = 'Disconnected from graph server. ';
if (environment === 'nx-console') {
message += 'Please refresh the page.';
const { environment, appConfig, watch } = useEnvironmentConfig();
const projectGraphDataService = getProjectGraphDataService();
const params = useParams();

const hasErrorData =
isRouteErrorResponse(error) && error.data.errors?.length > 0;

useIntervalWhen(
async () => {
fetchProjectGraph(projectGraphDataService, params, appConfig).then(
(data) => {
if (
isRouteErrorResponse(error) &&
error.data.id === 'project-not-found' &&
data.projects.find((p) => p.name === error.data.projectName)
) {
window.location.reload();
}
return;
}
);
},
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 += 'Please rerun your command and refresh the page.';
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">
<ProjectDetailsHeader />
<h1 className="mb-4 text-4xl dark:text-slate-100">Error</h1>
<div>
<p className="mb-4 text-lg dark:text-slate-200">{message}</p>
<p className="text-sm">Error message: {error?.toString()}</p>
{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>}
</div>
);
}
14 changes: 7 additions & 7 deletions graph/client/src/app/ui-components/project-details-modal.tsx
@@ -1,11 +1,11 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
// nx-ignore-next-line
import { ProjectDetailsWrapper } from '@nx/graph/project-details';
/* eslint-enable @nx/enforce-module-boundaries */
import { useFloating } from '@floating-ui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { ProjectDetailsWrapper } from '@nx/graph/project-details';
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import { ProjectGraphClientResponse } from 'nx/src/command-line/graph/graph';
import { useEffect, useState } from 'react';
import { useRouteLoaderData, useSearchParams } from 'react-router-dom';

Expand Down Expand Up @@ -50,15 +50,15 @@ export function ProjectDetailsModal() {
return (
isOpen && (
<div
className="top-24 z-20 right-4 opacity-100 bg-white dark:bg-slate-800 fixed h-max w-1/3"
className="fixed right-4 top-24 z-20 h-max w-1/3 bg-white opacity-100 dark:bg-slate-800"
style={{
height: 'calc(100vh - 6rem - 2rem)',
}}
ref={refs.setFloating}
>
<div className="rounded-md h-full border border-slate-500">
<div className="h-full rounded-md border border-slate-500">
<ProjectDetailsWrapper project={project} sourceMap={sourceMap} />
<div className="top-2 right-2 absolute" onClick={onClose}>
<div className="absolute right-2 top-2" onClick={onClose}>
<XMarkIcon className="h-4 w-4" />
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions graph/project-details/src/index.ts
@@ -1,2 +1,3 @@
export * from './lib/project-details-wrapper';
export * from './lib/project-details-page';
export * from './lib/project-details-header';
10 changes: 8 additions & 2 deletions graph/project-details/src/lib/project-details-page.tsx
@@ -1,6 +1,10 @@
/* eslint-disable @nx/enforce-module-boundaries */
// nx-ignore-next-line
import type { ProjectGraphProjectNode } from '@nx/devkit';
import { ProjectGraphProjectNode } from '@nx/devkit';
// nx-ignore-next-line
import { GraphError } from 'nx/src/command-line/graph/graph';
/* eslint-enable @nx/enforce-module-boundaries */

import {
ScrollRestoration,
useParams,
Expand All @@ -16,12 +20,13 @@ import {
import { ProjectDetailsHeader } from './project-details-header';

export function ProjectDetailsPage() {
const { project, sourceMap, hash } = useRouteLoaderData(
const { project, sourceMap, hash, errors } = useRouteLoaderData(
'selectedProjectDetails'
) as {
hash: string;
project: ProjectGraphProjectNode;
sourceMap: Record<string, string[]>;
errors?: GraphError[];
};

const { environment, watch, appConfig } = useEnvironmentConfig();
Expand Down Expand Up @@ -56,6 +61,7 @@ export function ProjectDetailsPage() {
<ProjectDetailsWrapper
project={project}
sourceMap={sourceMap}
errors={errors}
></ProjectDetailsWrapper>
</div>
</div>
Expand Down