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..2ebe0260a5 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,51 @@ 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'); + cy.findByText('Status overview').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/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/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/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 = ({ tabConfig }) => { - const { projectMetrics, namespace } = React.useContext(DistributedWorkloadsContext); - - if (projectMetrics.error) { - return ( - - ); - } - - if (!projectMetrics.loaded) { - return ( - - - - - - ); - } - - const { cpuRequested, cpuUtilized } = projectMetrics.data; - - return ( - <> - - - - -

TODO tab content for project metrics -- these are placeholders

-
-

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

-

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

-
-
-
- - ); -}; - -export default GlobalDistributedWorkloadsProjectMetricsTab; 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 deleted file mode 100644 index 0251dc9353..0000000000 --- a/frontend/src/pages/distributedWorkloads/global/GlobalDistributedWorkloadsWorkloadStatusTab.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from 'react'; -import { Bullseye, PageSection, Spinner, TabContent, TabContentBody } 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 { 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 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)}
-
-
-
-
- - ); -}; -export default GlobalDistributedWorkloadsWorkloadStatusTab; diff --git a/frontend/src/pages/distributedWorkloads/global/projectMetrics/GlobalDistributedWorkloadsProjectMetricsTab.tsx b/frontend/src/pages/distributedWorkloads/global/projectMetrics/GlobalDistributedWorkloadsProjectMetricsTab.tsx new file mode 100644 index 0000000000..92ea97345e --- /dev/null +++ b/frontend/src/pages/distributedWorkloads/global/projectMetrics/GlobalDistributedWorkloadsProjectMetricsTab.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import { DistributedWorkloadsContext } from '~/concepts/distributedWorkloads/DistributedWorkloadsContext'; +import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage'; + +// 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? + +const GlobalDistributedWorkloadsProjectMetricsTab: React.FC = () => { + const { projectMetrics, namespace } = React.useContext(DistributedWorkloadsContext); + + if (projectMetrics.error) { + return ( + + ); + } + + if (!projectMetrics.loaded) { + return ( + + + + ); + } + + const { cpuRequested, cpuUtilized } = projectMetrics.data; + + return ( + <> +

TODO tab content for project metrics -- these are placeholders

+
+

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

+

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

+ + ); +}; + +export default GlobalDistributedWorkloadsProjectMetricsTab; diff --git a/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx b/frontend/src/pages/distributedWorkloads/global/useDistributedWorkloadsTabs.tsx index 4f90683390..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', @@ -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[] => { 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;