diff --git a/README.md b/README.md index ef8f66a11..37ae95883 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Model registry provides a central repository for model developers to store and m - [playground](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/kubeflow/model-registry/main/api/openapi/model-registry.yaml) - [license scanning](https://github.com/kubeflow/model-registry/issues/323) - [monitoring image quality](https://github.com/kubeflow/model-registry/issues/327) +8. [UI](.clients/ui/README.md) ## Pre-requisites: - go >= 1.21 diff --git a/clients/ui/Makefile b/clients/ui/Makefile new file mode 100644 index 000000000..addcf8986 --- /dev/null +++ b/clients/ui/Makefile @@ -0,0 +1,32 @@ +CONTAINER_TOOL ?= docker + +.PHONY: all +all: build + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +.PHONY: dev-install-dependencies +dev-install-dependencies: + cd frontend && npm install + +.PHONY: dev-bff +dev-bff: + cd bff && make run PORT=4000 MOCK_K8S_CLIENT=true MOCK_MR_CLIENT=true + +.PHONY: dev-frontend +dev-frontend: + cd frontend && npm run start:dev + +.PHONY: dev-start +dev-start: + make -j 2 dev-bff dev-frontend + +.PHONY: docker-compose +docker-compose: + $(CONTAINER_TOOL) compose -f docker-compose.yaml up + +.PHONY: kind-deployment +kind-deployment: + ./scripts/deploy_kind_cluster.sh diff --git a/clients/ui/README.md b/clients/ui/README.md new file mode 100644 index 000000000..fe7b80a69 --- /dev/null +++ b/clients/ui/README.md @@ -0,0 +1,48 @@ +[frontend requirements]: ./frontend/docs/dev-setup.md#requirements +[BFF requirements]: ./bff/README.md#pre-requisites +[frontend dev setup]: ./frontend/docs/dev-setup.md#development +[BFF dev setup]: ./bff/README.md#development + +# Model Registry UI + +## Overview + +The Model Registry UI is a standalone web app for Kubeflow Model Registry. In this repository, you will find the frontend and backend for the Model Registry UI. + +## Prerequisites + +* [Frontend requirements] +* [BFF requirements] + +## Set Up + +### Development + +To run the a mocked dev environment you can either: + +* Use the makefile command to install dependencies `make dev-install-dependencies` and then start the dev environment `make dev-start`. + +* Or follow the [frontend dev setup] and [BFF dev setup]. + +### Docker deployment + +To build the Model Registry UI container, run the following command: + +```shell +make docker-compose +``` + +### Kubernetes Deployment + +For a in-depth guide on how to deploy the Model Registry UI, please refer to the [local kubernetes deployment](./bff/docs/dev-guide.md) documentation. + +To quickly enable the Model Registry UI in your Kind cluster, you can use the following command: + +```shell +make kind-deployment +``` + +## OpenAPI Specification + +You can find the OpenAPI specification for the Model Registry UI in the [openapi](./api/openapi) directory. +A live version of the OpenAPI specification can be found [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/kubeflow/model-registry/main/clients/ui/api/openapi/mod-arch.yaml). diff --git a/clients/ui/api/openapi/mod-arch.yaml b/clients/ui/api/openapi/mod-arch.yaml index 3479de762..d17494682 100644 --- a/clients/ui/api/openapi/mod-arch.yaml +++ b/clients/ui/api/openapi/mod-arch.yaml @@ -86,7 +86,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ModelVersionUpdate" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ModelVersionUpdate" required: true tags: - ModelRegistryService @@ -141,7 +147,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/RegisteredModelCreate" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/RegisteredModelCreate" required: true tags: - ModelRegistryService @@ -186,7 +198,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/RegisteredModelUpdate" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/RegisteredModelUpdate" required: true tags: - ModelRegistryService @@ -244,7 +262,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Artifact" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/Artifact" required: true tags: - ModelRegistryService @@ -305,7 +329,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ModelVersion" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ModelVersion" required: true tags: - ModelRegistryService @@ -972,99 +1002,195 @@ components: content: application/json: schema: - $ref: "#/components/schemas/Config" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/Config" description: A response containing a list of ModelArtifact entities. ModelRegistryRespone: content: application/json: schema: - type: array - items: - $ref: "#/components/schemas/ModelRegistry" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + type: array + items: + $ref: "#/components/schemas/ModelRegistry" description: A response containing a list of ModelArtifact entities. ModelArtifactListResponse: content: application/json: schema: - $ref: "#/components/schemas/ModelArtifactList" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ModelArtifactList" description: A response containing a list of ModelArtifact entities. ModelArtifactResponse: content: application/json: schema: - $ref: "#/components/schemas/ModelArtifact" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ModelArtifact" description: A response containing a `ModelArtifact` entity. ModelVersionListResponse: content: application/json: schema: - $ref: "#/components/schemas/ModelVersionList" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ModelVersionList" description: A response containing a list of `ModelVersion` entities. ModelVersionResponse: content: application/json: schema: - $ref: "#/components/schemas/ModelVersion" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ModelVersion" description: A response containing a `ModelVersion` entity. RegisteredModelListResponse: content: application/json: schema: - $ref: "#/components/schemas/RegisteredModelList" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/RegisteredModelList" description: A response containing a list of `RegisteredModel` entities. RegisteredModelResponse: content: application/json: schema: - $ref: "#/components/schemas/RegisteredModel" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/RegisteredModel" description: A response containing a `RegisteredModel` entity. ArtifactResponse: content: application/json: schema: - $ref: "#/components/schemas/Artifact" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/Artifact" description: A response containing an `Artifact` entity. ArtifactListResponse: content: application/json: schema: - $ref: "#/components/schemas/ArtifactList" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ArtifactList" description: A response containing a list of `Artifact` entities. ServingEnvironmentListResponse: content: application/json: schema: - $ref: "#/components/schemas/ServingEnvironmentList" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ServingEnvironmentList" description: A response containing a list of `ServingEnvironment` entities. ServingEnvironmentResponse: content: application/json: schema: - $ref: "#/components/schemas/ServingEnvironment" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ServingEnvironment" description: A response containing a `ServingEnvironment` entity. InferenceServiceListResponse: content: application/json: schema: - $ref: "#/components/schemas/InferenceServiceList" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/InferenceServiceList" description: A response containing a list of `InferenceService` entities. InferenceServiceResponse: content: application/json: schema: - $ref: "#/components/schemas/InferenceService" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/InferenceService" description: A response containing a `InferenceService` entity. ServeModelListResponse: content: application/json: schema: - $ref: "#/components/schemas/ServeModelList" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ServeModelList" description: A response containing a list of `ServeModel` entities. ServeModelResponse: content: application/json: schema: - $ref: "#/components/schemas/ServeModel" + type: object + properties: + metadata: + type: object + description: Metadata about the response + data: + $ref: "#/components/schemas/ServeModel" description: A response containing a `ServeModel` entity. parameters: modelRegistryName: diff --git a/clients/ui/docker-compose.yaml b/clients/ui/docker-compose.yaml index 6c5f4bd74..7fbc2c8ca 100644 --- a/clients/ui/docker-compose.yaml +++ b/clients/ui/docker-compose.yaml @@ -5,7 +5,7 @@ services: ports: - 8080:8080 environment: - API_URL: http://model-registry-bff:4001 + API_URL: http://model-registry-bff:4000 networks: - model_registry depends_on: @@ -15,6 +15,7 @@ services: container_name: model-registry-bff command: - "--mock-k8s-client=true" + - "--mock-mr-client=true" networks: - model_registry diff --git a/clients/ui/frontend/Dockerfile b/clients/ui/frontend/Dockerfile index 0e787fdee..c25a2b1c6 100644 --- a/clients/ui/frontend/Dockerfile +++ b/clients/ui/frontend/Dockerfile @@ -10,7 +10,7 @@ RUN npm run build FROM nginxinc/nginx-unprivileged -ENV API_URL="http://localhost:4001" +ENV API_URL="http://localhost:4000" ENV NGINX_ENVSUBST_FILTER="API_URL" COPY --from=build-stage /usr/src/app/dist/ "/usr/share/nginx/html" diff --git a/clients/ui/frontend/README.md b/clients/ui/frontend/README.md index c8f062d7d..63ca41ba7 100644 --- a/clients/ui/frontend/README.md +++ b/clients/ui/frontend/README.md @@ -7,7 +7,7 @@ The Kubeflow Model Registry UI is a standalone web app for Kubeflow Model Registry. -## Contributing: +## Contributing Individual bug fixes are welcome, it is recommended that you create a bug [issue] at the same time to describe the fix you're applying. If you are unsure how best to solve it, start with the issue and note your desire to contribute. diff --git a/clients/ui/frontend/docs/dev-setup.md b/clients/ui/frontend/docs/dev-setup.md index 327a40577..2fe6e9653 100644 --- a/clients/ui/frontend/docs/dev-setup.md +++ b/clients/ui/frontend/docs/dev-setup.md @@ -8,10 +8,6 @@ This project requires the following tools to be installed on your system: - Node recommended version -> `20.17.0` - NPM recommended version -> `10.8.2` -### Additional tooling - -[TBD] - ## Development 1. Clone the repository diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/appChrome.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/appChrome.ts index 8d30c9a48..5d6afc32d 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/appChrome.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/appChrome.ts @@ -9,7 +9,7 @@ class AppChrome { cy.testA11y(); } - // TODO: implement when authorization is enabled + // TODO: [Auth-enablement] Uncomment once auth is enabled // shouldBeUnauthorized() { // cy.findByTestId('unauthorized-error'); // return this; diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/support/commands/application.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/support/commands/application.ts index 7b5de43ea..c6100d0cd 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/support/commands/application.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/support/commands/application.ts @@ -7,7 +7,7 @@ import type { Matcher, MatcherOptions as DTLMatcherOptions } from '@testing-libr declare global { namespace Cypress { interface Chainable { - // TODO: Uncomment when authorization is enabled + // TODO: [Auth-enablement] Uncomment once auth is enabled // /** // * Visits the URL and performs a login if necessary. // * Uses credentials supplied by environment variables if not provided. @@ -121,7 +121,7 @@ declare global { } } -// TODO: Uncomment when authorization is enabled +// TODO: [Auth-enablement] Uncomment once auth is enabled // Cypress.Commands.add('visitWithLogin', (url, user = TEST_USER) => { // if (Cypress.env('MOCK')) { // cy.visit(url); diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts index dc9940b24..e6cb531c3 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts @@ -205,35 +205,22 @@ describe('Model Registry core', () => { }); }); -// TODO: Enable when model registration is there -// describe('Register Model button', () => { -// it('Navigates to register page from empty state', () => { -// initIntercepts({ disableModelRegistryFeature: false, registeredModels: [] }); -// modelRegistry.visit(); -// modelRegistry.findRegisterModelButton().click(); -// cy.findByTestId('app-page-title').should('exist'); -// cy.findByTestId('app-page-title').contains('Register model'); -// cy.findByText('Model registry - modelregistry-sample').should('exist'); -// }); - -// it('Navigates to register page from table toolbar', () => { -// initIntercepts({ disableModelRegistryFeature: false }); -// modelRegistry.visit(); -// modelRegistry.findRegisterModelButton().click(); -// cy.findByTestId('app-page-title').should('exist'); -// cy.findByTestId('app-page-title').contains('Register model'); -// cy.findByText('Model registry - modelregistry-sample').should('exist'); -// }); - -// it('should be accessible for non-admin users', () => { -// asProjectEditUser(); -// initIntercepts({ -// disableModelRegistryFeature: false, -// allowed: false, -// }); - -// modelRegistry.visit(); -// modelRegistry.navigate(); -// modelRegistry.shouldModelRegistrySelectorExist(); -// }); -// }); +describe('Register Model button', () => { + it('Navigates to register page from empty state', () => { + initIntercepts({ registeredModels: [] }); + modelRegistry.visit(); + modelRegistry.findRegisterModelButton().click(); + cy.findByTestId('app-page-title').should('exist'); + cy.findByTestId('app-page-title').contains('Register model'); + cy.findByText('Model registry - modelregistry-sample').should('exist'); + }); + + it('Navigates to register page from table toolbar', () => { + initIntercepts({ registeredModels: [] }); + modelRegistry.visit(); + modelRegistry.findRegisterModelButton().click(); + cy.findByTestId('app-page-title').should('exist'); + cy.findByTestId('app-page-title').contains('Register model'); + cy.findByText('Model registry - modelregistry-sample').should('exist'); + }); +}); diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersions.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersions.cy.ts index 3aa7f9c50..c3fd860e2 100644 --- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersions.cy.ts +++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/modelVersions.cy.ts @@ -130,7 +130,7 @@ describe('Model Versions', () => { }); it('Model versions table', () => { - // TODO: Uncomment when we fix finding listbox items + // TODO: [Testing] Uncomment when we fix finding listbox items initIntercepts({ modelRegistries: [ diff --git a/clients/ui/frontend/src/app/App.tsx b/clients/ui/frontend/src/app/App.tsx index 430a9ff07..494ee459e 100644 --- a/clients/ui/frontend/src/app/App.tsx +++ b/clients/ui/frontend/src/app/App.tsx @@ -19,11 +19,11 @@ import { StackItem, } from '@patternfly/react-core'; import { BarsIcon } from '@patternfly/react-icons'; -import ToastNotifications from '~/components/ToastNotifications'; +import ToastNotifications from '~/shared/components/ToastNotifications'; +import { useSettings } from '~/shared/hooks/useSettings'; import NavSidebar from './NavSidebar'; import AppRoutes from './AppRoutes'; import { AppContext } from './AppContext'; -import { useSettings } from './useSettings'; import { ModelRegistrySelectorContextProvider } from './context/ModelRegistrySelectorContext'; const App: React.FC = () => { @@ -62,7 +62,7 @@ const App: React.FC = () => {