Skip to content

Commit d07b2fb

Browse files
authored
enable and production-ize bulk termination (#936)
* re-enable batch termination * close pagination loophole * handle perPage params that aren't included in options * get rid of obsolete tests * add cy test for bulk terminate happy path * add store/guard for bulk actions * fix test * fix bulk terminate for small screens * add test for clusters that don't support advanced vis * fix test descriptions
1 parent dbca77c commit d07b2fb

19 files changed

+182
-56
lines changed

Diff for: cypress/fixtures/batch-operation-status.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"operationType": "Terminate",
3+
"jobId": "6216d7c6-c987-4b27-a854-3bb9c72b3bbb",
4+
"state": "Completed",
5+
"startTime": "2022-11-01T00:00:00.000000000Z",
6+
"closeTime": "2022-11-01T00:00:00.000000000Z",
7+
"totalOperationCount": "10",
8+
"completeOperationCount": "10",
9+
"failureOperationCount": "0",
10+
"identity": "",
11+
"reason": ""
12+
}

Diff for: cypress/fixtures/cluster-with-elasticsearch.json

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"supportedClients": {
3+
"temporal-cli": "\u003c2.0.0",
4+
"temporal-go": "\u003c2.0.0",
5+
"temporal-java": "\u003c2.0.0",
6+
"temporal-php": "\u003c2.0.0",
7+
"temporal-server": "\u003c2.0.0",
8+
"temporal-typescript": "\u003c2.0.0",
9+
"temporal-ui": "\u003c3.0.0"
10+
},
11+
"serverVersion": "1.18.4",
12+
"clusterId": "17235226-f862-4d2a-a809-898c52dc8ab4",
13+
"versionInfo": {
14+
"current": {
15+
"version": "1.18.4",
16+
"releaseTime": "2022-10-31T22:14:46Z",
17+
"notes": ""
18+
},
19+
"recommended": {
20+
"version": "1.18.4",
21+
"releaseTime": "2022-10-31T22:14:46Z",
22+
"notes": ""
23+
},
24+
"instructions": "",
25+
"alerts": [
26+
{
27+
"message": "🪐 A new release is available!",
28+
"severity": "Low"
29+
}
30+
],
31+
"lastUpdateTime": "2022-11-09T22:29:04.612659335Z"
32+
},
33+
"clusterName": "active",
34+
"historyShardCount": 4,
35+
"persistenceStore": "postgres",
36+
"visibilityStore": "postgres,elasticsearch"
37+
}

Diff for: cypress/integration/workflow-bulk-actions.spec.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// <reference types="cypress" />
2+
3+
describe('Bulk Termination', () => {
4+
it("disallows bulk actions for cluster that doesn't have elasticsearch enabled", () => {
5+
cy.interceptApi();
6+
7+
cy.visit('/namespaces/default/workflows');
8+
9+
cy.wait('@workflows-api');
10+
cy.wait('@cluster-api');
11+
12+
cy.get('#workflows-table-with-bulk-actions').should('not.exist');
13+
});
14+
15+
it('allows running workflows to be terminated for cluster that does have elasticsearch enabled', () => {
16+
cy.interceptApi();
17+
cy.intercept(Cypress.env('VITE_API_HOST') + '/api/v1/cluster*', {
18+
fixture: 'cluster-with-elasticsearch.json',
19+
}).as('cluster-api-elasticsearch');
20+
21+
cy.visit('/namespaces/default/workflows');
22+
23+
cy.wait('@workflows-api');
24+
cy.wait('@cluster-api-elasticsearch');
25+
26+
cy.get('#workflows-table-with-bulk-actions').should('exist');
27+
28+
cy.get('th.selectable > label.checkbox > span.label').click();
29+
cy.get('[data-cy="bulk-terminate-button"]').click();
30+
cy.get('#bulk-terminate-reason').type('Sarah Connor');
31+
cy.get('div.modal button.destructive').click();
32+
cy.get('#batch-terminate-success-toast');
33+
});
34+
});

