diff --git a/clients/ui/frontend/src/__mocks__/mockRegisteredModel.ts b/clients/ui/frontend/src/__mocks__/mockRegisteredModel.ts new file mode 100644 index 00000000..7c45fbe5 --- /dev/null +++ b/clients/ui/frontend/src/__mocks__/mockRegisteredModel.ts @@ -0,0 +1,30 @@ +import { ModelState, RegisteredModel } from '~/app/types'; +import { createModelRegistryLabelsObject } from './utils'; + +type MockRegisteredModelType = { + id?: string; + name?: string; + owner?: string; + state?: ModelState; + description?: string; + labels?: string[]; +}; + +export const mockRegisteredModel = ({ + name = 'test', + owner = 'Author 1', + state = ModelState.LIVE, + description = '', + labels = [], + id = '1', +}: MockRegisteredModelType): RegisteredModel => ({ + createTimeSinceEpoch: '1710404288975', + description, + externalID: '1234132asdfasdf', + id, + lastUpdateTimeSinceEpoch: '1710404288975', + name, + state, + owner, + customProperties: createModelRegistryLabelsObject(labels), +}); diff --git a/clients/ui/frontend/src/__mocks__/utils.ts b/clients/ui/frontend/src/__mocks__/utils.ts new file mode 100644 index 00000000..05406788 --- /dev/null +++ b/clients/ui/frontend/src/__mocks__/utils.ts @@ -0,0 +1,13 @@ +import { ModelRegistryMetadataType, ModelRegistryStringCustomProperties } from '~/app/types'; + +export const createModelRegistryLabelsObject = ( + labels: string[], +): ModelRegistryStringCustomProperties => + labels.reduce((acc, label) => { + acc[label] = { + metadataType: ModelRegistryMetadataType.STRING, + // eslint-disable-next-line camelcase + string_value: '', + }; + return acc; + }, {} as ModelRegistryStringCustomProperties); diff --git a/clients/ui/frontend/src/app/api/__tests__/errorUtils.spec.ts b/clients/ui/frontend/src/app/api/__tests__/errorUtils.spec.ts new file mode 100644 index 00000000..3c225152 --- /dev/null +++ b/clients/ui/frontend/src/app/api/__tests__/errorUtils.spec.ts @@ -0,0 +1,33 @@ +import { NotReadyError } from '~/utilities/useFetchState'; +import { APIError } from '~/types'; +import { handleRestFailures } from '~/app/api/errorUtils'; +import { mockRegisteredModel } from '~/__mocks__/mockRegisteredModel'; + +describe('handleRestFailures', () => { + it('should successfully return registered models', async () => { + const modelRegistryMock = mockRegisteredModel({}); + const result = await handleRestFailures(Promise.resolve(modelRegistryMock)); + expect(result).toStrictEqual(modelRegistryMock); + }); + + it('should handle and throw model registry errors', async () => { + const statusMock: APIError = { + code: '', + message: 'error', + }; + + await expect(handleRestFailures(Promise.resolve(statusMock))).rejects.toThrow('error'); + }); + + it('should handle common state errors ', async () => { + await expect(handleRestFailures(Promise.reject(new NotReadyError('error')))).rejects.toThrow( + 'error', + ); + }); + + it('should handle other errors', async () => { + await expect(handleRestFailures(Promise.reject(new Error('error')))).rejects.toThrow( + 'Error communicating with server', + ); + }); +}); diff --git a/clients/ui/frontend/src/app/api/__tests__/service.spec.ts b/clients/ui/frontend/src/app/api/__tests__/service.spec.ts new file mode 100644 index 00000000..1e2a36e2 --- /dev/null +++ b/clients/ui/frontend/src/app/api/__tests__/service.spec.ts @@ -0,0 +1,422 @@ +import { restCREATE, restGET, restPATCH } from '~/app/api/apiUtils'; +import { handleRestFailures } from '~/app/api/errorUtils'; +import { ModelState, ModelArtifactState } from '~/app/types'; +import { + createModelArtifact, + createModelVersion, + createRegisteredModel, + getRegisteredModel, + getModelVersion, + getModelArtifact, + getListModelVersions, + getListModelArtifacts, + getModelVersionsByRegisteredModel, + getListRegisteredModels, + patchModelArtifact, + patchModelVersion, + patchRegisteredModel, + getModelArtifactsByModelVersion, + createModelVersionForRegisteredModel, + createModelArtifactForModelVersion, +} from '~/app/api/service'; +import { BFF_API_VERSION } from '~/app/const'; + +const mockProxyPromise = Promise.resolve(); + +jest.mock('~/app/api/apiUtils', () => ({ + restCREATE: jest.fn(() => mockProxyPromise), + restGET: jest.fn(() => mockProxyPromise), + restPATCH: jest.fn(() => mockProxyPromise), +})); + +const mockResultPromise = Promise.resolve(); + +jest.mock('~/app/api/errorUtils', () => ({ + handleRestFailures: jest.fn(() => mockResultPromise), +})); + +const handleRestFailuresMock = jest.mocked(handleRestFailures); +const restCREATEMock = jest.mocked(restCREATE); +const restGETMock = jest.mocked(restGET); +const restPATCHMock = jest.mocked(restPATCH); + +const K8sAPIOptionsMock = {}; + +describe('createRegisteredModel', () => { + it('should call restCREATE and handleRestFailures to create registered model', () => { + expect( + createRegisteredModel('hostPath', 'model-registry-1')(K8sAPIOptionsMock, { + description: 'test', + externalID: '1', + name: 'test new registered model', + state: ModelState.LIVE, + customProperties: {}, + }), + ).toBe(mockResultPromise); + expect(restCREATEMock).toHaveBeenCalledTimes(1); + expect(restCREATEMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/registered_models`, + { + description: 'test', + externalID: '1', + name: 'test new registered model', + state: ModelState.LIVE, + customProperties: {}, + }, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('createModelVersion', () => { + it('should call restCREATE and handleRestFailures to create model version', () => { + expect( + createModelVersion('hostPath', 'model-registry-1')(K8sAPIOptionsMock, { + description: 'test', + externalID: '1', + author: 'test author', + registeredModelId: '1', + name: 'test new model version', + state: ModelState.LIVE, + customProperties: {}, + }), + ).toBe(mockResultPromise); + expect(restCREATEMock).toHaveBeenCalledTimes(1); + expect(restCREATEMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_versions`, + { + description: 'test', + externalID: '1', + author: 'test author', + registeredModelId: '1', + name: 'test new model version', + state: ModelState.LIVE, + customProperties: {}, + }, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('createModelVersionForRegisteredModel', () => { + it('should call restCREATE and handleRestFailures to create model version for a model', () => { + expect( + createModelVersionForRegisteredModel('hostPath', 'model-registry-1')(K8sAPIOptionsMock, '1', { + description: 'test', + externalID: '1', + author: 'test author', + registeredModelId: '1', + name: 'test new model version', + state: ModelState.LIVE, + customProperties: {}, + }), + ).toBe(mockResultPromise); + expect(restCREATEMock).toHaveBeenCalledTimes(1); + expect(restCREATEMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/registered_models/1/versions`, + { + description: 'test', + externalID: '1', + author: 'test author', + registeredModelId: '1', + name: 'test new model version', + state: ModelState.LIVE, + customProperties: {}, + }, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('createModelArtifact', () => { + it('should call restCREATE and handleRestFailures to create model artifact', () => { + expect( + createModelArtifact('hostPath', 'model-registry-1')(K8sAPIOptionsMock, { + description: 'test', + externalID: 'test', + uri: 'test-uri', + state: ModelArtifactState.LIVE, + name: 'test-name', + modelFormatName: 'test-modelformatname', + storageKey: 'teststoragekey', + storagePath: 'teststoragePath', + modelFormatVersion: 'testmodelFormatVersion', + serviceAccountName: 'testserviceAccountname', + customProperties: {}, + artifactType: 'model-artifact', + }), + ).toBe(mockResultPromise); + expect(restCREATEMock).toHaveBeenCalledTimes(1); + expect(restCREATEMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_artifacts`, + { + description: 'test', + externalID: 'test', + uri: 'test-uri', + state: ModelArtifactState.LIVE, + name: 'test-name', + modelFormatName: 'test-modelformatname', + storageKey: 'teststoragekey', + storagePath: 'teststoragePath', + modelFormatVersion: 'testmodelFormatVersion', + serviceAccountName: 'testserviceAccountname', + customProperties: {}, + artifactType: 'model-artifact', + }, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('createModelArtifactForModelVersion', () => { + it('should call restCREATE and handleRestFailures to create model artifact for version', () => { + expect( + createModelArtifactForModelVersion('hostPath', 'model-registry-1')(K8sAPIOptionsMock, '2', { + description: 'test', + externalID: 'test', + uri: 'test-uri', + state: ModelArtifactState.LIVE, + name: 'test-name', + modelFormatName: 'test-modelformatname', + storageKey: 'teststoragekey', + storagePath: 'teststoragePath', + modelFormatVersion: 'testmodelFormatVersion', + serviceAccountName: 'testserviceAccountname', + customProperties: {}, + artifactType: 'model-artifact', + }), + ).toBe(mockResultPromise); + expect(restCREATEMock).toHaveBeenCalledTimes(1); + expect(restCREATEMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_versions/2/artifacts`, + { + description: 'test', + externalID: 'test', + uri: 'test-uri', + state: ModelArtifactState.LIVE, + name: 'test-name', + modelFormatName: 'test-modelformatname', + storageKey: 'teststoragekey', + storagePath: 'teststoragePath', + modelFormatVersion: 'testmodelFormatVersion', + serviceAccountName: 'testserviceAccountname', + customProperties: {}, + artifactType: 'model-artifact', + }, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('getRegisteredModel', () => { + it('should call restGET and handleRestFailures to fetch registered model', () => { + expect(getRegisteredModel('hostPath', 'model-registry-1')(K8sAPIOptionsMock, '1')).toBe( + mockResultPromise, + ); + expect(restGETMock).toHaveBeenCalledTimes(1); + expect(restGETMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/registered_models/1`, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('getModelVersion', () => { + it('should call restGET and handleRestFailures to fetch model version', () => { + expect(getModelVersion('hostPath', 'model-registry-1')(K8sAPIOptionsMock, '1')).toBe( + mockResultPromise, + ); + expect(restGETMock).toHaveBeenCalledTimes(1); + expect(restGETMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_versions/1`, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('getModelArtifact', () => { + it('should call restGET and handleRestFailures to fetch model version', () => { + expect(getModelArtifact('hostPath', 'model-registry-1')(K8sAPIOptionsMock, '1')).toBe( + mockResultPromise, + ); + expect(restGETMock).toHaveBeenCalledTimes(1); + expect(restGETMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_artifacts/1`, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('getListRegisteredModels', () => { + it('should call restGET and handleRestFailures to list registered models', () => { + expect(getListRegisteredModels('hostPath', 'model-registry-1')({})).toBe(mockResultPromise); + expect(restGETMock).toHaveBeenCalledTimes(1); + expect(restGETMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/registered_models`, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('getListModelArtifacts', () => { + it('should call restGET and handleRestFailures to list models artifacts', () => { + expect(getListModelArtifacts('hostPath', 'model-registry-1')({})).toBe(mockResultPromise); + expect(restGETMock).toHaveBeenCalledTimes(1); + expect(restGETMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_artifacts`, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('getListModelVersions', () => { + it('should call restGET and handleRestFailures to list models versions', () => { + expect(getListModelVersions('hostPath', 'model-registry-1')({})).toBe(mockResultPromise); + expect(restGETMock).toHaveBeenCalledTimes(1); + expect(restGETMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_versions`, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('getModelVersionsByRegisteredModel', () => { + it('should call restGET and handleRestFailures to list models versions by registered model', () => { + expect(getModelVersionsByRegisteredModel('hostPath', 'model-registry-1')({}, '1')).toBe( + mockResultPromise, + ); + expect(restGETMock).toHaveBeenCalledTimes(1); + expect(restGETMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/registered_models/1/versions`, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('getModelArtifactsByModelVersion', () => { + it('should call restGET and handleRestFailures to list models artifacts by model version', () => { + expect(getModelArtifactsByModelVersion('hostPath', 'model-registry-1')({}, '1')).toBe( + mockResultPromise, + ); + expect(restGETMock).toHaveBeenCalledTimes(1); + expect(restGETMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_versions/1/artifacts`, + {}, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('patchRegisteredModel', () => { + it('should call restPATCH and handleRestFailures to update registered model', () => { + expect( + patchRegisteredModel('hostPath', 'model-registry-1')( + K8sAPIOptionsMock, + { description: 'new test' }, + '1', + ), + ).toBe(mockResultPromise); + expect(restPATCHMock).toHaveBeenCalledTimes(1); + expect(restPATCHMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/registered_models/1`, + { description: 'new test' }, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('patchModelVersion', () => { + it('should call restPATCH and handleRestFailures to update model version', () => { + expect( + patchModelVersion('hostPath', 'model-registry-1')( + K8sAPIOptionsMock, + { description: 'new test' }, + '1', + ), + ).toBe(mockResultPromise); + expect(restPATCHMock).toHaveBeenCalledTimes(1); + expect(restPATCHMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_versions/1`, + { description: 'new test' }, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); + +describe('patchModelArtifact', () => { + it('should call restPATCH and handleRestFailures to update model artifact', () => { + expect( + patchModelArtifact('hostPath', 'model-registry-1')( + K8sAPIOptionsMock, + { description: 'new test' }, + '1', + ), + ).toBe(mockResultPromise); + expect(restPATCHMock).toHaveBeenCalledTimes(1); + expect(restPATCHMock).toHaveBeenCalledWith( + 'hostPath', + `/api/${BFF_API_VERSION}/model_registry/model-registry-1/model_artifacts/1`, + { description: 'new test' }, + K8sAPIOptionsMock, + ); + expect(handleRestFailuresMock).toHaveBeenCalledTimes(1); + expect(handleRestFailuresMock).toHaveBeenCalledWith(mockProxyPromise); + }); +}); diff --git a/clients/ui/frontend/src/app/api/apiUtils.ts b/clients/ui/frontend/src/app/api/apiUtils.ts new file mode 100644 index 00000000..d4adff6c --- /dev/null +++ b/clients/ui/frontend/src/app/api/apiUtils.ts @@ -0,0 +1,163 @@ +import { APIOptions } from '~/types'; +import { EitherOrNone } from '~/typeHelpers'; + +export const mergeRequestInit = ( + opts: APIOptions = {}, + specificOpts: RequestInit = {}, +): RequestInit => ({ + ...specificOpts, + ...(opts.signal && { signal: opts.signal }), +}); + +type CallRestJSONOptions = { + queryParams?: Record; + parseJSON?: boolean; +} & EitherOrNone< + { + fileContents: string; + }, + { + data: Record; + } +>; + +const callRestJSON = ( + host: string, + path: string, + requestInit: RequestInit, + { data, fileContents, queryParams, parseJSON = true }: CallRestJSONOptions, +): Promise => { + const { method, ...otherOptions } = requestInit; + + const sanitizedQueryParams = queryParams + ? Object.entries(queryParams).reduce((acc, [key, value]) => { + if (value) { + return { ...acc, [key]: value }; + } + + return acc; + }, {}) + : null; + + const searchParams = sanitizedQueryParams + ? new URLSearchParams(sanitizedQueryParams).toString() + : null; + + let requestData: string | undefined; + let contentType: string | undefined; + let formData: FormData | undefined; + if (fileContents) { + formData = new FormData(); + formData.append( + 'uploadfile', + new Blob([fileContents], { type: 'application/x-yaml' }), + 'uploadedFile.yml', + ); + } else if (data) { + // It's OK for contentType and requestData to BOTH be undefined for e.g. a GET request or POST with no body. + contentType = 'application/json;charset=UTF-8'; + requestData = JSON.stringify(data); + } + + return fetch(`${host}${path}${searchParams ? `?${searchParams}` : ''}`, { + ...otherOptions, + ...(contentType && { headers: { 'Content-Type': contentType } }), + method, + body: formData ?? requestData, + }).then((response) => + response.text().then((fetchedData) => { + if (parseJSON) { + return JSON.parse(fetchedData); + } + return fetchedData; + }), + ); +}; + +export const restGET = ( + host: string, + path: string, + queryParams: Record = {}, + options?: APIOptions, +): Promise => + callRestJSON(host, path, mergeRequestInit(options, { method: 'GET' }), { + queryParams, + parseJSON: options?.parseJSON, + }); + +/** Standard POST */ +export const restCREATE = ( + host: string, + path: string, + data: Record, + queryParams: Record = {}, + options?: APIOptions, +): Promise => + callRestJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { + data, + queryParams, + parseJSON: options?.parseJSON, + }); + +/** POST -- but with file content instead of body data */ +export const restFILE = ( + host: string, + path: string, + fileContents: string, + queryParams: Record = {}, + options?: APIOptions, +): Promise => + callRestJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { + fileContents, + queryParams, + parseJSON: options?.parseJSON, + }); + +/** POST -- but no body data -- targets simple endpoints */ +export const restENDPOINT = ( + host: string, + path: string, + queryParams: Record = {}, + options?: APIOptions, +): Promise => + callRestJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { + queryParams, + parseJSON: options?.parseJSON, + }); + +export const restUPDATE = ( + host: string, + path: string, + data: Record, + queryParams: Record = {}, + options?: APIOptions, +): Promise => + callRestJSON(host, path, mergeRequestInit(options, { method: 'PUT' }), { + data, + queryParams, + parseJSON: options?.parseJSON, + }); + +export const restPATCH = ( + host: string, + path: string, + data: Record, + options?: APIOptions, +): Promise => + callRestJSON(host, path, mergeRequestInit(options, { method: 'PATCH' }), { + data, + parseJSON: options?.parseJSON, + }); + +export const restDELETE = ( + host: string, + path: string, + data: Record, + queryParams: Record = {}, + options?: APIOptions, +): Promise => + callRestJSON(host, path, mergeRequestInit(options, { method: 'DELETE' }), { + data, + queryParams, + parseJSON: options?.parseJSON, + }); diff --git a/clients/ui/frontend/src/app/api/errorUtils.ts b/clients/ui/frontend/src/app/api/errorUtils.ts new file mode 100644 index 00000000..4cb92823 --- /dev/null +++ b/clients/ui/frontend/src/app/api/errorUtils.ts @@ -0,0 +1,27 @@ +import { APIError } from '~/types'; +import { isCommonStateError } from '~/utilities/useFetchState'; + +const isError = (e: unknown): e is APIError => + typeof e === 'object' && e !== null && ['code', 'message'].every((key) => key in e); + +export const handleRestFailures = (promise: Promise): Promise => + promise + .then((result) => { + if (isError(result)) { + throw result; + } + return result; + }) + .catch((e) => { + if (isError(e)) { + throw new Error(e.message); + } + if (isCommonStateError(e)) { + // Common state errors are handled by useFetchState at storage level, let them deal with it + // TODO: check whether we need this or not + throw e; + } + // eslint-disable-next-line no-console + console.error('Unknown API error', e); + throw new Error('Error communicating with server'); + }); diff --git a/clients/ui/frontend/src/app/api/k8s.ts b/clients/ui/frontend/src/app/api/k8s.ts new file mode 100644 index 00000000..e17e55db --- /dev/null +++ b/clients/ui/frontend/src/app/api/k8s.ts @@ -0,0 +1,10 @@ +import { APIOptions } from '~/types'; +import { handleRestFailures } from '~/app/api/errorUtils'; +import { restGET } from '~/app/api/apiUtils'; +import { ModelRegistry } from '~/app/types'; +import { BFF_API_VERSION } from '~/app/const'; + +export const getModelRegistries = + (hostPath: string) => + (opts: APIOptions): Promise => + handleRestFailures(restGET(hostPath, `/api/${BFF_API_VERSION}/model_registry`, {}, opts)); diff --git a/clients/ui/frontend/src/app/api/service.ts b/clients/ui/frontend/src/app/api/service.ts new file mode 100644 index 00000000..42f8dbb5 --- /dev/null +++ b/clients/ui/frontend/src/app/api/service.ts @@ -0,0 +1,227 @@ +import { + CreateModelArtifactData, + CreateModelVersionData, + CreateRegisteredModelData, + ModelArtifact, + ModelArtifactList, + ModelVersionList, + ModelVersion, + RegisteredModelList, + RegisteredModel, +} from '~/app/types'; +import { restCREATE, restGET, restPATCH } from '~/app/api/apiUtils'; +import { APIOptions } from '~/types'; +import { handleRestFailures } from '~/app/api/errorUtils'; +import { BFF_API_VERSION } from '~/app/const'; + +export const createRegisteredModel = + (hostPath: string, mrName: string) => + (opts: APIOptions, data: CreateRegisteredModelData): Promise => + handleRestFailures( + restCREATE( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/registered_models`, + data, + {}, + opts, + ), + ); + +export const createModelVersion = + (hostPath: string, mrName: string) => + (opts: APIOptions, data: CreateModelVersionData): Promise => + handleRestFailures( + restCREATE( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_versions`, + data, + {}, + opts, + ), + ); +export const createModelVersionForRegisteredModel = + (hostPath: string, mrName: string) => + ( + opts: APIOptions, + registeredModelId: string, + data: CreateModelVersionData, + ): Promise => + handleRestFailures( + restCREATE( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/registered_models/${registeredModelId}/versions`, + data, + {}, + opts, + ), + ); + +export const createModelArtifact = + (hostPath: string, mrName: string) => + (opts: APIOptions, data: CreateModelArtifactData): Promise => + handleRestFailures( + restCREATE( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_artifacts`, + data, + {}, + opts, + ), + ); + +export const createModelArtifactForModelVersion = + (hostPath: string, mrName: string) => + ( + opts: APIOptions, + modelVersionId: string, + data: CreateModelArtifactData, + ): Promise => + handleRestFailures( + restCREATE( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_versions/${modelVersionId}/artifacts`, + data, + {}, + opts, + ), + ); + +export const getRegisteredModel = + (hostPath: string, mrName: string) => + (opts: APIOptions, registeredModelId: string): Promise => + handleRestFailures( + restGET( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/registered_models/${registeredModelId}`, + {}, + opts, + ), + ); + +export const getModelVersion = + (hostPath: string, mrName: string) => + (opts: APIOptions, modelversionId: string): Promise => + handleRestFailures( + restGET( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_versions/${modelversionId}`, + {}, + opts, + ), + ); + +export const getModelArtifact = + (hostPath: string, mrName: string) => + (opts: APIOptions, modelArtifactId: string): Promise => + handleRestFailures( + restGET( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_artifacts/${modelArtifactId}`, + {}, + opts, + ), + ); + +export const getListModelArtifacts = + (hostPath: string, mrName: string) => + (opts: APIOptions): Promise => + handleRestFailures( + restGET( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_artifacts`, + {}, + opts, + ), + ); + +export const getListModelVersions = + (hostPath: string, mrName: string) => + (opts: APIOptions): Promise => + handleRestFailures( + restGET( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_versions`, + {}, + opts, + ), + ); + +export const getListRegisteredModels = + (hostPath: string, mrName: string) => + (opts: APIOptions): Promise => + handleRestFailures( + restGET( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/registered_models`, + {}, + opts, + ), + ); + +export const getModelVersionsByRegisteredModel = + (hostPath: string, mrName: string) => + (opts: APIOptions, registeredmodelId: string): Promise => + handleRestFailures( + restGET( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/registered_models/${registeredmodelId}/versions`, + {}, + opts, + ), + ); + +export const getModelArtifactsByModelVersion = + (hostPath: string, mrName: string) => + (opts: APIOptions, modelVersionId: string): Promise => + handleRestFailures( + restGET( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_versions/${modelVersionId}/artifacts`, + {}, + opts, + ), + ); + +export const patchRegisteredModel = + (hostPath: string, mrName: string) => + ( + opts: APIOptions, + data: Partial, + registeredModelId: string, + ): Promise => + handleRestFailures( + restPATCH( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/registered_models/${registeredModelId}`, + data, + opts, + ), + ); + +export const patchModelVersion = + (hostPath: string, mrName: string) => + (opts: APIOptions, data: Partial, modelversionId: string): Promise => + handleRestFailures( + restPATCH( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_versions/${modelversionId}`, + data, + opts, + ), + ); + +export const patchModelArtifact = + (hostPath: string, mrName: string) => + ( + opts: APIOptions, + data: Partial, + modelartifactId: string, + ): Promise => + handleRestFailures( + restPATCH( + hostPath, + `/api/${BFF_API_VERSION}/model_registry/${mrName}/model_artifacts/${modelartifactId}`, + data, + opts, + ), + ); diff --git a/clients/ui/frontend/src/app/components/design/DividedGallery.scss b/clients/ui/frontend/src/app/components/design/DividedGallery.scss index e0a443f9..002cf987 100644 --- a/clients/ui/frontend/src/app/components/design/DividedGallery.scss +++ b/clients/ui/frontend/src/app/components/design/DividedGallery.scss @@ -1,4 +1,4 @@ -.odh-divided-gallery { +.kubeflowdivided-gallery { position: relative; background-color: var(--pf-v5-global--BackgroundColor--100); diff --git a/clients/ui/frontend/src/app/components/design/DividedGallery.tsx b/clients/ui/frontend/src/app/components/design/DividedGallery.tsx index 4f084790..8eb1eafa 100644 --- a/clients/ui/frontend/src/app/components/design/DividedGallery.tsx +++ b/clients/ui/frontend/src/app/components/design/DividedGallery.tsx @@ -25,15 +25,15 @@ const DividedGallery: React.FC = ({ closeTestId, ...rest }) => ( -
+
-
+
{children} {showClose ? ( -
+