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} >