From 06a56b41bc62c0f343867e49225db623789f6353 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Fri, 8 Mar 2024 11:59:16 -0500 Subject: [PATCH 1/3] Add missing cypress tests for initial Distributed Workloads views, fix minor architecture issues Signed-off-by: Mike Turley --- .../src/__mocks__/mockPrometheusDWQuery.ts | 21 +++++ .../__mocks__/mockPrometheusDWQueryRange.ts | 26 ++++++ .../GlobalDistributedWorkloads.cy.ts | 85 ++++++++++++++++++- .../cypress/pages/distributedWorkloads.ts | 8 ++ .../DistributedWorkloadsContext.tsx | 6 ++ ...lDistributedWorkloadsProjectMetricsTab.tsx | 48 +++-------- .../global/GlobalDistributedWorkloadsTabs.tsx | 30 ++++++- ...lDistributedWorkloadsWorkloadStatusTab.tsx | 78 ++++++----------- .../global/useDistributedWorkloadsTabs.tsx | 2 +- 9 files changed, 213 insertions(+), 91 deletions(-) create mode 100644 frontend/src/__mocks__/mockPrometheusDWQuery.ts create mode 100644 frontend/src/__mocks__/mockPrometheusDWQueryRange.ts diff --git a/frontend/src/__mocks__/mockPrometheusDWQuery.ts b/frontend/src/__mocks__/mockPrometheusDWQuery.ts new file mode 100644 index 0000000000..0b00d88107 --- /dev/null +++ b/frontend/src/__mocks__/mockPrometheusDWQuery.ts @@ -0,0 +1,21 @@ +import { PrometheusQueryResponse } from '~/types'; + +type MockPrometheusDWQueryType = { + result?: { value: [number, string] }[]; +}; + +export const mockPrometheusDWQuery = ({ + result, +}: MockPrometheusDWQueryType): { + code?: number; + response: PrometheusQueryResponse; +} => ({ + code: 200, + response: { + status: 'success', + data: { + resultType: 'vector', + result: result ?? [], + }, + }, +}); diff --git a/frontend/src/__mocks__/mockPrometheusDWQueryRange.ts b/frontend/src/__mocks__/mockPrometheusDWQueryRange.ts new file mode 100644 index 0000000000..0876350c8f --- /dev/null +++ b/frontend/src/__mocks__/mockPrometheusDWQueryRange.ts @@ -0,0 +1,26 @@ +import { PrometheusQueryRangeResponse, PrometheusQueryRangeResponseDataResult } from '~/types'; + +type MockPrometheusDWQueryRangeType = { + result?: PrometheusQueryRangeResponseDataResult[]; +}; + +export const mockPrometheusDWQueryRange = ({ + result, +}: MockPrometheusDWQueryRangeType): { + code?: number; + response: PrometheusQueryRangeResponse; +} => ({ + code: 200, + response: { + status: 'success', + data: { + resultType: 'matrix', + result: result ?? [ + { + metric: {}, + values: [], + }, + ], + }, + }, +}); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/distributedWorkloads/GlobalDistributedWorkloads.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/distributedWorkloads/GlobalDistributedWorkloads.cy.ts index aa34bdeef3..f6ba6a8f89 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/distributedWorkloads/GlobalDistributedWorkloads.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/distributedWorkloads/GlobalDistributedWorkloads.cy.ts @@ -4,15 +4,21 @@ import { mockStatus } from '~/__mocks__/mockStatus'; import { mockComponents } from '~/__mocks__/mockComponents'; import { explorePage } from '~/__tests__/cypress/cypress/pages/explore'; import { globalDistributedWorkloads } from '~/__tests__/cypress/cypress/pages/distributedWorkloads'; +import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; +import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; +import { mockPrometheusDWQuery } from '~/__mocks__/mockPrometheusDWQuery'; +import { mockPrometheusDWQueryRange } from '~/__mocks__/mockPrometheusDWQueryRange'; type HandlersProps = { isKueueInstalled?: boolean; disableDistributedWorkloads?: boolean; + hasProjects?: boolean; }; const initIntercepts = ({ isKueueInstalled = true, disableDistributedWorkloads = false, + hasProjects = true, }: HandlersProps) => { cy.intercept( '/api/dsc/status', @@ -28,8 +34,34 @@ const initIntercepts = ({ }), ); cy.intercept('/api/components', mockComponents()); - - // TODO mturley other intercepts here + cy.intercept( + { + method: 'GET', + pathname: '/api/k8s/apis/project.openshift.io/v1/projects', + }, + mockK8sResourceList( + hasProjects + ? [ + mockProjectK8sResource({ k8sName: 'test-project', displayName: 'Test Project' }), + mockProjectK8sResource({ k8sName: 'test-project-2', displayName: 'Test Project 2' }), + ] + : [], + ), + ); + cy.intercept( + { + method: 'POST', + pathname: '/api/prometheus/query', + }, + mockPrometheusDWQuery({ result: [] }), + ); + cy.intercept( + { + method: 'POST', + pathname: '/api/prometheus/queryRange', + }, + mockPrometheusDWQueryRange({ result: [] }), + ); }; describe('Workload Metrics', () => { @@ -71,5 +103,52 @@ describe('Workload Metrics', () => { globalDistributedWorkloads.findHeaderText().should('exist'); }); - // TODO mturley other tests here for tab navigation, empty states, etc. + it('Defaults to Project Metrics tab and automatically selects a project', () => { + initIntercepts({}); + globalDistributedWorkloads.visit(); + + cy.url().should('include', '/projectMetrics/test-project'); + // TODO mturley replace this with real identifiable text on the loaded tab when it is completed + cy.findByText('TODO tab content for project metrics -- these are placeholders').should('exist'); + }); + + it('Tabs navigate to corresponding routes and render their contents', () => { + initIntercepts({}); + globalDistributedWorkloads.visit(); + + cy.findByLabelText('Workload status tab').click(); + cy.url().should('include', '/workloadStatus/test-project'); + // TODO mturley replace this with real identifiable text on the loaded tab when it is completed + cy.findByText('TODO tab content for workload status -- these are placeholders').should('exist'); + + cy.findByLabelText('Project metrics tab').click(); + cy.url().should('include', '/projectMetrics/test-project'); + // TODO mturley replace this with real identifiable text on the loaded tab when it is completed + cy.findByText('TODO tab content for project metrics -- these are placeholders').should('exist'); + }); + + it('Changing the project and navigating between tabs or to the root of the page retains the new project', () => { + initIntercepts({}); + globalDistributedWorkloads.visit(); + cy.url().should('include', '/projectMetrics/test-project'); + + globalDistributedWorkloads.selectProjectByName('Test Project 2'); + cy.url().should('include', '/projectMetrics/test-project-2'); + + cy.findByLabelText('Workload status tab').click(); + cy.url().should('include', '/workloadStatus/test-project-2'); + + cy.findByLabelText('Project metrics tab').click(); + cy.url().should('include', '/projectMetrics/test-project-2'); + + globalDistributedWorkloads.navigate(); + cy.url().should('include', '/projectMetrics/test-project-2'); + }); + + it('Should show an empty state if there are no projects', () => { + initIntercepts({ hasProjects: false }); + globalDistributedWorkloads.visit(); + + cy.findByText('No data science projects').should('exist'); + }); }); diff --git a/frontend/src/__tests__/cypress/cypress/pages/distributedWorkloads.ts b/frontend/src/__tests__/cypress/cypress/pages/distributedWorkloads.ts index 37762e81f0..c1789f8c99 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/distributedWorkloads.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/distributedWorkloads.ts @@ -21,6 +21,14 @@ class GlobalDistributedWorkloads { return cy.findByText('Monitor the metrics of your active resources.'); } + findProjectSelect() { + return cy.findByTestId('project-selector-dropdown'); + } + + selectProjectByName(name: string) { + this.findProjectSelect().findDropdownItem(name).click(); + } + private wait() { this.findHeaderText(); cy.testA11y(); diff --git a/frontend/src/concepts/distributedWorkloads/DistributedWorkloadsContext.tsx b/frontend/src/concepts/distributedWorkloads/DistributedWorkloadsContext.tsx index fffe9405f9..8fa136bc5b 100644 --- a/frontend/src/concepts/distributedWorkloads/DistributedWorkloadsContext.tsx +++ b/frontend/src/concepts/distributedWorkloads/DistributedWorkloadsContext.tsx @@ -8,6 +8,8 @@ import { POLL_INTERVAL, } from '~/utilities/const'; import { SupportedArea, conditionalArea } from '~/concepts/areas'; +import useSyncPreferredProject from '~/concepts/projects/useSyncPreferredProject'; +import { ProjectsContext, byName } from '~/concepts/projects/ProjectsContext'; import { useMakeFetchObject } from '~/utilities/useMakeFetchObject'; import { DWProjectMetrics, @@ -81,6 +83,10 @@ export const DistributedWorkloadsContextProvider = SupportedArea.DISTRIBUTED_WORKLOADS, true, )(({ children, namespace }) => { + const { projects } = React.useContext(ProjectsContext); + const project = projects.find(byName(namespace)) ?? null; + useSyncPreferredProject(project); + const [refreshRate, setRefreshRate] = React.useState(POLL_INTERVAL); const [lastUpdateTime, setLastUpdateTime] = React.useState(Date.now()); diff --git a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsProjectMetricsTab.tsx b/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsProjectMetricsTab.tsx index 50a492f09e..92ea97345e 100644 --- a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsProjectMetricsTab.tsx +++ b/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsProjectMetricsTab.tsx @@ -1,19 +1,11 @@ import * as React from 'react'; -import { Bullseye, PageSection, Spinner, TabContent, TabContentBody } from '@patternfly/react-core'; +import { Bullseye, Spinner } from '@patternfly/react-core'; import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; -import { DistributedWorkloadsTabConfig } from './useDistributedWorkloadsTabs'; -import DistributedWorkloadsToolbar from './DistributedWorkloadsToolbar'; // TODO mturley render a "no data" state when we get undefined back for some metrics - why might we hit this? is there a message we can display about making sure things are configured correctly? -type GlobalDistributedWorkloadsProjectMetricsTabProps = { - tabConfig: DistributedWorkloadsTabConfig; -}; - -const GlobalDistributedWorkloadsProjectMetricsTab: React.FC< - GlobalDistributedWorkloadsProjectMetricsTabProps -> = ({ tabConfig }) => { +const GlobalDistributedWorkloadsProjectMetricsTab: React.FC = () => { const { projectMetrics, namespace } = React.useContext(DistributedWorkloadsContext); if (projectMetrics.error) { @@ -27,11 +19,9 @@ const GlobalDistributedWorkloadsProjectMetricsTab: React.FC< if (!projectMetrics.loaded) { return ( - - - - - + + + ); } @@ -39,26 +29,14 @@ const GlobalDistributedWorkloadsProjectMetricsTab: React.FC< return ( <> - - - - -

TODO tab content for project metrics -- these are placeholders

-
-

- CPU requested for project {namespace}: {cpuRequested.data} -

-

- CPU utilized for project {namespace}: {cpuUtilized.data} -

-
-
-
+

TODO tab content for project metrics -- these are placeholders

+
+

+ CPU requested for project {namespace}: {cpuRequested.data} +

+

+ CPU utilized for project {namespace}: {cpuUtilized.data} +

); }; diff --git a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsTabs.tsx b/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsTabs.tsx index c426b0b13e..83f6657d2a 100644 --- a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsTabs.tsx +++ b/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsTabs.tsx @@ -1,10 +1,18 @@ import * as React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { Tabs, Tab, TabTitleText, PageSection } from '@patternfly/react-core'; +import { + Tabs, + Tab, + TabTitleText, + PageSection, + TabContent, + TabContentBody, +} from '@patternfly/react-core'; import { DistributedWorkloadsTabId, useDistributedWorkloadsTabs, } from './useDistributedWorkloadsTabs'; +import DistributedWorkloadsToolbar from './DistributedWorkloadsToolbar'; type GlobalDistributedWorkloadsTabsProps = { activeTabId: DistributedWorkloadsTabId; @@ -45,7 +53,25 @@ const GlobalDistributedWorkloadsTabs: React.FC - {activeTab && } + {activeTab ? : null} + + {tabs + .filter((tab) => tab.isAvailable) + .map((tab) => { + const isActiveTab = tab.id === activeTab?.id; + return ( + + ); + })} + ); }; diff --git a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx b/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx index 0251dc9353..c96cd6113a 100644 --- a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx +++ b/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx @@ -1,17 +1,9 @@ import * as React from 'react'; -import { Bullseye, PageSection, Spinner, TabContent, TabContentBody } from '@patternfly/react-core'; +import { Bullseye, Spinner } from '@patternfly/react-core'; import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; -import { DistributedWorkloadsTabConfig } from './useDistributedWorkloadsTabs'; -import DistributedWorkloadsToolbar from './DistributedWorkloadsToolbar'; -type GlobalDistributedWorkloadsWorkloadStatusTabProps = { - tabConfig: DistributedWorkloadsTabConfig; -}; - -const GlobalDistributedWorkloadsWorkloadStatusTab: React.FC< - GlobalDistributedWorkloadsWorkloadStatusTabProps -> = ({ tabConfig }) => { +const GlobalDistributedWorkloadsWorkloadStatusTab: React.FC = () => { const { workloads, workloadCurrentMetrics, workloadTrendMetrics, namespace } = React.useContext( DistributedWorkloadsContext, ); @@ -25,11 +17,9 @@ const GlobalDistributedWorkloadsWorkloadStatusTab: React.FC< if (!workloads.loaded || !workloadCurrentMetrics.loaded || !workloadTrendMetrics.loaded) { return ( - - - - - + + + ); } @@ -40,41 +30,29 @@ const GlobalDistributedWorkloadsWorkloadStatusTab: React.FC< return ( <> - - - - -

TODO tab content for job metrics -- these are placeholders

-
-

Workloads matching statuses in project {namespace}:

-
    -
  • Running: {numJobsActive.data}
  • -
  • Succeeded: {numJobsSucceeded.data}
  • -
  • Failed: {numJobsFailed.data}
  • -
  • Inadmissible: {numJobsInadmissible.data}
  • -
  • Pending: {numJobsPending.data}
  • -
-
-

Workloads for project {namespace}:

-
{JSON.stringify(workloads.data, undefined, 4)}
-
-

Active jobs trend:

-
{JSON.stringify(jobsActiveTrend.data)}
-
-

Inadmissible jobs trend:

-
{JSON.stringify(jobsInadmissibleTrend.data)}
-
-

Pending jobs trend:

-
{JSON.stringify(jobsPendingTrend.data)}
-
-
-
-
+

TODO tab content for workload status -- these are placeholders

+
+

Workloads matching statuses in project {namespace}:

+
    +
  • Running: {numJobsActive.data}
  • +
  • Succeeded: {numJobsSucceeded.data}
  • +
  • Failed: {numJobsFailed.data}
  • +
  • Inadmissible: {numJobsInadmissible.data}
  • +
  • Pending: {numJobsPending.data}
  • +
+
+

Workloads for project {namespace}:

+
{JSON.stringify(workloads.data, undefined, 4)}
+
+

Active jobs trend:

+
{JSON.stringify(jobsActiveTrend.data)}
+
+

Inadmissible jobs trend:

+
{JSON.stringify(jobsInadmissibleTrend.data)}
+
+

Pending jobs trend:

+
{JSON.stringify(jobsPendingTrend.data)}
+
); }; diff --git a/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx b/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx index 4f90683390..cc92a85e67 100644 --- a/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx +++ b/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx @@ -15,7 +15,7 @@ export type DistributedWorkloadsTabConfig = { isAvailable: boolean; // TODO mturley remove this now that all our tabs here are single project only, or leave in case we add future tabs? projectSelectorMode: 'singleProjectOnly' | 'projectOrAll' | null; - ContentComponent: React.FC<{ tabConfig: DistributedWorkloadsTabConfig }>; + ContentComponent: React.FC; }; export const useDistributedWorkloadsTabs = (): DistributedWorkloadsTabConfig[] => { From 29bb4be3b3c96268a070873fcdaa68ac808a2b53 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 12 Mar 2024 12:08:06 -0400 Subject: [PATCH 2/3] Factor out components for each card on the workload status page Signed-off-by: Mike Turley --- .../GlobalDistributedWorkloads.cy.ts | 3 +- ...lDistributedWorkloadsWorkloadStatusTab.tsx | 59 ------------------- ...lDistributedWorkloadsProjectMetricsTab.tsx | 0 .../global/useDistributedWorkloadsTabs.tsx | 4 +- .../DWStatusOverviewDonutChart.tsx | 48 +++++++++++++++ .../workloadStatus/DWStatusTrendsChart.tsx | 52 ++++++++++++++++ .../workloadStatus/DWWorkloadsTable.tsx | 39 ++++++++++++ ...lDistributedWorkloadsWorkloadStatusTab.tsx | 21 +++++++ 8 files changed, 163 insertions(+), 63 deletions(-) delete mode 100644 frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx rename frontend/src/pages/distributedWorkloads/global/{ => projectMetrics}/GlobalDistributedWorkloadsProjectMetricsTab.tsx (100%) create mode 100644 frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusOverviewDonutChart.tsx create mode 100644 frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusTrendsChart.tsx create mode 100644 frontend/src/pages/distributedWorkloads/global/workloadStatus/DWWorkloadsTable.tsx create mode 100644 frontend/src/pages/distributedWorkloads/global/workloadStatus/GlobalDistributedWorkloadsWorkloadStatusTab.tsx diff --git a/frontend/src/__tests__/cypress/cypress/e2e/distributedWorkloads/GlobalDistributedWorkloads.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/distributedWorkloads/GlobalDistributedWorkloads.cy.ts index f6ba6a8f89..2ebe0260a5 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/distributedWorkloads/GlobalDistributedWorkloads.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/distributedWorkloads/GlobalDistributedWorkloads.cy.ts @@ -118,8 +118,7 @@ describe('Workload Metrics', () => { cy.findByLabelText('Workload status tab').click(); cy.url().should('include', '/workloadStatus/test-project'); - // TODO mturley replace this with real identifiable text on the loaded tab when it is completed - cy.findByText('TODO tab content for workload status -- these are placeholders').should('exist'); + cy.findByText('Status overview').should('exist'); cy.findByLabelText('Project metrics tab').click(); cy.url().should('include', '/projectMetrics/test-project'); diff --git a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx b/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx deleted file mode 100644 index c96cd6113a..0000000000 --- a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import { Bullseye, Spinner } from '@patternfly/react-core'; -import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; -import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; - -const GlobalDistributedWorkloadsWorkloadStatusTab: React.FC = () => { - const { workloads, workloadCurrentMetrics, workloadTrendMetrics, namespace } = React.useContext( - DistributedWorkloadsContext, - ); - - const error = workloads.error || workloadCurrentMetrics.error || workloadTrendMetrics.error; - if (error) { - return ( - - ); - } - - if (!workloads.loaded || !workloadCurrentMetrics.loaded || !workloadTrendMetrics.loaded) { - return ( - - - - ); - } - - const { numJobsActive, numJobsFailed, numJobsSucceeded, numJobsInadmissible, numJobsPending } = - workloadCurrentMetrics.data; - - const { jobsActiveTrend, jobsInadmissibleTrend, jobsPendingTrend } = workloadTrendMetrics.data; - - return ( - <> -

TODO tab content for workload status -- these are placeholders

-
-

Workloads matching statuses in project {namespace}:

-
    -
  • Running: {numJobsActive.data}
  • -
  • Succeeded: {numJobsSucceeded.data}
  • -
  • Failed: {numJobsFailed.data}
  • -
  • Inadmissible: {numJobsInadmissible.data}
  • -
  • Pending: {numJobsPending.data}
  • -
-
-

Workloads for project {namespace}:

-
{JSON.stringify(workloads.data, undefined, 4)}
-
-

Active jobs trend:

-
{JSON.stringify(jobsActiveTrend.data)}
-
-

Inadmissible jobs trend:

-
{JSON.stringify(jobsInadmissibleTrend.data)}
-
-

Pending jobs trend:

-
{JSON.stringify(jobsPendingTrend.data)}
-
- - ); -}; -export default GlobalDistributedWorkloadsWorkloadStatusTab; diff --git a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsProjectMetricsTab.tsx b/frontend/src/pages/distributedWorkloads/global/projectMetrics/GlobalDistributedWorkloadsProjectMetricsTab.tsx similarity index 100% rename from frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsProjectMetricsTab.tsx rename to frontend/src/pages/distributedWorkloads/global/projectMetrics/GlobalDistributedWorkloadsProjectMetricsTab.tsx diff --git a/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx b/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx index cc92a85e67..f0100c1f1b 100644 --- a/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx +++ b/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; -import GlobalDistributedWorkloadsProjectMetricsTab from './GlobalDistributedWorkloadsProjectMetricsTab'; -import GlobalDistributedWorkloadsWorkloadStatusTab from './GlobalDistributedWorkloadsWorkloadStatusTab'; +import GlobalDistributedWorkloadsProjectMetricsTab from './projectMetrics/GlobalDistributedWorkloadsProjectMetricsTab'; +import GlobalDistributedWorkloadsWorkloadStatusTab from './workloadStatus/GlobalDistributedWorkloadsWorkloadStatusTab'; export enum DistributedWorkloadsTabId { PROJECT_METRICS = 'project-metrics', diff --git a/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusOverviewDonutChart.tsx b/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusOverviewDonutChart.tsx new file mode 100644 index 0000000000..388800ca75 --- /dev/null +++ b/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusOverviewDonutChart.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { Card, CardTitle, CardBody, Bullseye, Spinner } from '@patternfly/react-core'; +import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; +import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; + +export const DWStatusOverviewDonutChart: React.FC = () => { + const { workloadCurrentMetrics } = React.useContext(DistributedWorkloadsContext); + + if (workloadCurrentMetrics.error) { + return ( + + + + ); + } + + if (!workloadCurrentMetrics.loaded) { + return ( + + + + + + ); + } + + const { numJobsActive, numJobsFailed, numJobsSucceeded, numJobsInadmissible, numJobsPending } = + workloadCurrentMetrics.data; + + return ( + + Status overview + +

TODO status overview donut chart

+
    +
  • Running: {numJobsActive.data}
  • +
  • Succeeded: {numJobsSucceeded.data}
  • +
  • Failed: {numJobsFailed.data}
  • +
  • Inadmissible: {numJobsInadmissible.data}
  • +
  • Pending: {numJobsPending.data}
  • +
+
+
+ ); +}; diff --git a/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusTrendsChart.tsx b/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusTrendsChart.tsx new file mode 100644 index 0000000000..4347ee817c --- /dev/null +++ b/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWStatusTrendsChart.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { Card, CardTitle, CardBody, Bullseye, Spinner } from '@patternfly/react-core'; +import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; +import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; + +export const DWStatusTrendsChart: React.FC = () => { + const { workloadTrendMetrics } = React.useContext(DistributedWorkloadsContext); + + if (workloadTrendMetrics.error) { + return ( + + + + ); + } + + if (!workloadTrendMetrics.loaded) { + return ( + + + + + + ); + } + + const { jobsActiveTrend, jobsInadmissibleTrend, jobsPendingTrend } = workloadTrendMetrics.data; + + return ( + + Status trends + +

TODO status trends line chart

+

Active jobs trend:

+
{JSON.stringify(jobsActiveTrend.data)}
+
+

Inadmissible jobs trend:

+
{JSON.stringify(jobsInadmissibleTrend.data)}
+
+

Pending jobs trend:

+
{JSON.stringify(jobsPendingTrend.data)}
+
+
+ ); +}; diff --git a/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWWorkloadsTable.tsx b/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWWorkloadsTable.tsx new file mode 100644 index 0000000000..baed061ba6 --- /dev/null +++ b/frontend/src/pages/distributedWorkloads/global/workloadStatus/DWWorkloadsTable.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { Card, CardTitle, CardBody, Bullseye, Spinner } from '@patternfly/react-core'; +import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; +import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; + +export const DWWorkloadsTable: React.FC = () => { + const { workloads } = React.useContext(DistributedWorkloadsContext); + + if (workloads.error) { + return ( + + + + ); + } + + if (!workloads.loaded) { + return ( + + + + + + ); + } + + return ( + + Workloads + +

TODO workloads table

+
{JSON.stringify(workloads.data, undefined, 4)}
+
+
+ ); +}; diff --git a/frontend/src/pages/distributedWorkloads/global/workloadStatus/GlobalDistributedWorkloadsWorkloadStatusTab.tsx b/frontend/src/pages/distributedWorkloads/global/workloadStatus/GlobalDistributedWorkloadsWorkloadStatusTab.tsx new file mode 100644 index 0000000000..f4c5b1c3e7 --- /dev/null +++ b/frontend/src/pages/distributedWorkloads/global/workloadStatus/GlobalDistributedWorkloadsWorkloadStatusTab.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { Grid, GridItem } from '@patternfly/react-core'; +import { DWStatusOverviewDonutChart } from './DWStatusOverviewDonutChart'; +import { DWStatusTrendsChart } from './DWStatusTrendsChart'; +import { DWWorkloadsTable } from './DWWorkloadsTable'; + +const GlobalDistributedWorkloadsWorkloadStatusTab: React.FC = () => ( + + + + + + + + + + + +); + +export default GlobalDistributedWorkloadsWorkloadStatusTab; From ef81fd9e65b1f41d9a05f1f7b9e5ab1e2102e77e Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Thu, 14 Mar 2024 11:40:48 -0400 Subject: [PATCH 3/3] Use initialPromisePurity for prometheus range queries in DW Signed-off-by: Mike Turley --- frontend/src/api/prometheus/distributedWorkloads.ts | 3 +++ frontend/src/api/prometheus/usePrometheusQueryRange.ts | 4 +++- frontend/src/api/prometheus/useQueryRangeResourceData.ts | 3 +++ frontend/src/concepts/distributedWorkloads/useWorkloads.ts | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/prometheus/distributedWorkloads.ts b/frontend/src/api/prometheus/distributedWorkloads.ts index cc0f928114..6066df6f79 100644 --- a/frontend/src/api/prometheus/distributedWorkloads.ts +++ b/frontend/src/api/prometheus/distributedWorkloads.ts @@ -160,6 +160,7 @@ export const useDWWorkloadTrendMetrics = ( defaultResponsePredicate, namespace || '', '/api/prometheus/queryRange', + { initialPromisePurity: true }, ), jobsInadmissibleTrend: useQueryRangeResourceData( !!queries, @@ -169,6 +170,7 @@ export const useDWWorkloadTrendMetrics = ( defaultResponsePredicate, namespace || '', '/api/prometheus/queryRange', + { initialPromisePurity: true }, ), jobsPendingTrend: useQueryRangeResourceData( !!queries, @@ -178,6 +180,7 @@ export const useDWWorkloadTrendMetrics = ( defaultResponsePredicate, namespace || '', '/api/prometheus/queryRange', + { initialPromisePurity: true }, ), }; diff --git a/frontend/src/api/prometheus/usePrometheusQueryRange.ts b/frontend/src/api/prometheus/usePrometheusQueryRange.ts index cb06ff4fb6..45214cfcc8 100644 --- a/frontend/src/api/prometheus/usePrometheusQueryRange.ts +++ b/frontend/src/api/prometheus/usePrometheusQueryRange.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import axios from 'axios'; import useFetchState, { + FetchOptions, FetchState, FetchStateCallbackPromise, NotReadyError, @@ -26,6 +27,7 @@ const usePrometheusQueryRange = ( step: number, responsePredicate: ResponsePredicate, namespace: string, + fetchOptions?: Partial, ): [...FetchState, boolean] => { const pendingRef = React.useRef(active); const fetchData = React.useCallback>(() => { @@ -59,7 +61,7 @@ const usePrometheusQueryRange = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [active, fetchData]); - return [...useFetchState(fetchData, []), pendingRef.current]; + return [...useFetchState(fetchData, [], fetchOptions), pendingRef.current]; }; export const defaultResponsePredicate: ResponsePredicate = (data) => data.result?.[0]?.values || []; diff --git a/frontend/src/api/prometheus/useQueryRangeResourceData.ts b/frontend/src/api/prometheus/useQueryRangeResourceData.ts index 128fd5f979..c72790f35d 100644 --- a/frontend/src/api/prometheus/useQueryRangeResourceData.ts +++ b/frontend/src/api/prometheus/useQueryRangeResourceData.ts @@ -2,6 +2,7 @@ import { TimeframeStep, TimeframeTimeRange } from '~/pages/modelServing/screens/const'; import { PrometheusQueryRangeResultValue } from '~/types'; import useRestructureContextResourceData from '~/utilities/useRestructureContextResourceData'; +import { FetchOptions } from '~/utilities/useFetchState'; import { TimeframeTitle } from '~/pages/modelServing/screens/types'; import usePrometheusQueryRange, { ResponsePredicate } from './usePrometheusQueryRange'; @@ -14,6 +15,7 @@ const useQueryRangeResourceData = ( responsePredicate: ResponsePredicate, namespace: string, apiPath = '/api/prometheus/serving', + fetchOptions?: Partial, ): ReturnType> => useRestructureContextResourceData( usePrometheusQueryRange( @@ -25,6 +27,7 @@ const useQueryRangeResourceData = ( TimeframeStep[timeframe], responsePredicate, namespace, + fetchOptions, ), ); diff --git a/frontend/src/concepts/distributedWorkloads/useWorkloads.ts b/frontend/src/concepts/distributedWorkloads/useWorkloads.ts index f63c7f7668..67603d1af4 100644 --- a/frontend/src/concepts/distributedWorkloads/useWorkloads.ts +++ b/frontend/src/concepts/distributedWorkloads/useWorkloads.ts @@ -14,7 +14,7 @@ const useWorkloads = (namespace?: string, refreshRate = 0): FetchState