diff --git a/.github/workflows/build-and-push-ui-images.yml b/.github/workflows/build-and-push-ui-images.yml new file mode 100644 index 00000000..b8c0d972 --- /dev/null +++ b/.github/workflows/build-and-push-ui-images.yml @@ -0,0 +1,65 @@ +name: Build and Push UI and BFF Images +on: + push: + branches: + - 'main' + tags: + - 'v*' + paths: + - 'clients/ui/**' +env: + IMG_ORG: kubeflow + IMG_UI_REPO: model-registry-ui + IMG_BFF_REPO: model-registry-bff + DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PWD: ${{ secrets.DOCKERHUB_TOKEN }} + PUSH_IMAGE: true +jobs: + build-image: + runs-on: ubuntu-latest + steps: + # Assign context variable for various action contexts (main, CI) + - name: Assigning main context + if: github.head_ref == '' && github.ref == 'refs/heads/main' + run: echo "BUILD_CONTEXT=main" >> $GITHUB_ENV + # checkout branch + - uses: actions/checkout@v4 + # set image version + - name: Set main-branch environment + if: env.BUILD_CONTEXT == 'main' + run: | + commit_sha=${{ github.event.after }} + tag=main-${commit_sha:0:7} + echo "VERSION=${tag}" >> $GITHUB_ENV + - name: Build and Push UI Image + shell: bash + env: + IMG_REPO: ${{ env.IMG_UI_REPO }} + run: ./scripts/build_deploy.sh + - name: Build and Push BFF Image + shell: bash + env: + IMG_REPO: ${{ env.IMG_BFF_REPO }} + run: ./scripts/build_deploy.sh + - name: Tag Latest UI Image + if: env.BUILD_CONTEXT == 'main' + shell: bash + env: + IMG_REPO: ${{ env.IMG_UI_REPO }} + IMG: ${{ env.IMG_ORG }}/${{ env.IMG_UI_REPO }} + BUILD_IMAGE: false # image is already built in "Build and Push UI Image" step + run: | + docker tag ${{ env.IMG }}:$VERSION ${{ env.IMG }}:latest + # BUILD_IMAGE=false skip the build, just push the tag made above + VERSION=latest ./scripts/build_deploy.sh + - name: Tag Latest BFF Image + if: env.BUILD_CONTEXT == 'main' + shell: bash + env: + IMG_REPO: ${{ env.IMG_BFF_REPO }} + IMG: ${{ env.IMG_ORG }}/${{ env.IMG_BFF_REPO }} + BUILD_IMAGE: false # image is already built in "Build and Push BFF Image" step + run: | + docker tag ${{ env.IMG }}:$VERSION ${{ env.IMG }}:latest + # BUILD_IMAGE=false skip the build, just push the tag made above + VERSION=latest ./scripts/build_deploy.sh \ No newline at end of file diff --git a/Makefile b/Makefile index 3081c1d0..72204844 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) PROJECT_PATH := $(patsubst %/,%,$(dir $(MKFILE_PATH))) PROJECT_BIN := $(PROJECT_PATH)/bin GO ?= "$(shell which go)" +BFF_PATH := $(PROJECT_PATH)/clients/ui/bff +UI_PATH := $(PROJECT_PATH)/clients/ui/frontend # add tools bin directory PATH := $(PROJECT_BIN):$(PATH) @@ -21,6 +23,8 @@ IMG_ORG ?= kubeflow IMG_VERSION ?= main # container image repository IMG_REPO ?= model-registry +# container image build path +BUILD_PATH ?= . # container image ifdef IMG_REGISTRY IMG := ${IMG_REGISTRY}/${IMG_ORG}/${IMG_REPO} @@ -28,6 +32,17 @@ else IMG := ${IMG_ORG}/${IMG_REPO} endif +# Change Dockerfile path depending on IMG_REPO +ifeq ($(IMG_REPO),model-registry-ui) + DOCKERFILE := $(UI_PATH)/Dockerfile + BUILD_PATH := $(UI_PATH) +endif + +ifeq ($(IMG_REPO),model-registry-bff) + DOCKERFILE := $(BFF_PATH)/Dockerfile + BUILD_PATH := $(BFF_PATH) +endif + model-registry: build # clean the ml-metadata protos and trigger a fresh new build which downloads @@ -216,7 +231,7 @@ endif # build docker image .PHONY: image/build image/build: - ${DOCKER} build . -f ${DOCKERFILE} -t ${IMG}:$(IMG_VERSION) + ${DOCKER} build ${BUILD_PATH} -f ${DOCKERFILE} -t ${IMG}:$(IMG_VERSION) # build docker image using buildx # PLATFORMS defines the target platforms for the model registry image be built to provide support to multiple diff --git a/clients/ui/bff/internal/mocks/model_registry_client_mock.go b/clients/ui/bff/internal/mocks/model_registry_client_mock.go index 1081bcc6..9a35e331 100644 --- a/clients/ui/bff/internal/mocks/model_registry_client_mock.go +++ b/clients/ui/bff/internal/mocks/model_registry_client_mock.go @@ -1,11 +1,12 @@ package mocks import ( + "log/slog" + "net/url" + "github.com/kubeflow/model-registry/pkg/openapi" "github.com/kubeflow/model-registry/ui/bff/internal/integrations" "github.com/stretchr/testify/mock" - "log/slog" - "net/url" ) type ModelRegistryClientMock struct { @@ -26,7 +27,11 @@ func (m *ModelRegistryClientMock) CreateRegisteredModel(_ integrations.HTTPClien return &mockData, nil } -func (m *ModelRegistryClientMock) GetRegisteredModel(_ integrations.HTTPClientInterface, _ string) (*openapi.RegisteredModel, error) { +func (m *ModelRegistryClientMock) GetRegisteredModel(_ integrations.HTTPClientInterface, id string) (*openapi.RegisteredModel, error) { + if id == "3" { + mockData := GetRegisteredModelMocks()[2] + return &mockData, nil + } mockData := GetRegisteredModelMocks()[0] return &mockData, nil } @@ -36,7 +41,12 @@ func (m *ModelRegistryClientMock) UpdateRegisteredModel(_ integrations.HTTPClien return &mockData, nil } -func (m *ModelRegistryClientMock) GetModelVersion(_ integrations.HTTPClientInterface, _ string) (*openapi.ModelVersion, error) { +func (m *ModelRegistryClientMock) GetModelVersion(_ integrations.HTTPClientInterface, id string) (*openapi.ModelVersion, error) { + if id == "3" { + mockData := GetModelVersionMocks()[2] + return &mockData, nil + } + mockData := GetModelVersionMocks()[0] return &mockData, nil } diff --git a/clients/ui/bff/internal/mocks/static_data_mock.go b/clients/ui/bff/internal/mocks/static_data_mock.go index 05474fd8..bb0408c2 100644 --- a/clients/ui/bff/internal/mocks/static_data_mock.go +++ b/clients/ui/bff/internal/mocks/static_data_mock.go @@ -184,6 +184,18 @@ func newCustomProperties() *map[string]openapi.MetadataValue { MetadataType: "MetadataStringValue", }, }, + "AWS_KEY": { + MetadataStringValue: &openapi.MetadataStringValue{ + StringValue: "asdf89asdf098asdfa", + MetadataType: "MetadataStringValue", + }, + }, + "AWS_PASSWORD": { + MetadataStringValue: &openapi.MetadataStringValue{ + StringValue: "*AadfeDs34adf", + MetadataType: "MetadataStringValue", + }, + }, } return &result diff --git a/clients/ui/frontend/package-lock.json b/clients/ui/frontend/package-lock.json index 3e2e3529..9eb0bdcc 100644 --- a/clients/ui/frontend/package-lock.json +++ b/clients/ui/frontend/package-lock.json @@ -32,7 +32,7 @@ "@babel/preset-typescript": "^7.21.5", "@cypress/code-coverage": "^3.13.4", "@mui/icons-material": "^6.1.2", - "@mui/material": "^6.1.1", + "@mui/material": "^6.1.3", "@mui/types": "^7.2.17", "@testing-library/cypress": "^10.0.1", "@testing-library/dom": "^10.4.0", @@ -80,7 +80,7 @@ "ts-loader": "^9.5.1", "tsconfig-paths-webpack-plugin": "^4.1.0", "tslib": "^2.7.0", - "typescript": "^5.5.4", + "typescript": "^5.6.3", "url-loader": "^4.1.1", "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.10.2", @@ -3629,9 +3629,9 @@ "license": "MIT" }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.2.tgz", - "integrity": "sha512-1oE4U38/TtzLWRYWEm/m70dUbpcvBx0QvDVg6NtpOmSNQC1Mbx0X/rNvYDdZnn8DIsAiVQ+SZ3am6doSswUQ4g==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.3.tgz", + "integrity": "sha512-ajMUgdfhTb++rwqj134Cq9f4SRN8oXUqMRnY72YBnXiXai3olJLLqETheRlq3MM8wCKrbq7g6j7iWL1VvP44VQ==", "dev": true, "funding": { "type": "opencollective", @@ -3665,16 +3665,16 @@ } }, "node_modules/@mui/material": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.2.tgz", - "integrity": "sha512-5TtHeAVX9D5d2LYfB1GAUn29BcVETVsrQ76Dwb2SpAfQGW3JVy4deJCAd0RrIkI3eEUrsl0E4xuBdreszxdTTg==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.3.tgz", + "integrity": "sha512-loV5MBoMKLrK80JeWINmQ1A4eWoLv51O2dBPLJ260IAhupkB3Wol8lEQTEvvR2vO3o6xRHuXe1WaQEP6N3riqg==", "dev": true, "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/core-downloads-tracker": "^6.1.2", - "@mui/system": "^6.1.2", - "@mui/types": "^7.2.17", - "@mui/utils": "^6.1.2", + "@mui/core-downloads-tracker": "^6.1.3", + "@mui/system": "^6.1.3", + "@mui/types": "^7.2.18", + "@mui/utils": "^6.1.3", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", @@ -3693,7 +3693,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.2", + "@mui/material-pigment-css": "^6.1.3", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3720,13 +3720,13 @@ "dev": true }, "node_modules/@mui/private-theming": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.2.tgz", - "integrity": "sha512-S8WcjZdNdi++8UhrrY8Lton5h/suRiQexvdTfdcPAlbajlvgM+kx+uJstuVIEyTb3gMkxzIZep87knZ0tqcR0g==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.3.tgz", + "integrity": "sha512-XK5OYCM0x7gxWb/WBEySstBmn+dE3YKX7U7jeBRLm6vHU5fGUd7GiJWRirpivHjOK9mRH6E1MPIVd+ze5vguKQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/utils": "^6.1.2", + "@mui/utils": "^6.1.3", "prop-types": "^15.8.1" }, "engines": { @@ -3747,13 +3747,14 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.2.tgz", - "integrity": "sha512-uKOfWkR23X39xj7th2nyTcCHqInTAXtUnqD3T5qRVdJcOPvu1rlgTleTwJC/FJvWZJBU6ieuTWDhbcx5SNViHQ==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.3.tgz", + "integrity": "sha512-i4yh9m+eMZE3cNERpDhVr6Wn73Yz6C7MH0eE2zZvw8d7EFkIJlCQNZd1xxGZqarD2DDq2qWHcjIOucWGhxACtA==", "dev": true, "dependencies": { "@babel/runtime": "^7.25.6", "@emotion/cache": "^11.13.1", + "@emotion/serialize": "^1.3.2", "@emotion/sheet": "^1.4.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -3780,16 +3781,16 @@ } }, "node_modules/@mui/system": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.2.tgz", - "integrity": "sha512-mzW7F1ZMIYS1aLON48Nrk9c65OrVEVQ+R4lUcTWs1lCSul0VGK23eo4dmY0NX5PS7Oe4xz3P5B9tQZZ7SYgxcg==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.3.tgz", + "integrity": "sha512-ILaD9UsLTBLjMcep3OumJMXh1PYr7aqnkHm/L47bH46+YmSL1zWAX6tWG8swEQROzW2GvYluEMp5FreoxOOC6w==", "dev": true, "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/private-theming": "^6.1.2", - "@mui/styled-engine": "^6.1.2", - "@mui/types": "^7.2.17", - "@mui/utils": "^6.1.2", + "@mui/private-theming": "^6.1.3", + "@mui/styled-engine": "^6.1.3", + "@mui/types": "^7.2.18", + "@mui/utils": "^6.1.3", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -3820,9 +3821,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.17", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz", - "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==", + "version": "7.2.18", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz", + "integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==", "dev": true, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3834,13 +3835,13 @@ } }, "node_modules/@mui/utils": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.2.tgz", - "integrity": "sha512-6+B1YZ8cCBWD1fc3RjqpclF9UA0MLUiuXhyCO+XowD/Z2ku5IlxeEhHHlgglyBWFGMu4kib4YU3CDsG5/zVjJQ==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.3.tgz", + "integrity": "sha512-4JBpLkjprlKjN10DGb1aiy/ii9TKbQ601uSHtAmYFAS879QZgAD7vRnv/YBE4iBbc7NXzFgbQMCOFrupXWekIA==", "dev": true, "dependencies": { "@babel/runtime": "^7.25.6", - "@mui/types": "^7.2.17", + "@mui/types": "^7.2.18", "@types/prop-types": "^15.7.13", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -22154,11 +22155,10 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/clients/ui/frontend/package.json b/clients/ui/frontend/package.json index 5a3479b5..35796b7b 100644 --- a/clients/ui/frontend/package.json +++ b/clients/ui/frontend/package.json @@ -35,7 +35,7 @@ "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.5", "@cypress/code-coverage": "^3.13.4", - "@mui/material": "^6.1.1", + "@mui/material": "^6.1.3", "@mui/icons-material": "^6.1.2", "@mui/types": "^7.2.17", "@testing-library/cypress": "^10.0.1", @@ -84,7 +84,7 @@ "ts-loader": "^9.5.1", "tsconfig-paths-webpack-plugin": "^4.1.0", "tslib": "^2.7.0", - "typescript": "^5.5.4", + "typescript": "^5.6.3", "url-loader": "^4.1.1", "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.10.2", diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelPropertiesTableRow.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelPropertiesTableRow.tsx index 74002b76..ef78584b 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelPropertiesTableRow.tsx +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/ModelPropertiesTableRow.tsx @@ -13,6 +13,7 @@ import { import { CheckIcon, TimesIcon } from '@patternfly/react-icons'; import { KeyValuePair } from '~/types'; import { EitherNotBoth } from '~/typeHelpers'; +import FormFieldset from '~/app/pages/modelRegistry/screens/components/FormFieldset'; type ModelPropertiesTableRowProps = { allExistingKeys: string[]; @@ -94,19 +95,27 @@ const ModelPropertiesTableRow: React.FC = ({ {isEditing ? ( <> - setUnsavedKey(str)} + validated={keyValidationError ? 'error' : 'default'} + /> } - isRequired - type="text" - value={unsavedKey} - onChange={(_event, str) => setUnsavedKey(str)} - validated={keyValidationError ? 'error' : 'default'} /> + {keyValidationError && ( @@ -121,17 +130,24 @@ const ModelPropertiesTableRow: React.FC = ({ {isEditing ? ( - setUnsavedValue(str)} + /> } - isRequired - type="text" - value={unsavedValue} - onChange={(_event, str) => setUnsavedValue(str)} /> ) : ( = ({ /> - { - setSearch(searchValue); - }} - onClear={() => setSearch('')} - style={{ minWidth: '200px' }} - data-testid="model-versions-table-search" + { + setSearch(searchValue); + }} + style={{ minWidth: '200px' }} + data-testid="model-versions-table-search" + aria-label="Search" + /> + } + field={`Find by ${searchType.toLowerCase()}`} /> diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisterModel/PrefilledModelRegistryField.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisterModel/PrefilledModelRegistryField.tsx index 980da8d3..1261a9df 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisterModel/PrefilledModelRegistryField.tsx +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisterModel/PrefilledModelRegistryField.tsx @@ -1,13 +1,19 @@ import React from 'react'; import { FormGroup, TextInput } from '@patternfly/react-core'; +import FormFieldset from '~/app/pages/modelRegistry/screens/components/FormFieldset'; type PrefilledModelRegistryFieldProps = { mrName?: string; }; const PrefilledModelRegistryField: React.FC = ({ mrName }) => ( - - + + + } + field="Model Registry" + /> ); diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisterModel/RegisterModel.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisterModel/RegisterModel.tsx index c6cffb9d..b9bf9764 100644 --- a/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisterModel/RegisterModel.tsx +++ b/clients/ui/frontend/src/app/pages/modelRegistry/screens/RegisterModel/RegisterModel.tsx @@ -13,6 +13,7 @@ import { import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; import { useParams, useNavigate } from 'react-router'; import { Link } from 'react-router-dom'; +import FormFieldset from '~/app/pages/modelRegistry/screens/components/FormFieldset'; import FormSection from '~/app/components/pf-overrides/FormSection'; import ApplicationsPage from '~/app/components/ApplicationsPage'; import { modelRegistryUrl, registeredModelUrl } from '~/app/pages/modelRegistry/screens/routeUtils'; @@ -21,7 +22,6 @@ import { useRegisterModelData, RegistrationCommonFormData } from './useRegisterM import { isRegisterModelSubmitDisabled, registerModel } from './utils'; import { useRegistrationCommonState } from './useRegistrationCommonState'; import RegistrationCommonFormSections from './RegistrationCommonFormSections'; -import PrefilledModelRegistryField from './PrefilledModelRegistryField'; import RegistrationFormFooter from './RegistrationFormFooter'; const RegisterModel: React.FC = () => { @@ -42,6 +42,31 @@ const RegisterModel: React.FC = () => { }); const onCancel = () => navigate(modelRegistryUrl(mrName)); + const modelRegistryInput = ( + + ); + + const modelNameInput = ( + setData('modelName', value)} + /> + ); + + const modelDescriptionInput = ( +