diff --git a/graph/client/src/app/feature-tasks/task-graph-error-tooltip.tsx b/graph/client/src/app/feature-tasks/task-graph-error-tooltip.tsx
new file mode 100644
index 0000000000000..7bac4ae33ff44
--- /dev/null
+++ b/graph/client/src/app/feature-tasks/task-graph-error-tooltip.tsx
@@ -0,0 +1,10 @@
+export const TaskGraphErrorTooltip = ({ error }: { error: string }) => {
+ return (
+ <>
+
+ There was a problem calculating the task graph for this task
+
+ {error}
+ >
+ );
+};
diff --git a/graph/client/src/app/feature-tasks/task-list.stories.tsx b/graph/client/src/app/feature-tasks/task-list.stories.tsx
index 029070f50d358..dc4aa853a5ff6 100644
--- a/graph/client/src/app/feature-tasks/task-list.stories.tsx
+++ b/graph/client/src/app/feature-tasks/task-list.stories.tsx
@@ -30,6 +30,7 @@ const args: Partial = {
defaultConfiguration: 'production',
},
},
+ files: [],
},
},
{
@@ -38,6 +39,7 @@ const args: Partial = {
data: {
root: 'apps/nested/app',
targets: { build: { configurations: { production: {} } } },
+ files: [],
},
},
{
@@ -46,6 +48,7 @@ const args: Partial = {
data: {
root: 'apps/app1-e2e',
targets: { e2e: { configurations: { production: {} } } },
+ files: [],
},
},
{
@@ -54,6 +57,7 @@ const args: Partial = {
data: {
root: 'libs/lib1',
targets: { lint: { configurations: { production: {} } } },
+ files: [],
},
},
],
@@ -62,6 +66,9 @@ const args: Partial = {
appsDir: 'apps',
libsDir: 'libs',
},
- selectedTask: 'app1:build:production',
+ selectedTarget: 'build',
+ errors: {
+ 'app1:build': 'Missing executor',
+ },
};
Primary.args = args;
diff --git a/graph/client/src/app/feature-tasks/task-list.tsx b/graph/client/src/app/feature-tasks/task-list.tsx
index 3f59eea6237b8..d88c451c823f4 100644
--- a/graph/client/src/app/feature-tasks/task-list.tsx
+++ b/graph/client/src/app/feature-tasks/task-list.tsx
@@ -1,13 +1,21 @@
// nx-ignore-next-line
import type { ProjectGraphNode } from '@nrwl/devkit';
-import { getProjectsByType, groupProjectsByDirectory } from '../util';
+import {
+ createTaskName,
+ getProjectsByType,
+ groupProjectsByDirectory,
+} from '../util';
import { WorkspaceLayout } from '../interfaces';
-import { EyeIcon } from '@heroicons/react/24/outline';
+import { ExclamationCircleIcon, EyeIcon } from '@heroicons/react/24/outline';
import { ReactNode } from 'react';
+import { Tooltip } from '@nrwl/graph/ui-tooltips';
+import { TaskGraphErrorTooltip } from './task-graph-error-tooltip';
+import { ExperimentalFeature } from '../ui-components/experimental-feature';
interface SidebarProject {
projectGraphNode: ProjectGraphNode;
isSelected: boolean;
+ error: string | null;
}
function ProjectListItem({
@@ -25,11 +33,31 @@ function ProjectListItem({
data-project={project.projectGraphNode.name}
title={project.projectGraphNode.name}
data-active={project.isSelected.toString()}
- onClick={() => toggleTask(project.projectGraphNode.name)}
+ onClick={() =>
+ !project.error ? toggleTask(project.projectGraphNode.name) : null
+ }
>
{project.projectGraphNode.name}
+
+
+ {project.error ? (
+ }
+ openAction="click"
+ floatingPortal={true}
+ >
+
+
+
+
+ ) : null}
+
+
{project.isSelected ? (
): SidebarProject {
+ const taskId = createTaskName(project.name, selectedTarget);
+
return {
projectGraphNode: project,
isSelected: selectedProjects.includes(project.name),
+ error: errors?.[taskId] ?? null,
};
}
@@ -96,6 +129,7 @@ export interface TaskListProps {
selectedProjects: string[];
toggleProject: (projectName: string) => void;
children: ReactNode | ReactNode[];
+ errors: Record;
}
export function TaskList({
@@ -105,13 +139,13 @@ export function TaskList({
selectedProjects,
toggleProject,
children,
+ errors,
}: TaskListProps) {
const filteredProjects = projects
.filter((project) =>
(project.data as any).targets.hasOwnProperty(selectedTarget)
)
.sort((a, b) => a.name.localeCompare(b.name));
-
const appProjects = getProjectsByType('app', filteredProjects);
const libProjects = getProjectsByType('lib', filteredProjects);
const e2eProjects = getProjectsByType('e2e', filteredProjects);
@@ -146,7 +180,12 @@ export function TaskList({
key={'app-' + directoryName}
headerText={directoryName}
projects={appDirectoryGroups[directoryName].map((project) =>
- mapToSidebarProjectWithTasks(project, selectedProjects)
+ mapToSidebarProjectWithTasks(
+ project,
+ selectedProjects,
+ selectedTarget,
+ errors
+ )
)}
toggleTask={toggleProject}
>
@@ -163,7 +202,12 @@ export function TaskList({
key={'e2e-' + directoryName}
headerText={directoryName}
projects={e2eDirectoryGroups[directoryName].map((project) =>
- mapToSidebarProjectWithTasks(project, selectedProjects)
+ mapToSidebarProjectWithTasks(
+ project,
+ selectedProjects,
+ selectedTarget,
+ errors
+ )
)}
toggleTask={toggleProject}
>
@@ -180,7 +224,12 @@ export function TaskList({
key={'lib-' + directoryName}
headerText={directoryName}
projects={libDirectoryGroups[directoryName].map((project) =>
- mapToSidebarProjectWithTasks(project, selectedProjects)
+ mapToSidebarProjectWithTasks(
+ project,
+ selectedProjects,
+ selectedTarget,
+ errors
+ )
)}
toggleTask={toggleProject}
>
diff --git a/graph/client/src/app/feature-tasks/tasks-sidebar-error-boundary.tsx b/graph/client/src/app/feature-tasks/tasks-sidebar-error-boundary.tsx
new file mode 100644
index 0000000000000..a174a6d68263f
--- /dev/null
+++ b/graph/client/src/app/feature-tasks/tasks-sidebar-error-boundary.tsx
@@ -0,0 +1,14 @@
+import { useRouteError } from 'react-router-dom';
+
+export function TasksSidebarErrorBoundary() {
+ let error = useRouteError();
+ console.error(error);
+ return (
+
+
+ Error
+
+
There was a problem loading your task graph.
+
+ );
+}
diff --git a/graph/client/src/app/feature-tasks/tasks-sidebar.tsx b/graph/client/src/app/feature-tasks/tasks-sidebar.tsx
index ab33f28828dfe..62982e77a5b63 100644
--- a/graph/client/src/app/feature-tasks/tasks-sidebar.tsx
+++ b/graph/client/src/app/feature-tasks/tasks-sidebar.tsx
@@ -17,19 +17,7 @@ import { CheckboxPanel } from '../ui-components/checkbox-panel';
import { Dropdown } from '@nrwl/graph/ui-components';
import { ShowHideAll } from '../ui-components/show-hide-all';
import { useCurrentPath } from '../hooks/use-current-path';
-import { useRouteConstructor } from '../util';
-
-function createTaskName(
- project: string,
- target: string,
- configuration?: string
-) {
- if (configuration) {
- return `${project}:${target}:${configuration}`;
- } else {
- return `${project}:${target}`;
- }
-}
+import { createTaskName, useRouteConstructor } from '../util';
export function TasksSidebar() {
const graphService = getGraphService();
@@ -47,8 +35,9 @@ export function TasksSidebar() {
const routeData = useRouteLoaderData(
'selectedTarget'
) as TaskGraphClientResponse;
- const { taskGraphs } = routeData;
- const { projects, targets } = selectedWorkspaceRouteData;
+ const { taskGraphs, errors } = routeData;
+ let { projects, targets } = selectedWorkspaceRouteData;
+
const selectedTarget = params['selectedTarget'] ?? targets[0];
const [selectedProjects, setSelectedProjects] = useState([]);
@@ -217,6 +206,7 @@ export function TasksSidebar() {
workspaceLayout={workspaceLayout}
selectedTarget={selectedTarget}
toggleProject={toggleProject}
+ errors={errors}
>