From 066183ea047846543c3634b07078e8758b2e63e7 Mon Sep 17 00:00:00 2001 From: Jeff Puzzo Date: Tue, 30 Jul 2024 14:28:01 -0400 Subject: [PATCH] [RHOAIENG-7485] Admin - Database status in Model Registry Table --- .../ModelRegistriesTableRow.tsx | 4 + .../ModelRegistryTableRowStatus.tsx | 150 ++++++++ .../ModelRegistryTableRowStatus.spec.tsx | 351 ++++++++++++++++++ .../pages/modelRegistrySettings/columns.ts | 5 + 4 files changed, 510 insertions(+) create mode 100644 frontend/src/pages/modelRegistrySettings/ModelRegistryTableRowStatus.tsx create mode 100644 frontend/src/pages/modelRegistrySettings/__tests__/ModelRegistryTableRowStatus.spec.tsx diff --git a/frontend/src/pages/modelRegistrySettings/ModelRegistriesTableRow.tsx b/frontend/src/pages/modelRegistrySettings/ModelRegistriesTableRow.tsx index 6a7e84615c..cd75a344f5 100644 --- a/frontend/src/pages/modelRegistrySettings/ModelRegistriesTableRow.tsx +++ b/frontend/src/pages/modelRegistrySettings/ModelRegistriesTableRow.tsx @@ -5,6 +5,7 @@ import { ModelRegistryKind } from '~/k8sTypes'; import ResourceNameTooltip from '~/components/ResourceNameTooltip'; import ViewDatabaseConfigModal from './ViewDatabaseConfigModal'; import DeleteModelRegistryModal from './DeleteModelRegistryModal'; +import { ModelRegistryTableRowStatus } from './ModelRegistryTableRowStatus'; type ModelRegistriesTableRowProps = { modelRegistry: ModelRegistryKind; @@ -30,6 +31,9 @@ const ModelRegistriesTableRow: React.FC = ({

{mr.metadata.annotations['openshift.io/description']}

)} + + + = ({ + conditions, +}) => { + const conditionsMap = + conditions?.reduce((acc: Record, condition) => { + acc[condition.type] = condition; + return acc; + }, {}) ?? {}; + let statusLabel: string = ModelRegistryStatusLabel.Progressing; + let icon = ; + let color: React.ComponentProps['color'] = 'blue'; + let popoverMessages: string[] = []; + let popoverTitle = ''; + + if (Object.values(conditionsMap).length) { + const { + [ModelRegistryStatus.Available]: availableCondition, + [ModelRegistryStatus.Progressing]: progressCondition, + [ModelRegistryStatus.Degraded]: degradedCondition, + } = conditionsMap; + const lastAvailableConditionTime = new Date( + availableCondition?.lastTransitionTime ?? '', + ).getTime(); + + popoverMessages = + availableCondition?.status === ConditionStatus.False + ? Object.values(conditionsMap).reduce((messages: string[], condition) => { + if (condition?.status === ConditionStatus.False && condition.message) { + messages.push(condition.message); + } + + return messages; + }, []) + : []; + + // Available + if (availableCondition?.status === ConditionStatus.True) { + statusLabel = ModelRegistryStatusLabel.Available; + icon = ; + color = 'green'; + } + // Progressing + else if ( + progressCondition?.status === ConditionStatus.True && + lastAvailableConditionTime <= new Date(progressCondition.lastTransitionTime ?? '').getTime() + ) { + statusLabel = ModelRegistryStatusLabel.Progressing; + icon = ; + color = 'blue'; + } + // Degrading + else if ( + degradedCondition?.status === ConditionStatus.True && + lastAvailableConditionTime <= new Date(degradedCondition.lastTransitionTime ?? '').getTime() + ) { + statusLabel = ModelRegistryStatusLabel.Degrading; + icon = ; + color = 'gold'; + popoverTitle = 'Service is degrading'; + } + // Unavailable + else { + statusLabel = ModelRegistryStatusLabel.Unavailable; + icon = ; + color = 'red'; + + const { + [ModelRegistryStatus.IstioAvailable]: istioAvailableCondition, + [ModelRegistryStatus.GatewayAvailable]: gatewayAvailableCondition, + } = conditionsMap; + + if ( + istioAvailableCondition?.status === ConditionStatus.False && + gatewayAvailableCondition?.status === ConditionStatus.False + ) { + popoverTitle = 'Istio resources and Istio Gateway resources are both unavailable'; + } else if (istioAvailableCondition?.status === ConditionStatus.False) { + popoverTitle = 'Istio resources are unavailable'; + } else if (gatewayAvailableCondition?.status === ConditionStatus.False) { + popoverTitle = 'Istio Gateway resources are unavailable'; + } else if ( + istioAvailableCondition?.status === ConditionStatus.True && + gatewayAvailableCondition?.status === ConditionStatus.True + ) { + popoverTitle = 'Deployment is unavailable'; + } else { + popoverTitle = 'Service is unavailable'; + } + } + } + + const label = ( + + ); + + return popoverTitle && popoverMessages.length ? ( + + {popoverMessages.map((message, index) => ( + {message} + ))} + + } + > + {label} + + ) : ( + label + ); +}; diff --git a/frontend/src/pages/modelRegistrySettings/__tests__/ModelRegistryTableRowStatus.spec.tsx b/frontend/src/pages/modelRegistrySettings/__tests__/ModelRegistryTableRowStatus.spec.tsx new file mode 100644 index 0000000000..299e092915 --- /dev/null +++ b/frontend/src/pages/modelRegistrySettings/__tests__/ModelRegistryTableRowStatus.spec.tsx @@ -0,0 +1,351 @@ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import '@testing-library/jest-dom'; + +import { ModelRegistryTableRowStatus } from '~/pages/modelRegistrySettings/ModelRegistryTableRowStatus'; + +describe('ModelRegistryTableRowStatus', () => { + it('renders "Istio resources and Istio Gateway resources are both unavailable" as popover title', async () => { + const user = userEvent.setup(); + + render( + , + ); + + await user.click(screen.getByText('Unavailable')); + expect( + screen.getByRole('heading', { + name: 'Istio resources and Istio Gateway resources are both unavailable', + }), + ).toBeVisible(); + }); + + it('renders "Istio resources are unavailable" as popover title', async () => { + const user = userEvent.setup(); + + render( + , + ); + + await user.click(screen.getByText('Unavailable')); + expect(screen.getByRole('heading', { name: 'Istio resources are unavailable' })).toBeVisible(); + }); + + it('renders "Istio Gateway resources are unavailable" as popover title', async () => { + const user = userEvent.setup(); + + render( + , + ); + + await user.click(screen.getByText('Unavailable')); + expect( + screen.getByRole('heading', { name: 'Istio Gateway resources are unavailable' }), + ).toBeVisible(); + }); + + it('renders "Deployment is unavailable" as popover title', async () => { + const user = userEvent.setup(); + + render( + , + ); + + await user.click(screen.getByText('Unavailable')); + expect(screen.getByRole('heading', { name: 'Deployment is unavailable' })).toBeVisible(); + }); + + it('renders "Available" status', () => { + render( + , + ); + expect(screen.getByText('Available')).toBeVisible(); + }); + + it('renders "Progressing" status when conditions are empty', () => { + render(); + expect(screen.getByText('Progressing')).toBeVisible(); + }); + + it('renders "Progressing" status when conditions are undefined', () => { + render(); + expect(screen.getByText('Progressing')).toBeVisible(); + }); + + it('renders "Progressing" status when the corresponding condition is "True"', () => { + render( + , + ); + expect(screen.getByText('Progressing')).toBeVisible(); + }); + + it('renders "Unavailable" status when the last "Progressing" time is less than the latest "Available" time', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const label = screen.getByText('Unavailable'); + expect(label).toBeVisible(); + + await user.click(label); + + expect(screen.getByRole('heading', { name: 'Service is unavailable' })).toBeVisible(); + expect(screen.getByText('Some unavailable message')).toBeVisible(); + }); + + it('renders "Progressing" status when the last "Progressing" time is greater than the latest "Available" time', () => { + render( + , + ); + + expect(screen.getByText('Progressing')).toBeVisible(); + }); + + it('renders "Unavailable" status when the last "Degraded" time is less than the latest "Available" time', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const label = screen.getByText('Unavailable'); + expect(label).toBeVisible(); + + await user.click(label); + + expect(screen.getByRole('heading', { name: 'Service is unavailable' })).toBeVisible(); + expect(screen.getByText('Some unavailable message')).toBeVisible(); + }); + + it('renders "Degrading" status when the last "Degraded" time is greater than the latest "Available" time', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const label = screen.getByText('Degrading'); + expect(label).toBeVisible(); + + await user.click(label); + + expect(screen.getByRole('heading', { name: 'Service is degrading' })).toBeVisible(); + expect(screen.getByText('Some unavailable message')).toBeVisible(); + }); + + it('renders "Degrading" status with popover message', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const label = screen.getByText('Degrading'); + expect(label).toBeVisible(); + + await user.click(label); + + expect(screen.getByRole('heading', { name: 'Service is degrading' })).toBeVisible(); + expect(screen.getByText('Some degraded message')).toBeVisible(); + }); + + it('renders "Unavailable" with multiple messages in popover', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const label = screen.getByText('Unavailable'); + expect(label).toBeVisible(); + + await user.click(label); + + expect(screen.getByRole('heading', { name: 'Service is unavailable' })).toBeVisible(); + expect(screen.getByText('Some unavailable message 1')).toBeVisible(); + expect(screen.getByText('Some unavailable message 2')).toBeVisible(); + expect(screen.getByText('Some unavailable message 3')).toBeVisible(); + }); + + it('renders "Unavailable" with an unknown status', async () => { + const user = userEvent.setup(); + + render( + , + ); + + const label = screen.getByText('Unavailable'); + expect(label).toBeVisible(); + + await user.click(label); + + expect(screen.getByRole('heading', { name: 'Service is unavailable' })).toBeVisible(); + expect(screen.getByText('Some unknown status message')).toBeVisible(); + }); +}); diff --git a/frontend/src/pages/modelRegistrySettings/columns.ts b/frontend/src/pages/modelRegistrySettings/columns.ts index e9711b1633..4362dd020c 100644 --- a/frontend/src/pages/modelRegistrySettings/columns.ts +++ b/frontend/src/pages/modelRegistrySettings/columns.ts @@ -8,6 +8,11 @@ export const modelRegistryColumns: SortableData[] = [ sortable: (a, b) => a.metadata.name.localeCompare(b.metadata.name), width: 30, }, + { + field: 'status', + label: 'Status', + sortable: false, + }, { field: 'manage permissions', label: '',