Skip to content

Commit cccd915

Browse files
committed
Raised a warning upon import/export of a workflow with a custom task
1 parent 8071fb0 commit cccd915

File tree

5 files changed

+146
-16
lines changed

5 files changed

+146
-16
lines changed

src/lib/components/common/StandardDismissableAlert.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script>
22
export let message;
33
export let autoDismiss = true;
4+
/** @type {'success'|'warning'} */
5+
export let alertType = 'success';
46
57
let timeout;
68
@@ -20,7 +22,10 @@
2022
</script>
2123

2224
{#if message}
23-
<div class="alert alert-success alert-dismissible fade show" role="alert">
25+
<div class="alert alert-{alertType} alert-dismissible fade show" role="alert">
26+
{#if alertType === 'warning'}
27+
<i class="bi bi-exclamation-triangle" />
28+
{/if}
2429
{message}
2530
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" />
2631
</div>

src/lib/components/v2/projects/CreateWorkflowModal.svelte

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import { page } from '$app/stores';
44
import Modal from '../../common/Modal.svelte';
55
import { goto } from '$app/navigation';
6+
import { tick } from 'svelte';
67
7-
/** @type {(workflow: import('$lib/types-v2').WorkflowV2) => void} */
8+
/** @type {(workflow: import('$lib/types-v2').WorkflowV2, warning: string) => void} */
89
export let handleWorkflowImported;
910
1011
// Component properties
@@ -41,16 +42,19 @@
4142
}
4243
4344
function handleImportOrCreateWorkflow() {
44-
modal.confirmAndHide(async () => {
45-
creating = true;
46-
if (workflowFileSelected) {
47-
await handleImportWorkflow();
48-
} else {
49-
await handleCreateWorkflow();
45+
modal.confirmAndHide(
46+
async () => {
47+
creating = true;
48+
if (workflowFileSelected) {
49+
await handleImportWorkflow();
50+
} else {
51+
await handleCreateWorkflow();
52+
}
53+
},
54+
() => {
55+
creating = false;
5056
}
51-
}, () => {
52-
creating = false;
53-
});
57+
);
5458
}
5559
5660
async function handleImportWorkflow() {
@@ -88,8 +92,21 @@
8892
importSuccess = false;
8993
}, 3000);
9094
reset();
95+
96+
/** @type {import('$lib/types-v2').WorkflowV2} */
9197
const workflow = result;
92-
handleWorkflowImported(workflow);
98+
99+
let customTaskWarning = '';
100+
await tick();
101+
const customTasks = workflow.task_list
102+
.map((w) => (w.is_legacy_task ? w.task_legacy : w.task))
103+
.filter((t) => t.owner);
104+
105+
if (customTasks.length > 0) {
106+
customTaskWarning = `Custom tasks (like the one with id=${customTasks[0].id} and source="${customTasks[0].source}") are not meant to be portable; importing this workflow may not work as expected.`;
107+
}
108+
109+
handleWorkflowImported(workflow, customTaskWarning);
93110
} else {
94111
console.error('Import workflow failed', result);
95112
throw new AlertError(result, response.status);

src/lib/components/v2/projects/WorkflowsList.svelte

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import CreateWorkflowModal from './CreateWorkflowModal.svelte';
66
import { onMount } from 'svelte';
77
import { saveSelectedDataset } from '$lib/common/workflow_utilities';
8+
import StandardDismissableAlert from '$lib/components/common/StandardDismissableAlert.svelte';
89
910
// The list of workflows
1011
/** @type {import('$lib/types-v2').WorkflowV2[]} */
@@ -14,6 +15,8 @@
1415
1516
let workflowSearch = '';
1617
18+
let customTaskWarning = '';
19+
1720
$: filteredWorkflows = workflows.filter((p) =>
1821
p.name.toLowerCase().includes(workflowSearch.toLowerCase())
1922
);
@@ -46,11 +49,16 @@
4649
4750
/**
4851
* @param {import('$lib/types-v2').WorkflowV2} importedWorkflow
52+
* @param {string} warningMessage
4953
*/
50-
function handleWorkflowImported(importedWorkflow) {
54+
function handleWorkflowImported(importedWorkflow, warningMessage) {
5155
workflows.push(importedWorkflow);
5256
workflows = workflows;
53-
goto(`/v2/projects/${projectId}/workflows/${importedWorkflow.id}`);
57+
if (warningMessage) {
58+
customTaskWarning = warningMessage;
59+
} else {
60+
goto(`/v2/projects/${projectId}/workflows/${importedWorkflow.id}`);
61+
}
5462
}
5563
5664
onMount(() => {
@@ -82,14 +90,20 @@
8290
<button
8391
class="btn btn-primary float-end"
8492
type="submit"
85-
on:click={() => createWorkflowModal.show()}
93+
on:click={() => {
94+
customTaskWarning = '';
95+
createWorkflowModal.show();
96+
}}
8697
>
8798
Create new workflow
8899
</button>
89100
</div>
90101
</div>
91102
</div>
92103
</div>
104+
105+
<StandardDismissableAlert message={customTaskWarning} alertType="warning" autoDismiss={false} />
106+
93107
<table class="table align-middle caption-top">
94108
<thead class="table-light">
95109
<tr>

src/routes/v2/projects/[projectId]/workflows/[workflowId]/+page.svelte

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
/** @type {import('$lib/types-v2').ApplyWorkflowV2|undefined} */
101101
let selectedSubmittedJob;
102102
103+
let customTaskWarning = '';
104+
103105
$: updatableWorkflowList = workflow.task_list || [];
104106
105107
$: sortedDatasets = datasets.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
@@ -162,6 +164,16 @@
162164
return;
163165
}
164166
167+
customTaskWarning = '';
168+
await tick();
169+
const customTasks = workflow.task_list
170+
.map((w) => (w.is_legacy_task ? w.task_legacy : w.task))
171+
.filter((t) => t.owner);
172+
173+
if (customTasks.length > 0) {
174+
customTaskWarning = `Custom tasks (like the one with id=${customTasks[0].id} and source="${customTasks[0].source}") are not meant to be portable; re-importing this workflow may not work as expected.`;
175+
}
176+
165177
const response = await fetch(`/api/v2/project/${project.id}/workflow/${workflow.id}/export`, {
166178
method: 'GET',
167179
credentials: 'include'
@@ -802,7 +814,11 @@
802814
<a href="/v2/projects/{project?.id}/workflows/{workflow?.id}/jobs" class="btn btn-light">
803815
<i class="bi-journal-code" /> List jobs
804816
</a>
805-
<button class="btn btn-light" on:click|preventDefault={handleExportWorkflow}>
817+
<button
818+
class="btn btn-light"
819+
on:click|preventDefault={handleExportWorkflow}
820+
aria-label="Export workflow"
821+
>
806822
<i class="bi-download" />
807823
</button>
808824
<a id="downloadWorkflowButton" class="d-none">Download workflow link</a>
@@ -818,6 +834,8 @@
818834
</div>
819835
</div>
820836
837+
<StandardDismissableAlert message={customTaskWarning} alertType="warning" autoDismiss={false} />
838+
821839
{#if workflow}
822840
<StandardDismissableAlert message={workflowSuccessMessage} />
823841
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { expect, test } from './workflow_fixture.js';
2+
import { waitModalClosed, waitPageLoading } from '../utils.js';
3+
import path from 'path';
4+
import { createFakeTask, deleteTask } from './task_utils.js';
5+
import os from 'os';
6+
import fs from 'fs';
7+
8+
test('Export and re-import a workflow with a custom task', async ({ page, workflow }) => {
9+
await page.waitForURL(workflow.url);
10+
await waitPageLoading(page);
11+
12+
let taskName;
13+
await test.step('Create test task', async () => {
14+
taskName = await createFakeTask(page, {
15+
type: 'non_parallel'
16+
});
17+
});
18+
19+
await test.step('Open workflow page', async () => {
20+
await workflow.openWorkflowPage();
21+
});
22+
23+
await test.step('Add task to workflow', async () => {
24+
await workflow.addUserTask(taskName);
25+
});
26+
27+
let downloadedFile;
28+
await test.step('Export workflow and verify warning message', async () => {
29+
const downloadPromise = page.waitForEvent('download');
30+
await page.getByLabel('Export workflow').click();
31+
const download = await downloadPromise;
32+
downloadedFile = path.join(os.tmpdir(), download.suggestedFilename());
33+
await download.saveAs(downloadedFile);
34+
await expect(page.getByText('are not meant to be portable')).toBeVisible();
35+
});
36+
37+
await test.step('Open project page', async () => {
38+
await page.goto(`/v2/projects/${workflow.projectId}`);
39+
});
40+
41+
await test.step('Open "Create new workflow" modal', async () => {
42+
const createWorkflowBtn = page.getByRole('button', { name: 'Create new workflow' });
43+
await createWorkflowBtn.waitFor();
44+
await createWorkflowBtn.click();
45+
const modalTitle = page.locator('.modal.show .modal-title');
46+
await modalTitle.waitFor();
47+
await expect(modalTitle).toHaveText('Create new workflow');
48+
});
49+
50+
await test.step('Import the workflow and verify warning message', async () => {
51+
await page
52+
.getByRole('textbox', { name: 'Workflow name' })
53+
.fill(`${workflow.workflowName}-imported`);
54+
const fileChooserPromise = page.waitForEvent('filechooser');
55+
await page.getByText('Import workflow from file').click();
56+
const fileChooser = await fileChooserPromise;
57+
await fileChooser.setFiles(downloadedFile);
58+
await page.getByRole('button', { name: 'Import workflow' }).click();
59+
await expect(page.getByText('are not meant to be portable')).toBeVisible();
60+
});
61+
62+
await test.step('Cleanup', async () => {
63+
await deleteWorkflow();
64+
await deleteWorkflow();
65+
await deleteTask(page, taskName);
66+
fs.rmSync(downloadedFile);
67+
});
68+
69+
async function deleteWorkflow() {
70+
await page.getByRole('button', { name: 'Delete' }).first().click();
71+
const modalTitle = page.locator('.modal.show .modal-title');
72+
await modalTitle.waitFor();
73+
await page.getByRole('button', { name: 'Confirm' }).click();
74+
await waitModalClosed(page);
75+
}
76+
});

0 commit comments

Comments
 (0)