diff --git a/frontend/src/__mocks__/mockModelRegistryService.ts b/frontend/src/__mocks__/mockModelRegistryService.ts index c90b17e6dc..1814e4d039 100644 --- a/frontend/src/__mocks__/mockModelRegistryService.ts +++ b/frontend/src/__mocks__/mockModelRegistryService.ts @@ -3,17 +3,26 @@ import { ServiceKind } from '~/k8sTypes'; type MockServiceType = { name?: string; namespace?: string; + description?: string; + serverUrl?: string; }; export const mockModelRegistryService = ({ name = 'modelregistry-sample', namespace = 'odh-model-registries', + description = 'Model registry description', + serverUrl = 'modelregistry-sample-rest.com:443', }: MockServiceType): ServiceKind => ({ kind: 'Service', apiVersion: 'v1', metadata: { name, namespace, + annotations: { + 'openshift.io/description': description, + 'openshift.io/display-name': name, + 'routing.opendatahub.io/external-address-rest': serverUrl, + }, }, spec: { selector: { diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts index d849603c11..b1ca58da86 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts @@ -95,6 +95,22 @@ class ModelRegistry { cy.findByTestId('empty-registered-models').should('exist'); } + findViewDetailsButton() { + return cy.findByTestId('view-details-button'); + } + + findDetailsPopover() { + return cy.findByTestId('mr-details-popover'); + } + + findHelpContentButton() { + return cy.findByTestId('model-registry-help-button'); + } + + findHelpContentPopover() { + return cy.findByTestId('model-registry-help-content'); + } + shouldmodelVersionsEmpty() { cy.findByTestId('empty-model-versions').should('exist'); } diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts index cbe875c99d..3c71d6f289 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts @@ -38,7 +38,11 @@ const initIntercepts = ({ disableModelRegistryFeature = false, modelRegistries = [ mockModelRegistryService({ name: 'modelregistry-sample' }), - mockModelRegistryService({ name: 'modelregistry-sample-2' }), + mockModelRegistryService({ + name: 'modelregistry-sample-2', + serverUrl: 'modelregistry-sample-2-rest.com:443', + description: '', + }), ], registeredModels = [ mockRegisteredModel({ @@ -228,6 +232,34 @@ describe('Model Registry core', () => { modelRegistry.navigate(); modelRegistry.shouldModelRegistrySelectorExist(); modelRegistry.shouldregisteredModelsEmpty(); + + modelRegistry.findViewDetailsButton().click(); + modelRegistry.findDetailsPopover().should('exist'); + modelRegistry.findDetailsPopover().findByText('Model registry description').should('exist'); + modelRegistry + .findDetailsPopover() + .findByText('https://modelregistry-sample-rest.com:443') + .should('exist'); + + // Model registry with no description + modelRegistry.findModelRegistry().findSelectOption('modelregistry-sample-2').click(); + modelRegistry.findViewDetailsButton().click(); + modelRegistry.findDetailsPopover().should('exist'); + modelRegistry.findDetailsPopover().findByText('No description').should('exist'); + modelRegistry + .findDetailsPopover() + .findByText('https://modelregistry-sample-2-rest.com:443') + .should('exist'); + + // Model registry help content + modelRegistry.findHelpContentButton().click(); + modelRegistry.findHelpContentPopover().should('exist'); + modelRegistry + .findHelpContentPopover() + .findByText( + 'To request access to a new or existing model registry, contact your administrator.', + ) + .should('exist'); }); describe('Registered model table', () => { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersions.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersions.cy.ts index 54ea2607c0..e08a3dd1ec 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersions.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelVersions.cy.ts @@ -179,7 +179,10 @@ describe('Model Versions', () => { }); modelRegistry.visit(); - modelRegistry.findModelRegistry().findSelectOption('modelregistry-sample').click(); + modelRegistry + .findModelRegistry() + .findSelectOption('modelregistry-sample Model registry description') + .click(); cy.reload(); const registeredModelRow = modelRegistry.getRow('Fraud detection model'); registeredModelRow.findName().contains('Fraud detection model').click(); diff --git a/frontend/src/components/PopoverListContent.tsx b/frontend/src/components/PopoverListContent.tsx index 6526597e0f..00c877db50 100644 --- a/frontend/src/components/PopoverListContent.tsx +++ b/frontend/src/components/PopoverListContent.tsx @@ -28,7 +28,7 @@ const PopoverListContent: React.FC = ({ {leadText ? {leadText} : null} {listHeading ? {listHeading} : null} - + {listItems.map((item, index) => ( {item} diff --git a/frontend/src/components/WhosMyAdministrator.tsx b/frontend/src/components/WhosMyAdministrator.tsx index 4ca7dae516..7b74d1e8fb 100644 --- a/frontend/src/components/WhosMyAdministrator.tsx +++ b/frontend/src/components/WhosMyAdministrator.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Button, Popover } from '@patternfly/react-core'; +import { Button, Popover, PopoverPosition } from '@patternfly/react-core'; import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; import PopoverListContent from '~/components/PopoverListContent'; import { FindAdministratorOptions } from '~/pages/projects/screens/projects/const'; @@ -11,6 +11,7 @@ type Props = { isInline?: boolean; contentTestId?: string; linkTestId?: string; + popoverPosition?: PopoverPosition; }; const WhosMyAdministrator: React.FC = ({ @@ -20,10 +21,11 @@ const WhosMyAdministrator: React.FC = ({ isInline, contentTestId, linkTestId, + popoverPosition = PopoverPosition.bottom, }) => ( ; name: string; namespace: string; labels?: Partial<{ diff --git a/frontend/src/pages/modelRegistry/screens/ModelRegistrySelector.tsx b/frontend/src/pages/modelRegistry/screens/ModelRegistrySelector.tsx index a07c0f7c55..1a3d35240c 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelRegistrySelector.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelRegistrySelector.tsx @@ -10,8 +10,10 @@ import { FlexItem, Icon, Popover, + PopoverPosition, Tooltip, } from '@patternfly/react-core'; +import text from '@patternfly/react-styles/css/utilities/Text/text'; import truncateStyles from '@patternfly/react-styles/css/components/Truncate/truncate'; import { InfoCircleIcon, BlueprintIcon } from '@patternfly/react-icons'; import { useBrowserStorage } from '~/components/browserStorage'; @@ -19,6 +21,9 @@ import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/M import { getDescriptionFromK8sResource, getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; import { ServiceKind } from '~/k8sTypes'; import SimpleSelect, { SimpleSelectOption } from '~/components/SimpleSelect'; +import WhosMyAdministrator from '~/components/WhosMyAdministrator'; +import InlineTruncatedClipboardCopy from '~/components/InlineTruncatedClipboardCopy'; +import { getServerAddress } from './utils'; const MODEL_REGISTRY_FAVORITE_STORAGE_KEY = 'odh.dashboard.model.registry.favorite'; @@ -133,29 +138,62 @@ const ModelRegistrySelector: React.FC = ({ } return ( - - - - - + + + + + + + + Model registry + + {selector} + {selection && ( - Model registry + + + Description + + {getDescriptionFromK8sResource(selection) || 'No description'} + + + + Server URL + + + + + + } + > + + - {selector} - {selection && getDescriptionFromK8sResource(selection) && ( - - - - - - )} - + )} + + + ); }; diff --git a/frontend/src/pages/modelRegistry/screens/utils.ts b/frontend/src/pages/modelRegistry/screens/utils.ts index 3c675c174d..7867a64276 100644 --- a/frontend/src/pages/modelRegistry/screens/utils.ts +++ b/frontend/src/pages/modelRegistry/screens/utils.ts @@ -6,6 +6,7 @@ import { ModelVersion, RegisteredModel, } from '~/concepts/modelRegistry/types'; +import { ServiceKind } from '~/k8sTypes'; import { KeyValuePair } from '~/types'; // Retrieves the labels from customProperties that have non-empty string_value. @@ -153,3 +154,6 @@ export const filterRegisteredModels = ( } }); }; + +export const getServerAddress = (resource: ServiceKind): string => + resource.metadata.annotations?.['routing.opendatahub.io/external-address-rest'] || '';