diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts index 7cf99e7a67..6b85f6647e 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts @@ -1,5 +1,6 @@ import { Contextual } from '~/__tests__/cypress/cypress/pages/components/Contextual'; import { DashboardCodeEditor } from '~/__tests__/cypress/cypress/pages/components/DashboardCodeEditor'; +import type { PipelineRecurringRunKFv2 } from '~/concepts/pipelines/kfTypes'; class TaskDrawer extends Contextual { findInputArtifacts() { @@ -194,6 +195,36 @@ class PipelineRecurringRunDetails extends RunDetails { selectActionDropdownItem(label: string) { this.findActionsDropdown().click().findByRole('menuitem', { name: label }).click(); } + + mockEnableRecurringRun(recurringRun: PipelineRecurringRunKFv2, namespace: string) { + return cy.interceptOdh( + 'POST /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/recurringruns/:recurringRunId:mode', + { + path: { + namespace, + serviceName: 'dspa', + recurringRunId: recurringRun.recurring_run_id, + mode: ':enable', + }, + }, + { data: {} }, + ); + } + + mockDisableRecurringRun(recurringRun: PipelineRecurringRunKFv2, namespace: string) { + return cy.interceptOdh( + 'POST /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/recurringruns/:recurringRunId:mode', + { + path: { + namespace, + serviceName: 'dspa', + recurringRunId: recurringRun.recurring_run_id, + mode: ':disable', + }, + }, + { data: {} }, + ); + } } class PipelineRunDetails extends RunDetails { diff --git a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts index e1637fb50c..b8ec8946ef 100644 --- a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts +++ b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts @@ -400,6 +400,18 @@ declare global { options: { path: { namespace: string; serviceName: string; recurringRunId: string } }, response: OdhResponse, ) => Cypress.Chainable) & + (( + type: `POST /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/recurringruns/:recurringRunId:mode`, + options: { + path: { + namespace: string; + serviceName: string; + recurringRunId: string; + mode: string; + }; + }, + response: OdhResponse<{ data: object }>, + ) => Cypress.Chainable) & (( type: `GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/recurringruns/:recurringRunId`, options: { path: { namespace: string; serviceName: string; recurringRunId: string } }, diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelinesTopology.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelinesTopology.cy.ts index 5da16f660e..d463fcd892 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelinesTopology.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelinesTopology.cy.ts @@ -29,6 +29,7 @@ import { SecretModel, } from '~/__tests__/cypress/cypress/utils/models'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; +import { RecurringRunStatus } from '~/concepts/pipelines/kfTypes'; const projectId = 'test-project'; const mockPipeline = buildMockPipelineV2({ @@ -508,6 +509,63 @@ describe('Pipeline topology', () => { }); }); + describe('Pipeline recurring run details', () => { + const mockDisabledRecurringRun = { ...mockRecurringRun, status: RecurringRunStatus.DISABLED }; + + beforeEach(() => { + initIntercepts(); + }); + + it('disables recurring run from action dropdown', () => { + pipelineRecurringRunDetails.mockDisableRecurringRun(mockRecurringRun, projectId); + pipelineRecurringRunDetails.visit( + projectId, + mockRecurringRun.recurring_run_id, + mockVersion.pipeline_version_id, + mockRecurringRun.recurring_run_id, + ); + + pipelineRecurringRunDetails.findActionsDropdown(); + pipelineRecurringRunDetails.selectActionDropdownItem('Disable'); + + pipelineRecurringRunDetails + .findActionsDropdown() + .click() + .findByRole('menuitem', { name: 'Enable' }) + .should('be.visible'); + }); + + it('enables recurring run from action dropdown', () => { + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/recurringruns/:recurringRunId', + { + path: { + namespace: projectId, + serviceName: 'dspa', + recurringRunId: mockDisabledRecurringRun.recurring_run_id, + }, + }, + mockDisabledRecurringRun, + ); + pipelineRecurringRunDetails.mockEnableRecurringRun(mockDisabledRecurringRun, projectId); + pipelineRecurringRunDetails.visit( + projectId, + mockDisabledRecurringRun.recurring_run_id, + mockVersion.pipeline_version_id, + mockRecurringRun.recurring_run_id, + ); + + pipelineRecurringRunDetails.findActionsDropdown(); + pipelineRecurringRunDetails.selectActionDropdownItem('Enable'); + + pipelineRecurringRunDetails + .findActionsDropdown() + .click() + .findByRole('menuitem', { name: 'Disable' }) + .should('be.visible'); + }); + }); + describe('Pipeline run Input/Output', () => { beforeEach(() => { initIntercepts(); diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRecurringRun/PipelineRecurringRunDetailsActions.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRecurringRun/PipelineRecurringRunDetailsActions.tsx index a5d31342e7..9b46350c3d 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRecurringRun/PipelineRecurringRunDetailsActions.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRecurringRun/PipelineRecurringRunDetailsActions.tsx @@ -1,13 +1,16 @@ -import * as React from 'react'; +import React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + import { Dropdown, DropdownItem, DropdownSeparator, DropdownToggle, } from '@patternfly/react-core/deprecated'; -import { useNavigate, useParams } from 'react-router-dom'; +import { Spinner } from '@patternfly/react-core'; + import { usePipelinesAPI } from '~/concepts/pipelines/context'; -import { PipelineRecurringRunKFv2 } from '~/concepts/pipelines/kfTypes'; +import { PipelineRecurringRunKFv2, RecurringRunStatus } from '~/concepts/pipelines/kfTypes'; import { cloneRecurringRunRoute } from '~/routes'; import { useIsAreaAvailable, SupportedArea } from '~/concepts/areas'; @@ -21,10 +24,46 @@ const PipelineRecurringRunDetailsActions: React.FC { const navigate = useNavigate(); - const { namespace } = usePipelinesAPI(); - const [open, setOpen] = React.useState(false); const { experimentId, pipelineId, pipelineVersionId } = useParams(); + const { namespace, api, refreshAllAPI } = usePipelinesAPI(); const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; + const [open, setOpen] = React.useState(false); + const [isEnabled, setIsEnabled] = React.useState( + recurringRun?.status === RecurringRunStatus.ENABLED, + ); + const [isStatusUpdating, setIsStatusUpdating] = React.useState(false); + + const updateStatus = React.useCallback(async () => { + if (recurringRun?.recurring_run_id) { + try { + setIsStatusUpdating(true); + + await api.updatePipelineRecurringRun({}, recurringRun.recurring_run_id, !isEnabled); + + refreshAllAPI(); + setIsEnabled((prevValue) => !prevValue); + setIsStatusUpdating(false); + } catch (e) { + setIsStatusUpdating(false); + } + } + }, [api, isEnabled, recurringRun?.recurring_run_id, refreshAllAPI]); + + const updateStatusActionLabel = React.useMemo(() => { + if (isStatusUpdating) { + if (isEnabled) { + return 'Disabling...'; + } + + return 'Enabling...'; + } + + if (isEnabled) { + return 'Disable'; + } + + return 'Enable'; + }, [isEnabled, isStatusUpdating]); return ( , + tooltip: 'Updating status...', + })} + > + {updateStatusActionLabel} + , diff --git a/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx b/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx index 6628b320b6..d6bbf208df 100644 --- a/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx +++ b/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx @@ -17,7 +17,7 @@ import { PipelineRunKFv2, runtimeStateLabels, PipelineRecurringRunKFv2, - RecurringRunMode, + RecurringRunStatus as RecurringRunStatusType, } from '~/concepts/pipelines/kfTypes'; import { getRunDuration, @@ -136,7 +136,7 @@ export const RecurringRunStatus: RecurringRunUtil<{ const [isChangingFlag, setIsChangingFlag] = React.useState(false); const isExperimentArchived = useContextExperimentArchived(); - const isEnabled = recurringRun.mode === RecurringRunMode.ENABLE; + const isEnabled = recurringRun.status === RecurringRunStatusType.ENABLED; React.useEffect(() => { // When the network updates, if we are currently locked fetching, disable it so we can accept the change setIsChangingFlag((v) => (v ? false : v)); diff --git a/frontend/src/concepts/pipelines/kfTypes.ts b/frontend/src/concepts/pipelines/kfTypes.ts index 3a3b9cda23..64bd3aa17e 100644 --- a/frontend/src/concepts/pipelines/kfTypes.ts +++ b/frontend/src/concepts/pipelines/kfTypes.ts @@ -126,8 +126,8 @@ export enum RecurringRunMode { export enum RecurringRunStatus { STATUS_UNSPECIFIED = 'STATUS_UNSPECIFIED', - ENABLED = 'ENABLE', - DISABLED = 'DISABLE', + ENABLED = 'ENABLED', + DISABLED = 'DISABLED', } export enum StorageStateKF {