Skip to content

Commit

Permalink
Merge pull request #701 from fractal-analytics-platform/type-filters-…
Browse files Browse the repository at this point in the history
…flow

Added type filters flow modal
  • Loading branch information
zonia3000 authored Jan 28, 2025
2 parents d4393d2 + b8e8426 commit 2a88bb4
Show file tree
Hide file tree
Showing 6 changed files with 395 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Unreleased

* Added type filters flow modal (\#701);
* Pre-populated the image zarr_url (\#703);
* Added validation for type filters and input types compatibility (\#703);
* Displayed input/output task type in "Type" tab on workflow page (\#691);
Expand Down
38 changes: 22 additions & 16 deletions components/src/lib/types/api.d.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import type { JSONSchemaObjectProperty } from './jschema';

export type User = {
id?: number
email: string
is_active: boolean
is_superuser: boolean
is_verified: boolean
username: string | null
password?: string
group_ids_names : Array<[number, string]> | null
oauth_accounts: Array<{
id: number
account_email: string
oauth_name: string
}>
id?: number
email: string
is_active: boolean
is_superuser: boolean
is_verified: boolean
username: string | null
password?: string
group_ids_names: Array<[number, string]> | null
oauth_accounts: Array<{
id: number
account_email: string
oauth_name: string
}>
}

export type DatasetHistoryItem = {
workflowtask: WorkflowTask
status: string
parallelization: object
workflowtask: WorkflowTask
status: string
parallelization: object
}

export type ProjectV2 = {
Expand Down Expand Up @@ -234,3 +234,9 @@ export type TaskGroupActivityV2 = {
action: TaskGroupActivityActionV2;
log: string | null;
};

export type TypeFiltersFlow = {
dataset_filters: Array<{ [key: string]: bool }>
input_filters: Array<{ [key: string]: bool }>
output_filters: Array<{ [key: string]: bool }>
}
21 changes: 21 additions & 0 deletions src/lib/components/v2/workflow/TypeFiltersCell.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script>
import BooleanIcon from 'fractal-components/common/BooleanIcon.svelte';
/** @type {{ [key: string]: boolean }} */
export let filters;
export let selectedTypeFilter = '';
</script>

{#each Object.entries(filters) as [key, value]}
{#if selectedTypeFilter === '' || key === selectedTypeFilter}
<div class="d-flex m-1">
<div class="flex-fill pe-3">
<code>{key}</code>
</div>
<div>
<BooleanIcon {value} />
</div>
</div>
{/if}
{/each}
229 changes: 229 additions & 0 deletions src/lib/components/v2/workflow/TypeFiltersFlowModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<script>
import { getAlertErrorFromResponse } from '$lib/common/errors';
import Modal from '$lib/components/common/Modal.svelte';
import TypeFiltersCell from './TypeFiltersCell.svelte';
/** @type {import("fractal-components/types/api").WorkflowV2} */
export let workflow;
/** @type {number|undefined} */
export let selectedDatasetId = undefined;
/** @type {import('fractal-components/types/api').DatasetV2[]} */
export let datasets;
/** @type {Modal} */
let modal;
let loading = false;
/** @type {import("fractal-components/types/api").TypeFiltersFlow|undefined} */
let typeFiltersFlow;
/** @type {number|undefined} */
let firstTaskIndex = undefined;
/** @type {number|undefined} */
let lastTaskIndex = undefined;
let selectedTypeFilter = '';
/** @type {string[]} */
let typeFilters = [];
export async function open() {
firstTaskIndex = undefined;
lastTaskIndex = undefined;
modal.show();
await loadData();
}
async function loadData() {
modal.hideErrorAlert();
loading = true;
const url = new URL(
`/api/v2/project/${workflow.project_id}/workflow/${workflow.id}/type-filters-flow`,
window.location.origin
);
if (selectedDatasetId !== undefined) {
url.searchParams.append('dataset_id', selectedDatasetId.toString());
}
if (firstTaskIndex !== undefined) {
url.searchParams.append('first_task_index', firstTaskIndex.toString());
}
if (lastTaskIndex !== undefined) {
url.searchParams.append('last_task_index', lastTaskIndex.toString());
}
const response = await fetch(url);
if (response.ok) {
typeFiltersFlow = await response.json();
typeFilters = getUniqueTypeFilters();
if (!typeFilters.includes(selectedTypeFilter)) {
selectedTypeFilter = '';
}
} else {
modal.displayErrorAlert(await getAlertErrorFromResponse(response));
}
loading = false;
}
function getUniqueTypeFilters() {
if (!typeFiltersFlow) {
return [];
}
const allTypeFilters = typeFiltersFlow.dataset_filters.flatMap((f) => Object.keys(f));
return [...new Set(allTypeFilters)].sort((t1, t2) =>
t1.localeCompare(t2, undefined, { sensitivity: 'base' })
);
}
async function onFirstTaskChanged() {
// reset last task
if (
lastTaskIndex !== undefined &&
firstTaskIndex !== undefined &&
firstTaskIndex > lastTaskIndex
) {
lastTaskIndex = undefined;
}
await loadData();
}
function onClose() {
typeFiltersFlow = undefined;
}
</script>

<Modal id="typeFiltersFlowModal" size="xl" bind:this={modal} {onClose}>
<svelte:fragment slot="header">
<h5 class="modal-title">Type filters</h5>
</svelte:fragment>
<svelte:fragment slot="body">
<div id="errorAlert-typeFiltersFlowModal" />
{#if loading && !typeFiltersFlow}
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
{/if}
{#if typeFiltersFlow}
<div class="row">
<div class="col">
<div class="input-group mb-3">
<label class="input-group-text" for="typeFiltersFlowDataset">Dataset</label>
<select
class="form-select"
id="typeFiltersFlowDataset"
bind:value={selectedDatasetId}
on:change={loadData}
>
<option value={undefined}>Select...</option>
{#each datasets as dataset}
<option value={dataset.id}>{dataset.name}</option>
{/each}
</select>
</div>
</div>
<div class="col">
<div class="input-group mb-3">
<label class="input-group-text" for="typeFiltersFlowFirstTaskIndex">First task</label>
<select
class="form-select"
id="typeFiltersFlowFirstTaskIndex"
bind:value={firstTaskIndex}
on:change={onFirstTaskChanged}
>
<option value={undefined}>Select...</option>
{#each workflow.task_list as wft}
<option value={wft.order}>{wft.task.name}</option>
{/each}
</select>
</div>
</div>
<div class="col">
<div class="input-group mb-3">
<label class="input-group-text" for="typeFiltersFlowLastTaskIndex">Last task</label>
<select
class="form-select"
id="typeFiltersFlowLastTaskIndex"
bind:value={lastTaskIndex}
on:change={loadData}
>
<option value={undefined}>Select...</option>
{#each workflow.task_list as wft}
{#if firstTaskIndex === undefined || wft.order >= firstTaskIndex}
<option value={wft.order}>{wft.task.name}</option>
{/if}
{/each}
</select>
</div>
</div>
<div class="col">
<div class="input-group mb-3">
<label class="input-group-text" for="typeFiltersFlowTypeFilter">Type</label>
<select
class="form-select"
id="typeFiltersFlowTypeFilter"
bind:value={selectedTypeFilter}
>
<option value="">Select...</option>
{#each typeFilters as type}
<option value={type}>{type}</option>
{/each}
</select>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Task</th>
<th>Pre</th>
<th>Input</th>
<th>Output</th>
<th>Post</th>
</tr>
</thead>
<tbody>
{#if !loading}
<!-- eslint-disable-next-line no-unused-vars -->
{#each Object.keys(typeFiltersFlow.input_filters) as _, index}
<tr>
<td>
{workflow.task_list[
firstTaskIndex === undefined ? index : firstTaskIndex + index
].task.name}
</td>
<td>
<TypeFiltersCell
{selectedTypeFilter}
filters={typeFiltersFlow.dataset_filters[index]}
/>
</td>
<td>
<TypeFiltersCell
{selectedTypeFilter}
filters={typeFiltersFlow.input_filters[index]}
/>
</td>
<td>
<TypeFiltersCell
{selectedTypeFilter}
filters={typeFiltersFlow.output_filters[index]}
/>
</td>
<td>
<TypeFiltersCell
{selectedTypeFilter}
filters={typeFiltersFlow.dataset_filters[index + 1]}
/>
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
{/if}
</svelte:fragment>
</Modal>
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import RunWorkflowModal from '$lib/components/v2/workflow/RunWorkflowModal.svelte';
import { getSelectedWorkflowDataset, saveSelectedDataset } from '$lib/common/workflow_utilities';
import AddWorkflowTaskModal from '$lib/components/v2/workflow/AddWorkflowTaskModal.svelte';
import TypeFiltersFlowModal from '$lib/components/v2/workflow/TypeFiltersFlowModal.svelte';
/** @type {import('fractal-components/types/api').WorkflowV2} */
let workflow = $page.data.workflow;
Expand Down Expand Up @@ -76,6 +77,8 @@
let addWorkflowTaskModal;
/** @type {Modal} */
let editWorkflowModal;
/** @type {TypeFiltersFlowModal} */
let typeFiltersFlowModal;
/** @type {{ [id: string]: import('fractal-components/types/api').TaskV2[] }} */
let newVersionsMap = {};
Expand Down Expand Up @@ -594,7 +597,7 @@
</nav>
</div>
<div class="row mt-2">
<div class="col-lg-9">
<div class="col-lg-8">
<div class="row">
<div class="col-lg-4 col-md-6">
<div class="input-group mb-3">
Expand Down Expand Up @@ -645,8 +648,17 @@
</div>
</div>
<div class="col-lg-3 mb-2">
<div class="col-lg-4 mb-2">
<div class="float-end">
{#if $page.data.userInfo.is_superuser}
<button
class="btn btn-light"
on:click|preventDefault={() => typeFiltersFlowModal.open()}
disabled={workflow.task_list.length === 0}
>
Type filters flow
</button>
{/if}
<a href="/v2/projects/{project?.id}/workflows/{workflow?.id}/jobs" class="btn btn-light">
<i class="bi-journal-code" /> List jobs
</a>
Expand All @@ -670,6 +682,13 @@
</div>
</div>
<TypeFiltersFlowModal
{workflow}
{selectedDatasetId}
datasets={sortedDatasets}
bind:this={typeFiltersFlowModal}
/>
{#if workflow}
<StandardDismissableAlert message={workflowSuccessMessage} />
Expand Down
Loading

0 comments on commit 2a88bb4

Please sign in to comment.