Diff for: cypress/support/commands.js

+18
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,22 @@ Cypress.Commands.add('interceptScheduleApi', () => {
133133
).as('schedule-api');
134134
});
135135

136+
Cypress.Commands.add('interceptBatchTerminateApi', () => {
137+
cy.intercept(
138+
Cypress.env('VITE_API_HOST') +
139+
`/api/v1/namespaces/*/workflows/batch/terminate?`,
140+
{ statusCode: 200, body: {} },
141+
).as('batch-terminate-api');
142+
});
143+
144+
Cypress.Commands.add('interceptDescribeBatchOperationApi', () => {
145+
cy.intercept(
146+
Cypress.env('VITE_API_HOST') +
147+
`/api/v1/namespaces/*/workflows/batch/describe?jobId=*`,
148+
{ fixture: 'batch-operation-status.json' },
149+
).as('describe-batch-operation-api');
150+
});
151+
136152
Cypress.Commands.add(
137153
'interceptApi',
138154
({ namespace, archived } = { namespace: 'default', archived: false }) => {
@@ -149,5 +165,7 @@ Cypress.Commands.add(
149165
cy.interceptSearchAttributesApi();
150166
cy.interceptSchedulesApi();
151167
cy.interceptScheduleApi();
168+
cy.interceptBatchTerminateApi();
169+
cy.interceptDescribeBatchOperationApi();
152170
},
153171
);

Diff for: src/lib/components/workflow/workflows-summary-table.svelte

+4-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
{#if bulkActionsEnabled}
2828
<SelectableTable
29+
id="workflows-table-with-bulk-actions"
2930
class="w-full md:table-fixed"
3031
bind:allSelected
3132
bind:selectedItems={selectedWorkflows}
@@ -41,12 +42,13 @@
4142
<th class="table-cell md:hidden" colspan="3">Summary</th>
4243
</svelte:fragment>
4344
<svelte:fragment slot="bulk-action-headers">
44-
<th class="hidden w-32 md:table-cell">
45+
<th class="inline-block w-32 md:table-cell">
4546
<span class="font-semibold">{selectedWorkflows.length} selected</span>
4647
</th>
47-
<th class="hidden h-10 md:table-cell md:w-60 xl:w-auto">
48+
<th class="h-10 md:w-60 xl:w-auto">
4849
{#if terminableWorkflowCount > 0}
4950
<BulkActionButton
51+
dataCy="bulk-terminate-button"
5052
disabled={terminateDisabled}
5153
on:click={handleBulkTerminate}>Terminate</BulkActionButton
5254
>

Diff for: src/lib/holocene/api-pagination.svelte

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import SkeletonTable from '$lib/holocene/skeleton/table.svelte';
66
77
import { createPaginationStore } from '$lib/stores/api-pagination';
8-
import { perPageOptions } from '$lib/stores/pagination';
8+
import { options } from '$lib/stores/pagination';
99
1010
type T = $$Generic;
1111
type PaginatedRequest = (
@@ -101,7 +101,7 @@
101101
label="Per Page"
102102
parameter={$store.key}
103103
value={String($store.pageSize)}
104-
options={perPageOptions($store.pageSize)}
104+
{options}
105105
/>
106106
<div class="flex items-center justify-center gap-1">
107107
<button
@@ -142,7 +142,7 @@
142142
label="Per Page"
143143
parameter={$store.key}
144144
value={String($store.pageSize)}
145-
options={perPageOptions($store.pageSize)}
145+
{options}
146146
/>
147147
<div class="flex items-center justify-center gap-1">
148148
<button

Diff for: src/lib/holocene/pagination.svelte

+22-4
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
import { onMount } from 'svelte';
33
import { page } from '$app/stores';
44
import {
5+
MAX_PAGE_SIZE,
56
pagination,
67
perPageFromSearchParameter,
7-
perPageOptions,
8+
options,
9+
defaultItemsPerPage,
810
} from '$lib/stores/pagination';
911
import { updateQueryParameters } from '$lib/utilities/update-query-parameters';
1012
import FilterSelect from '$lib/holocene/select/filter-select.svelte';
@@ -22,6 +24,22 @@
2224
$: perPage = String(
2325
perPageFromSearchParameter($page.url.searchParams.get(perPageKey)),
2426
).toString();
27+
28+
$: {
29+
if (parseInt(perPage, 10) > parseInt(MAX_PAGE_SIZE, 10)) {
30+
updateQueryParameters({
31+
parameter: perPageKey,
32+
value: MAX_PAGE_SIZE,
33+
url: $page.url,
34+
});
35+
} else if (!options.includes(perPage)) {
36+
updateQueryParameters({
37+
parameter: perPageKey,
38+
value: defaultItemsPerPage,
39+
url: $page.url,
40+
});
41+
}
42+
}
2543
$: store = pagination(items, perPage, startingIndex);
2644
$: currentPage =
2745
$page.url.searchParams.get(currentPageKey) ?? $store.currentPage;
@@ -70,7 +88,7 @@
7088
label="Per Page"
7189
parameter={perPageKey}
7290
value={perPage}
73-
options={perPageOptions(perPage)}
91+
{options}
7492
/>
7593
<div class="flex items-center justify-center gap-1">
7694
<button
@@ -112,8 +130,8 @@
112130
<FilterSelect
113131
label="Per Page"
114132
parameter={perPageKey}
115-
value={String(perPage)}
116-
options={perPageOptions(perPage)}
133+
value={perPage}
134+
{options}
117135
/>
118136
<div class="flex items-center justify-center gap-1">
119137
<button

Diff for: src/lib/holocene/table/bulk-action-button.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script lang="ts">
2+
export let dataCy: string = null;
23
export let disabled: boolean = true;
34
</script>
45

5-
<button {disabled} on:click class="bulk-action-button">
6+
<button data-cy={dataCy} {disabled} on:click class="bulk-action-button">
67
<slot />
78
</button>
89

Diff for: src/lib/holocene/table/selectable-table.svelte

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
allSelected?: boolean;
2323
checkboxLabel?: string;
2424
class?: string;
25+
id?: string;
2526
}
2627
2728
export let items: Item[];
2829
export let checkboxLabel: string = null;
2930
export let allSelected: boolean = false;
3031
export let selectedItems: Item[] = [];
32+
export let id: string = null;
3133
3234
const handleSelectAll = (event: CustomEvent<{ checked: boolean }>) => {
3335
allSelected = !allSelected;
@@ -58,7 +60,7 @@
5860
});
5961
</script>
6062

61-
<Table variant="fancy" class={$$props.class} {...$$props}>
63+
<Table variant="fancy" {id} class={$$props.class} {...$$props}>
6264
<TableHeaderRow
6365
slot="headers"
6466
selectable

Diff for: src/lib/holocene/table/table-header-row.svelte

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<th class:selectable>
1212
{#if selectable}
1313
<Checkbox
14+
id="selectable-table-check-all"
1415
onDark
1516
bind:checked={selected}
1617
{indeterminate}

Diff for: src/lib/holocene/table/table.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
44
export let variant: 'simple' | 'fancy' = 'fancy';
55
export let updating = false;
6+
export let id: string = null;
67
</script>
78

8-
<table class="{variant} {$$props.class}">
9+
<table class="{variant} {$$props.class}" {id}>
910
<thead>
1011
<slot name="headers" />
1112
{#if updating}

Diff for: src/lib/holocene/toaster.svelte

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
<div class="absolute top-4 right-4 z-50 flex flex-col items-end gap-2">
6464
{#each $toasts as { message, variant, id: key } (key)}
6565
<article
66+
id={key}
6667
class="flex grow-0 items-center justify-between gap-4 rounded py-3 px-4 shadow {variant}"
6768
in:send={{ key }}
6869
out:receive={{ key }}

Diff for: src/lib/pages/workflows.svelte

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
toaster.push({
9090
variant: 'primary',
9191
message: `Successfully terminated ${terminableWorkflows.length} workflows.`,
92+
id: 'batch-terminate-success-toast',
9293
});
9394
selectedWorkflows = [];
9495
allSelected = false;

Diff for: src/lib/stores/bulk-actions.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { derived } from 'svelte/store';
2+
3+
import { advancedVisibilityEnabled } from '$lib/utilities/advanced-visibility-enabled';
4+
import { isVersionNewer } from '$lib/utilities/version-check';
5+
6+
import { cluster } from './cluster';
7+
import { temporalVersion } from './versions';
8+
9+
export const supportsBulkActions = derived(
10+
[temporalVersion, cluster],
11+
([$temporalVersion, $cluster]) =>
12+
isVersionNewer($temporalVersion, '1.18.0') &&
13+
advancedVisibilityEnabled($cluster),
14+
);

Diff for: src/lib/stores/pagination.test.ts

-37
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
getValidPage,
1010
pagination,
1111
perPageFromSearchParameter,
12-
perPageOptions,
1312
} from './pagination';
1413
import { stringifyWithBigInt } from '../utilities/parse-with-big-int';
1514

@@ -384,39 +383,3 @@ describe('perPageFromSearchParameter', () => {
384383
expect(perPageFromSearchParameter({} as any)).toBe(100);
385384
});
386385
});
387-
388-
describe('perPageOptions', () => {
389-
it('should return the default options', () => {
390-
expect(perPageOptions(undefined)).toEqual(['100', '250', '500']);
391-
});
392-
393-
it('should return the default options with an additional string', () => {
394-
expect(perPageOptions('25')).toEqual(['25', '100', '250', '500']);
395-
});
396-
397-
it('should return the default options with an additional number a a string', () => {
398-
expect(perPageOptions(25)).toEqual(['25', '100', '250', '500']);
399-
});
400-
401-
it('should not return duplicate options', () => {
402-
expect(perPageOptions('250')).toEqual(['100', '250', '500']);
403-
});
404-
405-
it('should return not return duplicate options if passed in as a number', () => {
406-
expect(perPageOptions(250)).toEqual(['100', '250', '500']);
407-
});
408-
409-
it('should accept bogus strings', () => {
410-
expect(perPageOptions('bogus input')).toEqual(['100', '250', '500']);
411-
});
412-
413-
for (const input of [null, undefined, {}, []]) {
414-
it(`should ignore ${stringifyWithBigInt(input)}`, () => {
415-
expect(perPageOptions(input as unknown as any)).toEqual([
416-
'100',
417-
'250',
418-
'500',
419-
]);
420-
});
421-
}
422-
});

Diff for: src/lib/stores/pagination.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { derived, writable, get } from 'svelte/store';
22
import type { Readable } from 'svelte/store';
33

44
export const defaultItemsPerPage = 100;
5-
const options: string[] = ['100', '250', '500'];
5+
export const options: string[] = ['100', '250', '500'];
6+
export const MAX_PAGE_SIZE = options[options.length - 1];
67

78
type PaginationMethods<T> = {
89
adjustPageSize: (n: number | string) => void;
@@ -28,11 +29,6 @@ type PaginationStore<T> = PaginationMethods<T> &
2829
totalPages: number;
2930
}>;
3031

31-
export const perPageOptions = (perPage: number | string): string[] => {
32-
const itemsPerPage = String(perPageFromSearchParameter(perPage));
33-
return options.includes(itemsPerPage) ? options : [itemsPerPage, ...options];
34-
};
35-
3632
export const getPageForIndex = (i: number, pageSize: number): number => {
3733
return Math.floor(i / pageSize) + 1;
3834
};

0 commit comments

Comments
 (0)