diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts index 6a4798e3ae..21dee41842 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts @@ -98,6 +98,52 @@ describe('Pipelines', () => { pipelinesTable.findRowByName('New pipeline'); }); + it('imports a new pipeline by url', () => { + const createPipelineAndVersionParams = { + pipeline: { + display_name: 'New pipeline', + description: 'New pipeline description', + }, + pipeline_version: { + display_name: 'New pipeline', + description: 'New pipeline description', + package_url: { + pipeline_url: 'https://example.com/pipeline.yaml', + }, + }, + }; + const createdMockPipeline = buildMockPipelineV2(createPipelineAndVersionParams.pipeline); + + // Intercept upload/re-fetch of pipelines + pipelineImportModal + .mockCreatePipelineAndVersion(createPipelineAndVersionParams) + .as('createPipelineAndVersion'); + pipelinesTable + .mockGetPipelines([initialMockPipeline, createdMockPipeline]) + .as('refreshPipelines'); + + // Wait for the pipelines table to load + pipelinesTable.find(); + + // Open the "Import pipeline" modal + pipelinesGlobal.findImportPipelineButton().click(); + + // Fill out the "Import pipeline" modal and submit + pipelineImportModal.shouldBeOpen(); + pipelineImportModal.fillPipelineName('New pipeline'); + pipelineImportModal.fillPipelineDescription('New pipeline description'); + pipelineImportModal.findImportPipelineRadio().check(); + pipelineImportModal.findPipelineUrlInput().type('https://example.com/pipeline.yaml'); + pipelineImportModal.submit(); + + // Wait for upload/fetch requests + cy.wait('@createPipelineAndVersion'); + cy.wait('@refreshPipelines'); + + // Verify the uploaded pipeline is in the table + pipelinesTable.findRowByName('New pipeline'); + }); + it('uploads a new pipeline version', () => { const uploadVersionParams = { display_name: 'New pipeline version', @@ -137,6 +183,51 @@ describe('Pipelines', () => { pipelinesTable.findRowByName('New pipeline version'); }); + it('imports a new pipeline version by url', () => { + const createPipelineVersionParams = { + pipeline_id: 'test-pipeline', + display_name: 'New pipeline version', + description: 'New pipeline description', + package_url: { + pipeline_url: 'https://example.com/pipeline.yaml', + }, + }; + // Wait for the pipelines table to load + pipelinesTable.find(); + + // Open the "Upload new version" modal + pipelinesGlobal.findUploadVersionButton().click(); + + // Intercept upload/re-fetch of pipeline versions + pipelinesTable + .mockGetPipelineVersions( + [initialMockPipelineVersion, buildMockPipelineVersionV2(createPipelineVersionParams)], + initialMockPipeline.pipeline_id, + ) + .as('refreshVersions'); + + pipelineVersionImportModal + .mockCreatePipelineVersion(createPipelineVersionParams) + .as('createVersion'); + + // Fill out the "Upload new version" modal and submit + pipelineVersionImportModal.shouldBeOpen(); + pipelineVersionImportModal.selectPipelineByName('Test pipeline'); + pipelineVersionImportModal.fillVersionName('New pipeline version'); + pipelineVersionImportModal.fillVersionDescription('New pipeline version description'); + pipelineVersionImportModal.findImportPipelineRadio().check(); + pipelineVersionImportModal.findPipelineUrlInput().type('https://example.com/pipeline.yaml'); + pipelineVersionImportModal.submit(); + + // Wait for upload/fetch requests + cy.wait('@createVersion'); + cy.wait('@refreshVersions'); + + // Verify the uploaded pipeline version is in the table + pipelinesTable.toggleExpandRowByIndex(0); + pipelinesTable.findRowByName('New pipeline version'); + }); + it('delete a single pipeline version', () => { createDeleteVersionIntercept( initialMockPipelineVersion.pipeline_id, diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts index d7e87b6540..ea7cb8fdbb 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts @@ -1,6 +1,7 @@ -import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; +import { CreatePipelineAndVersionKFData, PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; import { buildMockPipelineV2 } from '~/__mocks__/mockPipelinesProxy'; import { Modal } from '~/__tests__/cypress/cypress/pages/components/Modal'; +import { buildMockPipelineVersionV2 } from '~/__mocks__'; class PipelineImportModal extends Modal { constructor() { @@ -27,6 +28,22 @@ class PipelineImportModal extends Modal { return this.findFooter().findByRole('button', { name: 'Import pipeline' }); } + findUploadPipelineRadio() { + return this.find().findByTestId('upload-file-radio'); + } + + findImportPipelineRadio() { + return this.find().findByTestId('import-url-radio'); + } + + findPipelineUrlInput() { + return this.find().findByTestId('pipeline-url-input'); + } + + findCodeSourceInput() { + return this.find().findByTestId('code-source-input'); + } + fillPipelineName(value: string) { this.findPipelineNameInput().clear().type(value); } @@ -39,6 +56,20 @@ class PipelineImportModal extends Modal { this.findUploadPipelineInput().selectFile([filePath], { force: true }); } + mockCreatePipelineAndVersion(params: CreatePipelineAndVersionKFData) { + return cy.intercept( + { + method: 'POST', + pathname: '/api/proxy/apis/v2beta1/pipelines/create', + times: 1, + }, + { + pipeline: buildMockPipelineV2(params.pipeline), + pipelineVersion: buildMockPipelineVersionV2(params.pipeline_version), + }, + ); + } + mockUploadPipeline(params: Partial) { return cy.intercept( { diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts index 5e120f3caa..8ec3697aa0 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineVersionImportModal.ts @@ -1,4 +1,4 @@ -import { PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes'; +import { CreatePipelineVersionKFData, PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes'; import { buildMockPipelineVersionV2 } from '~/__mocks__/mockPipelineVersionsProxy'; import { Modal } from '~/__tests__/cypress/cypress/pages/components/Modal'; @@ -38,6 +38,22 @@ class PipelineImportModal extends Modal { this.findUploadPipelineInput().selectFile([filePath], { force: true }); } + findUploadPipelineRadio() { + return this.find().findByTestId('upload-file-radio'); + } + + findImportPipelineRadio() { + return this.find().findByTestId('import-url-radio'); + } + + findPipelineUrlInput() { + return this.find().findByTestId('pipeline-url-input'); + } + + findCodeSourceInput() { + return this.find().findByTestId('code-source-input'); + } + selectPipelineByName(name: string) { this.findPipelineSelect() .click() @@ -58,6 +74,17 @@ class PipelineImportModal extends Modal { this.findSubmitButton().click(); } + mockCreatePipelineVersion(params: CreatePipelineVersionKFData) { + return cy.intercept( + { + method: 'POST', + pathname: `/api/proxy/apis/v2beta1/pipelines/${params.pipeline_id}/versions`, + times: 1, + }, + buildMockPipelineVersionV2(params), + ); + } + mockUploadVersion(params: Partial) { return cy.intercept( { diff --git a/frontend/src/api/pipelines/callTypes.ts b/frontend/src/api/pipelines/callTypes.ts index e662fae3eb..15db7f7f0c 100644 --- a/frontend/src/api/pipelines/callTypes.ts +++ b/frontend/src/api/pipelines/callTypes.ts @@ -20,6 +20,8 @@ import { GetPipelineVersion, DeletePipelineVersion, ListPipelineVersions, + CreatePipelineAndVersion, + CreatePipelineVersion, } from '~/concepts/pipelines/types'; import { K8sAPIOptions } from '~/k8sTypes'; @@ -28,6 +30,8 @@ import { K8sAPIOptions } from '~/k8sTypes'; type KubeflowSpecificAPICall = (opts: K8sAPIOptions, ...args: any[]) => Promise; type KubeflowAPICall = (hostPath: string) => ActualCall; +export type CreatePipelineVersionAPI = KubeflowAPICall; +export type CreatePipelineAndVersionAPI = KubeflowAPICall; export type CreateExperimentAPI = KubeflowAPICall; export type CreatePipelineRunAPI = KubeflowAPICall; export type CreatePipelineRunJobAPI = KubeflowAPICall; diff --git a/frontend/src/api/pipelines/custom.ts b/frontend/src/api/pipelines/custom.ts index 5255d75f94..d33c331caa 100644 --- a/frontend/src/api/pipelines/custom.ts +++ b/frontend/src/api/pipelines/custom.ts @@ -23,6 +23,8 @@ import { GetPipelineVersionAPI, ListPipelineVersionsAPI, UpdatePipelineRunAPI, + CreatePipelineAndVersionAPI, + CreatePipelineVersionAPI, } from './callTypes'; import { handlePipelineFailures } from './errorUtils'; @@ -48,6 +50,14 @@ export const createExperiment: CreateExperimentAPI = (hostPath) => (opts, name, handlePipelineFailures( proxyCREATE(hostPath, `/apis/v2beta1/experiments`, { name, description }, {}, opts), ); +export const createPipelineAndVersion: CreatePipelineAndVersionAPI = (hostPath) => (opts, data) => + handlePipelineFailures(proxyCREATE(hostPath, `/apis/v2beta1/pipelines/create`, data, {}, opts)); + +export const createPipelineVersion: CreatePipelineVersionAPI = + (hostPath) => (opts, pipelineId, data) => + handlePipelineFailures( + proxyCREATE(hostPath, `/apis/v2beta1/pipelines/${pipelineId}/versions`, data, {}, opts), + ); export const createPipelineRun: CreatePipelineRunAPI = (hostPath) => (opts, data) => handlePipelineFailures(proxyCREATE(hostPath, `/apis/v2beta1/runs`, data, {}, opts)); diff --git a/frontend/src/concepts/pipelines/content/import/PipelineImportModal.tsx b/frontend/src/concepts/pipelines/content/import/PipelineImportModal.tsx index d76482ed84..b7e1d53917 100644 --- a/frontend/src/concepts/pipelines/content/import/PipelineImportModal.tsx +++ b/frontend/src/concepts/pipelines/content/import/PipelineImportModal.tsx @@ -4,6 +4,9 @@ import { Button, Form, FormGroup, + FormHelperText, + HelperText, + HelperTextItem, Modal, Stack, StackItem, @@ -12,8 +15,9 @@ import { import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { usePipelineImportModalData } from '~/concepts/pipelines/content/import/useImportModalData'; import { getProjectDisplayName } from '~/pages/projects/utils'; -import PipelineFileUpload from '~/concepts/pipelines/content/import/PipelineFileUpload'; import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; +import PipelineUploadRadio from './PipelineUploadRadio'; +import { PipelineUploadOption } from './utils'; type PipelineImportModalProps = { isOpen: boolean; @@ -24,9 +28,17 @@ const PipelineImportModal: React.FC = ({ isOpen, onClo const { project, api, apiAvailable } = usePipelinesAPI(); const [importing, setImporting] = React.useState(false); const [error, setError] = React.useState(); - const [{ name, description, fileContents }, setData, resetData] = usePipelineImportModalData(); + const [ + { name, description, fileContents, pipelineUrl, uploadOption, codeSource }, + setData, + resetData, + ] = usePipelineImportModalData(); - const isImportButtonDisabled = !apiAvailable || importing || !name || !fileContents; + const isImportButtonDisabled = + !apiAvailable || + importing || + !name || + (uploadOption === PipelineUploadOption.URL_IMPORT ? !pipelineUrl : !fileContents); const onBeforeClose = (pipeline?: PipelineKFv2) => { onClose(pipeline); @@ -35,6 +47,51 @@ const PipelineImportModal: React.FC = ({ isOpen, onClo resetData(); }; + const onSubmit = () => { + setImporting(true); + setError(undefined); + + if (uploadOption === PipelineUploadOption.FILE_UPLOAD) { + api + .uploadPipeline({}, name, description, fileContents) + .then((pipeline) => onBeforeClose(pipeline)) + .catch((e) => { + setImporting(false); + setError(e); + }); + } else { + api + .createPipelineAndVersion( + {}, + { + pipeline: { + // eslint-disable-next-line camelcase + display_name: name, + description, + }, + // eslint-disable-next-line camelcase + pipeline_version: { + // eslint-disable-next-line camelcase + display_name: name, + description, + // eslint-disable-next-line camelcase + package_url: { + // eslint-disable-next-line camelcase + pipeline_url: pipelineUrl, + }, + // eslint-disable-next-line camelcase + code_source_url: codeSource, + }, + }, + ) + .then((pipeline) => onBeforeClose(pipeline)) + .catch((e) => { + setImporting(false); + setError(e); + }); + } + }; + return ( = ({ isOpen, onClo variant="primary" isDisabled={isImportButtonDisabled} isLoading={importing} - onClick={() => { - setImporting(true); - setError(undefined); - api - .uploadPipeline({}, name, description, fileContents) - .then((pipeline) => onBeforeClose(pipeline)) - .catch((e) => { - setImporting(false); - setError(e); - }); - }} + onClick={onSubmit} > Import pipeline , @@ -99,11 +146,39 @@ const PipelineImportModal: React.FC = ({ isOpen, onClo - setData('fileContents', data)} + setFileContents={(data) => setData('fileContents', data)} + pipelineUrl={pipelineUrl} + setPipelineUrl={(url) => setData('pipelineUrl', url)} + uploadOption={uploadOption} + setUploadOption={(option) => { + setData('uploadOption', option); + // Clear code source when switching between file and url import as this hides the field + setData('codeSource', ''); + }} /> + {uploadOption === PipelineUploadOption.URL_IMPORT && ( + + + setData('codeSource', value)} + /> + + + + Must specify either package url or file .yaml, zip, or .tar.gz + + + + + + )} {error && ( diff --git a/frontend/src/concepts/pipelines/content/import/PipelineUploadRadio.tsx b/frontend/src/concepts/pipelines/content/import/PipelineUploadRadio.tsx new file mode 100644 index 0000000000..1b69bf585f --- /dev/null +++ b/frontend/src/concepts/pipelines/content/import/PipelineUploadRadio.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import { + Alert, + AlertActionLink, + FormGroup, + FormHelperText, + HelperText, + HelperTextItem, + Radio, + Split, + SplitItem, + Stack, + StackItem, + TextInput, +} from '@patternfly/react-core'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import { COMPILE_PIPELINE_DOCUMENTATION_URL, PipelineUploadOption } from './utils'; +import PipelineFileUpload from './PipelineFileUpload'; + +type PipelineFileUploadProps = { + fileContents: string; + setFileContents: (fileContents: string) => void; + pipelineUrl: string; + setPipelineUrl: (url: string) => void; + uploadOption: PipelineUploadOption; + setUploadOption: (option: PipelineUploadOption) => void; +}; + +const PipelineUploadRadio: React.FC = ({ + fileContents, + setFileContents, + pipelineUrl, + setPipelineUrl, + uploadOption, + setUploadOption, +}) => ( + + + + + { + setUploadOption(PipelineUploadOption.FILE_UPLOAD); + setPipelineUrl(''); + }} + label="Upload a file" + id="upload-file" + data-testid="upload-file-radio" + /> + + + { + setUploadOption(PipelineUploadOption.URL_IMPORT); + setFileContents(''); + }} + label="Import by url" + id="import-url" + data-testid="import-url-radio" + /> + + + + + + + + View documentation + + + + + + + } + /> + + + {uploadOption === PipelineUploadOption.FILE_UPLOAD ? ( + + ) : ( + + setPipelineUrl(value)} + /> + + + URL must be publicly accessible + + + + )} + + +); + +export default PipelineUploadRadio; diff --git a/frontend/src/concepts/pipelines/content/import/PipelineVersionImportModal.tsx b/frontend/src/concepts/pipelines/content/import/PipelineVersionImportModal.tsx index 313f2c9be0..7da605b475 100644 --- a/frontend/src/concepts/pipelines/content/import/PipelineVersionImportModal.tsx +++ b/frontend/src/concepts/pipelines/content/import/PipelineVersionImportModal.tsx @@ -4,6 +4,9 @@ import { Button, Form, FormGroup, + FormHelperText, + HelperText, + HelperTextItem, Modal, Stack, StackItem, @@ -12,10 +15,10 @@ import { import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { usePipelineVersionImportModalData } from '~/concepts/pipelines/content/import/useImportModalData'; import { getProjectDisplayName } from '~/pages/projects/utils'; -import PipelineFileUpload from '~/concepts/pipelines/content/import/PipelineFileUpload'; import { PipelineKFv2, PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes'; import PipelineSelector from '~/concepts/pipelines/content/pipelineSelector/PipelineSelector'; -import { generatePipelineVersionName } from './utils'; +import { PipelineUploadOption, generatePipelineVersionName } from './utils'; +import PipelineUploadRadio from './PipelineUploadRadio'; type PipelineVersionImportModalProps = { existingPipeline?: PipelineKFv2 | null; @@ -29,13 +32,21 @@ const PipelineVersionImportModal: React.FC = ({ const { project, api, apiAvailable } = usePipelinesAPI(); const [importing, setImporting] = React.useState(false); const [error, setError] = React.useState(); - const [{ name, description, pipeline, fileContents }, setData, resetData] = - usePipelineVersionImportModalData(existingPipeline); + const [ + { name, description, pipeline, fileContents, uploadOption, pipelineUrl, codeSource }, + setData, + resetData, + ] = usePipelineVersionImportModalData(existingPipeline); const pipelineId = pipeline?.pipeline_id || ''; const pipelineName = pipeline?.display_name || ''; - const isImportButtonDisabled = !apiAvailable || importing || !name || !fileContents || !pipeline; + const isImportButtonDisabled = + !apiAvailable || + importing || + !name || + !pipeline || + (uploadOption === PipelineUploadOption.URL_IMPORT ? !pipelineUrl : !fileContents); const onBeforeClose = ( pipelineVersion?: PipelineVersionKFv2, @@ -47,6 +58,42 @@ const PipelineVersionImportModal: React.FC = ({ resetData(); }; + const onSubmit = () => { + setImporting(true); + setError(undefined); + + if (uploadOption === PipelineUploadOption.FILE_UPLOAD) { + api + .uploadPipelineVersion({}, name, description, fileContents, pipelineId) + .then((pipelineVersion) => onBeforeClose(pipelineVersion, pipeline)) + .catch((e) => { + setImporting(false); + setError(e); + }); + } else { + api + .createPipelineVersion({}, pipelineId, { + // eslint-disable-next-line camelcase + pipeline_id: pipelineId, + // eslint-disable-next-line camelcase + display_name: name, + description, + // eslint-disable-next-line camelcase + package_url: { + // eslint-disable-next-line camelcase + pipeline_url: pipelineUrl, + }, + // eslint-disable-next-line camelcase + code_source_url: codeSource, + }) + .then((pipelineVersion) => onBeforeClose(pipelineVersion)) + .catch((e) => { + setImporting(false); + setError(e); + }); + } + }; + return ( = ({ variant="primary" isDisabled={isImportButtonDisabled} isLoading={importing} - onClick={() => { - setImporting(true); - setError(undefined); - api - .uploadPipelineVersion({}, name, description, fileContents, pipelineId) - .then((pipelineVersion) => onBeforeClose(pipelineVersion, pipeline)) - .catch((e) => { - setImporting(false); - setError(e); - }); - }} + onClick={onSubmit} data-testid="upload-version-submit-button" > Upload @@ -121,11 +158,40 @@ const PipelineVersionImportModal: React.FC = ({ - setData('fileContents', data)} + setFileContents={(data) => setData('fileContents', data)} + pipelineUrl={pipelineUrl} + setPipelineUrl={(url) => setData('pipelineUrl', url)} + uploadOption={uploadOption} + setUploadOption={(option) => { + setData('uploadOption', option); + // Clear code source when switching between file and url import as this hides the field + setData('codeSource', ''); + }} /> + {uploadOption === PipelineUploadOption.URL_IMPORT && ( + + + setData('codeSource', value)} + /> + + + + Must specify either package url or file .yaml, zip, or .tar.gz + + + + + + )} {error && ( diff --git a/frontend/src/concepts/pipelines/content/import/useImportModalData.ts b/frontend/src/concepts/pipelines/content/import/useImportModalData.ts index fe9b89f51e..cc99218788 100644 --- a/frontend/src/concepts/pipelines/content/import/useImportModalData.ts +++ b/frontend/src/concepts/pipelines/content/import/useImportModalData.ts @@ -1,26 +1,38 @@ import React from 'react'; -import { generatePipelineVersionName } from '~/concepts/pipelines/content/import/utils'; +import { + PipelineUploadOption, + generatePipelineVersionName, +} from '~/concepts/pipelines/content/import/utils'; import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; import useGenericObjectState, { GenericObjectState } from '~/utilities/useGenericObjectState'; type PipelineModalData = { name: string; description: string; + uploadOption: PipelineUploadOption; fileContents: string; + pipelineUrl: string; + codeSource: string; }; export const usePipelineImportModalData = (): GenericObjectState => useGenericObjectState({ name: '', description: '', + uploadOption: PipelineUploadOption.FILE_UPLOAD, fileContents: '', + pipelineUrl: '', + codeSource: '', }); type PipelineVersionModalData = { name: string; description: string; pipeline: PipelineKFv2 | null; + uploadOption: PipelineUploadOption; fileContents: string; + pipelineUrl: string; + codeSource: string; }; export const usePipelineVersionImportModalData = ( @@ -30,7 +42,10 @@ export const usePipelineVersionImportModalData = ( name: React.useMemo(() => generatePipelineVersionName(existingPipeline), [existingPipeline]), description: '', pipeline: existingPipeline ?? null, + uploadOption: PipelineUploadOption.FILE_UPLOAD, fileContents: '', + pipelineUrl: '', + codeSource: '', }); return createDataState; diff --git a/frontend/src/concepts/pipelines/content/import/utils.ts b/frontend/src/concepts/pipelines/content/import/utils.ts index c1254bdbc3..42cb8b6f20 100644 --- a/frontend/src/concepts/pipelines/content/import/utils.ts +++ b/frontend/src/concepts/pipelines/content/import/utils.ts @@ -2,3 +2,11 @@ import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; export const generatePipelineVersionName = (pipeline?: PipelineKFv2 | null): string => pipeline ? `${pipeline.display_name}_version_at_${new Date().toISOString()}` : ''; + +export const COMPILE_PIPELINE_DOCUMENTATION_URL = + 'https://www.kubeflow.org/docs/components/pipelines/v2/compile-a-pipeline/'; + +export enum PipelineUploadOption { + URL_IMPORT, + FILE_UPLOAD, +} diff --git a/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts b/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts index 598a737fb9..faa2b311a2 100644 --- a/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts +++ b/frontend/src/concepts/pipelines/context/usePipelineAPIState.ts @@ -25,6 +25,8 @@ import { uploadPipelineVersion, archivePipelineRun, unarchivePipelineRun, + createPipelineAndVersion, + createPipelineVersion, } from '~/api'; import { PipelineAPIs } from '~/concepts/pipelines/types'; import { APIState } from '~/concepts/proxy/types'; @@ -37,6 +39,8 @@ const usePipelineAPIState = ( ): [apiState: PipelineAPIState, refreshAPIState: () => void] => { const createAPI = React.useCallback( (path: string) => ({ + createPipelineVersion: createPipelineVersion(path), + createPipelineAndVersion: createPipelineAndVersion(path), createExperiment: createExperiment(path), createPipelineRun: createPipelineRun(path), createPipelineRunJob: createPipelineRunJob(path), diff --git a/frontend/src/concepts/pipelines/kfTypes.ts b/frontend/src/concepts/pipelines/kfTypes.ts index 11b29d7dba..6f1c3a1817 100644 --- a/frontend/src/concepts/pipelines/kfTypes.ts +++ b/frontend/src/concepts/pipelines/kfTypes.ts @@ -472,6 +472,19 @@ export type ListPipelineVersionsKF = PipelineKFCallCommon<{ pipeline_versions: PipelineVersionKFv2[]; }>; +export type CreatePipelineAndVersionKFData = { + pipeline: Omit; + pipeline_version: Omit< + PipelineVersionKFv2, + 'pipeline_id' | 'pipeline_version_id' | 'error' | 'created_at' | 'pipeline_spec' + >; +}; + +export type CreatePipelineVersionKFData = Omit< + PipelineVersionKFv2, + 'pipeline_version_id' | 'error' | 'created_at' | 'pipeline_spec' +>; + export type CreatePipelineRunKFData = Omit< PipelineRunKFv2, | 'run_id' diff --git a/frontend/src/concepts/pipelines/types.ts b/frontend/src/concepts/pipelines/types.ts index 685ee49ae0..202c4129b7 100644 --- a/frontend/src/concepts/pipelines/types.ts +++ b/frontend/src/concepts/pipelines/types.ts @@ -14,6 +14,8 @@ import { PipelineCoreResourceKFv2, PipelineRunKFv2, PipelineRunJobKFv2, + CreatePipelineAndVersionKFData, + CreatePipelineVersionKFData, } from './kfTypes'; export type PipelinesFilter = { @@ -40,6 +42,15 @@ export type PipelineListPaged = { items: T[]; }; +export type CreatePipelineVersion = ( + opts: K8sAPIOptions, + pipelineId: string, + data: CreatePipelineVersionKFData, +) => Promise; +export type CreatePipelineAndVersion = ( + opts: K8sAPIOptions, + data: CreatePipelineAndVersionKFData, +) => Promise; export type CreateExperiment = ( opts: K8sAPIOptions, name: string, @@ -118,6 +129,8 @@ export type UploadPipelineVersion = ( ) => Promise; export type PipelineAPIs = { + createPipelineVersion: CreatePipelineVersion; + createPipelineAndVersion: CreatePipelineAndVersion; createExperiment: CreateExperiment; createPipelineRun: CreatePipelineRun; createPipelineRunJob: CreatePipelineRunJob;