Skip to content

Commit a7b60ea

Browse files
committed
Allow naming single dataset pipeline output
1 parent e8128d0 commit a7b60ea

8 files changed

Lines changed: 225 additions & 48 deletions

File tree

client/dive-common/apispec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ interface DatasetMeta extends DatasetMetaMutable {
155155

156156
interface Api {
157157
getPipelineList(): Promise<Pipelines>;
158-
runPipeline(itemId: string, pipeline: Pipe): Promise<unknown>;
158+
runPipeline(itemId: string, pipeline: Pipe, additionalConfig?: Record<string, string>): Promise<unknown>;
159159
deleteTrainedPipeline(pipeline: Pipe): Promise<void>;
160160
exportTrainedPipeline(path: string, pipeline: Pipe): Promise<unknown>;
161161

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<script lang="ts">
2+
import {
3+
defineComponent,
4+
watch,
5+
// onMounted,
6+
ref,
7+
Ref,
8+
PropType,
9+
} from 'vue';
10+
// import sanitize from 'sanitize-filename';
11+
12+
export default defineComponent({
13+
props: {
14+
value: {
15+
type: Boolean,
16+
default: false,
17+
},
18+
datasetName: {
19+
type: String,
20+
required: true,
21+
},
22+
pipelineName: {
23+
type: String,
24+
required: true,
25+
},
26+
// Consumers of this component should make sure that for each i,
27+
// selectedDatasetIds[i] is the ID for the dataset with the name selectedDatasetNames[i]
28+
// Additionally, since this feature is currently only available in Desktop, and Desktop
29+
// runs a pipeline on a single dataset at a time, this array should always be length 1.
30+
selectedDatasetIds: {
31+
type: Array as PropType<string[]>,
32+
default: () => [],
33+
},
34+
selectedDatasetNames: {
35+
type: Array as PropType<string[]>,
36+
default: () => [],
37+
},
38+
},
39+
emits: ['cancel', 'submit'],
40+
setup(props, { emit }) {
41+
const formValid = ref(false);
42+
const dialogOpenTimestamp = ref('');
43+
const outputDatasetNames: Ref<Record<string, string>> = ref({});
44+
45+
watch(() => props.value, () => {
46+
dialogOpenTimestamp.value = (new Date()).toISOString().replace(/[:.]/g, '-');
47+
props.selectedDatasetIds.forEach((id: string) => {
48+
outputDatasetNames.value[id] = `${props.pipelineName}_${id}_${dialogOpenTimestamp.value}`;
49+
});
50+
});
51+
52+
function cancelPipeline() {
53+
emit('cancel');
54+
}
55+
56+
function submitPipelines() {
57+
emit('submit', outputDatasetNames.value);
58+
}
59+
60+
return {
61+
formValid,
62+
outputDatasetNames,
63+
cancelPipeline,
64+
submitPipelines,
65+
};
66+
},
67+
});
68+
</script>
69+
70+
<template>
71+
<v-dialog
72+
v-model="value"
73+
width="50%"
74+
>
75+
<v-card outlined>
76+
<v-card-title>
77+
Job Configuration
78+
</v-card-title>
79+
<v-card-text class="d-flex flex-column justify-center">
80+
You have selected a pipeline that will create a new dataset. Please choose a
81+
name for that dataset.
82+
<v-form
83+
v-model="formValid"
84+
class="mt-2"
85+
>
86+
<v-col>
87+
<v-row
88+
v-for="datasetId in selectedDatasetIds"
89+
:key="datasetId"
90+
class="d-flex justify-center align-end"
91+
>
92+
<v-label>
93+
{{ datasetId }}
94+
</v-label>
95+
<v-text-field
96+
v-model="outputDatasetNames[datasetId]"
97+
class="ml-2"
98+
label="Output dataset name"
99+
:rules="[v => !!v || 'Each output dataset must have a name']"
100+
hide-details
101+
/>
102+
</v-row>
103+
</v-col>
104+
</v-form>
105+
</v-card-text>
106+
<v-card-actions>
107+
<v-spacer />
108+
<v-btn
109+
color="error"
110+
@click="cancelPipeline"
111+
>
112+
Cancel
113+
</v-btn>
114+
<v-btn
115+
color="primary"
116+
:disabled="!formValid"
117+
@click="submitPipelines"
118+
>
119+
Submit
120+
</v-btn>
121+
</v-card-actions>
122+
</v-card>
123+
</v-dialog>
124+
</template>

client/dive-common/components/RunPipelineMenu.vue

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
<script lang="ts">
22
import {
3-
defineComponent, computed, PropType, ref, onBeforeMount,
3+
defineComponent,
4+
computed,
5+
PropType,
6+
ref,
7+
Ref,
8+
onBeforeMount,
49
} from 'vue';
510
import {
611
Pipelines,
@@ -10,14 +15,25 @@ import {
1015
DatasetType,
1116
} from 'dive-common/apispec';
1217
import JobLaunchDialog from 'dive-common/components/JobLaunchDialog.vue';
13-
import { stereoPipelineMarker, multiCamPipelineMarkers, LargeImageType } from 'dive-common/constants';
18+
import JobConfigFilterTranscodeDialog from 'dive-common/components/JobConfigFilterTranscodeDialog.vue';
19+
import {
20+
stereoPipelineMarker,
21+
multiCamPipelineMarkers,
22+
LargeImageType,
23+
pipelineCreatesDatasetMarkers,
24+
} from 'dive-common/constants';
1425
import { useRequest } from 'dive-common/use';
1526
import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
1627
28+
type MenuState = 'idle' | 'configuring';
29+
1730
export default defineComponent({
1831
name: 'RunPipelineMenu',
1932
20-
components: { JobLaunchDialog },
33+
components: {
34+
JobLaunchDialog,
35+
JobConfigFilterTranscodeDialog,
36+
},
2137
2238
props: {
2339
selectedDatasetIds: {
@@ -69,6 +85,14 @@ export default defineComponent({
6985
state: jobState,
7086
} = useRequest();
7187
88+
const menuState: Ref<MenuState> = ref('idle');
89+
const configuring = computed(() => menuState.value === 'configuring');
90+
const selectedPipeline: Ref<Pipe | null> = ref(null);
91+
const selectedPipelineName = computed(() => (selectedPipeline.value ? selectedPipeline.value.name : ''));
92+
function cancelConfig() {
93+
menuState.value = 'idle';
94+
}
95+
7296
const includesLargeImage = computed(() => props.typeList.includes(LargeImageType));
7397
7498
const successMessage = computed(() => (
@@ -124,7 +148,10 @@ export default defineComponent({
124148
return false;
125149
});
126150
127-
async function runPipelineOnSelectedItem(pipeline: Pipe) {
151+
async function _runPipelineOnSelectedItemInner(
152+
pipeline: Pipe,
153+
additionalConfigById?: Record<string, Record<string, string> | undefined>,
154+
) {
128155
if (props.selectedDatasetIds.length === 0) {
129156
throw new Error('No selected datasets to run on');
130157
}
@@ -150,10 +177,46 @@ export default defineComponent({
150177
}
151178
selectedPipe.value = pipeline;
152179
await _runPipelineRequest(() => Promise.all(
153-
datasetIds.map((id) => runPipeline(id, pipeline)),
180+
datasetIds.map((id) => {
181+
const additionalConfig = additionalConfigById ? additionalConfigById[id] : undefined;
182+
return runPipeline(id, pipeline, additionalConfig);
183+
}),
154184
));
155185
}
156186
187+
async function runPipelineOnSelectedItem(pipeline: Pipe) {
188+
if (!pipelineCreatesDatasetMarkers.includes(pipeline.type)) {
189+
_runPipelineOnSelectedItemInner(pipeline);
190+
} else {
191+
// If a pipeline creates datasets, open the configuration dialog
192+
// to allow users to name that dataset
193+
// This is relevant for filter and transcode pipeline types
194+
selectedPipeline.value = pipeline;
195+
menuState.value = 'configuring'; // force the dialog open
196+
}
197+
}
198+
199+
/**
200+
* Handle a user confirming additional configuration for filter
201+
* or transcode pipelines, which create new datasets.
202+
*
203+
* @param outputNameMap Map selected dataset IDs to the name
204+
* of the resultant dataset created by the pipeline
205+
*/
206+
async function exitPipelineConfig(outputNameMap: Record<string, string>) {
207+
menuState.value = 'idle'; // close the dialog
208+
const additionalConfigById: Record<string, Record<string, string>> = {};
209+
Object.keys(outputNameMap).forEach((id: string) => {
210+
additionalConfigById[id] = {
211+
outputDatasetName: outputNameMap[id],
212+
};
213+
});
214+
if (selectedPipeline.value) {
215+
_runPipelineOnSelectedItemInner(selectedPipeline.value, additionalConfigById);
216+
}
217+
selectedPipeline.value = null; // reset selected pipeline state
218+
}
219+
157220
function pipeTypeDisplay(pipeType: string) {
158221
switch (pipeType) {
159222
case 'trained':
@@ -179,6 +242,12 @@ export default defineComponent({
179242
runPipelineOnSelectedItem,
180243
pipelinesCurrentlyRunning,
181244
singlePipelineValue,
245+
selectedPipeline,
246+
selectedPipelineName,
247+
configuring,
248+
cancelConfig,
249+
menuState,
250+
exitPipelineConfig,
182251
};
183252
},
184253
});
@@ -337,5 +406,13 @@ export default defineComponent({
337406
:message="successMessage"
338407
@close="dismissLaunchDialog"
339408
/>
409+
<JobConfigFilterTranscodeDialog
410+
:value="menuState === 'configuring'"
411+
:dataset-name="'foo'"
412+
:pipeline-name="selectedPipelineName"
413+
:selected-dataset-ids="selectedDatasetIds"
414+
@cancel="cancelConfig"
415+
@submit="exitPipelineConfig"
416+
/>
340417
</div>
341418
</template>

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"lodash": "^4.17.19",
5151
"moment": "^2.29.1",
5252
"mousetrap": "^1.6.5",
53+
"sanitize-filename": "^1.6.3",
5354
"semver": "^7.3.5",
5455
"vue": "^2.7.16",
5556
"vue-gtag": "^1.9.1",

client/platform/desktop/frontend/api.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,12 @@ async function getTrainingConfigurations(): Promise<TrainingConfigs> {
9191
return ipcRenderer.invoke('get-training-configs');
9292
}
9393

94-
async function runPipeline(itemId: string, pipeline: Pipe): Promise<void> {
94+
async function runPipeline(itemId: string, pipeline: Pipe, additionalConfig?: Record<string, string>): Promise<void> {
9595
const args: RunPipeline = {
9696
type: JobType.RunPipeline,
9797
pipeline,
9898
datasetId: itemId,
99-
};
100-
gpuJobQueue.enqueue(args);
101-
}
102-
103-
async function runPipelineWithOutput(itemId: string, pipeline: Pipe, outputDatasetName: string): Promise<void> {
104-
const args: RunPipeline = {
105-
type: JobType.RunPipeline,
106-
pipeline,
107-
datasetId: itemId,
108-
outputDatasetName,
99+
...additionalConfig,
109100
};
110101
gpuJobQueue.enqueue(args);
111102
}
@@ -287,5 +278,4 @@ export {
287278
openLink,
288279
nvidiaSmi,
289280
cancelJob,
290-
runPipelineWithOutput,
291281
};

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
import { DataTableHeader } from 'vuetify';
1010
import { useRouter } from 'vue-router/composables';
1111
import { Pipe, Pipelines, useApi } from 'dive-common/apispec';
12-
import { runPipelineWithOutput } from 'platform/desktop/frontend/api';
1312
import {
1413
itemsPerPageOptions,
1514
stereoPipelineMarker,
@@ -133,7 +132,9 @@ async function runPipelineForDatasets() {
133132
if (!datasetMeta) {
134133
throw new Error(`Attempted to run pipeline on nonexistant dataset ${datasetId}`);
135134
}
136-
return runPipelineWithOutput(datasetId, selectedPipeline.value!, computeOutputDatasetName(datasetMeta));
135+
return runPipeline(datasetId, selectedPipeline.value!, {
136+
outputDatasetName: computeOutputDatasetName(datasetMeta),
137+
});
137138
}
138139
return runPipeline(datasetId, selectedPipeline.value!);
139140
}),

client/platform/web-girder/views/Home.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ export default defineComponent({
8080
({ _modelType, meta }) => _modelType === 'folder' && meta && meta.annotate,
8181
).map(({ _id }) => _id);
8282
},
83+
selectedViameFolderNames() {
84+
return this.selected.filter(
85+
({ _modelType, meta }) => _modelType === 'folder' && meta && meta.annotate,
86+
).map(({ name }) => name);
87+
},
8388
selectedFileIds() {
8489
return this.selected.filter(
8590
(element) => element._modelType === 'item',
@@ -93,6 +98,9 @@ export default defineComponent({
9398
locationInputs() {
9499
return this.locationIsViameFolder ? [this.location._id] : this.selectedViameFolderIds;
95100
},
101+
locationInputNames() {
102+
return this.locationIsViameFolder ? [this.location.name] : this.selectedViameFolderNames;
103+
},
96104
selectedDescription() {
97105
return this.location?.description;
98106
},
@@ -179,6 +187,7 @@ export default defineComponent({
179187
menuOptions,
180188
}"
181189
:selected-dataset-ids="locationInputs"
190+
:selected-dataset-name="locationInputNames"
182191
:running-pipelines="runningPipelines"
183192
/>
184193
<export

0 commit comments

Comments
 (0)