diff --git a/chaoscenter/web/src/api/core/experiments/index.ts b/chaoscenter/web/src/api/core/experiments/index.ts index ffd7cc4b0ae..0966ec61328 100644 --- a/chaoscenter/web/src/api/core/experiments/index.ts +++ b/chaoscenter/web/src/api/core/experiments/index.ts @@ -6,3 +6,4 @@ export * from './updateChaosWorkflow'; export * from './stopWorkflow'; export * from './deleteChaosWorkflow'; export * from './saveChaosExperiment'; +export * from './updateCronExperimentState'; diff --git a/chaoscenter/web/src/api/core/experiments/listWorkflowRun.ts b/chaoscenter/web/src/api/core/experiments/listWorkflowRun.ts index 9df1866f9ca..6027cf200f5 100644 --- a/chaoscenter/web/src/api/core/experiments/listWorkflowRun.ts +++ b/chaoscenter/web/src/api/core/experiments/listWorkflowRun.ts @@ -62,6 +62,7 @@ export function listExperimentRunForHistory({ updatedBy { username } + experimentManifest updatedAt resiliencyScore phase diff --git a/chaoscenter/web/src/api/core/experiments/updateCronExperimentState.ts b/chaoscenter/web/src/api/core/experiments/updateCronExperimentState.ts new file mode 100644 index 00000000000..8c63781f464 --- /dev/null +++ b/chaoscenter/web/src/api/core/experiments/updateCronExperimentState.ts @@ -0,0 +1,30 @@ +import { gql, useMutation } from '@apollo/client'; +import type { GqlAPIMutationRequest, GqlAPIMutationResponse } from '@api/types'; + +export interface UpdateCronExperimentStateRequest { + disable: boolean; + projectID: string; + experimentID?: string; +} + +export interface UpdateCronExperimentStateResponse { + updateCronExperimentState: boolean; +} + +export function useUpdateCronExperimentStateMutation( + options?: GqlAPIMutationRequest +): GqlAPIMutationResponse { + const [updateCronExperimentStateMutation, result] = useMutation< + UpdateCronExperimentStateResponse, + UpdateCronExperimentStateRequest + >( + gql` + mutation updateCronExperimentState($experimentID: String!, $disable: Boolean!, $projectID: ID!) { + updateCronExperimentState(experimentID: $experimentID, disable: $disable, projectID: $projectID) + } + `, + options + ); + + return [updateCronExperimentStateMutation, result]; +} diff --git a/chaoscenter/web/src/components/ExperimentActionButtons/ExperimentActionButtons.tsx b/chaoscenter/web/src/components/ExperimentActionButtons/ExperimentActionButtons.tsx index 9889357eaf3..9cb652607d6 100644 --- a/chaoscenter/web/src/components/ExperimentActionButtons/ExperimentActionButtons.tsx +++ b/chaoscenter/web/src/components/ExperimentActionButtons/ExperimentActionButtons.tsx @@ -13,7 +13,13 @@ import { Intent, Position } from '@blueprintjs/core'; import { useHistory } from 'react-router-dom'; import { parse } from 'yaml'; import { useStrings } from '@strings'; -import { listExperiment, runChaosExperiment, stopExperiment, stopExperimentRun } from '@api/core'; +import { + listExperiment, + runChaosExperiment, + stopExperiment, + stopExperimentRun, + useUpdateCronExperimentStateMutation +} from '@api/core'; import { useRouteWithBaseUrl } from '@hooks'; import type { RefetchExperimentRuns, RefetchExperiments } from '@controllers/ExperimentDashboardV2'; import { PermissionGroup, StudioTabs } from '@models'; @@ -351,3 +357,78 @@ export const DownloadExperimentButton = ({ ); }; + +interface EnableDisableCronButtonProps extends ActionButtonProps, Partial { + isCronEnabled: boolean; +} + +export const EnableDisableCronButton = ({ + experimentID, + tooltipProps, + isCronEnabled, + refetchExperiments +}: EnableDisableCronButtonProps): React.ReactElement => { + const scope = getScope(); + const { getString } = useStrings(); + const { showSuccess, showError } = useToaster(); + const { + isOpen: isOpenCronEnableDisableDialog, + open: openCronEnableDisableDialog, + close: closeCronEnableDisableDialog + } = useToggleOpen(); + + const [updateCronExperimentStateMutation] = useUpdateCronExperimentStateMutation({ + onCompleted: () => { + showSuccess(isCronEnabled ? getString('cronHalted') : getString('cronResumed')); + refetchExperiments?.(); + }, + onError: err => showError(err.message) + }); + + const cronEnableDisableDialogProps: ConfirmationDialogProps = { + isOpen: isOpenCronEnableDisableDialog, + contentText: isCronEnabled ? getString('disableCronDesc') : getString('enableCronDesc'), + titleText: isCronEnabled ? `${getString('disableCron')}?` : `${getString('enableCron')}?`, + cancelButtonText: getString('cancel'), + confirmButtonText: getString('confirm'), + intent: Intent.WARNING, + onClose: (isConfirmed: boolean) => { + if (isConfirmed) { + updateCronExperimentStateMutation({ + variables: { + projectID: scope.projectID, + experimentID: experimentID, + disable: isCronEnabled ? true : false + } + }); + } + closeCronEnableDisableDialog(); + } + }; + + const cronEnableDisableDialog = ; + + return ( +
+
+ +
+ {cronEnableDisableDialog} +
+ ); +}; diff --git a/chaoscenter/web/src/components/RightSideBarV2/RightSideBarV2.tsx b/chaoscenter/web/src/components/RightSideBarV2/RightSideBarV2.tsx index d22eb29609d..f98a94b82fd 100644 --- a/chaoscenter/web/src/components/RightSideBarV2/RightSideBarV2.tsx +++ b/chaoscenter/web/src/components/RightSideBarV2/RightSideBarV2.tsx @@ -7,6 +7,7 @@ import { CloneExperimentButton, DownloadExperimentButton, EditExperimentButton, + EnableDisableCronButton, RunExperimentButton, StopExperimentButton, StopExperimentRunButton @@ -21,6 +22,7 @@ interface RightSideBarViewV2Props extends Partial, Partial + {showEnableDisableCronButton && ( + // + + + + + {isCronEnabled ? getString('disableCron') : getString('enableCron')} + + + + )} + {showStopButton ? ( experimentRunID || notifyID ? ( // diff --git a/chaoscenter/web/src/controllers/ChaosStudioEdit/ChaosStudioEdit.tsx b/chaoscenter/web/src/controllers/ChaosStudioEdit/ChaosStudioEdit.tsx index bb9c2129f13..b482ef43902 100644 --- a/chaoscenter/web/src/controllers/ChaosStudioEdit/ChaosStudioEdit.tsx +++ b/chaoscenter/web/src/controllers/ChaosStudioEdit/ChaosStudioEdit.tsx @@ -6,11 +6,12 @@ import { getScope } from '@utils'; import ChaosStudioView from '@views/ChaosStudio'; import { listExperiment, runChaosExperiment, saveChaosExperiment } from '@api/core'; import experimentYamlService from '@services/experiment'; -import { InfrastructureType, RecentExperimentRun } from '@api/entities'; +import { ExperimentType, InfrastructureType, RecentExperimentRun } from '@api/entities'; import Loader from '@components/Loader'; import { useSearchParams, useUpdateSearchParams } from '@hooks'; import RightSideBarV2 from '@components/RightSideBarV2'; import { StudioMode } from '@models'; +import { cronEnabled } from 'utils'; export default function ChaosStudioEditController(): React.ReactElement { const scope = getScope(); @@ -18,6 +19,7 @@ export default function ChaosStudioEditController(): React.ReactElement { const searchParams = useSearchParams(); const updateSearchParams = useUpdateSearchParams(); const hasUnsavedChangesInURL = searchParams.get('unsavedChanges') === 'true'; + const experimentType = searchParams.get('experimentType'); // const [showStudio, setShowStudio] = React.useState(0); @@ -39,6 +41,7 @@ export default function ChaosStudioEditController(): React.ReactElement { )[0]; const [lastExperimentRun, setLastExperimentRun] = React.useState(); + const [isCronEnabled, setIsCronEnabled] = React.useState(); React.useEffect(() => { if (experimentData && showStudio < 2 && !hasUnsavedChangesInURL) { @@ -65,6 +68,10 @@ export default function ChaosStudioEditController(): React.ReactElement { ?.updateExperimentManifest(experimentID, parse(experimentData.experimentManifest)) .then(() => setShowStudio(oldState => oldState + 1)); setLastExperimentRun(experimentData.recentExperimentRunDetails?.[0]); + + const parsedManifest = JSON.parse(experimentData.experimentManifest); + const validateCron = experimentData?.experimentType === ExperimentType.CRON && cronEnabled(parsedManifest); + setIsCronEnabled(validateCron); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [experimentData, experimentID, hasUnsavedChangesInURL]); @@ -78,7 +85,15 @@ export default function ChaosStudioEditController(): React.ReactElement { onCompleted: () => listExperimentRefetch() }); - const rightSideBarV2 = ; + const rightSideBarV2 = ( + + ); return ( diff --git a/chaoscenter/web/src/controllers/ExperimentRunDetails/ExperimentRunDetails.tsx b/chaoscenter/web/src/controllers/ExperimentRunDetails/ExperimentRunDetails.tsx index a6feb46841a..7e785cefc1a 100644 --- a/chaoscenter/web/src/controllers/ExperimentRunDetails/ExperimentRunDetails.tsx +++ b/chaoscenter/web/src/controllers/ExperimentRunDetails/ExperimentRunDetails.tsx @@ -1,9 +1,8 @@ import { useToaster } from '@harnessio/uicore'; import React from 'react'; import { useParams } from 'react-router-dom'; -import type { ExecutionData } from '@api/entities'; -import { ExperimentRunStatus } from '@api/entities'; -import { getScope } from '@utils'; +import { ExecutionData, ExperimentType, ExperimentRunStatus } from '@api/entities'; +import { cronEnabled, getScope } from '@utils'; import ExperimentRunDetailsView from '@views/ExperimentRunDetails'; import RightSideBarV2 from '@components/RightSideBarV2'; import { getExperimentRun } from '@api/core/experiments/getExperimentRun'; @@ -46,11 +45,17 @@ export default function ExperimentRunDetailsController(): React.ReactElement { ? (JSON.parse(specificRunData.executionData) as ExecutionData) : undefined; + const parsedManifest = + specificRunData && specificRunData?.experimentManifest ? JSON.parse(specificRunData.experimentManifest) : undefined; + const isCronEnabled = + specificRunExists && specificRunData?.experimentType === ExperimentType.CRON && cronEnabled(parsedManifest); + const rightSideBarV2 = ( diff --git a/chaoscenter/web/src/controllers/ExperimentRunHistory/ExperimentRunHistory.tsx b/chaoscenter/web/src/controllers/ExperimentRunHistory/ExperimentRunHistory.tsx index d8c3ac2188d..8c1602302e9 100644 --- a/chaoscenter/web/src/controllers/ExperimentRunHistory/ExperimentRunHistory.tsx +++ b/chaoscenter/web/src/controllers/ExperimentRunHistory/ExperimentRunHistory.tsx @@ -4,10 +4,10 @@ import { useParams } from 'react-router-dom'; import { Color } from '@harnessio/design-system'; import { isEqual } from 'lodash-es'; import { listExperimentRunForHistory } from '@api/core'; -import { getScope, getColorBasedOnResilienceScore } from '@utils'; +import { getScope, getColorBasedOnResilienceScore, cronEnabled } from '@utils'; import ExperimentRunHistoryView from '@views/ExperimentRunHistory'; import { useStrings } from '@strings'; -import type { ExperimentRun } from '@api/entities'; +import { ExperimentRun, ExperimentType } from '@api/entities'; import type { ColumnData } from '@components/ColumnChart/ColumnChart.types'; import { initialExperimentRunFilterState, @@ -119,6 +119,7 @@ export default function ExperimentRunHistoryController(): React.ReactElement { const experimentName = experimentRunsWithExecutionData?.[0]?.experimentName; const experimentPhase = experimentRunsWithExecutionData?.[0]?.phase; const experimentType = experimentRunsWithExecutionData?.[0]?.experimentType; + const experimentManifest = experimentRunsWithExecutionData?.[0]?.experimentManifest; React.useEffect(() => { if (experimentName) setExperimentNamePersistent(experimentName); @@ -149,11 +150,17 @@ export default function ExperimentRunHistoryController(): React.ReactElement { const areFiltersSet = !(isEqual(state, initialExperimentRunFilterState) && page === 0); + const parsedManifest = experimentManifest && JSON.parse(experimentManifest); + + const isCronEnabled = + experimentRunsWithExecutionData && experimentType === ExperimentType.CRON && cronEnabled(parsedManifest); + const rightSideBarV2 = ( ); diff --git a/chaoscenter/web/src/strings/strings.en.yaml b/chaoscenter/web/src/strings/strings.en.yaml index 0684e4e7682..3ae8e473ff9 100644 --- a/chaoscenter/web/src/strings/strings.en.yaml +++ b/chaoscenter/web/src/strings/strings.en.yaml @@ -209,6 +209,15 @@ createdOn: Created On creationTime: Creation Time criteria: Criteria criteriaForData: Criteria for data +cron: Cron +cronDisabled: Cron Schedule is disabled +cronHalted: This cron experiment as been halted successfully. +cronResumed: This cron experiment has re-scheduled successfully. +disableCron: Disable Cron +disableCronDesc: This will disable the cron schedule for this experiment. +enableCron: Enable Cron +enableCronDesc: This will enable the cron schedule for this experiment. +nonCron: Non-Cron cronExpression: Cron Expression cronExpressionRequired: Cron Expression required cronSelectOption: Cron (Recurring run) diff --git a/chaoscenter/web/src/strings/types.ts b/chaoscenter/web/src/strings/types.ts index 8f7e64999f7..35e4909223a 100644 --- a/chaoscenter/web/src/strings/types.ts +++ b/chaoscenter/web/src/strings/types.ts @@ -177,8 +177,12 @@ export interface StringsMap { 'creationTime': unknown 'criteria': unknown 'criteriaForData': unknown + 'cron': unknown + 'cronDisabled': unknown 'cronExpression': unknown 'cronExpressionRequired': unknown + 'cronHalted': unknown + 'cronResumed': unknown 'cronSelectOption': unknown 'cronText': unknown 'currentRun': unknown @@ -219,6 +223,8 @@ export interface StringsMap { 'detailsAndProperties': unknown 'disable': unknown 'disableChaosInfrastructure': unknown + 'disableCron': unknown + 'disableCronDesc': unknown 'disableUser': unknown 'disableUserDescription': unknown 'discard': unknown @@ -260,6 +266,8 @@ export interface StringsMap { 'enableChaosInfraButton': unknown 'enableChaosInfrastructure': unknown 'enableChaosInfrastructureDesc': unknown + 'enableCron': unknown + 'enableCronDesc': unknown 'enableImageRegistryChanges': unknown 'enableSSLCheck': unknown 'enableUser': unknown @@ -569,6 +577,7 @@ export interface StringsMap { 'nodeSelectorPlaceholderForKey': unknown 'nodeSelectorPlaceholderForValue': unknown 'nodeSelectorText': unknown + 'nonCron': unknown 'nonCronSelectOption': unknown 'nonCronText': unknown 'nonProd': unknown diff --git a/chaoscenter/web/src/utils/yamlUtils.ts b/chaoscenter/web/src/utils/yamlUtils.ts index a27d507adae..8b233a0f664 100644 --- a/chaoscenter/web/src/utils/yamlUtils.ts +++ b/chaoscenter/web/src/utils/yamlUtils.ts @@ -1,4 +1,5 @@ import { CreateNodeOptions, Document, DocumentOptions, ParseOptions, SchemaOptions } from 'yaml'; +import type { CronWorkflow } from '@models'; // https://github.com/eemeli/yaml/issues/211 export function yamlStringify( @@ -39,3 +40,11 @@ export const downloadYamlAsFile = async (yamlResponse: any, fileName: string): P return { status: false }; } }; + +export function cronEnabled(workflowManifest: CronWorkflow) { + if ((workflowManifest as CronWorkflow)?.spec?.suspend === undefined) { + return true; + } else if (workflowManifest && (workflowManifest as CronWorkflow)?.spec?.suspend !== undefined) { + return !(workflowManifest as CronWorkflow)?.spec?.suspend; + } +}