Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions client/platform/desktop/frontend/components/MultiPipeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
watch,
} from 'vue';
import { DataTableHeader } from 'vuetify';
import { useRouter } from 'vue-router/composables';
import { useRoute, useRouter } from 'vue-router/composables';
import { Pipe, Pipelines, useApi } from 'dive-common/apispec';
import { parentDatasetId } from 'dive-common/compositeDatasetId';
import {
Expand All @@ -30,6 +30,16 @@ import { datasets, JsonConfigCache } from '../store/dataset';
const { getPipelineList, runPipeline, hasCalibrationFile } = useApi();
const { prompt } = usePrompt();
const router = useRouter();
const route = useRoute();

/** Dataset ids handed off by another page (e.g. the Library selection). */
function preselectedDatasetIds(): string[] {
const query = route.query.datasetIds;
const values = Array.isArray(query) ? query : [query];
return values
.flatMap((value) => (value || '').split(','))
.filter((id) => id in datasets.value);
}

const unsortedPipelines = ref({} as Pipelines);
const selectedPipelineType: Ref<string | null> = ref(null);
Expand Down Expand Up @@ -173,8 +183,11 @@ function toggleStaged(item: JsonConfigCache) {

async function runPipelineForDatasets() {
if (selectedPipeline.value !== null) {
// Only the staged datasets compatible with (and displayed for) the
// selected pipeline; staged ids can hold datasets other pipelines accept.
const runIds = stagedDatasets.value.map((item: JsonConfigCache) => item.id);
const results = await Promise.allSettled(
stagedDatasetIds.value.map((datasetId: string) => {
runIds.map((datasetId: string) => {
if (pipelineCreatesNewDataset(selectedPipeline.value)) {
const datasetMeta = availableItems.value.find((item: JsonConfigCache) => item.id === datasetId);
if (!datasetMeta) {
Expand All @@ -188,7 +201,7 @@ async function runPipelineForDatasets() {
}),
);
const failed = results
.map((result, i) => ({ result, datasetId: stagedDatasetIds.value[i] }))
.map((result, i) => ({ result, datasetId: runIds[i] }))
.filter(({ result }) => result.status === 'rejected');

if (failed.length > 0) {
Expand All @@ -204,6 +217,7 @@ async function runPipelineForDatasets() {
}

onBeforeMount(async () => {
stagedDatasetIds.value = preselectedDatasetIds();
unsortedPipelines.value = await getPipelineList();
availableItems.value = getAvailableItems();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
import { itemsPerPageOptions, simplifyTrainingName } from 'dive-common/constants';
import { clientSettings } from 'dive-common/store/settings';

import { useRouter } from 'vue-router/composables';
import { useRoute, useRouter } from 'vue-router/composables';
import { DesktopJob, RunTraining } from 'platform/desktop/constants';
import {
listResumableTrainingJobs, resumeTraining, discardResumableTraining,
Expand All @@ -37,6 +37,7 @@ export default defineComponent({
} = useApi();
const { prompt } = usePrompt();
const router = useRouter();
const route = useRoute();

const unsortedPipelines = ref({} as Pipelines);
const labelFile = ref(null as File | null);
Expand All @@ -60,6 +61,19 @@ export default defineComponent({
unsortedPipelines.value = await getPipelineList();
});

/* Stage dataset ids handed off by another page (e.g. the Library selection). */
onBeforeMount(() => {
const query = route.query.datasetIds;
const values = Array.isArray(query) ? query : [query];
values
.flatMap((value) => (value || '').split(','))
.forEach((id) => {
const meta = datasets.value[id];
if (meta && meta.subType === null) {
set(data.stagedItems, id, meta);
}
});
});
const resumableJobs = ref([] as DesktopJob[]);

async function refreshResumable() {
Expand Down
202 changes: 178 additions & 24 deletions client/platform/desktop/frontend/components/Recent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ImportButton from 'dive-common/components/ImportButton.vue';
import ImportMultiCamDialog from 'dive-common/components/ImportMultiCamDialog.vue';
import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
import { useRequest } from 'dive-common/use';
import { getResponseError } from 'vue-media-annotator/utils';
import { DataTableHeader } from 'vuetify';

import { useRouter } from 'vue-router/composables';
Expand Down Expand Up @@ -147,26 +148,98 @@ export default defineComponent({
pendingImportPayload.value = [await request(() => api.importMultiCam(args))];
}

async function confirmDeleteDataset(datasetId: string, datasetName: string) {
const selectedRecents = ref([] as JsonConfigCache[]);
const selectedIds = computed(() => new Set(selectedRecents.value.map((item) => item.id)));

function isSelected(item: JsonConfigCache) {
return selectedIds.value.has(item.id);
}

function toggleSelected(item: JsonConfigCache) {
if (isSelected(item)) {
selectedRecents.value = selectedRecents.value.filter((v) => v.id !== item.id);
} else {
selectedRecents.value = selectedRecents.value.concat([item]);
}
}

async function confirmDeleteSelected() {
const items = selectedRecents.value;
if (items.length === 0) {
return;
}
const result = await prompt({
title: 'Warning Deleting Dataset',
text: [`Do you want to delete dataset ${datasetName}?`,
'1. Deleting dataset will not remove source media, such as images or video.',
'2. It will not remove annotations files that were imported when the dataset was created.',
'3. This will remove any annotations that bave been created in DIVE for this dataset',
'4. Use the Export button for the dataset to create a copy of the last set of annotations'],
title: `Delete ${items.length} dataset${items.length > 1 ? 's' : ''}`,
text: ['Do you want to delete the selected datasets?',
'1. Deleting datasets will not remove source media, such as images or video.',
'2. It will not remove annotations files that were imported when the datasets were created.',
'3. This will remove any annotations that have been created in DIVE for these datasets',
'4. Use the Export button for a dataset to create a copy of the last set of annotations'],
positiveButton: 'Delete',
negativeButton: 'Cancel',
confirm: true,
});
if (!result) {
return;
}
await request(() => api.deleteDataset(datasetId));
//Now we need to update recents by removing the dataset from localStorage
removeRecents(datasetId);
const failures: { id: string; name: string; reason: string }[] = [];
// Continue through the full selection so one failure does not skip the rest
// eslint-disable-next-line no-restricted-syntax
for (const item of items) {
try {
// eslint-disable-next-line no-await-in-loop
await api.deleteDataset(item.id);
removeRecents(item.id);
} catch (err) {
failures.push({ id: item.id, name: item.name, reason: getResponseError(err) });
}
}
const failedIds = new Set(failures.map((failure) => failure.id));
selectedRecents.value = items.filter((item) => failedIds.has(item.id));
if (failures.length > 0) {
const deletedCount = items.length - failures.length;
await prompt({
title: deletedCount > 0
? 'Some datasets could not be deleted'
: 'Failed to delete datasets',
text: [
deletedCount > 0
? `Deleted ${deletedCount} of ${items.length} datasets. The following could not be deleted:`
: 'None of the selected datasets could be deleted:',
...failures.map((failure) => `${failure.name}: ${failure.reason}`),
],
positiveButton: 'Okay',
});
}
}

const filteredRecents = computed(() => recents.value
.filter((v) => v.name.toLowerCase().indexOf((searchText.value || '').toLowerCase()) >= 0));
const allSelected = computed(() => filteredRecents.value.length > 0
&& filteredRecents.value.every((item) => selectedIds.value.has(item.id)));
const someSelected = computed(() => filteredRecents.value.some(
(item) => selectedIds.value.has(item.id),
));

function toggleSelectAll() {
if (allSelected.value) {
selectedRecents.value = [];
} else {
selectedRecents.value = filteredRecents.value.slice();
}
}

function selectedIdsQuery() {
return { datasetIds: selectedRecents.value.map((item) => item.id).join(',') };
}

function runPipelineOnSelected() {
router.push({ name: 'pipeline', query: selectedIdsQuery() });
}

function runTrainingOnSelected() {
router.push({ name: 'training', query: selectedIdsQuery() });
}
function getTypeIcon(recent: JsonConfigCache) {
if (recent.subType) {
if (recent.subType === 'stereo') {
Expand Down Expand Up @@ -217,6 +290,12 @@ export default defineComponent({
}

const headers: DataTableHeader[] = [
{
text: '',
value: 'select',
sortable: false,
width: 40,
},
{
text: 'Type',
value: 'type',
Expand All @@ -235,12 +314,6 @@ export default defineComponent({
sort: (a: string, b: string) => parseRecentDate(b).valueOf() - parseRecentDate(a).valueOf(),
width: 140,
},
{
text: '',
value: 'delete',
sortable: false,
width: 40,
},
];
const toDisplayString = (dateString: string) => {
const parsed = parseRecentDate(dateString);
Expand All @@ -258,14 +331,22 @@ export default defineComponent({
openMultiCamDialog,
getTypeIcon,
importMedia: api.importMedia,
confirmDeleteDataset,
confirmDeleteSelected,
runPipelineOnSelected,
runTrainingOnSelected,
isSelected,
toggleSelected,
toggleSelectAll,
preloadCheck,
toDisplayString,
resetError,
// state
multiCamOpenType,
stereo,
filteredRecents,
selectedRecents,
allSelected,
someSelected,
pendingImportPayload,
bulkImport,
searchText,
Expand Down Expand Up @@ -470,14 +551,79 @@ export default defineComponent({
Recent
</div>
<v-spacer />
<template v-if="selectedRecents.length > 0">
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
class="align-self-center"
color="primary"
outlined
small
v-on="on"
@click="runPipelineOnSelected"
>
<v-icon
left
small
>
mdi-play
</v-icon>
Run Pipeline
</v-btn>
</template>
<span>Run a pipeline on the selected datasets</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
class="ml-2 align-self-center"
color="primary"
outlined
small
v-on="on"
@click="runTrainingOnSelected"
>
<v-icon
left
small
>
mdi-brain
</v-icon>
Run Training
</v-btn>
</template>
<span>Train a model on the selected datasets</span>
</v-tooltip>
<v-tooltip bottom>
<template #activator="{ on }">
<v-btn
class="ml-2 align-self-center"
color="error"
outlined
small
v-on="on"
@click="confirmDeleteSelected"
>
<v-icon
left
small
>
mdi-delete
</v-icon>
Delete ({{ selectedRecents.length }})
</v-btn>
</template>
<span>Delete all selected datasets</span>
</v-tooltip>
</template>
<v-text-field
v-model="searchText"
dense
outlined
clearable
hide-details
placeholder="search"
class="shrink"
class="shrink ml-4"
color="grey darken-1"
>
<template #append>
Expand All @@ -499,10 +645,19 @@ export default defineComponent({
dense
v-bind="{ headers: headers, items: filteredRecents }"
sort-by="accessedAt"
item-key="id"
:footer-props="{ itemsPerPageOptions }"
:items-per-page.sync="clientSettings.rowsPerPage"
no-data-text="No data loaded"
>
<template #[`header.select`]>
<v-simple-checkbox
:value="allSelected"
:indeterminate="someSelected && !allSelected"
:ripple="false"
@input="toggleSelectAll"
/>
</template>
<template #[`item.type`]="{ item }">
<tooltip-btn
:key="item.id"
Expand Down Expand Up @@ -588,13 +743,12 @@ export default defineComponent({
{{ toDisplayString(item.accessedAt) }}
</span>
</template>
<template #[`item.delete`]="{ item }">
<tooltip-btn
<template #[`item.select`]="{ item }">
<v-simple-checkbox
:key="item.id"
color="error"
icon="mdi-delete"
:tooltip-text="'Delete'"
@click="confirmDeleteDataset(item.id, item.name)"
:value="isSelected(item)"
:ripple="false"
@input="toggleSelected(item)"
/>
</template>
</v-data-table>
Expand Down
Loading