Skip to content

Commit 4492c6e

Browse files
authored
Merge pull request #467 from fractal-analytics-platform/import-export-dataset
Implemented import and export of datasets
2 parents 4c4e6ca + 0d49ca4 commit 4492c6e

22 files changed

+1078
-94
lines changed

.github/workflows/end_to_end_tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
end_to_end_tests:
1111
name: 'Node ${{ matrix.node-version }}'
1212
runs-on: ubuntu-22.04
13-
timeout-minutes: 10
13+
timeout-minutes: 20
1414

1515
strategy:
1616
matrix:

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
* implemented continue/restart workflow (\#465);
2222
* set default first task when continuing a workflow (\#466);
2323
* displayed applied filters in workflow execution modal (\#466);
24+
* implemented import and export of datasets (\#467);
25+
* handled selection of default dataset on workflow page (\#467);
2426

2527
# 0.10.2
2628

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
deleteDatasetSelectionsForProject,
4+
getDefaultWorkflowDataset,
5+
getSelectedWorkflowDataset,
6+
saveSelectedDataset
7+
} from '$lib/common/workflow_utilities.js';
8+
9+
describe('Workflow Utilities', () => {
10+
it('Default dataset is taken from most recent job', () => {
11+
const id = getDefaultWorkflowDataset(
12+
[],
13+
[
14+
{ start_timestamp: '2024-04-24T10:18:37.868655+00:00', dataset_id: 1 },
15+
{ start_timestamp: '2024-04-24T15:18:37.868655+00:00', dataset_id: 2 }
16+
]
17+
);
18+
expect(id).toEqual(2);
19+
});
20+
21+
it('Default dataset is taken from most recent dataset', () => {
22+
const id = getDefaultWorkflowDataset(
23+
[
24+
{ id: 1, timestamp_created: '2024-04-24T10:18:37.868655+00:00' },
25+
{ id: 2, timestamp_created: '2024-04-24T15:18:37.868655+00:00' }
26+
],
27+
[]
28+
);
29+
expect(id).toEqual(2);
30+
});
31+
32+
it('Default dataset is undefined if there are no jobs and no datasets', () => {
33+
const id = getDefaultWorkflowDataset([], []);
34+
expect(id).toEqual(undefined);
35+
});
36+
37+
it('Store and retrieve selected dataset from localStorage', () => {
38+
const workflow1 = { id: 1, project_id: 1 };
39+
const workflow2 = { id: 2, project_id: 1 };
40+
const datasets = [{ id: 1 }, { id: 2 }, { id: 3 }];
41+
let datasetId = getSelectedWorkflowDataset(workflow1, datasets, 3);
42+
// return default dataset if nothing is stored in localStorage
43+
expect(datasetId).toEqual(3);
44+
45+
saveSelectedDataset(workflow1, 2);
46+
datasetId = getSelectedWorkflowDataset(workflow1, datasets, 3);
47+
// return dataset saved in localStorage
48+
expect(datasetId).toEqual(2);
49+
50+
saveSelectedDataset(workflow1, 4); // dataset doesn't exist
51+
datasetId = getSelectedWorkflowDataset(workflow1, datasets, 3);
52+
// return default dataset if the dataset stored in localstorage doesn't exist
53+
expect(datasetId).toEqual(3);
54+
55+
saveSelectedDataset(workflow1, 1); // replace stored selection
56+
datasetId = getSelectedWorkflowDataset(workflow1, datasets, 3);
57+
// return dataset saved in localStorage
58+
expect(datasetId).toEqual(1);
59+
60+
saveSelectedDataset(workflow2, 2); // add new selection
61+
datasetId = getSelectedWorkflowDataset(workflow2, datasets, 3);
62+
// return dataset saved in localStorage
63+
expect(datasetId).toEqual(2);
64+
65+
saveSelectedDataset(workflow1, undefined); // remove stored value
66+
datasetId = getSelectedWorkflowDataset(workflow1, datasets, 3);
67+
// return default dataset
68+
expect(datasetId).toEqual(3);
69+
70+
deleteDatasetSelectionsForProject(1); // remove all selections for the project
71+
datasetId = getSelectedWorkflowDataset(workflow2, datasets, 3);
72+
// return default dataset
73+
expect(datasetId).toEqual(3);
74+
});
75+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @param {Array<import("$lib/types-v2").DatasetV2>} datasets
3+
* @param {Array<import("$lib/types-v2").ApplyWorkflowV2>} jobs
4+
* @returns {number|undefined}
5+
*/
6+
export function getDefaultWorkflowDataset(datasets, jobs) {
7+
if (jobs.length > 0) {
8+
return jobs.sort((j1, j2) => (j1.start_timestamp > j2.start_timestamp ? -1 : 1))[0].dataset_id;
9+
}
10+
if (datasets.length > 0) {
11+
return datasets.sort((d1, d2) => (d1.timestamp_created > d2.timestamp_created ? -1 : 1))[0].id;
12+
}
13+
return undefined;
14+
}
15+
16+
/**
17+
* @param {import("$lib/types-v2").WorkflowV2} workflow
18+
* @param {Array<import("$lib/types-v2").DatasetV2>} datasets
19+
* @param {number|undefined} defaultDatasetId
20+
* @returns {number|undefined}
21+
*/
22+
export function getSelectedWorkflowDataset(workflow, datasets, defaultDatasetId) {
23+
const selectedFromLocalStorage = getDatasetIdFromLocalStorage(workflow);
24+
if (selectedFromLocalStorage === undefined) {
25+
return defaultDatasetId;
26+
}
27+
const datasetExists = datasets.filter((d) => d.id === selectedFromLocalStorage).length > 0;
28+
if (datasetExists) {
29+
return selectedFromLocalStorage;
30+
}
31+
saveSelectedDataset(workflow, undefined);
32+
return defaultDatasetId;
33+
}
34+
35+
const LOCAL_STORAGE_SELECTED_DATASETS = 'SelectedDatasets';
36+
37+
/**
38+
* @param {import("$lib/types-v2").WorkflowV2} workflow
39+
* @param {number|undefined} datasetId
40+
*/
41+
export function saveSelectedDataset(workflow, datasetId) {
42+
let savedSelections = getDatasetSelectionsFromLocalStorage();
43+
const selection = savedSelections.filter(
44+
(s) => s.project_id === workflow.project_id && s.workflow_id === workflow.id
45+
);
46+
if (datasetId === undefined) {
47+
savedSelections = savedSelections.filter((s) => s.workflow_id !== workflow.id);
48+
} else if (selection.length > 0) {
49+
savedSelections = savedSelections.map((s) =>
50+
s.project_id === workflow.project_id && s.workflow_id === workflow.id
51+
? { ...s, dataset_id: datasetId }
52+
: s
53+
);
54+
} else {
55+
savedSelections.push({
56+
project_id: workflow.project_id,
57+
workflow_id: workflow.id,
58+
dataset_id: datasetId
59+
});
60+
}
61+
localStorage.setItem(LOCAL_STORAGE_SELECTED_DATASETS, JSON.stringify(savedSelections));
62+
}
63+
64+
/**
65+
* @param {number} projectId
66+
*/
67+
export function deleteDatasetSelectionsForProject(projectId) {
68+
let savedSelections = getDatasetSelectionsFromLocalStorage();
69+
savedSelections = savedSelections.filter((s) => s.project_id !== projectId);
70+
localStorage.setItem(LOCAL_STORAGE_SELECTED_DATASETS, JSON.stringify(savedSelections));
71+
}
72+
73+
/**
74+
* @param {import("$lib/types-v2").WorkflowV2} workflow
75+
* @returns {number|undefined}
76+
*/
77+
function getDatasetIdFromLocalStorage(workflow) {
78+
const savedSelections = getDatasetSelectionsFromLocalStorage();
79+
const selection = savedSelections.filter(
80+
(s) => s.project_id === workflow.project_id && s.workflow_id === workflow.id
81+
);
82+
if (selection.length > 0) {
83+
return selection[0].dataset_id;
84+
}
85+
return undefined;
86+
}
87+
88+
/**
89+
* @returns {Array<{project_id: number, workflow_id: number, dataset_id: number}>}
90+
*/
91+
function getDatasetSelectionsFromLocalStorage() {
92+
const storageContent = window.localStorage.getItem(LOCAL_STORAGE_SELECTED_DATASETS);
93+
if (storageContent) {
94+
try {
95+
const parsedValue = JSON.parse(storageContent);
96+
if (Array.isArray(parsedValue)) {
97+
return parsedValue;
98+
}
99+
} catch (ex) {
100+
console.error(`Invalid JSON inside localStorage item ${LOCAL_STORAGE_SELECTED_DATASETS}`);
101+
}
102+
}
103+
return [];
104+
}

src/lib/components/common/jschema/schema_management.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ export function stripSchemaProperties(schema, legacy = true) {
541541
if (schema.required) {
542542
schema.required = schema.required.filter((k) => !ignoreProperties.includes(k));
543543
}
544+
return schema;
544545
}
545546

546547
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import Modal from '../../common/Modal.svelte';
55
import { goto } from '$app/navigation';
66
7-
/** @type {(workflow: import('$lib/types').Workflow) => void} */
7+
/** @type {(workflow: import('$lib/types-v2').WorkflowV2) => void} */
88
export let handleWorkflowImported;
99
1010
// Component properties

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
);
1515
1616
function createDatasetCallback(/** @type {import('$lib/types-v2').DatasetV2} */ newDataset) {
17-
datasets = [...datasets, newDataset];
17+
datasets = [...datasets, newDataset].sort((a, b) =>
18+
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
19+
);
1820
}
1921
2022
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { AlertError, getValidationMessagesMap } from '$lib/common/errors';
55
import { goto } from '$app/navigation';
66
import Modal from '../../common/Modal.svelte';
7+
import { deleteDatasetSelectionsForProject } from '$lib/common/workflow_utilities';
78
89
// List of projects to be displayed
910
/** @type {import('$lib/types').Project[]} */
@@ -92,6 +93,7 @@
9293
console.log('Project deleted successfully');
9394
// If the response is successful
9495
projects = projects.filter((p) => p.id !== projectId);
96+
deleteDatasetSelectionsForProject(projectId);
9597
} else {
9698
const result = await response.json();
9799
console.error(`Unable to delete project ${projectId}`);

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import { AlertError } from '$lib/common/errors';
55
import CreateWorkflowModal from './CreateWorkflowModal.svelte';
66
import { onMount } from 'svelte';
7+
import { saveSelectedDataset } from '$lib/common/workflow_utilities';
78
89
// The list of workflows
9-
/** @type {import('$lib/types').Workflow[]} */
10+
/** @type {import('$lib/types-v2').WorkflowV2[]} */
1011
export let workflows = [];
1112
// Set the projectId prop to reference a specific project for each workflow
1213
export let projectId = undefined;
@@ -33,9 +34,9 @@
3334
3435
if (response.ok) {
3536
console.log('Workflow deleted');
36-
workflows = workflows.filter((wkf) => {
37-
return wkf.id !== workflowId;
38-
});
37+
const deletedWorkflow = workflows.filter((w) => w.id === workflowId)[0];
38+
workflows = workflows.filter((w) => w.id !== workflowId);
39+
saveSelectedDataset(deletedWorkflow, undefined);
3940
} else {
4041
const result = await response.json();
4142
console.error('Workflow not deleted', result);
@@ -44,7 +45,7 @@
4445
}
4546
4647
/**
47-
* @param {import('$lib/types').Workflow} importedWorkflow
48+
* @param {import('$lib/types-v2').WorkflowV2} importedWorkflow
4849
*/
4950
function handleWorkflowImported(importedWorkflow) {
5051
workflows.push(importedWorkflow);

0 commit comments

Comments
 (0)