Skip to content

Commit

Permalink
DT-143 - batch cancel workflows on Recent Workflows page (#959)
Browse files Browse the repository at this point in the history
* DT-143 - batch cancel

* fix cy tests

* use old batch api endpoints for versoins !> 2.9.0

* fix cy tests

* fix some layout issues

* change cancel single workflow to opt in

* remove extra th and fix filter indicator animation

Co-authored-by: Alex Tideman <alex.tideman@gmail.com>
  • Loading branch information
rossedfort and Alex-Tideman committed Dec 5, 2022
1 parent cd9d776 commit 6a0953e
Show file tree
Hide file tree
Showing 16 changed files with 385 additions and 163 deletions.
60 changes: 40 additions & 20 deletions cypress/integration/workflow-bulk-actions.spec.js
@@ -1,20 +1,23 @@
/// <reference types="cypress" />

describe('Bulk Terminate', () => {
it("disallows bulk actions for cluster that doesn't have elasticsearch enabled", () => {
cy.interceptApi();
describe('Batch and Bulk Workflow Actions', () => {
describe('when advanced visibility is disabled', () => {
it('disallows bulk and batch actions', () => {
cy.interceptApi();

cy.visit('/namespaces/default/workflows');
cy.visit('/namespaces/default/workflows');

cy.wait('@cluster-api');
cy.wait('@workflows-api');
cy.wait('@cluster-api');
cy.wait('@workflows-api');

cy.get('#workflows-table-with-bulk-actions').should('not.exist');
cy.get('#workflows-table-with-bulk-actions').should('not.exist');
});
});

describe('for cluster that does have elasticsearch enabled', () => {
it('allows running workflows to be terminated by ID', () => {
describe('when advanced visibility is enabled', () => {
beforeEach(() => {
cy.interceptApi();

cy.intercept(Cypress.env('VITE_API_HOST') + '/api/v1/cluster*', {
fixture: 'cluster-with-elasticsearch.json',
}).as('cluster-api-elasticsearch');
Expand All @@ -23,39 +26,56 @@ describe('Bulk Terminate', () => {

cy.wait('@cluster-api-elasticsearch');
cy.wait('@workflows-api');
});

it('allows running workflows to be terminated by ID', () => {
cy.get('#workflows-table-with-bulk-actions').should('exist');

cy.get('#select-visible-workflows').click({ force: true });
cy.get('[data-cy="bulk-terminate-button"]').click();
cy.get('#bulk-terminate-reason').type('Sarah Connor');
cy.get('#bulk-action-reason').type('Sarah Connor');
cy.get('div.modal button.destructive').click();
cy.get('#batch-terminate-success-toast');
});

it('allows running workflows to be terminated by a query', () => {
cy.interceptApi();
cy.intercept(Cypress.env('VITE_API_HOST') + '/api/v1/cluster*', {
fixture: 'cluster-with-elasticsearch.json',
}).as('cluster-api-elasticsearch');
cy.get('#workflows-table-with-bulk-actions').should('exist');

cy.visit('/namespaces/default/workflows');
cy.get('#select-visible-workflows').click({ force: true });
cy.get('[data-cy="select-all-workflows"]').click();
cy.get('[data-cy="bulk-terminate-button"]').click();
cy.get('[data-cy="batch-action-workflows-query"]').should(
'have.text',
'ExecutionStatus="Running"',
);
cy.get('#bulk-action-reason').type('Sarah Connor');
cy.get('div.modal button.destructive').click();
cy.get('#batch-terminate-success-toast');
});

cy.wait('@cluster-api-elasticsearch');
cy.wait('@workflows-api');
it('allows running workflows to be cancelled by ID', () => {
cy.get('#workflows-table-with-bulk-actions').should('exist');

cy.get('#select-visible-workflows').click({ force: true });
cy.get('[data-cy="bulk-cancel-button"]').click();
cy.get('#bulk-action-reason').type('Sarah Connor');
cy.get('div.modal button.destructive').click();
cy.get('#batch-cancel-success-toast');
});

it('allows running workflows to be cancelled by a query', () => {
cy.get('#workflows-table-with-bulk-actions').should('exist');

cy.get('#select-visible-workflows').click({ force: true });
cy.get('[data-cy="select-all-workflows"]').click();
cy.get('[data-cy="bulk-terminate-button"]').click();
cy.get('[data-cy="bulk-cancel-button"]').click();
cy.get('[data-cy="batch-action-workflows-query"]').should(
'have.text',
'ExecutionStatus="Running"',
);
cy.get('#bulk-terminate-reason').type('Sarah Connor');
cy.get('#bulk-action-reason').type('Sarah Connor');
cy.get('div.modal button.destructive').click();
cy.get('#batch-terminate-success-toast');
cy.get('#batch-cancel-success-toast');
});
});
});
6 changes: 3 additions & 3 deletions cypress/support/commands.js
Expand Up @@ -133,12 +133,12 @@ Cypress.Commands.add('interceptScheduleApi', () => {
).as('schedule-api');
});

Cypress.Commands.add('interceptBatchTerminateApi', () => {
Cypress.Commands.add('interceptCreateBatchOperationApi', () => {
cy.intercept(
Cypress.env('VITE_API_HOST') +
`/api/v1/namespaces/*/workflows/batch/terminate?`,
{ statusCode: 200, body: {} },
).as('batch-terminate-api');
).as('create-batch-operation-api');
});

Cypress.Commands.add('interceptDescribeBatchOperationApi', () => {
Expand Down Expand Up @@ -181,7 +181,7 @@ Cypress.Commands.add(
cy.interceptSearchAttributesApi();
cy.interceptSchedulesApi();
cy.interceptScheduleApi();
cy.interceptBatchTerminateApi();
cy.interceptCreateBatchOperationApi();
cy.interceptDescribeBatchOperationApi();
cy.interceptTerminateWorkflowApi();
cy.interceptCancelWorkflowApi();
Expand Down
16 changes: 12 additions & 4 deletions src/api.d.ts
@@ -1,9 +1,7 @@
type WorkflowsAPIRoutePath =
| 'workflows'
| 'workflows.archived'
| 'workflows.count'
| 'workflows.batch.terminate'
| 'workflows.batch.describe';
| 'workflows.count';

type WorkflowAPIRoutePath =
| 'workflow'
Expand All @@ -13,6 +11,13 @@ type WorkflowAPIRoutePath =
| 'events.descending'
| 'query';

type BatchAPIRoutePath =
| 'batch-operations'
| 'batch-operation.describe'
// TODO: Remove when new batch APIs are deployed
| 'workflows.batch.terminate'
| 'workflows.batch.describe';

type NamespaceAPIRoutePath = 'namespace';

type TaskQueueAPIRoutePath = 'task-queue';
Expand All @@ -29,7 +34,8 @@ type APIRoutePath =
| TaskQueueAPIRoutePath
| WorkflowAPIRoutePath
| WorkflowsAPIRoutePath
| NamespaceAPIRoutePath;
| NamespaceAPIRoutePath
| BatchAPIRoutePath;

type APIRouteParameters = {
namespace: string;
Expand All @@ -49,6 +55,8 @@ type WorkflowRouteParameters = Pick<
'namespace' | 'workflowId' | 'runId'
>;

type BatchRouteParameters = Pick<APIRouteParameters, 'namespace'>;

type TaskQueueRouteParameters = Pick<APIRouteParameters, 'namespace' | 'queue'>;

type ValidWorkflowEndpoints = WorkflowsAPIRoutePath;
Expand Down
6 changes: 1 addition & 5 deletions src/lib/components/dropdown-menu.svelte
Expand Up @@ -86,11 +86,7 @@
</div>
{/if}
{#if value}
<span
in:scale={{ duration: 200, start: 0.65 }}
out:scale={{ duration: 100, start: 0.65 }}
class="dot"
/>
<span in:scale={{ duration: 200, start: 0.65 }} class="dot" />
{/if}
</div>
</Tooltip>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/workflow-actions.svelte
Expand Up @@ -2,7 +2,7 @@
import { tick } from 'svelte';
import { refresh } from '$lib/stores/workflow-run';
import { terminateWorkflow } from '$lib/services/terminate-service';
import { terminateWorkflow } from '$lib/services/workflow-service';
import { settings } from '$lib/stores/settings';
import { writeActionsAreAllowed } from '$lib/utilities/write-actions-are-allowed';
Expand Down
@@ -0,0 +1,80 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Modal from '$lib/holocene/modal.svelte';
import { pluralize } from '$lib/utilities/pluralize';
import Input from '$lib/holocene/input/input.svelte';
export let open: boolean;
export let action: 'Terminate' | 'Cancel';
export let loading: boolean;
export let allSelected: boolean;
export let actionableWorkflowsLength: number;
export let query: string;
const dispatch = createEventDispatcher<{
confirm: { reason: string };
}>();
let reason: string;
const handleConfirmModal = () => {
dispatch('confirm', { reason });
open = false;
reason = '';
};
const handleCancelModal = () => {
open = false;
reason = '';
};
$: confirmText = action === 'Cancel' ? 'Confirm' : action;
</script>

<Modal
{open}
confirmType="destructive"
confirmDisabled={reason === ''}
{confirmText}
{loading}
on:cancelModal={handleCancelModal}
on:confirmModal={handleConfirmModal}
>
<h3 slot="title">{action} Workflows</h3>
<svelte:fragment slot="content">
<div class="mb-4 flex flex-col">
{#if allSelected}
<p class="mb-2">
Are you sure you want to {action.toLowerCase()} all worklfows matching
the following query? This action cannot be undone.
</p>
<div
class="mb-2 overflow-scroll whitespace-nowrap rounded border border-primary bg-gray-100 p-2"
>
<code data-cy="batch-action-workflows-query">
{query}
</code>
</div>
<span class="text-xs"
>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 "{confirmText}".</span
>
{:else}
<p class="mb-4">
Are you sure you want to {action.toLowerCase()}
<strong
>{actionableWorkflowsLength} running {pluralize(
'workflow',
actionableWorkflowsLength,
)}</strong
>?
</p>
{/if}
</div>
<Input
id="bulk-action-reason"
bind:value={reason}
placeholder="Enter a reason"
/>
</svelte:fragment>
</Modal>
Expand Up @@ -16,11 +16,13 @@
const dispatch = createEventDispatcher<{
terminateWorkflows: undefined;
cancelWorkflows: undefined;
toggleAll: { checked: boolean };
togglePage: { checked: boolean; visibleWorkflows: WorkflowExecution[] };
}>();
export let bulkActionsEnabled: boolean = false;
export let cancelEnabled: boolean = false;
export let updating: boolean = false;
export let visibleWorkflows: WorkflowExecution[];
export let selectedWorkflowsCount: number;
Expand All @@ -35,6 +37,10 @@
dispatch('terminateWorkflows');
};
const handleBulkCancel = () => {
dispatch('cancelWorkflows');
};
const handleSelectAll = (event: MouseEvent | KeyboardEvent) => {
if (
event instanceof MouseEvent ||
Expand All @@ -55,7 +61,7 @@
let coreUser = coreUserStore();
$: terminateDisabled = $coreUser.namespaceWriteDisabled(
$: namespaceWriteDisabled = $coreUser.namespaceWriteDisabled(
$page.params.namespace,
);
Expand Down Expand Up @@ -107,18 +113,27 @@
>)
</span>
{/if}
<BulkActionButton
class="ml-4"
dataCy="bulk-terminate-button"
disabled={terminateDisabled}
on:click={handleBulkTerminate}>Terminate</BulkActionButton
>
<div class="ml-4 inline-flex gap-2">
{#if cancelEnabled}
<BulkActionButton
dataCy="bulk-cancel-button"
disabled={namespaceWriteDisabled}
on:click={handleBulkCancel}
>Request Cancellation</BulkActionButton
>
{/if}
<BulkActionButton
variant="destructive"
dataCy="bulk-terminate-button"
disabled={namespaceWriteDisabled}
on:click={handleBulkTerminate}>Terminate</BulkActionButton
>
</div>
</th>
<th class="table-cell md:w-60 xl:w-auto" />
<th class="hidden md:table-cell md:w-60 xl:w-80" />
<th class="table-cell md:w-60 xl:w-80" />
<th class="hidden xl:table-cell xl:w-60" />
<th class="hidden xl:table-cell xl:w-60" />
<th class="table-cell md:hidden" />
{:else}
<th class="table-cell w-48"
><div class="flex items-center gap-1">
Expand Down
15 changes: 12 additions & 3 deletions src/lib/holocene/table/bulk-action-button.svelte
@@ -1,20 +1,29 @@
<script lang="ts">
export let dataCy: string = null;
export let disabled: boolean = true;
export let disabled: boolean = false;
export let variant: 'primary' | 'destructive' = 'primary';
</script>

<button
data-cy={dataCy}
{disabled}
on:click
class="bulk-action-button {$$props.class}"
class="bulk-action-button {variant} {$$props.class}"
>
<slot />
</button>

<style lang="postcss">
.bulk-action-button {
@apply rounded border border-danger bg-danger px-2 py-1 leading-3 text-white hover:border-white hover:bg-blue-700;
@apply rounded border px-2 py-1 text-xs leading-3 text-white;
}
.bulk-action-button.primary {
@apply border-white bg-primary hover:bg-blue-700;
}
.bulk-action-button.destructive {
@apply border-danger bg-danger hover:border-white hover:bg-blue-700;
}
.bulk-action-button:disabled {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/layouts/workflow-header.svelte
Expand Up @@ -31,7 +31,7 @@
export let namespace: string;
export let workflow: WorkflowExecution;
export let workers: GetPollersResponse;
export let cancelEnabled: boolean = true;
export let cancelEnabled: boolean = false;
let refreshInterval;
const refreshRate = 15000;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/layouts/workflow-run-layout.svelte
Expand Up @@ -9,7 +9,7 @@
import { onDestroy, onMount } from 'svelte';
import { eventFilterSort, EventSortOrder } from '$lib/stores/event-view';
export let cancelEnabled: boolean = true;
export let cancelEnabled: boolean = false;
onMount(() => {
const sort = $page.url.searchParams.get('sort');
Expand Down

2 comments on commit 6a0953e

@vercel
Copy link

@vercel vercel bot commented on 6a0953e Dec 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

holocene – ./

holocene-git-main.preview.thundergun.io
holocene.preview.thundergun.io

@vercel
Copy link

@vercel vercel bot commented on 6a0953e Dec 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ui – ./

ui-lyart.vercel.app
ui-git-main.preview.thundergun.io
ui.preview.thundergun.io

Please sign in to comment.