Skip to content

Commit e184c0f

Browse files
authored
Merge pull request #410 from fractal-analytics-platform/import-export-wft-args
Implemented import and export of workflow task arguments
2 parents fdc4553 + 908e6ed commit e184c0f

File tree

19 files changed

+544
-134
lines changed

19 files changed

+544
-134
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
# Unreleased
44

5+
* Added spinner on workflow task modal when tasks list is loading (\#410).
6+
* Implemented import and export of workflow task arguments (\#410).
57
* Improved sorting of users in dropdown of the admin jobs page (\#402).
68
* Fixed bug in retrieval of job log from the admin jobs page (\#402).
79
* Highlighted relevant part of the error message in workflow job log modal (\#402).

src/lib/common/component_utilities.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,20 @@ export function sortProjectsByTimestampCreatedDesc(projects) {
225225
: 0
226226
);
227227
}
228+
229+
/**
230+
* @param {string} content
231+
* @param {string} filename
232+
* @param {string} contentType
233+
*/
234+
export function downloadBlob(content, filename, contentType) {
235+
// Create a blob
236+
var blob = new Blob([content], { type: contentType });
237+
var url = URL.createObjectURL(blob);
238+
239+
// Create a link to download it
240+
var downloader = document.createElement('a');
241+
downloader.href = url;
242+
downloader.setAttribute('download', filename);
243+
downloader.click();
244+
}

src/lib/components/common/jschema/JSchema.svelte

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@
3030
} from '$lib/components/common/jschema/schema_management.js';
3131
import { SchemaValidator } from '$lib/common/jschema_validation.js';
3232
import PropertiesBlock from '$lib/components/common/jschema/PropertiesBlock.svelte';
33+
import { AlertError } from '$lib/common/errors';
3334
3435
/** @type {import('./jschema-types').JSONSchemaObjectProperty|undefined} */
3536
export let schema = undefined;
3637
export let schemaData = undefined;
3738
/** @type {((value: object) => Promise<object>)|undefined} */
3839
export let handleSaveChanges = undefined;
39-
export let saveChanges = undefined;
4040
export let handleValidationErrors = undefined;
4141
4242
let validator = undefined;
@@ -101,24 +101,38 @@
101101
}
102102
}
103103
104-
saveChanges = async function () {
105-
// Trigger validation on input fields
106-
for (const field of document.querySelectorAll('#json-schema input, #json-schema select')) {
107-
field.dispatchEvent(new Event('input'));
104+
/**
105+
* Save changes to schema arguments. Used both by the "Save changes" button
106+
* and by the "Import" button.
107+
* @param {Event|object} param click event when using "Save changes" or
108+
* arguments object passed by the "Import" button.
109+
*/
110+
export async function saveChanges(param) {
111+
const isImport = !(param instanceof Event);
112+
if (!isImport) {
113+
// Trigger validation on input fields, when we are using the "Save changes" button
114+
for (const field of document.querySelectorAll('#json-schema input, #json-schema select')) {
115+
field.dispatchEvent(new Event('input'));
116+
}
108117
}
109118
110-
const data = schemaManager.data;
119+
const data = isImport ? param : schemaManager.data;
111120
// The following is required to remove all null values from the data object
112121
// We suppose that null values are not valid, hence we remove them
113122
// Deep copy the data object
114123
const toStripData = JSON.parse(JSON.stringify(data));
115124
const strippedNullData = stripNullAndEmptyObjectsAndArrays(toStripData);
116125
const isDataValid = validator.isValid(strippedNullData);
117126
if (!isDataValid) {
127+
const errors = validator.getErrors();
128+
console.error('Could not save changes. Data is invalid', errors);
129+
if (isImport) {
130+
// Stop inside import modal
131+
throw new AlertError(errors);
132+
}
118133
if (handleValidationErrors !== null && handleValidationErrors !== undefined) {
119-
handleValidationErrors(validator.getErrors());
134+
handleValidationErrors(errors);
120135
}
121-
console.error('Could not save changes. Data is invalid', validator.getErrors());
122136
return;
123137
}
124138
@@ -131,7 +145,7 @@
131145
console.error(err);
132146
}
133147
}
134-
};
148+
}
135149
136150
/**
137151
* @param {object} args

src/lib/components/workflow/ArgumentForm.svelte

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { updateFormEntry } from '$lib/components/workflow/task_form_utils';
44
import FormBuilder from '$lib/components/workflow/common/FormBuilder.svelte';
55
import { displayStandardErrorAlert } from '$lib/common/errors';
6+
import ImportExportArgs from './ImportExportArgs.svelte';
7+
import { onMount } from 'svelte';
68
79
// This component shall handle a form which the user can use to specify arguments of a workflow-task
810
// Upon interacting with this component, a representation of the arguments to be used with a workflow task
@@ -12,27 +14,30 @@
1214
// - store the sequence in a coherent object
1315
// - enable the usage of the object that keeps the representation of the list
1416
15-
export let workflowId = undefined;
16-
export let workflowTaskId = undefined;
17+
export let workflowId;
18+
/** @type {import('$lib/types').WorkflowTask} */
19+
export let workflowTask;
1720
18-
// The main property managed by this component
19-
export let workflowTaskArgs = {};
20-
21-
if (workflowTaskArgs == null || workflowTaskArgs === undefined) {
22-
workflowTaskArgs = {};
23-
}
21+
onMount(() => {
22+
if (!workflowTask.args) {
23+
workflowTask.args = {};
24+
}
25+
});
2426
27+
/**
28+
* @param {object} updatedEntry
29+
*/
2530
async function handleEntryUpdate(updatedEntry) {
2631
const projectId = $page.params.projectId;
2732
try {
2833
const response = await updateFormEntry(
2934
projectId,
3035
workflowId,
31-
workflowTaskId,
36+
workflowTask.id,
3237
updatedEntry,
3338
'args'
3439
);
35-
workflowTaskArgs = response.args;
40+
workflowTask.args = response.args;
3641
} catch (error) {
3742
console.error(error);
3843
displayStandardErrorAlert(error, 'argsPropertiesFormError');
@@ -42,5 +47,21 @@
4247

4348
<div>
4449
<span id="argsPropertiesFormError" />
45-
<FormBuilder entry={workflowTaskArgs} updateEntry={handleEntryUpdate} />
50+
<FormBuilder entry={workflowTask.args} updateEntry={handleEntryUpdate} />
51+
<div class="d-flex args-controls-bar p-3 mt-3">
52+
<ImportExportArgs
53+
taskName={workflowTask.task.name}
54+
args={workflowTask.args}
55+
onImport={(json) => handleEntryUpdate(json)}
56+
exportDisabled={false}
57+
/>
58+
</div>
4659
</div>
60+
61+
<style>
62+
.args-controls-bar {
63+
background-color: whitesmoke;
64+
margin-top: 5px;
65+
border-top: 1px solid lightgray;
66+
}
67+
</style>

src/lib/components/workflow/ArgumentsSchema.svelte

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import JSchema from '$lib/components/common/jschema/JSchema.svelte';
55
import { updateFormEntry } from '$lib/components/workflow/task_form_utils';
66
import { displayStandardErrorAlert } from '$lib/common/errors';
7+
import ImportExportArgs from './ImportExportArgs.svelte';
78
89
const SUPPORTED_SCHEMA_VERSIONS = ['pydantic_v1'];
910
@@ -15,8 +16,11 @@
1516
export let argumentsSchemaVersion = undefined;
1617
export let validSchema = undefined;
1718
export let args = undefined;
19+
/** @type {string} */
20+
export let taskName;
1821
19-
let schemaComponent = undefined;
22+
/** @type {JSchema} */
23+
let schemaComponent;
2024
export let unsavedChanges = false;
2125
let resetChanges = undefined;
2226
export let saveChanges = undefined;
@@ -71,10 +75,17 @@
7175
bind:this={schemaComponent}
7276
/>
7377
</div>
74-
<div class="d-flex justify-content-end jschema-controls-bar p-3">
78+
<div class="d-flex jschema-controls-bar p-3">
79+
<ImportExportArgs
80+
{taskName}
81+
{args}
82+
onImport={(json) => schemaComponent.saveChanges(json)}
83+
exportDisabled={unsavedChanges || savingChanges}
84+
/>
7585
<div>
7686
<button
77-
class="btn btn-warning {unsavedChanges ? '' : 'disabled'}"
87+
class="btn btn-warning"
88+
disabled={!unsavedChanges || savingChanges}
7889
on:click={() => resetChanges(args)}
7990
>
8091
Discard changes
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script>
2+
import { downloadBlob } from '$lib/common/component_utilities';
3+
import Modal from '../common/Modal.svelte';
4+
5+
/** @type {string} */
6+
export let taskName;
7+
/** @type {object} */
8+
export let args;
9+
/** @type {(json: string) => Promise<void>} */
10+
export let onImport;
11+
/** @type {boolean} */
12+
export let exportDisabled;
13+
14+
/** @type {Modal} */
15+
let importArgsModal;
16+
17+
/** @type {FileList|null} */
18+
let importArgsFiles = null;
19+
/** @type {HTMLInputElement|undefined} */
20+
let importArgsFileInput = undefined;
21+
let importArgsError = '';
22+
23+
function onImportArgsModalOpen() {
24+
importArgsFiles = null;
25+
if (importArgsFileInput) {
26+
importArgsFileInput.value = '';
27+
}
28+
importArgsError = '';
29+
}
30+
31+
async function importArgs() {
32+
importArgsError = '';
33+
if (importArgsFiles === null || importArgsFiles.length === 0) {
34+
return;
35+
}
36+
const file = importArgsFiles[0];
37+
let content = await file.text();
38+
let json;
39+
try {
40+
json = JSON.parse(content);
41+
} catch (err) {
42+
importArgsError = "File doesn't contain valid JSON";
43+
return;
44+
}
45+
importArgsModal.confirmAndHide(async function () {
46+
await onImport(json);
47+
});
48+
}
49+
50+
function exportArgs() {
51+
const serializedArgs = JSON.stringify(args, null, 2);
52+
downloadBlob(serializedArgs, `args-${createSlug(taskName)}.json`, 'text/json;charset=utf-8;');
53+
}
54+
55+
/**
56+
* @param {string} value
57+
*/
58+
function createSlug(value) {
59+
return value.trim().toLowerCase().replace(/\s+/g, '-');
60+
}
61+
</script>
62+
63+
<div class="me-auto">
64+
<button class="btn btn-outline-primary" on:click={() => importArgsModal.show()}>
65+
<i class="bi bi-upload" />
66+
Import
67+
</button>
68+
<button class="btn btn-outline-primary" on:click={exportArgs} disabled={exportDisabled}>
69+
<i class="bi bi-download" />
70+
Export
71+
</button>
72+
</div>
73+
74+
<Modal
75+
id="importArgumentsModal"
76+
onOpen={onImportArgsModalOpen}
77+
bind:this={importArgsModal}
78+
size="lg"
79+
>
80+
<svelte:fragment slot="header">
81+
<h1 class="h5 modal-title">Import arguments</h1>
82+
</svelte:fragment>
83+
<svelte:fragment slot="body">
84+
<div class="row">
85+
<div class="col needs-validation">
86+
<label for="importArgsFile"> Select arguments file </label>
87+
<input
88+
class="form-control mt-1"
89+
accept="application/json"
90+
type="file"
91+
name="importArgsFile"
92+
id="importArgsFile"
93+
bind:this={importArgsFileInput}
94+
bind:files={importArgsFiles}
95+
class:is-invalid={importArgsError}
96+
/>
97+
<span class="invalid-feedback">{importArgsError}</span>
98+
</div>
99+
<div class="form-text">JSON containing workflow task arguments</div>
100+
</div>
101+
<div class="row">
102+
<div class="col">
103+
<div class="alert alert-warning mt-3">
104+
<i class="bi bi-exclamation-triangle" />
105+
The current arguments will be completely overwritten, disregarding any pending changes.
106+
</div>
107+
</div>
108+
</div>
109+
<div class="row">
110+
<div class="col">
111+
<div id="errorAlert-importArgumentsModal" />
112+
</div>
113+
</div>
114+
</svelte:fragment>
115+
<svelte:fragment slot="footer">
116+
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
117+
<button
118+
class="btn btn-primary"
119+
disabled={importArgsFiles === null || importArgsFiles.length === 0}
120+
on:click={importArgs}
121+
>
122+
Confirm
123+
</button>
124+
</svelte:fragment>
125+
</Modal>

src/lib/components/workflow/WorkflowTaskSelection.svelte

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@
77
orderTasksByOwnerThenByNameThenByVersion
88
} from '$lib/common/component_utilities.js';
99
10-
export let tasks = undefined;
10+
/** @type {import('$lib/types').Task[]} */
11+
export let tasks;
12+
let loadingTasks = false;
13+
14+
/**
15+
* @param {boolean} loading
16+
*/
17+
export function setLoadingTasks(loading) {
18+
loadingTasks = loading;
19+
}
1120
1221
let selectedTypeOfTask = 'common';
1322
let selectionTasks = new Map();
@@ -242,7 +251,13 @@
242251
</div>
243252
</div>
244253
<div class="card-body">
245-
<label for="taskId" class="form-label">Select task</label>
254+
<label for="taskId" class="form-label">
255+
Select task
256+
{#if loadingTasks}
257+
&nbsp;
258+
<span class="spinner-border spinner-border-sm" aria-hidden="true" />
259+
{/if}
260+
</label>
246261
{#if selectedTypeOfTask}
247262
<select id="advanced-select" />
248263
{/if}

src/lib/components/workflow/common/NewEntryProperty.svelte

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,12 @@
122122
</form>
123123
</div>
124124
<div>
125-
<button class="btn btn-primary" type="submit" form={uuid}
126-
><i class="bi-check-square" /></button
127-
>
128-
<button class="btn btn-danger" on:click={resetComponent}><i class="bi-trash" /></button>
125+
<button class="btn btn-primary" type="submit" form={uuid} aria-label="Save argument">
126+
<i class="bi-check-square" />
127+
</button>
128+
<button class="btn btn-danger" on:click={resetComponent} aria-label="Delete argument">
129+
<i class="bi-trash" />
130+
</button>
129131
</div>
130132
</div>
131133
{/if}

0 commit comments

Comments
 (0)