feat(graph): show partial project graph & errors in graph app (#22838)
This commit is contained in:
parent
0ceea2f7da
commit
c8d44b0355
@ -219,7 +219,7 @@ function SubProjectList({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<ul className="mt-2 -ml-3">
|
<ul className="-ml-3 mt-2">
|
||||||
{sortedProjects.map((project) => {
|
{sortedProjects.map((project) => {
|
||||||
return (
|
return (
|
||||||
<ProjectListItem
|
<ProjectListItem
|
||||||
|
|||||||
@ -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 { ProjectsSidebar } from './feature-projects/projects-sidebar';
|
||||||
import { TasksSidebar } from './feature-tasks/tasks-sidebar';
|
import { TasksSidebar } from './feature-tasks/tasks-sidebar';
|
||||||
import { Shell } from './shell';
|
import { Shell } from './shell';
|
||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
// nx-ignore-next-line
|
// 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
|
// nx-ignore-next-line
|
||||||
import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
|
import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
import {
|
import {
|
||||||
getEnvironmentConfig,
|
getEnvironmentConfig,
|
||||||
getProjectGraphDataService,
|
getProjectGraphDataService,
|
||||||
@ -78,6 +82,7 @@ const projectDetailsLoader = async (
|
|||||||
hash: string;
|
hash: string;
|
||||||
project: ProjectGraphProjectNode;
|
project: ProjectGraphProjectNode;
|
||||||
sourceMap: Record<string, string[]>;
|
sourceMap: Record<string, string[]>;
|
||||||
|
errors?: GraphError[];
|
||||||
}> => {
|
}> => {
|
||||||
const workspaceData = await workspaceDataLoader(selectedWorkspaceId);
|
const workspaceData = await workspaceDataLoader(selectedWorkspaceId);
|
||||||
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
|
const sourceMaps = await sourceMapsLoader(selectedWorkspaceId);
|
||||||
@ -85,10 +90,18 @@ const projectDetailsLoader = async (
|
|||||||
const project = workspaceData.projects.find(
|
const project = workspaceData.projects.find(
|
||||||
(project) => project.name === projectName
|
(project) => project.name === projectName
|
||||||
);
|
);
|
||||||
|
if (!project) {
|
||||||
|
throw json({
|
||||||
|
id: 'project-not-found',
|
||||||
|
projectName,
|
||||||
|
errors: workspaceData.errors,
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
hash: workspaceData.hash,
|
hash: workspaceData.hash,
|
||||||
project,
|
project,
|
||||||
sourceMap: sourceMaps[project.data.root],
|
sourceMap: sourceMaps[project.data.root],
|
||||||
|
errors: workspaceData.errors,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
import {
|
||||||
ArrowDownTrayIcon,
|
ArrowDownTrayIcon,
|
||||||
ArrowLeftCircleIcon,
|
ArrowLeftCircleIcon,
|
||||||
InformationCircleIcon,
|
InformationCircleIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} 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 classNames from 'classnames';
|
||||||
import { DebuggerPanel } from './ui-components/debugger-panel';
|
import { useLayoutEffect, useState } from 'react';
|
||||||
import { getGraphService } from './machines/graph.service';
|
|
||||||
import {
|
import {
|
||||||
Outlet,
|
Outlet,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
useNavigation,
|
useNavigation,
|
||||||
useParams,
|
useParams,
|
||||||
|
useRouteLoaderData,
|
||||||
} from 'react-router-dom';
|
} 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 { RankdirPanel } from './feature-projects/panels/rankdir-panel';
|
|
||||||
import { getProjectGraphService } from './machines/get-services';
|
|
||||||
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
||||||
import { Tooltip } from '@nx/graph/ui-tooltips';
|
import { RankdirPanel } from './feature-projects/panels/rankdir-panel';
|
||||||
|
import { useCurrentPath } from './hooks/use-current-path';
|
||||||
|
import { getProjectGraphService } from './machines/get-services';
|
||||||
|
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 { TooltipDisplay } from './ui-tooltips/graph-tooltip-display';
|
||||||
import { useEnvironmentConfig } from '@nx/graph/shared';
|
|
||||||
|
|
||||||
export function Shell(): JSX.Element {
|
export function Shell(): JSX.Element {
|
||||||
const projectGraphService = getProjectGraphService();
|
const projectGraphService = getProjectGraphService();
|
||||||
|
const projectGraphDataService = getProjectGraphDataService();
|
||||||
|
|
||||||
const graphService = getGraphService();
|
const graphService = getGraphService();
|
||||||
|
|
||||||
const lastPerfReport = useSyncExternalStore(
|
const lastPerfReport = useSyncExternalStore(
|
||||||
@ -43,9 +61,30 @@ export function Shell(): JSX.Element {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { state: navigationState } = useNavigation();
|
const { state: navigationState } = useNavigation();
|
||||||
const currentPath = useCurrentPath();
|
const currentPath = useCurrentPath();
|
||||||
const { selectedWorkspaceId } = useParams();
|
const params = useParams();
|
||||||
const currentRoute = currentPath.currentPath;
|
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')
|
const topLevelRoute = currentRoute.startsWith('/tasks')
|
||||||
? '/tasks'
|
? '/tasks'
|
||||||
: '/projects';
|
: '/projects';
|
||||||
@ -84,7 +123,7 @@ export function Shell(): JSX.Element {
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
environmentConfig.environment === 'nx-console'
|
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'
|
: '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`}
|
} 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"
|
id="sidebar"
|
||||||
@ -165,7 +204,7 @@ export function Shell(): JSX.Element {
|
|||||||
{environment.appConfig.showDebugger ? (
|
{environment.appConfig.showDebugger ? (
|
||||||
<DebuggerPanel
|
<DebuggerPanel
|
||||||
projects={environment.appConfig.workspaces}
|
projects={environment.appConfig.workspaces}
|
||||||
selectedProject={selectedWorkspaceId}
|
selectedProject={params.selectedWorkspaceId}
|
||||||
lastPerfReport={lastPerfReport}
|
lastPerfReport={lastPerfReport}
|
||||||
selectedProjectChange={projectChange}
|
selectedProjectChange={projectChange}
|
||||||
></DebuggerPanel>
|
></DebuggerPanel>
|
||||||
@ -212,11 +251,12 @@ export function Shell(): JSX.Element {
|
|||||||
data-cy="downloadImageButton"
|
data-cy="downloadImageButton"
|
||||||
onClick={downloadImage}
|
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>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ErrorToast errors={errors} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,100 @@
|
|||||||
import { useEnvironmentConfig } from '@nx/graph/shared';
|
import { ProjectDetailsHeader } from '@nx/graph/project-details';
|
||||||
import { ProjectDetailsHeader } from 'graph/project-details/src/lib/project-details-header';
|
import {
|
||||||
import { useRouteError } from 'react-router-dom';
|
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() {
|
export function ErrorBoundary() {
|
||||||
let error = useRouteError();
|
let error = useRouteError();
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const environment = useEnvironmentConfig()?.environment;
|
|
||||||
|
|
||||||
let message = 'Disconnected from graph server. ';
|
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 = 'Disconnected from graph server. ';
|
||||||
if (environment === 'nx-console') {
|
if (environment === 'nx-console') {
|
||||||
message += 'Please refresh the page.';
|
message += 'Please refresh the page.';
|
||||||
} else {
|
} else {
|
||||||
message += 'Please rerun your command and refresh the page.';
|
message += 'Please rerun your command and refresh the page.';
|
||||||
}
|
}
|
||||||
|
stack = error.toString();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col items-center">
|
<div className="flex h-screen w-full flex-col items-center">
|
||||||
<ProjectDetailsHeader />
|
{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>
|
<h1 className="mb-4 text-4xl dark:text-slate-100">Error</h1>
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-4 text-lg dark:text-slate-200">{message}</p>
|
<ErrorWithStack message={message} stack={stack} />
|
||||||
<p className="text-sm">Error message: {error?.toString()}</p>
|
</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>
|
||||||
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
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 { 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 { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouteLoaderData, useSearchParams } from 'react-router-dom';
|
import { useRouteLoaderData, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
@ -50,15 +50,15 @@ export function ProjectDetailsModal() {
|
|||||||
return (
|
return (
|
||||||
isOpen && (
|
isOpen && (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
height: 'calc(100vh - 6rem - 2rem)',
|
height: 'calc(100vh - 6rem - 2rem)',
|
||||||
}}
|
}}
|
||||||
ref={refs.setFloating}
|
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} />
|
<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" />
|
<XMarkIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from './lib/project-details-wrapper';
|
export * from './lib/project-details-wrapper';
|
||||||
export * from './lib/project-details-page';
|
export * from './lib/project-details-page';
|
||||||
|
export * from './lib/project-details-header';
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
// nx-ignore-next-line
|
// 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 {
|
import {
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
useParams,
|
useParams,
|
||||||
@ -16,12 +20,13 @@ import {
|
|||||||
import { ProjectDetailsHeader } from './project-details-header';
|
import { ProjectDetailsHeader } from './project-details-header';
|
||||||
|
|
||||||
export function ProjectDetailsPage() {
|
export function ProjectDetailsPage() {
|
||||||
const { project, sourceMap, hash } = useRouteLoaderData(
|
const { project, sourceMap, hash, errors } = useRouteLoaderData(
|
||||||
'selectedProjectDetails'
|
'selectedProjectDetails'
|
||||||
) as {
|
) as {
|
||||||
hash: string;
|
hash: string;
|
||||||
project: ProjectGraphProjectNode;
|
project: ProjectGraphProjectNode;
|
||||||
sourceMap: Record<string, string[]>;
|
sourceMap: Record<string, string[]>;
|
||||||
|
errors?: GraphError[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const { environment, watch, appConfig } = useEnvironmentConfig();
|
const { environment, watch, appConfig } = useEnvironmentConfig();
|
||||||
@ -56,6 +61,7 @@ export function ProjectDetailsPage() {
|
|||||||
<ProjectDetailsWrapper
|
<ProjectDetailsWrapper
|
||||||
project={project}
|
project={project}
|
||||||
sourceMap={sourceMap}
|
sourceMap={sourceMap}
|
||||||
|
errors={errors}
|
||||||
></ProjectDetailsWrapper>
|
></ProjectDetailsWrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { ProjectGraphProjectNode } from '@nx/devkit';
|
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 { useNavigate, useNavigation, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useNavigation, useSearchParams } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
|
ErrorToast,
|
||||||
getExternalApiService,
|
getExternalApiService,
|
||||||
useEnvironmentConfig,
|
useEnvironmentConfig,
|
||||||
useRouteConstructor,
|
useRouteConstructor,
|
||||||
@ -23,6 +27,7 @@ type ProjectDetailsProps = mapStateToPropsType &
|
|||||||
mapDispatchToPropsType & {
|
mapDispatchToPropsType & {
|
||||||
project: ProjectGraphProjectNode;
|
project: ProjectGraphProjectNode;
|
||||||
sourceMap: Record<string, string[]>;
|
sourceMap: Record<string, string[]>;
|
||||||
|
errors?: GraphError[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProjectDetailsWrapperComponent({
|
export function ProjectDetailsWrapperComponent({
|
||||||
@ -31,6 +36,7 @@ export function ProjectDetailsWrapperComponent({
|
|||||||
setExpandTargets,
|
setExpandTargets,
|
||||||
expandTargets,
|
expandTargets,
|
||||||
collapseAllTargets,
|
collapseAllTargets,
|
||||||
|
errors,
|
||||||
}: ProjectDetailsProps) {
|
}: ProjectDetailsProps) {
|
||||||
const environment = useEnvironmentConfig()?.environment;
|
const environment = useEnvironmentConfig()?.environment;
|
||||||
const externalApiService = getExternalApiService();
|
const externalApiService = getExternalApiService();
|
||||||
@ -158,6 +164,7 @@ export function ProjectDetailsWrapperComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ProjectDetails
|
<ProjectDetails
|
||||||
project={project}
|
project={project}
|
||||||
sourceMap={sourceMap}
|
sourceMap={sourceMap}
|
||||||
@ -165,6 +172,8 @@ export function ProjectDetailsWrapperComponent({
|
|||||||
onViewInTaskGraph={handleViewInTaskGraph}
|
onViewInTaskGraph={handleViewInTaskGraph}
|
||||||
onRunTarget={environment === 'nx-console' ? handleRunTarget : undefined}
|
onRunTarget={environment === 'nx-console' ? handleRunTarget : undefined}
|
||||||
/>
|
/>
|
||||||
|
<ErrorToast errors={errors} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,3 +6,4 @@ export * from './lib/use-route-constructor';
|
|||||||
export * from './lib/use-interval-when';
|
export * from './lib/use-interval-when';
|
||||||
export * from './lib/project-graph-data-service/get-project-graph-data-service';
|
export * from './lib/project-graph-data-service/get-project-graph-data-service';
|
||||||
export * from './lib/fetch-project-graph';
|
export * from './lib/fetch-project-graph';
|
||||||
|
export * from './lib/error-toast';
|
||||||
|
|||||||
132
graph/shared/src/lib/error-toast.tsx
Normal file
132
graph/shared/src/lib/error-toast.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { GraphError } from 'nx/src/command-line/graph/graph';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
|
||||||
|
import {
|
||||||
|
createRef,
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
useLayoutEffect,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { Transition } from '@headlessui/react';
|
||||||
|
import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { SetURLSearchParams, useSearchParams } from 'react-router-dom';
|
||||||
|
import { ErrorRenderer, Modal, ModalHandle } from '@nx/graph/ui-components';
|
||||||
|
|
||||||
|
export interface ErrorToastImperativeHandle {
|
||||||
|
closeModal: () => void;
|
||||||
|
openModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorToastProps {
|
||||||
|
errors?: GraphError[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorToast = forwardRef(
|
||||||
|
(
|
||||||
|
{ errors }: ErrorToastProps,
|
||||||
|
ref: ForwardedRef<ErrorToastImperativeHandle>
|
||||||
|
) => {
|
||||||
|
const inputsModalRef = createRef<ModalHandle>();
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
openModal: () => {
|
||||||
|
inputsModalRef?.current?.openModal();
|
||||||
|
},
|
||||||
|
closeModal: () => {
|
||||||
|
inputsModalRef?.current?.closeModal();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleModalOpen = useCallback(() => {
|
||||||
|
if (searchParams.get('show-error') === 'true') return;
|
||||||
|
setSearchParams(
|
||||||
|
(currentSearchParams) => {
|
||||||
|
currentSearchParams.set('show-error', 'true');
|
||||||
|
return currentSearchParams;
|
||||||
|
},
|
||||||
|
{ replace: true, preventScrollReset: true }
|
||||||
|
);
|
||||||
|
}, [setSearchParams, searchParams]);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
if (!searchParams.get('show-error')) return;
|
||||||
|
setSearchParams(
|
||||||
|
(currentSearchParams) => {
|
||||||
|
currentSearchParams.delete('show-error');
|
||||||
|
return currentSearchParams;
|
||||||
|
},
|
||||||
|
{ replace: true, preventScrollReset: true }
|
||||||
|
);
|
||||||
|
}, [setSearchParams, searchParams]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (searchParams.get('show-error') === 'true') {
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
inputsModalRef.current?.openModal();
|
||||||
|
} else {
|
||||||
|
setSearchParams(
|
||||||
|
(currentSearchParams) => {
|
||||||
|
currentSearchParams.delete('show-error');
|
||||||
|
return currentSearchParams;
|
||||||
|
},
|
||||||
|
{ replace: true, preventScrollReset: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchParams, inputsModalRef, errors, setSearchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
show={!!errors && errors.length > 0}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-x-0 bottom-0 px-4 py-2 text-center">
|
||||||
|
<div
|
||||||
|
onClick={() => inputsModalRef.current?.openModal()}
|
||||||
|
className="z-50 mx-auto flex w-fit max-w-[75%] cursor-pointer items-center rounded-md bg-red-600 p-4 text-slate-200 shadow-lg"
|
||||||
|
>
|
||||||
|
<ExclamationCircleIcon className="mr-2 inline-block h-6 w-6" />
|
||||||
|
Some project information might be missing. Click to see errors.
|
||||||
|
</div>
|
||||||
|
{errors?.length > 0 && (
|
||||||
|
<Modal
|
||||||
|
title={`Project Graph Errors`}
|
||||||
|
ref={inputsModalRef}
|
||||||
|
onOpen={handleModalOpen}
|
||||||
|
onClose={handleModalClose}
|
||||||
|
>
|
||||||
|
<ErrorRenderer errors={errors ?? []} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useRouterHandleModalOpen = (
|
||||||
|
searchParams: URLSearchParams,
|
||||||
|
setSearchParams: SetURLSearchParams
|
||||||
|
) =>
|
||||||
|
useCallback(() => {
|
||||||
|
if (searchParams.get('show-error') === 'true') return;
|
||||||
|
setSearchParams(
|
||||||
|
(currentSearchParams) => {
|
||||||
|
currentSearchParams.set('show-error', 'true');
|
||||||
|
return currentSearchParams;
|
||||||
|
},
|
||||||
|
{ replace: true, preventScrollReset: true }
|
||||||
|
);
|
||||||
|
}, [setSearchParams, searchParams]);
|
||||||
@ -61,6 +61,7 @@ export class MockProjectGraphService implements ProjectGraphService {
|
|||||||
focus: null,
|
focus: null,
|
||||||
exclude: [],
|
exclude: [],
|
||||||
groupByFolder: false,
|
groupByFolder: false,
|
||||||
|
isPartial: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
private taskGraphsResponse: TaskGraphClientResponse = {
|
private taskGraphsResponse: TaskGraphClientResponse = {
|
||||||
|
|||||||
@ -2,3 +2,5 @@ export * from './lib/debounced-text-input';
|
|||||||
export * from './lib/tag';
|
export * from './lib/tag';
|
||||||
export * from './lib/dropdown';
|
export * from './lib/dropdown';
|
||||||
export * from './lib/spinner';
|
export * from './lib/spinner';
|
||||||
|
export * from './lib/error-renderer';
|
||||||
|
export * from './lib/modal';
|
||||||
|
|||||||
63
graph/ui-components/src/lib/error-renderer.tsx
Normal file
63
graph/ui-components/src/lib/error-renderer.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
|
// nx-ignore-next-line
|
||||||
|
import { GraphError } from 'nx/src/command-line/graph/graph';
|
||||||
|
/* eslint-enable @nx/enforce-module-boundaries */
|
||||||
|
export function ErrorRenderer({ errors }: { errors: GraphError[] }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{errors.map((error, index) => {
|
||||||
|
const errorHeading =
|
||||||
|
error.pluginName && error.name
|
||||||
|
? `${error.name} - ${error.pluginName}`
|
||||||
|
: error.name ?? error.message;
|
||||||
|
const fileSpecifier =
|
||||||
|
isCauseWithLocation(error.cause) && error.cause.errors.length === 1
|
||||||
|
? `${error.fileName}:${error.cause.errors[0].location.line}:${error.cause.errors[0].location.column}`
|
||||||
|
: error.fileName;
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden pb-4">
|
||||||
|
<span className="inline-flex max-w-full flex-col break-words font-bold font-normal text-gray-900 md:inline dark:text-slate-200">
|
||||||
|
<span>{errorHeading}</span>
|
||||||
|
<span className="hidden px-1 md:inline">-</span>
|
||||||
|
<span>{fileSpecifier}</span>
|
||||||
|
</span>
|
||||||
|
<pre className="overflow-x-scroll pl-4 pt-3">
|
||||||
|
{isCauseWithErrors(error.cause) &&
|
||||||
|
error.cause.errors.length === 1 ? (
|
||||||
|
<div>
|
||||||
|
{error.message} <br />
|
||||||
|
{error.cause.errors[0].text}{' '}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>{error.stack}</div>
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCauseWithLocation(cause: unknown): cause is {
|
||||||
|
errors: {
|
||||||
|
location: {
|
||||||
|
column: number;
|
||||||
|
line: number;
|
||||||
|
};
|
||||||
|
text: string;
|
||||||
|
}[];
|
||||||
|
} {
|
||||||
|
return (
|
||||||
|
isCauseWithErrors(cause) &&
|
||||||
|
(cause as any).errors[0].location &&
|
||||||
|
(cause as any).errors[0].location.column &&
|
||||||
|
(cause as any).errors[0].location.line
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCauseWithErrors(
|
||||||
|
cause: unknown
|
||||||
|
): cause is { errors: { text: string }[] } {
|
||||||
|
return cause && (cause as any).errors && (cause as any).errors[0].text;
|
||||||
|
}
|
||||||
108
graph/ui-components/src/lib/modal.tsx
Normal file
108
graph/ui-components/src/lib/modal.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// component from https://tailwindui.com/components/application-ui/overlays/dialogs
|
||||||
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
|
import {
|
||||||
|
ForwardedRef,
|
||||||
|
Fragment,
|
||||||
|
ReactNode,
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
children: ReactNode;
|
||||||
|
title: string;
|
||||||
|
onOpen?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalHandle {
|
||||||
|
openModal: () => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Modal = forwardRef(
|
||||||
|
(
|
||||||
|
{ children, title, onOpen, onClose }: ModalProps,
|
||||||
|
ref: ForwardedRef<ModalHandle>
|
||||||
|
) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
onOpen?.();
|
||||||
|
} else {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
}, [open, onOpen, onClose]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
closeModal: () => {
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
openModal: () => {
|
||||||
|
setOpen(true);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={open} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-10" onClose={setOpen}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end items-center justify-center p-4 text-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel
|
||||||
|
className={`relative mx-4 my-8 w-full transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all md:max-w-5xl xl:max-w-7xl dark:bg-slate-900
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between rounded-t border-b bg-white p-2 pb-1 md:p-4 md:pb-2 dark:border-gray-600 dark:bg-slate-900">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-gray-900 dark:text-slate-200"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Dialog.Title>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||||
|
data-modal-hide="default-modal"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<XMarkIcon />
|
||||||
|
<span className="sr-only">Close modal</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[90vh] overflow-y-auto bg-white p-2 md:p-4 dark:bg-slate-900">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@ -3,6 +3,9 @@
|
|||||||
/* eslint-disable @nx/enforce-module-boundaries */
|
/* eslint-disable @nx/enforce-module-boundaries */
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
import type { ProjectGraphProjectNode } from '@nx/devkit';
|
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 { EyeIcon } from '@heroicons/react/24/outline';
|
import { EyeIcon } from '@heroicons/react/24/outline';
|
||||||
import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips';
|
import { PropertyInfoTooltip, Tooltip } from '@nx/graph/ui-tooltips';
|
||||||
@ -15,6 +18,7 @@ import { TargetTechnologies } from '../target-technologies/target-technologies';
|
|||||||
export interface ProjectDetailsProps {
|
export interface ProjectDetailsProps {
|
||||||
project: ProjectGraphProjectNode;
|
project: ProjectGraphProjectNode;
|
||||||
sourceMap: Record<string, string[]>;
|
sourceMap: Record<string, string[]>;
|
||||||
|
errors?: GraphError[];
|
||||||
variant?: 'default' | 'compact';
|
variant?: 'default' | 'compact';
|
||||||
onViewInProjectGraph?: (data: { projectName: string }) => void;
|
onViewInProjectGraph?: (data: { projectName: string }) => void;
|
||||||
onViewInTaskGraph?: (data: {
|
onViewInTaskGraph?: (data: {
|
||||||
@ -82,7 +86,7 @@ export const ProjectDetails = ({
|
|||||||
<span>
|
<span>
|
||||||
{onViewInProjectGraph ? (
|
{onViewInProjectGraph ? (
|
||||||
<button
|
<button
|
||||||
className="inline-flex cursor-pointer items-center gap-2 rounded-md py-1 px-2 text-base text-slate-600 ring-2 ring-inset ring-slate-400/40 hover:bg-slate-50 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-800/60"
|
className="inline-flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-base text-slate-600 ring-2 ring-inset ring-slate-400/40 hover:bg-slate-50 dark:text-slate-300 dark:ring-slate-400/30 dark:hover:bg-slate-800/60"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onViewInProjectGraph({ projectName: project.name })
|
onViewInProjectGraph({ projectName: project.name })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export function TargetConfigurationGroupList({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed top-0 left-0 right-0 z-10 mb-8 border-b-2 border-slate-900/10 bg-slate-50 dark:border-slate-300/10 dark:bg-slate-800 dark:text-slate-300">
|
<div className="fixed left-0 right-0 top-0 z-10 mb-8 border-b-2 border-slate-900/10 bg-slate-50 dark:border-slate-300/10 dark:bg-slate-800 dark:text-slate-300">
|
||||||
<div className="mx-auto max-w-6xl px-8 pt-2">
|
<div className="mx-auto max-w-6xl px-8 pt-2">
|
||||||
<TargetConfigurationGroupHeader
|
<TargetConfigurationGroupHeader
|
||||||
targetGroupName={stickyHeaderContent}
|
targetGroupName={stickyHeaderContent}
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export const TargetConfigurationDetailsHeader = ({
|
|||||||
collapsable ? 'cursor-pointer' : '',
|
collapsable ? 'cursor-pointer' : '',
|
||||||
isCompact ? 'px-2 py-1' : 'p-2',
|
isCompact ? 'px-2 py-1' : 'p-2',
|
||||||
!isCollasped || !collapsable
|
!isCollasped || !collapsable
|
||||||
? 'border-b bg-slate-50 dark:border-slate-700/60 dark:border-slate-300/10 dark:bg-slate-800 '
|
? 'border-b bg-slate-50 dark:border-slate-300/10 dark:border-slate-700/60 dark:bg-slate-800 '
|
||||||
: ''
|
: ''
|
||||||
)}
|
)}
|
||||||
onClick={collapsable ? toggleCollapse : undefined}
|
onClick={collapsable ? toggleCollapse : undefined}
|
||||||
@ -148,7 +148,7 @@ export const TargetConfigurationDetailsHeader = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isCollasped && (
|
{!isCollasped && (
|
||||||
<div className="mt-2 ml-5 text-sm">
|
<div className="ml-5 mt-2 text-sm">
|
||||||
<SourceInfo
|
<SourceInfo
|
||||||
data={sourceMap[`targets.${targetName}`]}
|
data={sourceMap[`targets.${targetName}`]}
|
||||||
propertyKey={`targets.${targetName}`}
|
propertyKey={`targets.${targetName}`}
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export const TargetConfigurationDetailsComponent = ({
|
|||||||
{singleCommand ? (
|
{singleCommand ? (
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
Command
|
Command
|
||||||
<span className="ml-2 mb-1 hidden group-hover:inline">
|
<span className="mb-1 ml-2 hidden group-hover:inline">
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
onCopy={() =>
|
onCopy={() =>
|
||||||
handleCopyClick(`"command": "${singleCommand}"`)
|
handleCopyClick(`"command": "${singleCommand}"`)
|
||||||
@ -191,7 +191,7 @@ export const TargetConfigurationDetailsComponent = ({
|
|||||||
<TooltipTriggerText>Inputs</TooltipTriggerText>
|
<TooltipTriggerText>Inputs</TooltipTriggerText>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="ml-2 mb-1 hidden group-hover:inline">
|
<span className="mb-1 ml-2 hidden group-hover:inline">
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
onCopy={() =>
|
onCopy={() =>
|
||||||
handleCopyClick(
|
handleCopyClick(
|
||||||
@ -241,7 +241,7 @@ export const TargetConfigurationDetailsComponent = ({
|
|||||||
<TooltipTriggerText>Outputs</TooltipTriggerText>
|
<TooltipTriggerText>Outputs</TooltipTriggerText>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="ml-2 mb-1 hidden group-hover:inline">
|
<span className="mb-1 ml-2 hidden group-hover:inline">
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
onCopy={() =>
|
onCopy={() =>
|
||||||
handleCopyClick(
|
handleCopyClick(
|
||||||
|
|||||||
@ -453,7 +453,7 @@ export default function NewYear(): JSX.Element {
|
|||||||
className="py-18 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"
|
className="py-18 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"
|
||||||
>
|
>
|
||||||
<article id="nx-new-year-tips-intro" className="relative">
|
<article id="nx-new-year-tips-intro" className="relative">
|
||||||
<h1 className="my-8 text-3xl font-semibold dark:text-white sm:text-5xl">
|
<h1 className="my-8 text-3xl font-semibold sm:text-5xl dark:text-white">
|
||||||
Nx New Year Tips
|
Nx New Year Tips
|
||||||
</h1>
|
</h1>
|
||||||
<p>
|
<p>
|
||||||
@ -477,7 +477,7 @@ export default function NewYear(): JSX.Element {
|
|||||||
className="py-18 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"
|
className="py-18 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"
|
||||||
>
|
>
|
||||||
<article id="nx-new-year-tips" className="relative">
|
<article id="nx-new-year-tips" className="relative">
|
||||||
<div className="mx-auto grid grid-cols-1 items-stretch gap-8 py-12 dark:text-slate-100 sm:grid-cols-2 md:grid-cols-3 lg:py-16">
|
<div className="mx-auto grid grid-cols-1 items-stretch gap-8 py-12 sm:grid-cols-2 md:grid-cols-3 lg:py-16 dark:text-slate-100">
|
||||||
{shownTips.map((tip) => (
|
{shownTips.map((tip) => (
|
||||||
<FlipCard
|
<FlipCard
|
||||||
key={tip.day}
|
key={tip.day}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export function CallToAction({
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="not-prose group relative my-12 mx-auto flex w-full max-w-md items-center gap-3 overflow-hidden rounded-lg bg-slate-50 shadow-md transition hover:text-white dark:bg-slate-800/60">
|
<div className="not-prose group relative mx-auto my-12 flex w-full max-w-md items-center gap-3 overflow-hidden rounded-lg bg-slate-50 shadow-md transition hover:text-white dark:bg-slate-800/60">
|
||||||
<div className="absolute inset-0 z-0 w-2 bg-blue-500 transition-all duration-150 group-hover:w-full dark:bg-sky-500"></div>
|
<div className="absolute inset-0 z-0 w-2 bg-blue-500 transition-all duration-150 group-hover:w-full dark:bg-sky-500"></div>
|
||||||
<div className="w-2 bg-blue-500 dark:bg-sky-500"></div>
|
<div className="w-2 bg-blue-500 dark:bg-sky-500"></div>
|
||||||
|
|
||||||
|
|||||||
@ -198,7 +198,7 @@ export function Card({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/*HOVER ICON*/}
|
{/*HOVER ICON*/}
|
||||||
<span className="absolute right-2 top-1/2 -translate-y-2.5 -translate-x-2 opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100">
|
<span className="absolute right-2 top-1/2 -translate-x-2 -translate-y-2.5 opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100">
|
||||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import { pruneExternalNodes } from '../../project-graph/operators';
|
|||||||
import {
|
import {
|
||||||
createProjectGraphAndSourceMapsAsync,
|
createProjectGraphAndSourceMapsAsync,
|
||||||
createProjectGraphAsync,
|
createProjectGraphAsync,
|
||||||
|
handleProjectGraphError,
|
||||||
} from '../../project-graph/project-graph';
|
} from '../../project-graph/project-graph';
|
||||||
import {
|
import {
|
||||||
createTaskGraph,
|
createTaskGraph,
|
||||||
@ -48,21 +49,35 @@ import { HashPlanner, transferProjectGraph } from '../../native';
|
|||||||
import { transformProjectGraphForRust } from '../../native/transform-objects';
|
import { transformProjectGraphForRust } from '../../native/transform-objects';
|
||||||
import { getAffectedGraphNodes } from '../affected/affected';
|
import { getAffectedGraphNodes } from '../affected/affected';
|
||||||
import { readFileMapCache } from '../../project-graph/nx-deps-cache';
|
import { readFileMapCache } from '../../project-graph/nx-deps-cache';
|
||||||
|
import { Hash, getNamedInputs } from '../../hasher/task-hasher';
|
||||||
import { ConfigurationSourceMaps } from '../../project-graph/utils/project-configuration-utils';
|
import { ConfigurationSourceMaps } from '../../project-graph/utils/project-configuration-utils';
|
||||||
|
|
||||||
import { filterUsingGlobPatterns } from '../../hasher/task-hasher';
|
|
||||||
import { createTaskHasher } from '../../hasher/create-task-hasher';
|
import { createTaskHasher } from '../../hasher/create-task-hasher';
|
||||||
|
|
||||||
|
import { filterUsingGlobPatterns } from '../../hasher/task-hasher';
|
||||||
|
import { ProjectGraphError } from '../../project-graph/error-types';
|
||||||
|
|
||||||
|
export interface GraphError {
|
||||||
|
message: string;
|
||||||
|
stack: string;
|
||||||
|
cause: unknown;
|
||||||
|
name: string;
|
||||||
|
pluginName: string;
|
||||||
|
fileName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectGraphClientResponse {
|
export interface ProjectGraphClientResponse {
|
||||||
hash: string;
|
hash: string;
|
||||||
projects: ProjectGraphProjectNode[];
|
projects: ProjectGraphProjectNode[];
|
||||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||||
fileMap: ProjectFileMap;
|
fileMap?: ProjectFileMap;
|
||||||
layout: { appsDir: string; libsDir: string };
|
layout: { appsDir: string; libsDir: string };
|
||||||
affected: string[];
|
affected: string[];
|
||||||
focus: string;
|
focus: string;
|
||||||
groupByFolder: boolean;
|
groupByFolder: boolean;
|
||||||
exclude: string[];
|
exclude: string[];
|
||||||
|
isPartial: boolean;
|
||||||
|
errors?: GraphError[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskGraphClientResponse {
|
export interface TaskGraphClientResponse {
|
||||||
@ -273,10 +288,30 @@ export async function generateGraph(
|
|||||||
? args.targets[0]
|
? args.targets[0]
|
||||||
: args.targets;
|
: args.targets;
|
||||||
|
|
||||||
const { projectGraph: rawGraph, sourceMaps } =
|
let rawGraph: ProjectGraph;
|
||||||
|
let sourceMaps: ConfigurationSourceMaps;
|
||||||
|
let isPartial = false;
|
||||||
|
try {
|
||||||
|
const projectGraphAndSourceMaps =
|
||||||
await createProjectGraphAndSourceMapsAsync({
|
await createProjectGraphAndSourceMapsAsync({
|
||||||
exitOnError: true,
|
exitOnError: false,
|
||||||
});
|
});
|
||||||
|
rawGraph = projectGraphAndSourceMaps.projectGraph;
|
||||||
|
sourceMaps = projectGraphAndSourceMaps.sourceMaps;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ProjectGraphError) {
|
||||||
|
output.warn({
|
||||||
|
title: 'Failed to process project graph. Showing partial graph.',
|
||||||
|
});
|
||||||
|
rawGraph = e.getPartialProjectGraph();
|
||||||
|
sourceMaps = e.getPartialSourcemaps();
|
||||||
|
|
||||||
|
isPartial = true;
|
||||||
|
}
|
||||||
|
if (!rawGraph) {
|
||||||
|
handleProjectGraphError({ exitOnError: true }, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
let prunedGraph = pruneExternalNodes(rawGraph);
|
let prunedGraph = pruneExternalNodes(rawGraph);
|
||||||
|
|
||||||
const projects = Object.values(
|
const projects = Object.values(
|
||||||
@ -632,6 +667,8 @@ let currentProjectGraphClientResponse: ProjectGraphClientResponse = {
|
|||||||
focus: null,
|
focus: null,
|
||||||
groupByFolder: false,
|
groupByFolder: false,
|
||||||
exclude: [],
|
exclude: [],
|
||||||
|
isPartial: false,
|
||||||
|
errors: [],
|
||||||
};
|
};
|
||||||
let currentSourceMapsClientResponse: ConfigurationSourceMaps = {};
|
let currentSourceMapsClientResponse: ConfigurationSourceMaps = {};
|
||||||
|
|
||||||
@ -649,7 +686,11 @@ function debounce(fn: (...args) => void, time: number) {
|
|||||||
|
|
||||||
function createFileWatcher() {
|
function createFileWatcher() {
|
||||||
return daemonClient.registerFileWatcher(
|
return daemonClient.registerFileWatcher(
|
||||||
{ watchProjects: 'all', includeGlobalWorkspaceFiles: true },
|
{
|
||||||
|
watchProjects: 'all',
|
||||||
|
includeGlobalWorkspaceFiles: true,
|
||||||
|
allowPartialGraph: true,
|
||||||
|
},
|
||||||
debounce(async (error, changes) => {
|
debounce(async (error, changes) => {
|
||||||
if (error === 'closed') {
|
if (error === 'closed') {
|
||||||
output.error({ title: `Watch error: Daemon closed the connection` });
|
output.error({ title: `Watch error: Daemon closed the connection` });
|
||||||
@ -687,11 +728,39 @@ async function createProjectGraphAndSourceMapClientResponse(
|
|||||||
}> {
|
}> {
|
||||||
performance.mark('project graph watch calculation:start');
|
performance.mark('project graph watch calculation:start');
|
||||||
|
|
||||||
const { projectGraph, sourceMaps } =
|
let projectGraph: ProjectGraph;
|
||||||
await createProjectGraphAndSourceMapsAsync({ exitOnError: true });
|
let sourceMaps: ConfigurationSourceMaps;
|
||||||
|
let isPartial = false;
|
||||||
|
let errors: GraphError[] | undefined;
|
||||||
|
try {
|
||||||
|
const projectGraphAndSourceMaps =
|
||||||
|
await createProjectGraphAndSourceMapsAsync({ exitOnError: false });
|
||||||
|
projectGraph = projectGraphAndSourceMaps.projectGraph;
|
||||||
|
sourceMaps = projectGraphAndSourceMaps.sourceMaps;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ProjectGraphError) {
|
||||||
|
projectGraph = e.getPartialProjectGraph();
|
||||||
|
sourceMaps = e.getPartialSourcemaps();
|
||||||
|
errors = e.getErrors().map((e) => ({
|
||||||
|
message: e.message,
|
||||||
|
stack: e.stack,
|
||||||
|
cause: e.cause,
|
||||||
|
name: e.name,
|
||||||
|
pluginName: (e as any).pluginName,
|
||||||
|
fileName:
|
||||||
|
(e as any).file ?? (e.cause as any)?.errors?.[0]?.location?.file,
|
||||||
|
}));
|
||||||
|
isPartial = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectGraph) {
|
||||||
|
handleProjectGraphError({ exitOnError: true }, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let graph = pruneExternalNodes(projectGraph);
|
let graph = pruneExternalNodes(projectGraph);
|
||||||
let fileMap = readFileMapCache().fileMap.projectFileMap;
|
let fileMap: ProjectFileMap | undefined =
|
||||||
|
readFileMapCache()?.fileMap.projectFileMap;
|
||||||
performance.mark('project graph watch calculation:end');
|
performance.mark('project graph watch calculation:end');
|
||||||
performance.mark('project graph response generation:start');
|
performance.mark('project graph response generation:start');
|
||||||
|
|
||||||
@ -700,7 +769,9 @@ async function createProjectGraphAndSourceMapClientResponse(
|
|||||||
const dependencies = graph.dependencies;
|
const dependencies = graph.dependencies;
|
||||||
|
|
||||||
const hasher = createHash('sha256');
|
const hasher = createHash('sha256');
|
||||||
hasher.update(JSON.stringify({ layout, projects, dependencies, sourceMaps }));
|
hasher.update(
|
||||||
|
JSON.stringify({ layout, projects, dependencies, sourceMaps, errors })
|
||||||
|
);
|
||||||
|
|
||||||
const hash = hasher.digest('hex');
|
const hash = hasher.digest('hex');
|
||||||
|
|
||||||
@ -727,6 +798,8 @@ async function createProjectGraphAndSourceMapClientResponse(
|
|||||||
dependencies,
|
dependencies,
|
||||||
affected,
|
affected,
|
||||||
fileMap,
|
fileMap,
|
||||||
|
isPartial,
|
||||||
|
errors,
|
||||||
},
|
},
|
||||||
sourceMapResponse: sourceMaps,
|
sourceMapResponse: sourceMaps,
|
||||||
};
|
};
|
||||||
@ -736,12 +809,15 @@ async function createTaskGraphClientResponse(
|
|||||||
pruneExternal: boolean = false
|
pruneExternal: boolean = false
|
||||||
): Promise<TaskGraphClientResponse> {
|
): Promise<TaskGraphClientResponse> {
|
||||||
let graph: ProjectGraph;
|
let graph: ProjectGraph;
|
||||||
|
try {
|
||||||
|
graph = await createProjectGraphAsync({ exitOnError: false });
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ProjectGraphError) {
|
||||||
|
graph = e.getPartialProjectGraph();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (pruneExternal) {
|
if (pruneExternal) {
|
||||||
graph = pruneExternalNodes(
|
graph = pruneExternalNodes(graph);
|
||||||
await createProjectGraphAsync({ exitOnError: true })
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
graph = await createProjectGraphAsync({ exitOnError: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nxJson = readNxJson();
|
const nxJson = readNxJson();
|
||||||
|
|||||||
@ -179,6 +179,7 @@ export class DaemonClient {
|
|||||||
watchProjects: string[] | 'all';
|
watchProjects: string[] | 'all';
|
||||||
includeGlobalWorkspaceFiles?: boolean;
|
includeGlobalWorkspaceFiles?: boolean;
|
||||||
includeDependentProjects?: boolean;
|
includeDependentProjects?: boolean;
|
||||||
|
allowPartialGraph?: boolean;
|
||||||
},
|
},
|
||||||
callback: (
|
callback: (
|
||||||
error: Error | null | 'closed',
|
error: Error | null | 'closed',
|
||||||
@ -188,7 +189,15 @@ export class DaemonClient {
|
|||||||
} | null
|
} | null
|
||||||
) => void
|
) => void
|
||||||
): Promise<UnregisterCallback> {
|
): Promise<UnregisterCallback> {
|
||||||
|
try {
|
||||||
await this.getProjectGraphAndSourceMaps();
|
await this.getProjectGraphAndSourceMaps();
|
||||||
|
} catch (e) {
|
||||||
|
if (config.allowPartialGraph && e instanceof ProjectGraphError) {
|
||||||
|
// we are fine with partial graph
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
let messenger: DaemonSocketMessenger | undefined;
|
let messenger: DaemonSocketMessenger | undefined;
|
||||||
|
|
||||||
await this.queue.sendToQueue(() => {
|
await this.queue.sendToQueue(() => {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Socket } from 'net';
|
import { Socket } from 'net';
|
||||||
import { findMatchingProjects } from '../../../utils/find-matching-projects';
|
import { findMatchingProjects } from '../../../utils/find-matching-projects';
|
||||||
import { ProjectGraph } from '../../../config/project-graph';
|
|
||||||
import { findAllProjectNodeDependencies } from '../../../utils/project-graph-utils';
|
import { findAllProjectNodeDependencies } from '../../../utils/project-graph-utils';
|
||||||
import { PromisedBasedQueue } from '../../../utils/promised-based-queue';
|
import { PromisedBasedQueue } from '../../../utils/promised-based-queue';
|
||||||
import { currentProjectGraph } from '../project-graph-incremental-recomputation';
|
import { currentProjectGraph } from '../project-graph-incremental-recomputation';
|
||||||
|
|||||||
@ -261,7 +261,7 @@ async function processFilesAndCreateAndSerializeProjectGraph(
|
|||||||
const errors = [...(projectConfigurationsError?.errors ?? [])];
|
const errors = [...(projectConfigurationsError?.errors ?? [])];
|
||||||
|
|
||||||
if (g.error) {
|
if (g.error) {
|
||||||
if (isAggregateProjectGraphError(g.error)) {
|
if (isAggregateProjectGraphError(g.error) && g.error.errors?.length) {
|
||||||
errors.push(...g.error.errors);
|
errors.push(...g.error.errors);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ProjectConfiguration } from '../../../config/workspace-json-project-jso
|
|||||||
import { toProjectName } from '../../../config/workspaces';
|
import { toProjectName } from '../../../config/workspaces';
|
||||||
import { readJsonFile } from '../../../utils/fileutils';
|
import { readJsonFile } from '../../../utils/fileutils';
|
||||||
import { NxPluginV2 } from '../../../project-graph/plugins';
|
import { NxPluginV2 } from '../../../project-graph/plugins';
|
||||||
|
import { CreateNodesError } from '../../../project-graph/error-types';
|
||||||
|
|
||||||
export const ProjectJsonProjectsPlugin: NxPluginV2 = {
|
export const ProjectJsonProjectsPlugin: NxPluginV2 = {
|
||||||
name: 'nx/core/project-json',
|
name: 'nx/core/project-json',
|
||||||
@ -13,6 +14,7 @@ export const ProjectJsonProjectsPlugin: NxPluginV2 = {
|
|||||||
const json = readJsonFile<ProjectConfiguration>(
|
const json = readJsonFile<ProjectConfiguration>(
|
||||||
join(workspaceRoot, file)
|
join(workspaceRoot, file)
|
||||||
);
|
);
|
||||||
|
|
||||||
const project = buildProjectFromProjectJson(json, file);
|
const project = buildProjectFromProjectJson(json, file);
|
||||||
return {
|
return {
|
||||||
projects: {
|
projects: {
|
||||||
|
|||||||
@ -171,7 +171,7 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProjectGraphError(opts: { exitOnError: boolean }, e) {
|
export function handleProjectGraphError(opts: { exitOnError: boolean }, e) {
|
||||||
if (opts.exitOnError) {
|
if (opts.exitOnError) {
|
||||||
const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
|
const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true';
|
||||||
if (e instanceof ProjectGraphError) {
|
if (e instanceof ProjectGraphError) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user