From 92f8aef2bc7f7f493b7738928673a8c75027e78f Mon Sep 17 00:00:00 2001 From: Scott J Dickerson Date: Wed, 12 Jun 2024 10:36:42 -0400 Subject: [PATCH] :sparkles: [WIP] Implement the Task Manager drawer Resolves: #1938 Add a queued tasks count badge plus and item drawer. - Use the standard `NotificationDrawer` attached to the `Page` layout to render the task manager item drawer. - Add a `TaskManagerContext` to control the count indicator and visibility of the drawer. This is a top level context so the task manager is available on all pages. - Add `TaskQueue` and query for the notification badge __Still Needs__: - Task rows in the notification drawer - Infinite scroll on the task list (or at least a load more link/icon, maybe a visual indicator that more can be fetched on scroll or click) - Individual task actions Related changes: - Update the `HeaderApp` to handle visibility of masthead toolbar items at the `ToolbarGroup` level. - Rename `SSOMenu` to `SsoToolbarItem`. Signed-off-by: Scott J Dickerson --- client/src/app/App.tsx | 9 +- client/src/app/api/models.ts | 13 +++ client/src/app/api/rest.ts | 44 ++++---- .../task-manager/TaskManagerContext.tsx | 45 ++++++++ .../task-manager/TaskManagerDrawer.tsx | 40 +++++++ .../task-manager/TaskNotificaitonBadge.tsx | 22 ++++ .../layout/DefaultLayout/DefaultLayout.tsx | 21 +++- client/src/app/layout/HeaderApp/HeaderApp.tsx | 43 ++++++-- client/src/app/layout/HeaderApp/SSOMenu.tsx | 93 ---------------- .../app/layout/HeaderApp/SsoToolbarItem.tsx | 87 +++++++++++++++ .../__snapshots__/HeaderApp.test.tsx.snap | 104 ++++++++++++++++-- client/src/app/queries/tasks.ts | 37 ++++++- 12 files changed, 422 insertions(+), 136 deletions(-) create mode 100644 client/src/app/components/task-manager/TaskManagerContext.tsx create mode 100644 client/src/app/components/task-manager/TaskManagerDrawer.tsx create mode 100644 client/src/app/components/task-manager/TaskNotificaitonBadge.tsx delete mode 100644 client/src/app/layout/HeaderApp/SSOMenu.tsx create mode 100644 client/src/app/layout/HeaderApp/SsoToolbarItem.tsx diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index 3414477eda..084e59d413 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -4,6 +4,7 @@ import { BrowserRouter } from "react-router-dom"; import { AppRoutes } from "./Routes"; import { DefaultLayout } from "./layout"; import { NotificationsProvider } from "./components/NotificationsContext"; +import { TaskManagerProvider } from "./components/task-manager/TaskManagerContext"; import "./app.css"; @@ -11,9 +12,11 @@ const App: React.FC = () => { return ( - - - + + + + + ); diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 63feb1e2cd..06a700f1e0 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -413,6 +413,19 @@ export interface Taskgroup { tasks: TaskgroupTask[]; } +export interface TaskQueue { + /** Total number of tasks scheduled */ + total: number; + /** number of tasks ready to run */ + ready: number; + /** number of postponed tasks */ + postponed: number; + /** number of tasks with pods created awaiting node scheduler */ + pending: number; + /** number of tasks with running pods */ + running: number; +} + export interface Cache { path: string; capacity: string; diff --git a/client/src/app/api/rest.ts b/client/src/app/api/rest.ts index 4b839e92e9..ed90b8f7c8 100644 --- a/client/src/app/api/rest.ts +++ b/client/src/app/api/rest.ts @@ -1,27 +1,36 @@ import axios, { AxiosPromise, RawAxiosRequestHeaders } from "axios"; import { + AnalysisAppDependency, + AnalysisAppReport, AnalysisDependency, - BaseAnalysisRuleReport, - BaseAnalysisIssueReport, - AnalysisIssue, AnalysisFileReport, AnalysisIncident, + AnalysisIssue, Application, ApplicationAdoptionPlan, ApplicationDependency, ApplicationImport, ApplicationImportSummary, + Archetype, Assessment, + BaseAnalysisIssueReport, + BaseAnalysisRuleReport, BusinessService, Cache, + HubFile, HubPaginatedResult, HubRequestParams, Identity, + InitialAssessment, IReadFile, - Tracker, JobFunction, + MigrationWave, + MimeType, + New, Proxy, + Questionnaire, + Ref, Review, Setting, SettingTypes, @@ -29,23 +38,15 @@ import { StakeholderGroup, Tag, TagCategory, + Target, Task, Taskgroup, - MigrationWave, + TaskQueue, Ticket, - New, - Ref, + Tracker, TrackerProject, TrackerProjectIssuetype, UnstructuredFact, - AnalysisAppDependency, - AnalysisAppReport, - Target, - HubFile, - Questionnaire, - Archetype, - InitialAssessment, - MimeType, } from "./models"; import { serializeRequestParamsForHub } from "@app/hooks/table-controls"; @@ -321,7 +322,7 @@ export const getApplicationImports = ( export function getTaskById( id: number, - format: string, + format: "json" | "yaml", merged: boolean = false ): Promise { const headers = format === "yaml" ? { ...yamlHeaders } : { ...jsonHeaders }; @@ -333,7 +334,7 @@ export function getTaskById( } return axios - .get(url, { + .get(url, { headers: headers, responseType: responseType, }) @@ -348,10 +349,15 @@ export const getTasks = () => export const getServerTasks = (params: HubRequestParams = {}) => getHubPaginatedResult(TASKS, params); -export const deleteTask = (id: number) => axios.delete(`${TASKS}/${id}`); +export const deleteTask = (id: number) => axios.delete(`${TASKS}/${id}`); export const cancelTask = (id: number) => - axios.put(`${TASKS}/${id}/cancel`); + axios.put(`${TASKS}/${id}/cancel`); + +export const getTaskQueue = (addon?: string): Promise => + axios + .get(`${TASKS}/report/queue`, { params: { addon } }) + .then(({ data }) => data); export const createTaskgroup = (obj: Taskgroup) => axios.post(TASKGROUPS, obj).then((response) => response.data); diff --git a/client/src/app/components/task-manager/TaskManagerContext.tsx b/client/src/app/components/task-manager/TaskManagerContext.tsx new file mode 100644 index 0000000000..b852ef8223 --- /dev/null +++ b/client/src/app/components/task-manager/TaskManagerContext.tsx @@ -0,0 +1,45 @@ +import { useFetchTaskQueue } from "@app/queries/tasks"; +import React, { useContext, useState } from "react"; + +interface TaskManagerContextProps { + /** Count of the currently "queued" Tasks */ + queuedCount: number; + + /** Is the task manager drawer currently visible? */ + isExpanded: boolean; + + /** Control if the task manager drawer is visible */ + setIsExpanded: (value: boolean) => void; +} + +const TaskManagerContext = React.createContext({ + queuedCount: 0, + + isExpanded: false, + setIsExpanded: () => undefined, +}); + +export const useTaskManagerContext = () => { + const values = useContext(TaskManagerContext); + + return values; +}; + +export const TaskManagerProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const { taskQueue } = useFetchTaskQueue(); + const [isExpanded, setIsExpanded] = useState(false); + + return ( + + {children} + + ); +}; diff --git a/client/src/app/components/task-manager/TaskManagerDrawer.tsx b/client/src/app/components/task-manager/TaskManagerDrawer.tsx new file mode 100644 index 0000000000..952ad30544 --- /dev/null +++ b/client/src/app/components/task-manager/TaskManagerDrawer.tsx @@ -0,0 +1,40 @@ +import React, { forwardRef } from "react"; +import { Link } from "react-router-dom"; +import { + NotificationDrawer, + NotificationDrawerBody, + NotificationDrawerHeader, + NotificationDrawerList, +} from "@patternfly/react-core"; +import { useTaskManagerContext } from "./TaskManagerContext"; + +interface TaskManagerDrawerProps { + ref?: React.ForwardedRef; +} + +export const TaskManagerDrawer: React.FC = forwardRef( + (_props, ref) => { + const { isExpanded, setIsExpanded, queuedCount } = useTaskManagerContext(); + + const closeDrawer = () => { + setIsExpanded(!isExpanded); + }; + + return ( + + + View All Tasks + + + + + + ); + } +); + +TaskManagerDrawer.displayName = "TaskManagerDrawer"; diff --git a/client/src/app/components/task-manager/TaskNotificaitonBadge.tsx b/client/src/app/components/task-manager/TaskNotificaitonBadge.tsx new file mode 100644 index 0000000000..bda4471f82 --- /dev/null +++ b/client/src/app/components/task-manager/TaskNotificaitonBadge.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { NotificationBadge } from "@patternfly/react-core"; +import { useTaskManagerContext } from "./TaskManagerContext"; + +export const TaskNotificationBadge: React.FC = () => { + const { isExpanded, setIsExpanded, queuedCount } = useTaskManagerContext(); + + const badgeClick = () => { + setIsExpanded(!isExpanded); + }; + + return ( + 0 ? "unread" : "read"} + count={queuedCount} + onClick={badgeClick} + isExpanded={isExpanded} + /> + ); +}; diff --git a/client/src/app/layout/DefaultLayout/DefaultLayout.tsx b/client/src/app/layout/DefaultLayout/DefaultLayout.tsx index 7cb4ce9376..6d65293fc9 100644 --- a/client/src/app/layout/DefaultLayout/DefaultLayout.tsx +++ b/client/src/app/layout/DefaultLayout/DefaultLayout.tsx @@ -1,10 +1,12 @@ -import React from "react"; +import React, { useRef } from "react"; import { Page, SkipToContent } from "@patternfly/react-core"; import { HeaderApp } from "../HeaderApp"; import { SidebarApp } from "../SidebarApp"; import { Notifications } from "@app/components/Notifications"; import { PageContentWithDrawerProvider } from "@app/components/PageDrawerContext"; +import { TaskManagerDrawer } from "@app/components/task-manager/TaskManagerDrawer"; +import { useTaskManagerContext } from "@app/components/task-manager/TaskManagerContext"; export interface DefaultLayoutProps {} @@ -14,6 +16,20 @@ export const DefaultLayout: React.FC = ({ children }) => { Skip to content ); + const drawerRef = useRef(null); + const focusDrawer = () => { + if (drawerRef.current === null) { + return; + } + const firstTabbableItem = drawerRef.current.querySelector("a, button") as + | HTMLAnchorElement + | HTMLButtonElement + | null; + firstTabbableItem?.focus(); + }; + + const { isExpanded } = useTaskManagerContext(); + return ( } @@ -21,6 +37,9 @@ export const DefaultLayout: React.FC = ({ children }) => { isManagedSidebar skipToContent={PageSkipToContent} mainContainerId={pageId} + isNotificationDrawerExpanded={isExpanded} + notificationDrawer={} + onNotificationDrawerExpand={() => focusDrawer()} > {children} diff --git a/client/src/app/layout/HeaderApp/HeaderApp.tsx b/client/src/app/layout/HeaderApp/HeaderApp.tsx index a36705672a..1d6da7579a 100644 --- a/client/src/app/layout/HeaderApp/HeaderApp.tsx +++ b/client/src/app/layout/HeaderApp/HeaderApp.tsx @@ -19,8 +19,9 @@ import HelpIcon from "@patternfly/react-icons/dist/esm/icons/help-icon"; import BarsIcon from "@patternfly/react-icons/dist/js/icons/bars-icon"; import useBranding from "@app/hooks/useBranding"; +import { TaskNotificationBadge } from "@app/components/task-manager/TaskNotificaitonBadge"; import { AppAboutModalState } from "../AppAboutModalState"; -import { SSOMenu } from "./SSOMenu"; +import { SsoToolbarItem } from "./SsoToolbarItem"; import { MobileDropdown } from "./MobileDropdown"; import "./header.css"; @@ -33,9 +34,21 @@ export const HeaderApp: React.FC = () => { const toolbar = ( + {/* toolbar items to always show */} + + + + + + {/* toolbar items to show at desktop sizes */} + { - -