Skip to content

Commit 6cde9f0

Browse files
authored
Add bulk pipeline interface (#1531)
* Add bulk pipeline tab and basic interface * Start pipeline jobs when button is clicked
1 parent d7284b7 commit 6cde9f0

File tree

4 files changed

+274
-0
lines changed

4 files changed

+274
-0
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
<script setup lang="ts">
2+
import {
3+
computed,
4+
onBeforeMount,
5+
ref,
6+
Ref,
7+
watch,
8+
} from 'vue';
9+
import { DataTableHeader } from 'vuetify';
10+
import { useRouter } from 'vue-router/composables';
11+
import { Pipe, Pipelines, useApi } from 'dive-common/apispec';
12+
import {
13+
itemsPerPageOptions,
14+
stereoPipelineMarker,
15+
multiCamPipelineMarkers,
16+
MultiType,
17+
} from 'dive-common/constants';
18+
import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
19+
import { clientSettings } from 'dive-common/store/settings';
20+
import { datasets, JsonMetaCache } from '../store/dataset';
21+
22+
const { getPipelineList, runPipeline } = useApi();
23+
const { prompt } = usePrompt();
24+
const router = useRouter();
25+
26+
const unsortedPipelines = ref({} as Pipelines);
27+
const selectedPipelineType: Ref<string | null> = ref(null);
28+
const pipelineTypes = computed(() => (
29+
// For now, exclude 2-cam and 3-cam pipeline types from
30+
// bulk pipeline operations.
31+
Object.keys(unsortedPipelines.value)
32+
.filter((key) => !multiCamPipelineMarkers.includes(key))
33+
));
34+
const selectedPipeline: Ref<Pipe | null> = ref(null);
35+
const pipesForSelectedType = computed(() => {
36+
if (!selectedPipelineType.value) {
37+
return null;
38+
}
39+
return unsortedPipelines.value[selectedPipelineType.value].pipes;
40+
});
41+
watch(selectedPipelineType, () => {
42+
selectedPipeline.value = null;
43+
});
44+
45+
const headersTmpl: DataTableHeader[] = [
46+
{
47+
text: 'Dataset',
48+
value: 'name',
49+
sortable: true,
50+
},
51+
{
52+
text: 'Type',
53+
value: 'type',
54+
sortable: true,
55+
width: 160,
56+
},
57+
{
58+
text: 'fps',
59+
value: 'fps',
60+
sortable: true,
61+
width: 80,
62+
},
63+
];
64+
const availableDatasetHeaders = headersTmpl.concat(
65+
{
66+
text: 'Include',
67+
value: 'include',
68+
sortable: false,
69+
width: 80,
70+
},
71+
);
72+
const stagedDatasetHeaders: DataTableHeader[] = headersTmpl.concat([
73+
{
74+
text: 'Remove',
75+
value: 'remove',
76+
sortable: false,
77+
width: 80,
78+
},
79+
]);
80+
function getAvailableItems(): JsonMetaCache[] {
81+
if (!selectedPipelineType.value || !selectedPipeline.value) {
82+
return [];
83+
}
84+
if (selectedPipelineType.value === stereoPipelineMarker) {
85+
// Only allow stereo datasets to be included for bulk pipeline
86+
// operations if the selected pipeline type is a measurement.
87+
return Object.values(datasets.value).filter((dataset: JsonMetaCache) => (
88+
dataset.type === MultiType && dataset.cameraNumber === 2
89+
));
90+
}
91+
return Object.values(datasets.value);
92+
}
93+
const availableItems: Ref<JsonMetaCache[]> = ref([]);
94+
const availableDatasetSearch = ref('');
95+
const stagedDatasetIds: Ref<string[]> = ref([]);
96+
const stagedDatasets = computed(() => availableItems.value.filter((item: JsonMetaCache) => stagedDatasetIds.value.includes(item.id)));
97+
watch(selectedPipeline, () => {
98+
availableItems.value = getAvailableItems();
99+
});
100+
function toggleStaged(item: JsonMetaCache) {
101+
if (stagedDatasetIds.value.includes(item.id)) {
102+
stagedDatasetIds.value = stagedDatasetIds.value.filter((id: string) => id !== item.id);
103+
} else {
104+
stagedDatasetIds.value.push(item.id);
105+
}
106+
}
107+
108+
async function runPipelineForDatasets() {
109+
if (selectedPipeline.value !== null) {
110+
const results = await Promise.allSettled(
111+
stagedDatasetIds.value.map((datasetId: string) => runPipeline(datasetId, selectedPipeline.value!)),
112+
);
113+
const failed = results
114+
.map((result, i) => ({ result, datasetId: stagedDatasetIds.value[i] }))
115+
.filter(({ result }) => result.status === 'rejected');
116+
117+
if (failed.length > 0) {
118+
prompt({
119+
title: 'Pipeline Errors',
120+
text: `Failed to run pipeline for ${failed.length} dataset${failed.length > 1 ? 's' : ''}.`,
121+
positiveButton: 'OK',
122+
});
123+
} else {
124+
router.push({ name: 'jobs' });
125+
}
126+
}
127+
}
128+
129+
onBeforeMount(async () => {
130+
unsortedPipelines.value = await getPipelineList();
131+
availableItems.value = getAvailableItems();
132+
});
133+
134+
</script>
135+
136+
<template>
137+
<div>
138+
<div class="mb-4">
139+
<v-card-title class="text-h4">
140+
Run a pipeline on multiple datasets
141+
</v-card-title>
142+
<v-card-text>Choose a pipeline to run, then select datasets.</v-card-text>
143+
</div>
144+
<div class="mb-4">
145+
<v-card-title class="text-h4">
146+
Choose a VIAME pipeline
147+
</v-card-title>
148+
<v-card-text>
149+
<v-row>
150+
<v-col cols="6">
151+
<v-select
152+
v-model="selectedPipelineType"
153+
:items="pipelineTypes"
154+
outlined
155+
persistent-hint
156+
dense
157+
label="Pipeline Type"
158+
hint="Select which type of pipeline to run"
159+
/>
160+
</v-col>
161+
<v-col>
162+
<v-select
163+
v-model="selectedPipeline"
164+
:items="pipesForSelectedType"
165+
:disabled="!selectedPipelineType"
166+
return-object
167+
item-text="name"
168+
outlined
169+
persistent-hint
170+
dense
171+
label="Pipeline"
172+
hint="Select the pipeline to run"
173+
/>
174+
</v-col>
175+
</v-row>
176+
</v-card-text>
177+
<div v-if="selectedPipeline">
178+
<v-card-title>Datasets staged for selected pipeline</v-card-title>
179+
<v-data-table
180+
dense
181+
v-bind="{ headers: stagedDatasetHeaders, items: stagedDatasets }"
182+
:items-per-page.sync="clientSettings.rowsPerPage"
183+
hide-default-footer
184+
:hide-default-header="stagedDatasets.length === 0"
185+
no-data-text="Select datasets from the table below"
186+
>
187+
<template #[`item.remove`]="{ item }">
188+
<v-btn
189+
color="error"
190+
x-small
191+
@click="toggleStaged(item)"
192+
>
193+
<v-icon>mdi-minus</v-icon>
194+
</v-btn>
195+
</template>
196+
</v-data-table>
197+
</div>
198+
<v-row class="mt-7">
199+
<v-spacer />
200+
<v-col cols="auto">
201+
<v-btn
202+
:disabled="stagedDatasets.length === 0"
203+
color="primary"
204+
@click="runPipelineForDatasets"
205+
>
206+
Run pipeline for ({{ stagedDatasets.length }}) Datasets
207+
</v-btn>
208+
</v-col>
209+
</v-row>
210+
</div>
211+
<div
212+
v-if="selectedPipeline"
213+
class="mb-4"
214+
>
215+
<v-card-title class="text-h4">
216+
Available datasets
217+
</v-card-title>
218+
<v-card-text>These datasets are compatible with the chosen pipeline.</v-card-text>
219+
<v-row class="mb-2">
220+
<v-col cols="6">
221+
<v-text-field
222+
v-model="availableDatasetSearch"
223+
append-icon="mdi-magnify"
224+
label="Search"
225+
single-line
226+
hide-details
227+
/>
228+
</v-col>
229+
</v-row>
230+
<v-data-table
231+
dense
232+
v-bind="{ headers: availableDatasetHeaders, items: availableItems }"
233+
:footer-props="{ itemsPerPageOptions }"
234+
:items-per-page.sync="clientSettings.rowsPerPage"
235+
:search="availableDatasetSearch"
236+
no-data-text="No compatible datasets found for the selected pipeline."
237+
>
238+
<template #[`item.include`]="{ item }">
239+
<v-btn
240+
:key="item.name"
241+
:disabled="stagedDatasetIds.includes(item.id)"
242+
color="success"
243+
x-small
244+
@click="toggleStaged(item)"
245+
>
246+
<v-icon>mdi-plus</v-icon>
247+
</v-btn>
248+
</template>
249+
</v-data-table>
250+
</div>
251+
</div>
252+
</template>

client/platform/desktop/frontend/components/NavigationBar.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export default defineComponent({
2828
<v-tab :to="{ name: 'training' }">
2929
Training<v-icon>mdi-brain</v-icon>
3030
</v-tab>
31+
<v-tab :to="{ name: 'pipeline' }">
32+
Pipeline<v-icon>mdi-pipe</v-icon>
33+
</v-tab>
3134
<v-tab :to="{ name: 'settings' }">
3235
Settings<v-icon>mdi-cog</v-icon>
3336
</v-tab>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script setup lang="ts">
2+
import NavigationBar from './NavigationBar.vue';
3+
import MultiPipeline from './MultiPipeline.vue';
4+
</script>
5+
6+
<template>
7+
<v-main>
8+
<navigation-bar />
9+
<v-container>
10+
<multi-pipeline />
11+
</v-container>
12+
</v-main>
13+
</template>

client/platform/desktop/router.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Recent from './frontend/components/Recent.vue';
66
import Settings from './frontend/components/Settings.vue';
77
import TrainingPage from './frontend/components/TrainingPage.vue';
88
import ViewerLoader from './frontend/components/ViewerLoader.vue';
9+
import PipelinePage from './frontend/components/PipelinePage.vue';
910

1011
Vue.use(Router);
1112

@@ -26,6 +27,11 @@ export default new Router({
2627
name: 'training',
2728
component: TrainingPage,
2829
},
30+
{
31+
path: '/pipeline',
32+
name: 'pipeline',
33+
component: PipelinePage,
34+
},
2935
{
3036
path: '/jobs',
3137
name: 'jobs',

0 commit comments

Comments
 (0)