From 5cf7e0d2300a697c50f415d2c5d87d27c0087df9 Mon Sep 17 00:00:00 2001 From: ppadti Date: Tue, 10 Sep 2024 14:42:31 +0530 Subject: [PATCH] Upadte archived models and archived versions components --- .../cypress/cypress/pages/modelRegistry.ts | 4 + .../modelRegistry/registeredModelArchive.ts | 6 + .../registeredModelArchive.cy.ts | 67 +++++- .../EditableLabelsDescriptionListGroup.tsx | 4 +- .../EditableTextDescriptionListGroup.tsx | 4 +- .../modelRegistry/ModelRegistryRoutes.tsx | 20 ++ .../ModelPropertiesDescriptionListGroup.tsx | 27 ++- .../screens/ModelPropertiesTableRow.tsx | 78 ++++--- .../ModelVersionDetails.tsx | 37 ++- .../ModelVersionDetailsTabs.tsx | 8 +- .../ModelVersionDetailsView.tsx | 5 + .../ModelVersions/ModelDetailsView.tsx | 10 +- .../ModelVersions/ModelVersionListView.tsx | 220 +++++++++++------- .../screens/ModelVersions/ModelVersions.tsx | 13 +- .../ModelVersions/ModelVersionsTable.tsx | 9 +- .../ModelVersions/ModelVersionsTableRow.tsx | 127 +++++----- .../ModelVersions/ModelVersionsTabs.tsx | 9 +- .../ArchiveModelVersionDetails.tsx | 92 ++++++++ .../ArchiveModelVersionDetailsBreadcrumb.tsx | 41 ++++ .../ModelVersionArchiveDetails.tsx | 29 ++- .../RegisteredModelTableRow.tsx | 17 +- .../RegisteredModelArchiveDetails.tsx | 10 +- .../pages/modelRegistry/screens/routeUtils.ts | 11 + 23 files changed, 631 insertions(+), 217 deletions(-) create mode 100644 frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetails.tsx create mode 100644 frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetailsBreadcrumb.tsx diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts index aab67eb538..0dcb63d14f 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts @@ -95,6 +95,10 @@ class ModelRegistry { cy.findByTestId('empty-model-versions').should('exist'); } + shouldArchveModelVersionsEmpty() { + cy.findByTestId('empty-archive-model-versions').should('exist'); + } + shouldModelRegistrySelectorExist() { cy.findByTestId('model-registry-selector-dropdown').should('exist'); } diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry/registeredModelArchive.ts b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry/registeredModelArchive.ts index fcba9d4102..05b1186ea3 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry/registeredModelArchive.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry/registeredModelArchive.ts @@ -66,6 +66,12 @@ class ModelArchive { cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/archive/${rmId}`); } + visitArchiveModelVersionList() { + const rmId = '2'; + const preferredModelRegistry = 'modelregistry-sample'; + cy.visit(`/modelRegistry/${preferredModelRegistry}/registeredModels/archive/${rmId}/versions`); + } + visitModelList() { cy.visit('/modelRegistry/modelregistry-sample'); this.wait(); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registeredModelArchive.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registeredModelArchive.cy.ts index 9495185943..b751cfa404 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registeredModelArchive.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registeredModelArchive.cy.ts @@ -47,7 +47,7 @@ const initIntercepts = ({ mockRegisteredModel({ id: '4', name: 'model 4' }), ], modelVersions = [ - mockModelVersion({ author: 'Author 1' }), + mockModelVersion({ author: 'Author 1', registeredModelId: '2' }), mockModelVersion({ name: 'model version' }), ], }: HandlersProps) => { @@ -74,13 +74,25 @@ const initIntercepts = ({ mockRegisteredModelList({ items: registeredModels }), ); + cy.interceptOdh( + `GET /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/model_versions/:modelVersionId`, + { + path: { + serviceName: 'modelregistry-sample', + apiVersion: MODEL_REGISTRY_API_VERSION, + modelVersionId: 1, + }, + }, + mockModelVersion({ id: '1', name: 'Version 2' }), + ); + cy.interceptOdh( `GET /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/registered_models/:registeredModelId/versions`, { path: { serviceName: 'modelregistry-sample', apiVersion: MODEL_REGISTRY_API_VERSION, - registeredModelId: 1, + registeredModelId: 2, }, }, mockModelVersionList({ items: modelVersions }), @@ -97,6 +109,18 @@ const initIntercepts = ({ }, mockRegisteredModel({ id: '2', name: 'model 2', state: ModelState.ARCHIVED }), ); + + cy.interceptOdh( + 'GET /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/registered_models/:registeredModelId', + { + path: { + serviceName: 'modelregistry-sample', + apiVersion: MODEL_REGISTRY_API_VERSION, + registeredModelId: 3, + }, + }, + mockRegisteredModel({ id: '3', name: 'model 3' }), + ); }; describe('Model archive list', () => { @@ -123,6 +147,32 @@ describe('Model archive list', () => { registeredModelArchive.findArchiveModelTable().should('be.visible'); }); + it('Archived model with no versions', () => { + initIntercepts({ modelVersions: [] }); + registeredModelArchive.visit(); + verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive'); + registeredModelArchive.findArchiveModelBreadcrumbItem().contains('Archived models'); + const archiveModelRow = registeredModelArchive.getRow('model 2'); + archiveModelRow.findName().contains('model 2').click(); + modelRegistry.shouldArchveModelVersionsEmpty(); + }); + + it('Archived model flow', () => { + initIntercepts({}); + registeredModelArchive.visitArchiveModelVersionList(); + verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive/2/versions'); + + modelRegistry.findModelVersionsTable().should('be.visible'); + modelRegistry.findModelVersionsTableRows().should('have.length', 2); + const version = modelRegistry.getModelVersionRow('model version'); + version.findModelVersionName().contains('model version').click(); + verifyRelativeURL( + '/modelRegistry/modelregistry-sample/registeredModels/archive/2/versions/1/details', + ); + cy.go('back'); + verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/archive/2/versions'); + }); + it('Archive models list', () => { initIntercepts({}); registeredModelArchive.visit(); @@ -180,7 +230,7 @@ it('Opens the detail page when we select "View Details" from action menu', () => archiveModelRow.findKebabAction('View details').click(); cy.location('pathname').should( 'be.equals', - '/modelRegistry/modelregistry-sample/registeredModels/2/details', + '/modelRegistry/modelregistry-sample/registeredModels/archive/2/details', ); }); @@ -276,21 +326,24 @@ describe('Archiving model', () => { path: { serviceName: 'modelregistry-sample', apiVersion: MODEL_REGISTRY_API_VERSION, - registeredModelId: 2, + registeredModelId: 3, }, }, - mockRegisteredModel({ id: '2', name: 'model 2', state: ModelState.ARCHIVED }), + mockRegisteredModel({ id: '3', name: 'model 3', state: ModelState.ARCHIVED }), ).as('modelArchived'); initIntercepts({}); - registeredModelArchive.visitModelDetails(); + registeredModelArchive.visitModelList(); + + const modelRow = modelRegistry.getRow('model 3'); + modelRow.findName().contains('model 3').click(); registeredModelArchive .findModelVersionsDetailsHeaderAction() .findDropdownItem('Archive model') .click(); archiveModelModal.findArchiveButton().should('be.disabled'); - archiveModelModal.findModalTextInput().fill('model 2'); + archiveModelModal.findModalTextInput().fill('model 3'); archiveModelModal.findArchiveButton().should('be.enabled').click(); cy.wait('@modelArchived').then((interception) => { expect(interception.request.body).to.eql({ diff --git a/frontend/src/components/EditableLabelsDescriptionListGroup.tsx b/frontend/src/components/EditableLabelsDescriptionListGroup.tsx index f25f1b6c9f..c0e3958017 100644 --- a/frontend/src/components/EditableLabelsDescriptionListGroup.tsx +++ b/frontend/src/components/EditableLabelsDescriptionListGroup.tsx @@ -22,6 +22,7 @@ type EditableTextDescriptionListGroupProps = Partial< labels: string[]; saveEditedLabels: (labels: string[]) => Promise; allExistingKeys?: string[]; + isArchive?: boolean; }; const EditableLabelsDescriptionListGroup: React.FC = ({ @@ -29,6 +30,7 @@ const EditableLabelsDescriptionListGroup: React.FC { const [isEditing, setIsEditing] = React.useState(false); @@ -98,7 +100,7 @@ const EditableLabelsDescriptionListGroup: React.FC Promise; testid?: string; + isArchive?: boolean; }; const EditableTextDescriptionListGroup: React.FC = ({ title, contentWhenEmpty, value, + isArchive, saveEditedValue, testid, }) => { @@ -29,7 +31,7 @@ const EditableTextDescriptionListGroup: React.FC ( @@ -91,6 +92,25 @@ const ModelRegistryRoutes: React.FC = () => ( } /> + + } /> + + } + /> + + } + /> + } /> + } /> } /> diff --git a/frontend/src/pages/modelRegistry/screens/ModelPropertiesDescriptionListGroup.tsx b/frontend/src/pages/modelRegistry/screens/ModelPropertiesDescriptionListGroup.tsx index c493a3babe..bdded1d537 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelPropertiesDescriptionListGroup.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelPropertiesDescriptionListGroup.tsx @@ -11,11 +11,13 @@ import { getProperties, mergeUpdatedProperty } from './utils'; type ModelPropertiesDescriptionListGroupProps = { customProperties: ModelRegistryCustomProperties; + isArchive?: boolean; saveEditedCustomProperties: (properties: ModelRegistryCustomProperties) => Promise; }; const ModelPropertiesDescriptionListGroup: React.FC = ({ customProperties = {}, + isArchive, saveEditedCustomProperties, }) => { const [editingPropertyKeys, setEditingPropertyKeys] = React.useState([]); @@ -51,16 +53,18 @@ const ModelPropertiesDescriptionListGroup: React.FC} - iconPosition="start" - isDisabled={isAdding || isSavingEdits} - onClick={() => setIsAdding(true)} - > - Add property - + !isArchive && ( + + ) } isEmpty={!isAdding && keys.length === 0} contentWhenEmpty="No properties" @@ -70,13 +74,14 @@ const ModelPropertiesDescriptionListGroup: React.FC Key {isEditingSomeRow && requiredAsterisk} Value {isEditingSomeRow && requiredAsterisk} - + {shownKeys.map((key) => ( void; isSavingEdits: boolean; + isArchive?: boolean; setIsSavingEdits: (isSaving: boolean) => void; saveEditedProperty: (oldKey: string, newPair: KeyValuePair) => Promise; } & EitherNotBoth< @@ -38,6 +39,7 @@ const ModelPropertiesTableRow: React.FC = ({ setIsEditing, isSavingEdits, setIsSavingEdits, + isArchive, saveEditedProperty, }) => { const { key, value } = keyValuePair; @@ -143,43 +145,45 @@ const ModelPropertiesTableRow: React.FC = ({ )} - - {isEditing ? ( - - - - - - - - - ) : ( - - )} - + {!isArchive && ( + + {isEditing ? ( + + + + + + + + + ) : ( + + )} + + )} ); }; diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetails.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetails.tsx index a54bb93504..0e0e91053e 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetails.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetails.tsx @@ -1,15 +1,21 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router'; import { Breadcrumb, BreadcrumbItem, Flex, FlexItem, Truncate } from '@patternfly/react-core'; import { Link } from 'react-router-dom'; import ApplicationsPage from '~/pages/ApplicationsPage'; import useModelVersionById from '~/concepts/modelRegistry/apiHooks/useModelVersionById'; import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; -import { modelVersionUrl, registeredModelUrl } from '~/pages/modelRegistry/screens/routeUtils'; +import { + archiveModelVersionDetailsUrl, + modelVersionArchiveDetailsUrl, + modelVersionUrl, + registeredModelUrl, +} from '~/pages/modelRegistry/screens/routeUtils'; import useRegisteredModelById from '~/concepts/modelRegistry/apiHooks/useRegisteredModelById'; import useInferenceServices from '~/pages/modelServing/useInferenceServices'; import useServingRuntimes from '~/pages/modelServing/useServingRuntimes'; import { useMakeFetchObject } from '~/utilities/useMakeFetchObject'; +import { ModelState } from '~/concepts/modelRegistry/types'; import { ModelVersionDetailsTab } from './const'; import ModelVersionsDetailsHeaderActions from './ModelVersionDetailsHeaderActions'; import ModelVersionDetailsTabs from './ModelVersionDetailsTabs'; @@ -41,6 +47,33 @@ const ModelVersionsDetails: React.FC = ({ tab, ...page servingRuntimes.refresh(); }, [inferenceServices, servingRuntimes, refreshModelVersion]); + useEffect(() => { + if (rm?.state === ModelState.ARCHIVED && mv?.id) { + navigate( + archiveModelVersionDetailsUrl( + mv.id, + mv.registeredModelId, + preferredModelRegistry?.metadata.name, + ), + ); + } else if (mv?.state === ModelState.ARCHIVED) { + navigate( + modelVersionArchiveDetailsUrl( + mv.id, + mv.registeredModelId, + preferredModelRegistry?.metadata.name, + ), + ); + } + }, [ + rm?.state, + mv?.id, + mv?.state, + mv?.registeredModelId, + preferredModelRegistry?.metadata.name, + navigate, + ]); + return ( ; servingRuntimes: FetchStateObject; + isArchiveVersion?: boolean; refresh: () => void; }; @@ -22,6 +23,7 @@ const ModelVersionDetailsTabs: React.FC = ({ modelVersion: mv, inferenceServices, servingRuntimes, + isArchiveVersion, refresh, }) => { const navigate = useNavigate(); @@ -40,7 +42,11 @@ const ModelVersionDetailsTabs: React.FC = ({ data-testid="model-versions-details-tab" > - + void; }; const ModelVersionDetailsView: React.FC = ({ modelVersion: mv, + isArchiveVersion, refresh, }) => { const [modelArtifact] = useModelArtifactsByVersionId(mv.id); @@ -36,6 +38,7 @@ const ModelVersionDetailsView: React.FC = ({ = ({ /> apiState.api @@ -67,6 +71,7 @@ const ModelVersionDetailsView: React.FC = ({ } /> apiState.api diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelDetailsView.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelDetailsView.tsx index bdf7f79801..249569a121 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelDetailsView.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelDetailsView.tsx @@ -12,9 +12,14 @@ import ModelPropertiesDescriptionListGroup from '~/pages/modelRegistry/screens/M type ModelDetailsViewProps = { registeredModel: RegisteredModel; refresh: () => void; + isArchiveModel?: boolean; }; -const ModelDetailsView: React.FC = ({ registeredModel: rm, refresh }) => { +const ModelDetailsView: React.FC = ({ + registeredModel: rm, + refresh, + isArchiveModel, +}) => { const { apiState } = React.useContext(ModelRegistryContext); return ( = ({ registeredModel: rm @@ -42,6 +48,7 @@ const ModelDetailsView: React.FC = ({ registeredModel: rm /> apiState.api @@ -56,6 +63,7 @@ const ModelDetailsView: React.FC = ({ registeredModel: rm } /> apiState.api diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionListView.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionListView.tsx index 458ef9f5a8..b35266881b 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionListView.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionListView.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { + Alert, Button, Dropdown, DropdownItem, @@ -35,12 +36,14 @@ import ModelVersionsTable from './ModelVersionsTable'; type ModelVersionListViewProps = { modelVersions: ModelVersion[]; registeredModel?: RegisteredModel; + isArchiveModel?: boolean; refresh: () => void; }; const ModelVersionListView: React.FC = ({ modelVersions: unfilteredModelVersions, registeredModel: rm, + isArchiveModel, refresh, }) => { const navigate = useNavigate(); @@ -55,8 +58,24 @@ const ModelVersionListView: React.FC = ({ React.useState(false); const filteredModelVersions = filterModelVersions(unfilteredModelVersions, search, searchType); + const date = rm?.lastUpdateTimeSinceEpoch && new Date(parseInt(rm.lastUpdateTimeSinceEpoch)); if (unfilteredModelVersions.length === 0) { + if (isArchiveModel) { + return ( + ( + missing version + )} + description={`${rm?.name} has no registered versions.`} + /> + ); + } return ( = ({ } return ( - setSearch('')} - modelVersions={sortModelVersionsByCreateTime(filteredModelVersions)} - toolbarContent={ - - } breakpoint="xl"> - - setSearch('')} - deleteChipGroup={() => setSearch('')} - categoryName={searchType} - > - ({ - key, - label: key, - }))} - value={searchType} - onChange={(newSearchType) => { - const enumMember = asEnumMember(newSearchType, SearchType); - if (enumMember !== null) { - setSearchType(enumMember); - } - }} - icon={} - /> - - - { - setSearch(searchValue); - }} - onClear={() => setSearch('')} - style={{ minWidth: '200px' }} - data-testid="model-versions-table-search" - /> - - - - - - - - setIsArchivedModelVersionKebabOpen(false)} - onOpenChange={(isOpen: boolean) => setIsArchivedModelVersionKebabOpen(isOpen)} - toggle={(tr: React.Ref) => ( - - setIsArchivedModelVersionKebabOpen(!isArchivedModelVersionKebabOpen) - } - isExpanded={isArchivedModelVersionKebabOpen} - aria-label="View archived versions" + <> + {isArchiveModel && ( + + )} + setSearch('')} + modelVersions={sortModelVersionsByCreateTime(filteredModelVersions)} + toolbarContent={ + + } breakpoint="xl"> + + setSearch('')} + deleteChipGroup={() => setSearch('')} + categoryName={searchType} > - - - )} - shouldFocusToggleOnSelect - > - - - navigate(modelVersionArchiveUrl(rm?.id, preferredModelRegistry?.metadata.name)) - } - > - View archived versions - - - - - - } - /> + ({ + key, + label: key, + }))} + value={searchType} + onChange={(newSearchType) => { + const enumMember = asEnumMember(newSearchType, SearchType); + if (enumMember !== null) { + setSearchType(enumMember); + } + }} + icon={} + /> + + + { + setSearch(searchValue); + }} + onClear={() => setSearch('')} + style={{ minWidth: '200px' }} + data-testid="model-versions-table-search" + /> + + + + {!isArchiveModel && ( + <> + + + + + setIsArchivedModelVersionKebabOpen(false)} + onOpenChange={(isOpen: boolean) => setIsArchivedModelVersionKebabOpen(isOpen)} + toggle={(tr: React.Ref) => ( + + setIsArchivedModelVersionKebabOpen(!isArchivedModelVersionKebabOpen) + } + isExpanded={isArchivedModelVersionKebabOpen} + aria-label="View archived versions" + > + + + )} + shouldFocusToggleOnSelect + > + + + navigate( + modelVersionArchiveUrl(rm?.id, preferredModelRegistry?.metadata.name), + ) + } + > + View archived versions + + + + + + )} + + } + /> + ); }; diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersions.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersions.tsx index b86fa15fdf..283b37957f 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersions.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersions.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { useParams } from 'react-router'; +import React, { useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router'; import { Breadcrumb, BreadcrumbItem, Truncate } from '@patternfly/react-core'; import { Link } from 'react-router-dom'; import ApplicationsPage from '~/pages/ApplicationsPage'; @@ -7,6 +7,8 @@ import useModelVersionsByRegisteredModel from '~/concepts/modelRegistry/apiHooks import useRegisteredModelById from '~/concepts/modelRegistry/apiHooks/useRegisteredModelById'; import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; import { filterLiveVersions } from '~/concepts/modelRegistry/utils'; +import { ModelState } from '~/concepts/modelRegistry/types'; +import { registeredModelArchiveDetailsUrl } from '~/pages/modelRegistry/screens/routeUtils'; import ModelVersionsTabs from './ModelVersionsTabs'; import ModelVersionsHeaderActions from './ModelVersionsHeaderActions'; import { ModelVersionsTab } from './const'; @@ -25,6 +27,13 @@ const ModelVersions: React.FC = ({ tab, ...pageProps }) => { const [rm, rmLoaded, rmLoadError, rmRefresh] = useRegisteredModelById(rmId); const loadError = mvLoadError || rmLoadError; const loaded = mvLoaded && rmLoaded; + const navigate = useNavigate(); + + useEffect(() => { + if (rm?.state === ModelState.ARCHIVED) { + navigate(registeredModelArchiveDetailsUrl(rm.id, preferredModelRegistry?.metadata.name)); + } + }, [rm?.state, rm?.id, preferredModelRegistry?.metadata.name, navigate]); return ( void; modelVersions: ModelVersion[]; + isArchiveModel?: boolean; refresh: () => void; } & Partial, 'toolbarContent'>>; @@ -15,6 +16,7 @@ const ModelVersionsTable: React.FC = ({ clearFilters, modelVersions, toolbarContent, + isArchiveModel, refresh, }) => ( = ({ enablePagination emptyTableView={} rowRenderer={(mv) => ( - + )} /> ); diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionsTableRow.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionsTableRow.tsx index fc97221cb6..88e7fe3a11 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionsTableRow.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionsTableRow.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { ActionsColumn, Td, Tr } from '@patternfly/react-table'; +import { ActionsColumn, IAction, Td, Tr } from '@patternfly/react-table'; import { Text, TextVariants, Truncate, FlexItem } from '@patternfly/react-core'; import { Link, useNavigate } from 'react-router-dom'; import { ModelVersion, ModelState } from '~/concepts/modelRegistry/types'; @@ -7,6 +7,7 @@ import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/M import ModelLabels from '~/pages/modelRegistry/screens/components/ModelLabels'; import ModelTimestamp from '~/pages/modelRegistry/screens/components/ModelTimestamp'; import { + archiveModelVersionDetailsUrl, modelVersionArchiveDetailsUrl, modelVersionDeploymentsUrl, modelVersionUrl, @@ -19,12 +20,14 @@ import DeployRegisteredModelModal from '~/pages/modelRegistry/screens/components type ModelVersionsTableRowProps = { modelVersion: ModelVersion; isArchiveRow?: boolean; + isArchiveModel?: boolean; refresh: () => void; }; const ModelVersionsTableRow: React.FC = ({ modelVersion: mv, isArchiveRow, + isArchiveModel, refresh, }) => { const navigate = useNavigate(); @@ -34,7 +37,7 @@ const ModelVersionsTableRow: React.FC = ({ const [isDeployModalOpen, setIsDeployModalOpen] = React.useState(false); const { apiState } = React.useContext(ModelRegistryContext); - const actions = isArchiveRow + const actions: IAction[] = isArchiveRow ? [ { title: 'Restore version', @@ -59,7 +62,13 @@ const ModelVersionsTableRow: React.FC = ({ = ({ - + } + onCancel={() => setIsDeployModalOpen(false)} + isOpen={isDeployModalOpen} + modelVersion={mv} + /> + setIsRestoreModalOpen(false)} + onSubmit={() => + apiState.api + .patchModelVersion( + {}, + { + state: ModelState.LIVE, + }, + mv.id, + ) + .then(() => + navigate( + modelVersionUrl( + mv.id, + mv.registeredModelId, + preferredModelRegistry?.metadata.name, + ), + ), + ) + } + isOpen={isRestoreModalOpen} + modelVersionName={mv.name} + /> + + )} ); }; diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionsTabs.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionsTabs.tsx index 562c9c2068..6eaaf75df0 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionsTabs.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersions/ModelVersionsTabs.tsx @@ -11,6 +11,7 @@ type ModelVersionsTabProps = { tab: ModelVersionsTab; registeredModel: RegisteredModel; modelVersions: ModelVersion[]; + isArchiveModel?: boolean; refresh: () => void; mvRefresh: () => void; }; @@ -20,6 +21,7 @@ const ModelVersionsTabs: React.FC = ({ registeredModel: rm, modelVersions, refresh, + isArchiveModel, mvRefresh, }) => { const navigate = useNavigate(); @@ -39,6 +41,7 @@ const ModelVersionsTabs: React.FC = ({ > = ({ data-testid="model-details-tab" > - + diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetails.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetails.tsx new file mode 100644 index 0000000000..69a5344231 --- /dev/null +++ b/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetails.tsx @@ -0,0 +1,92 @@ +import React, { useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router'; +import { Button, Flex, FlexItem, Label, Text, Tooltip, Truncate } from '@patternfly/react-core'; +import ApplicationsPage from '~/pages/ApplicationsPage'; +import useModelVersionById from '~/concepts/modelRegistry/apiHooks/useModelVersionById'; +import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; +import useRegisteredModelById from '~/concepts/modelRegistry/apiHooks/useRegisteredModelById'; +import { ModelVersionDetailsTab } from '~/pages/modelRegistry/screens/ModelVersionDetails/const'; +import ModelVersionDetailsTabs from '~/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetailsTabs'; +import useInferenceServices from '~/pages/modelServing/useInferenceServices'; +import useServingRuntimes from '~/pages/modelServing/useServingRuntimes'; +import { useMakeFetchObject } from '~/utilities/useMakeFetchObject'; +import { ModelState } from '~/concepts/modelRegistry/types'; +import { modelVersionUrl } from '~/pages/modelRegistry/screens/routeUtils'; +import ArchiveModelVersionDetailsBreadcrumb from './ArchiveModelVersionDetailsBreadcrumb'; + +type ArchiveModelVersionDetailsProps = { + tab: ModelVersionDetailsTab; +} & Omit< + React.ComponentProps, + 'breadcrumb' | 'title' | 'description' | 'loadError' | 'loaded' | 'provideChildrenPadding' +>; + +const ArchiveModelVersionDetails: React.FC = ({ + tab, + ...pageProps +}) => { + const { preferredModelRegistry } = React.useContext(ModelRegistrySelectorContext); + const { modelVersionId: mvId, registeredModelId: rmId } = useParams(); + const [rm] = useRegisteredModelById(rmId); + const [mv, mvLoaded, mvLoadError, refreshModelVersion] = useModelVersionById(mvId); + const inferenceServices = useMakeFetchObject( + useInferenceServices(undefined, mv?.registeredModelId, mv?.id), + ); + const navigate = useNavigate(); + const servingRuntimes = useMakeFetchObject(useServingRuntimes()); + + useEffect(() => { + if (rm?.state === ModelState.LIVE && mv?.id) { + navigate(modelVersionUrl(mv.id, mv.registeredModelId, preferredModelRegistry?.metadata.name)); + } + }, [rm?.state, mv?.id, mv?.registeredModelId, preferredModelRegistry?.metadata.name, navigate]); + + return ( + + } + title={ + mv && ( + + + {mv.name} + + + + + + ) + } + headerAction={ + + + + } + description={} + loadError={mvLoadError} + loaded={mvLoaded} + provideChildrenPadding + > + {mv !== null && ( + + )} + + ); +}; + +export default ArchiveModelVersionDetails; diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetailsBreadcrumb.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetailsBreadcrumb.tsx new file mode 100644 index 0000000000..15fdcaf124 --- /dev/null +++ b/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ArchiveModelVersionDetailsBreadcrumb.tsx @@ -0,0 +1,41 @@ +import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { RegisteredModel } from '~/concepts/modelRegistry/types'; +import { + registeredModelArchiveDetailsUrl, + registeredModelArchiveUrl, +} from '~/pages/modelRegistry/screens/routeUtils'; + +type ArchiveModelVersionDetailsBreadcrumbProps = { + preferredModelRegistry?: string; + registeredModel: RegisteredModel | null; + modelVersionName?: string; +}; + +const ArchiveModelVersionDetailsBreadcrumb: React.FC = ({ + preferredModelRegistry, + registeredModel, + modelVersionName, +}) => ( + + Model registry - {preferredModelRegistry}} + /> + ( + Archived models + )} + /> + ( + + {registeredModel?.name || 'Loading...'} + + )} + /> + {modelVersionName || 'Loading...'} + +); + +export default ArchiveModelVersionDetailsBreadcrumb; diff --git a/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionArchiveDetails.tsx b/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionArchiveDetails.tsx index b522672092..8fee6dde10 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionArchiveDetails.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelVersionsArchive/ModelVersionArchiveDetails.tsx @@ -1,10 +1,13 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router'; import { Button, Flex, FlexItem, Label, Text, Truncate } from '@patternfly/react-core'; import ApplicationsPage from '~/pages/ApplicationsPage'; import useModelVersionById from '~/concepts/modelRegistry/apiHooks/useModelVersionById'; import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; -import { modelVersionUrl } from '~/pages/modelRegistry/screens/routeUtils'; +import { + archiveModelVersionDetailsUrl, + modelVersionUrl, +} from '~/pages/modelRegistry/screens/routeUtils'; import useRegisteredModelById from '~/concepts/modelRegistry/apiHooks/useRegisteredModelById'; import { ModelVersionDetailsTab } from '~/pages/modelRegistry/screens/ModelVersionDetails/const'; import ModelVersionDetailsTabs from '~/pages/modelRegistry/screens/ModelVersionDetails/ModelVersionDetailsTabs'; @@ -41,6 +44,27 @@ const ModelVersionsArchiveDetails: React.FC = ); const servingRuntimes = useMakeFetchObject(useServingRuntimes()); + useEffect(() => { + if (rm?.state === ModelState.ARCHIVED && mv?.id) { + navigate( + archiveModelVersionDetailsUrl( + mv.id, + mv.registeredModelId, + preferredModelRegistry?.metadata.name, + ), + ); + } else if (mv?.state === ModelState.LIVE) { + navigate(modelVersionUrl(mv.id, mv.registeredModelId, preferredModelRegistry?.metadata.name)); + } + }, [ + rm?.state, + mv?.state, + mv?.id, + mv?.registeredModelId, + preferredModelRegistry?.metadata.name, + navigate, + ]); + return ( <> = > {mv !== null && ( = ({ const [isRestoreModalOpen, setIsRestoreModalOpen] = React.useState(false); const rmUrl = registeredModelUrl(rm.id, preferredModelRegistry?.metadata.name); - const actions = [ + const actions: IAction[] = [ { title: 'View details', - onClick: () => navigate(`${rmUrl}/${ModelVersionsTab.DETAILS}`), + onClick: () => { + if (isArchiveRow) { + navigate( + `${registeredModelArchiveUrl(preferredModelRegistry?.metadata.name)}/${rm.id}/${ + ModelVersionsTab.DETAILS + }`, + ); + } else { + navigate(`${rmUrl}/${ModelVersionsTab.DETAILS}`); + } + }, }, isArchiveRow ? { diff --git a/frontend/src/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelArchiveDetails.tsx b/frontend/src/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelArchiveDetails.tsx index d75bfd17bb..5b718dc1b9 100644 --- a/frontend/src/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelArchiveDetails.tsx +++ b/frontend/src/pages/modelRegistry/screens/RegisteredModelsArchive/RegisteredModelArchiveDetails.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router'; import { Button, Flex, FlexItem, Label, Text, Truncate } from '@patternfly/react-core'; import ApplicationsPage from '~/pages/ApplicationsPage'; @@ -34,6 +34,12 @@ const RegisteredModelsArchiveDetails: React.FC { + if (rm?.state === ModelState.LIVE) { + navigate(registeredModelUrl(rm.id, preferredModelRegistry?.metadata.name)); + } + }, [rm?.state, preferredModelRegistry?.metadata.name, rm?.id, navigate]); + return ( <> )} + {rm !== null && ( setIsRestoreModalOpen(false)} diff --git a/frontend/src/pages/modelRegistry/screens/routeUtils.ts b/frontend/src/pages/modelRegistry/screens/routeUtils.ts index e7ec95efe8..358f30b825 100644 --- a/frontend/src/pages/modelRegistry/screens/routeUtils.ts +++ b/frontend/src/pages/modelRegistry/screens/routeUtils.ts @@ -18,6 +18,11 @@ export const registeredModelArchiveDetailsUrl = ( export const modelVersionListUrl = (rmId?: string, preferredModelRegistry?: string): string => `${registeredModelUrl(rmId, preferredModelRegistry)}/versions`; +export const archiveModelVersionListUrl = ( + rmId?: string, + preferredModelRegistry?: string, +): string => `${registeredModelArchiveDetailsUrl(rmId, preferredModelRegistry)}/versions`; + export const modelVersionUrl = ( mvId: string, rmId?: string, @@ -27,6 +32,12 @@ export const modelVersionUrl = ( export const modelVersionArchiveUrl = (rmId?: string, preferredModelRegistry?: string): string => `${modelVersionListUrl(rmId, preferredModelRegistry)}/archive`; +export const archiveModelVersionDetailsUrl = ( + mvId: string, + rmId?: string, + preferredModelRegistry?: string, +): string => `${archiveModelVersionListUrl(rmId, preferredModelRegistry)}/${mvId}`; + export const modelVersionArchiveDetailsUrl = ( mvId: string, rmId?: string,
- - setIsArchiveModalOpen(false)} - onSubmit={() => - apiState.api - .patchModelVersion( - {}, - { - state: ModelState.ARCHIVED, - }, - mv.id, - ) - .then(refresh) - } - isOpen={isArchiveModalOpen} - modelVersionName={mv.name} - /> - - navigate( - modelVersionDeploymentsUrl( - mv.id, - mv.registeredModelId, - preferredModelRegistry?.metadata.name, - ), - ) - } - onCancel={() => setIsDeployModalOpen(false)} - isOpen={isDeployModalOpen} - modelVersion={mv} - /> - setIsRestoreModalOpen(false)} - onSubmit={() => - apiState.api - .patchModelVersion( - {}, - { - state: ModelState.LIVE, - }, - mv.id, - ) - .then(() => - navigate( - modelVersionUrl( - mv.id, - mv.registeredModelId, - preferredModelRegistry?.metadata.name, - ), + {!isArchiveModel && ( + + + setIsArchiveModalOpen(false)} + onSubmit={() => + apiState.api + .patchModelVersion( + {}, + { + state: ModelState.ARCHIVED, + }, + mv.id, + ) + .then(refresh) + } + isOpen={isArchiveModalOpen} + modelVersionName={mv.name} + /> + + navigate( + modelVersionDeploymentsUrl( + mv.id, + mv.registeredModelId, + preferredModelRegistry?.metadata.name, ), ) - } - isOpen={isRestoreModalOpen} - modelVersionName={mv.name} - /> -