diff --git a/src/lib/components/workflow/client-actions/batch-cancel-confirmation-modal.svelte b/src/lib/components/workflow/client-actions/batch-cancel-confirmation-modal.svelte index 506a0f64d..286ea2e5d 100644 --- a/src/lib/components/workflow/client-actions/batch-cancel-confirmation-modal.svelte +++ b/src/lib/components/workflow/client-actions/batch-cancel-confirmation-modal.svelte @@ -15,6 +15,7 @@ import { batchCancelWorkflows } from '$lib/services/batch-service'; import { authUser } from '$lib/stores/auth-user'; import { toaster } from '$lib/stores/toaster'; + import { workflowsQuery } from '$lib/stores/workflows'; import { isNetworkError } from '$lib/utilities/is-network-error'; import { getPlacholder } from '$lib/utilities/workflow-actions'; @@ -29,7 +30,7 @@ let jobIdPlaceholder = v4(); let error = ''; - const { allSelected, cancelableWorkflows, query } = + const { allSelected, cancelableWorkflows } = getContext(BATCH_OPERATION_CONTEXT); const resetForm = () => { @@ -48,7 +49,7 @@ reason: $reason || reasonPlaceholder, jobId: $jobId || jobIdPlaceholder, ...($allSelected - ? { query: $query } + ? { query: $workflowsQuery } : { workflows: $cancelableWorkflows }), }; try { diff --git a/src/lib/components/workflow/client-actions/batch-operation-confirmation-form.svelte b/src/lib/components/workflow/client-actions/batch-operation-confirmation-form.svelte index 32ac1e06b..7da9eca95 100644 --- a/src/lib/components/workflow/client-actions/batch-operation-confirmation-form.svelte +++ b/src/lib/components/workflow/client-actions/batch-operation-confirmation-form.svelte @@ -1,4 +1,6 @@ -
+
{#if $allSelected}

- {$query} + {$workflowsQuery}

@@ -57,12 +85,18 @@ {:else}

- + {#if action === Action.Reset} + + {:else} + + {/if}

{/if} +
diff --git a/src/lib/components/workflow/client-actions/batch-reset-confirmation-modal.svelte b/src/lib/components/workflow/client-actions/batch-reset-confirmation-modal.svelte new file mode 100644 index 000000000..4ad022135 --- /dev/null +++ b/src/lib/components/workflow/client-actions/batch-reset-confirmation-modal.svelte @@ -0,0 +1,116 @@ + + + +

+ +

+ + + + + + + + +
diff --git a/src/lib/components/workflow/client-actions/batch-terminate-confirmation-modal.svelte b/src/lib/components/workflow/client-actions/batch-terminate-confirmation-modal.svelte index 6c5aae777..dad386033 100644 --- a/src/lib/components/workflow/client-actions/batch-terminate-confirmation-modal.svelte +++ b/src/lib/components/workflow/client-actions/batch-terminate-confirmation-modal.svelte @@ -15,6 +15,7 @@ import { batchTerminateWorkflows } from '$lib/services/batch-service'; import { authUser } from '$lib/stores/auth-user'; import { toaster } from '$lib/stores/toaster'; + import { workflowsQuery } from '$lib/stores/workflows'; import { isNetworkError } from '$lib/utilities/is-network-error'; import { getPlacholder } from '$lib/utilities/workflow-actions'; @@ -23,13 +24,13 @@ export let namespace: string; export let open: boolean; const reason = writable(''); - const reasonPlaceholder = getPlacholder(Action.Cancel, $authUser.email); + const reasonPlaceholder = getPlacholder(Action.Terminate, $authUser.email); const jobId = writable(''); const jobIdValid = writable(true); let jobIdPlaceholder = v4(); let error = ''; - const { allSelected, terminableWorkflows, query } = + const { allSelected, terminableWorkflows } = getContext(BATCH_OPERATION_CONTEXT); const resetForm = () => { @@ -49,7 +50,7 @@ reason: $reason || reasonPlaceholder, jobId: $jobId || jobIdPlaceholder, ...($allSelected - ? { query: $query } + ? { query: $workflowsQuery } : { workflows: $terminableWorkflows }), }; await batchTerminateWorkflows(options); diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/batch-actions.svelte b/src/lib/components/workflow/workflows-summary-configurable-table/batch-actions.svelte index 8b77648ce..54924833e 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table/batch-actions.svelte +++ b/src/lib/components/workflow/workflows-summary-configurable-table/batch-actions.svelte @@ -3,7 +3,7 @@ import { page } from '$app/stores'; - import BulkActionButton from '$lib/holocene/table/bulk-action-button.svelte'; + import Button from '$lib/holocene/button.svelte'; import { translate } from '$lib/i18n/translate'; import Translate from '$lib/i18n/translate.svelte'; import { @@ -11,9 +11,12 @@ type BatchOperationContext, } from '$lib/pages/workflows-with-new-search.svelte'; import { coreUserStore } from '$lib/stores/core-user'; - import { workflowCount } from '$lib/stores/workflows'; + import { temporalVersion } from '$lib/stores/versions'; + import { workflowCount, workflowsQuery } from '$lib/stores/workflows'; import type { WorkflowExecution } from '$lib/types/workflows'; + import { minimumVersionRequired } from '$lib/utilities/version-check'; import { workflowCancelEnabled } from '$lib/utilities/workflow-cancel-enabled'; + import { workflowResetEnabled } from '$lib/utilities/workflow-reset-enabled'; import { workflowTerminateEnabled } from '$lib/utilities/workflow-terminate-enabled'; export let workflows: WorkflowExecution[]; @@ -25,6 +28,7 @@ cancelableWorkflows, openBatchCancelConfirmationModal, openBatchTerminateConfirmationModal, + openBatchResetConfirmationModal, } = getContext(BATCH_OPERATION_CONTEXT); let coreUser = coreUserStore(); @@ -36,6 +40,9 @@ $: terminateEnabled = workflowTerminateEnabled($page.data.settings); $: cancelEnabled = workflowCancelEnabled($page.data.settings); + $: resetEnabled = + workflowResetEnabled($page.data.settings) && + minimumVersionRequired('1.23', $temporalVersion); $: namespaceWriteDisabled = $coreUser.namespaceWriteDisabled( $page.params.namespace, ); @@ -52,34 +59,51 @@ count={selectedWorkflowsCount} /> - - (or ) - + {#if $workflowsQuery} + + ({translate('workflows.select-all-leading')} + + {translate('workflows.select-all-trailing')}) + + {/if} {/if}
{#if cancelEnabled} - {translate('workflows.request-cancellation')}{translate('workflows.request-cancellation')} + {/if} + {#if resetEnabled} + {/if} {#if terminateEnabled} - {translate('workflows.terminate')}{translate('workflows.terminate')} {/if}
diff --git a/src/lib/holocene/button.svelte b/src/lib/holocene/button.svelte index bb74aa2bc..87f1fe121 100644 --- a/src/lib/holocene/button.svelte +++ b/src/lib/holocene/button.svelte @@ -38,11 +38,11 @@ secondary: 'border-secondary text-primary focus-visible:shadow-focus focus-visible:shadow-primary/70 hover:surface-interactive-secondary hover:border-interactive-secondary dark:hover:border-transparent focus-visible:surface-interactive-secondary focus-visible:border-white dark:focus-visible:border-black', destructive: - 'border-danger bg-danger hover:bg-red-400 hover:border-red-400 focus-visible:shadow-focus focus-visible:shadow-danger/50 focus-visible:border-white dark:focus-visible:border-red-400/50 dark:focus-visible:bg-red-400', + 'border-danger bg-danger text-primary hover:bg-red-400 hover:border-red-400 focus-visible:shadow-focus focus-visible:shadow-danger/50 focus-visible:border-white dark:focus-visible:border-red-400/50 dark:focus-visible:bg-red-400', ghost: 'border-transparent bg-transparent text-primary hover:surface-interactive-secondary focus-visible:border-white dark:hover:border-black dark:focus-visible:border-black focus-visible:shadow-focus focus-visible:shadow-secondary/70 focus-visible:surface-interactive-secondary ', 'table-header': - 'border-transparent text-white focus-visible:shadow-focus focus-visible:shadow-primary/50 focus-visible:border-white', + 'border-inverse surface-inverse hover:surface-primary focus-visible:shadow-focus focus-visible:shadow-primary/50 focus-visible:border-white', }, size: { xs: 'h-8 text-xs px-2 py-1', diff --git a/src/lib/holocene/table/bulk-action-button.svelte b/src/lib/holocene/table/bulk-action-button.svelte deleted file mode 100644 index ba1f9f5fb..000000000 --- a/src/lib/holocene/table/bulk-action-button.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/src/lib/i18n/locales/en/workflows.ts b/src/lib/i18n/locales/en/workflows.ts index 1cf6fbf69..d7095012a 100644 --- a/src/lib/i18n/locales/en/workflows.ts +++ b/src/lib/i18n/locales/en/workflows.ts @@ -13,6 +13,7 @@ export const Strings = { terminate: 'Terminate', 'batch-terminate-modal-title': 'Terminate Workflows', 'batch-cancel-modal-title': 'Cancel Workflows', + 'batch-reset-modal-title': 'Reset Workflows', 'workflow-action-reason-placeholder': '{{action}} from the Web UI', 'workflow-action-reason-placeholder-with-email': '{{action}} from the Web UI by {{email}}', @@ -20,20 +21,22 @@ export const Strings = { 'Are you sure you want to {{action}} all workflows matching the following query? This action cannot be undone.', 'batch-operation-count-disclaimer': 'Note: The actual count of workflows that will be affected is the total number of running workflows matching this query at the time of clicking "{{action}}".', - 'batch-cancel-confirmation_one': - 'Are you sure you want to cancel {{count, number}} running workflow?', - 'batch-cancel-confirmation_other': - 'Are you sure you want to cancel {{count, number}} running workflows?', - 'batch-terminate-confirmation_one': - 'Are you sure you want to terminate {{count, number}} running workflow?', - 'batch-terminate-confirmation_other': - 'Are you sure you want to terminate {{count, number}} running workflows?', + 'batch-confirmation_one': + 'Are you sure you want to {{action}} one running workflow?', + 'batch-confirmation_other': + 'Are you sure you want to {{action}} {{count, number}} running workflows?', + 'batch-reset-confirmation_one': + 'Are you sure you want to reset one workflow?', + 'batch-reset-confirmation_other': + 'Are you sure you want to reset {{count, number}} workflows?', 'batch-operation-confirmation-input-hint': 'If you supply a custom reason, "{{placeholder}}" will be appended to it.', 'batch-terminate-all-success': 'The batch terminate request is processing in the background.', 'batch-cancel-all-success': 'The batch cancel request is processing in the background.', + 'batch-reset-all-success': + 'The batch reset request is processing in the background.', 'configure-workflows': 'Configure Workflow List', 'open-configure-workflows': 'Open workflow list configuration', 'close-configure-workflows': 'Close workflow list configuration', @@ -53,7 +56,9 @@ export const Strings = { signal: 'Send a Signal', 'n-selected': '{{count, number}} selected', 'all-selected': 'All {{count, number}} selected.', - 'select-all': 'select all {{count, number}}', + 'select-all-leading': 'or ', + 'select-all': 'select all {{count, number}} workflows', + 'select-all-trailing': ' matching your query', 'request-cancellation': 'Request Cancellation', 'back-to-workflows': 'Back to Workflows', input: 'Input', diff --git a/src/lib/i18n/translate.svelte b/src/lib/i18n/translate.svelte index 5c61696a4..819425c4c 100644 --- a/src/lib/i18n/translate.svelte +++ b/src/lib/i18n/translate.svelte @@ -12,7 +12,7 @@ export let count: number = undefined; export let replace: I18nReplace = undefined; - $: translated = translate(key, { count, ...replace }); + $: translated = translate(key, { ...replace, count }); {#if translated !== key} diff --git a/src/lib/i18n/translate.test.ts b/src/lib/i18n/translate.test.ts index e7b64632a..823596c55 100644 --- a/src/lib/i18n/translate.test.ts +++ b/src/lib/i18n/translate.test.ts @@ -32,7 +32,7 @@ describe('translate', () => { translate('common.loading', { foo: 'bar' }); expect(i18next.t).toHaveBeenCalledWith('common:loading', { - replace: { foo: 'bar' }, + foo: 'bar', }); }); @@ -56,8 +56,8 @@ describe('translate', () => { translate('common.loading', { count: 10, foo: 'bar' }); expect(i18next.t).toHaveBeenCalledWith('common:loading', { + foo: 'bar', count: 10, - replace: { foo: 'bar' }, }); }); }); diff --git a/src/lib/i18n/translate.ts b/src/lib/i18n/translate.ts index fbedf05ad..2a9f87b05 100644 --- a/src/lib/i18n/translate.ts +++ b/src/lib/i18n/translate.ts @@ -1,8 +1,4 @@ -import { is_empty } from 'svelte/internal'; - -import { t, type TOptions } from 'i18next'; - -import { omit } from '$lib/utilities/omit'; +import { t } from 'i18next'; import type { I18nKey, I18nReplace, I18nResources } from '.'; @@ -11,19 +7,10 @@ const translateGeneric = ( replace: I18nReplace = {}, ): string => { const [namespace, ...keys] = key.split('.'); - const options: TOptions = {}; - - if (replace && replace.count !== undefined) { - options.count = replace.count; - } - - if (!is_empty(omit(replace, 'count'))) { - options.replace = omit(replace, 'count'); - } if (namespace && keys.length > 0) { const k = keys.join('.'); - return t(`${namespace}:${k}`, options); + return t(`${namespace}:${k}`, replace); } }; diff --git a/src/lib/pages/workflows-with-new-search.svelte b/src/lib/pages/workflows-with-new-search.svelte index b18dc626c..f0ffec2da 100644 --- a/src/lib/pages/workflows-with-new-search.svelte +++ b/src/lib/pages/workflows-with-new-search.svelte @@ -12,9 +12,9 @@ cancelableWorkflows: Readable; selectedWorkflows: Writable; batchActionsVisible: Readable; - query: Readable; openBatchCancelConfirmationModal: () => void; openBatchTerminateConfirmationModal: () => void; + openBatchResetConfirmationModal: () => void; handleSelectAll: (workflows: WorkflowExecution[]) => void; handleSelectPage: ( checked: boolean, @@ -31,8 +31,10 @@ import { page } from '$app/stores'; import BatchCancelConfirmationModal from '$lib/components/workflow/client-actions/batch-cancel-confirmation-modal.svelte'; + import BatchResetConfirmationModal from '$lib/components/workflow/client-actions/batch-reset-confirmation-modal.svelte'; import BatchTerminateConfirmationModal from '$lib/components/workflow/client-actions/batch-terminate-confirmation-modal.svelte'; import CancelConfirmationModal from '$lib/components/workflow/client-actions/cancel-confirmation-modal.svelte'; + import ResetConfirmationModal from '$lib/components/workflow/client-actions/reset-confirmation-modal.svelte'; import TerminateConfirmationModal from '$lib/components/workflow/client-actions/terminate-confirmation-modal.svelte'; import WorkflowFilterSearch from '$lib/components/workflow/filter-search/index.svelte'; import WorkflowCountRefresh from '$lib/components/workflow/workflow-count-refresh.svelte'; @@ -80,12 +82,10 @@ let batchTerminateConfirmationModalOpen = false; let batchCancelConfirmationModalOpen = false; + let batchResetConfirmationModalOpen = false; let terminateConfirmationModalOpen = false; let cancelConfirmationModalOpen = false; - const batchOperationQuery = derived( - workflowsQuery, - (query) => query ?? 'ExecutionStatus="Running"', - ); + let resetConfirmationModalOpen = false; const allSelected = writable(false); const pageSelected = writable(false); const selectedWorkflows = writable([]); @@ -114,6 +114,12 @@ : (terminateConfirmationModalOpen = true); }; + const openBatchResetConfirmationModal = () => { + $selectedWorkflows.length > 1 + ? (batchResetConfirmationModalOpen = true) + : (resetConfirmationModalOpen = true); + }; + const handleSelectAll = (workflows: WorkflowExecution[]) => { allSelected.set(true); selectedWorkflows.set([...workflows]); @@ -141,9 +147,9 @@ batchActionsVisible, openBatchCancelConfirmationModal, openBatchTerminateConfirmationModal, + openBatchResetConfirmationModal, handleSelectAll, handleSelectPage, - query: batchOperationQuery, }); $: { @@ -163,6 +169,11 @@ bind:open={batchCancelConfirmationModalOpen} /> + + + +
diff --git a/src/lib/services/batch-service.ts b/src/lib/services/batch-service.ts index 0584108a5..0ae806c32 100644 --- a/src/lib/services/batch-service.ts +++ b/src/lib/services/batch-service.ts @@ -33,6 +33,7 @@ type CreateBatchOperationOptions = { jobId: string; query?: string; workflows?: WorkflowExecution[]; + resetType?: 'first' | 'last'; }; type DescribeBatchOperationOptions = { @@ -56,10 +57,8 @@ const queryFromWorkflows = ( const batchActionToOperation = ( action: Action, -): Pick< - StartBatchOperationRequest, - 'cancellationOperation' | 'terminationOperation' -> => { + resetType?: 'first' | 'last', +): StartBatchOperationRequest => { const identity = getAuthUser().email; switch (action) { @@ -71,8 +70,15 @@ const batchActionToOperation = ( return { terminationOperation: { identity }, }; - default: - return {}; + case Action.Reset: { + const options = + resetType === 'first' + ? { firstWorkflowTask: {} } + : { lastWorkflowTask: {} }; + return { + resetOperation: { identity, options }, + }; + } } }; @@ -89,7 +95,7 @@ const createBatchOperationRequest = ( jobId: options.jobId, namespace: options.namespace, reason: options.reason, - ...batchActionToOperation(action), + ...batchActionToOperation(action, options.resetType), }; if (options.workflows) { @@ -163,6 +169,30 @@ export async function batchTerminateWorkflows( }); } +export const batchResetWorkflows = async ( + options: CreateBatchOperationOptions, +): Promise => { + const route = routeForApi('batch-operations', { + namespace: options.namespace, + batchJobId: options.jobId, + }); + + const body = createBatchOperationRequest(Action.Reset, options); + + await requestFromAPI(route, { + options: { + method: 'POST', + body: stringifyWithBigInt(body), + }, + notifyOnError: false, + }); + + inProgressBatchOperation.set({ + jobId: body.jobId, + namespace: body.namespace, + }); +}; + export async function pollBatchOperation({ namespace, jobId, diff --git a/src/lib/utilities/version-check.test.ts b/src/lib/utilities/version-check.test.ts index 0d68a514b..a200235ba 100644 --- a/src/lib/utilities/version-check.test.ts +++ b/src/lib/utilities/version-check.test.ts @@ -39,6 +39,10 @@ describe('minimumVersionRequired', () => { expect(minimumVersionRequired('1', '2')).toBe(true); expect(minimumVersionRequired('1', '1.20')).toBe(true); }); + it('should return true when current release candidate version is equal to or higher than minimum', () => { + expect(minimumVersionRequired('1.23', '1.23.0-rc.19')).toBe(true); + }); + it('should return false when current version is less than than minimum', () => { expect(minimumVersionRequired('1.20.1', '1.20.0')).toBe(false); expect(minimumVersionRequired('1.20.0', '1.19.0')).toBe(false); diff --git a/src/lib/utilities/version-check.ts b/src/lib/utilities/version-check.ts index 39e78e62c..d04cff0fd 100644 --- a/src/lib/utilities/version-check.ts +++ b/src/lib/utilities/version-check.ts @@ -37,7 +37,8 @@ export const minimumVersionRequired = ( return minor2 > minor1; } else { if (patch1 === undefined && !!patch2) return true; - if (patch1 === undefined && patch2 === undefined) return true; + if (patch1 === undefined && (patch2 === undefined || isNaN(patch2))) + return true; return patch2 >= patch1; } }; diff --git a/tests/integration/workflow-bulk-actions.spec.ts b/tests/integration/workflow-bulk-actions.spec.ts index 23b848e59..a204eaf45 100644 --- a/tests/integration/workflow-bulk-actions.spec.ts +++ b/tests/integration/workflow-bulk-actions.spec.ts @@ -37,6 +37,9 @@ test.describe('Batch and Bulk Workflow Actions', () => { test('allows running workflows to be terminated by a query', async ({ page, }) => { + await page.getByTestId('manual-search-toggle').click(); + await page.locator('#manual-search').fill('ExecutionStatus="Running"'); + await page.getByTestId('manual-search-button').click(); await page.getByTestId('batch-actions-checkbox').click(); await page.click('[data-testid="select-all-workflows"]'); await page.click('[data-testid="bulk-terminate-button"]'); @@ -74,6 +77,9 @@ test.describe('Batch and Bulk Workflow Actions', () => { test('allows running workflows to be cancelled by a query', async ({ page, }) => { + await page.getByTestId('manual-search-toggle').click(); + await page.locator('#manual-search').fill('ExecutionStatus="Running"'); + await page.getByTestId('manual-search-button').click(); await page.getByTestId('batch-actions-checkbox').click(); await page.click('[data-testid="select-all-workflows"]'); await page.click('[data-testid="bulk-cancel-button"]');