diff --git a/client/dive-common/components/SidebarContext.vue b/client/dive-common/components/SidebarContext.vue index ac5900737..8818d6438 100644 --- a/client/dive-common/components/SidebarContext.vue +++ b/client/dive-common/components/SidebarContext.vue @@ -9,7 +9,6 @@ export default defineComponent({ default: 300, }, }, - components: context.componentMap, setup() { return { context }; }, @@ -40,9 +39,7 @@ export default defineComponent({ diff --git a/client/dive-common/components/TypeThreshold.vue b/client/dive-common/components/TypeThreshold.vue index d259d5329..570fb35b8 100644 --- a/client/dive-common/components/TypeThreshold.vue +++ b/client/dive-common/components/TypeThreshold.vue @@ -13,7 +13,6 @@ import { DefaultConfidence } from 'vue-media-annotator/use/useTrackFilters'; export default defineComponent({ name: 'TypeThreshold', - description: 'Threshold Controls', components: { ConfidenceFilter }, diff --git a/client/dive-common/components/Viewer.vue b/client/dive-common/components/Viewer.vue index 6d9d29e86..b8bed61c4 100644 --- a/client/dive-common/components/Viewer.vue +++ b/client/dive-common/components/Viewer.vue @@ -34,7 +34,6 @@ import UserGuideButton from 'dive-common/components/UserGuideButton.vue'; import DeleteControls from 'dive-common/components/DeleteControls.vue'; import ControlsContainer from 'dive-common/components/ControlsContainer.vue'; import Sidebar from 'dive-common/components/Sidebar.vue'; -import SidebarContext from 'dive-common/components/SidebarContext.vue'; import { useModeManager, useSave } from 'dive-common/use'; import clientSettingsSetup, { clientSettings } from 'dive-common/store/settings'; import { useApi, FrameImage, DatasetType } from 'dive-common/apispec'; @@ -48,7 +47,6 @@ export default defineComponent({ ControlsContainer, DeleteControls, Sidebar, - SidebarContext, LayerManager, VideoAnnotator, ImageAnnotator, @@ -422,6 +420,8 @@ export default defineComponent({ intervalTree, mergeList, pendingSaveCount, + progress, + revisionId: toRef(props, 'revision'), trackMap, filteredTracks, typeStyling, @@ -555,6 +555,7 @@ export default defineComponent({ {{ item }} {{ item === defaultCamera ? '(Default)': '' }} + @@ -666,7 +667,7 @@ export default defineComponent({ - + diff --git a/client/dive-common/store/context.ts b/client/dive-common/store/context.ts index 7356802bc..d07912654 100644 --- a/client/dive-common/store/context.ts +++ b/client/dive-common/store/context.ts @@ -1,36 +1,57 @@ import Install, { reactive } from '@vue/composition-api'; -import Vue from 'vue'; +import Vue, { VueConstructor } from 'vue'; /* Components */ import TypeThreshold from 'dive-common/components/TypeThreshold.vue'; Vue.use(Install); - -const componentMap = { - TypeThreshold, -}; - -type ContextType = keyof typeof componentMap; - interface ContextState { - active: ContextType | null; + active: string | null; +} + +interface ComponentMapItem { + description: string; + component: VueConstructor; } const state: ContextState = reactive({ active: null, }); -function toggle(active: ContextType | null) { +const componentMap: Record = { + TypeThreshold: { + description: 'Threshold Controls', + component: TypeThreshold, + }, +}; + +function register(item: ComponentMapItem) { + componentMap[item.component.name] = item; +} + +function getComponents() { + const components: Record> = {}; + Object.values(componentMap).forEach((v) => { + components[v.component.name] = v.component; + }); + return components; +} + +function toggle(active: string | null) { if (active && state.active === active) { state.active = null; - } else { + } else if (active === null || active in componentMap) { state.active = active; + } else { + throw new Error(`${active} is not a valid context component`); } window.dispatchEvent(new Event('resize')); } export default { toggle, + register, + getComponents, componentMap, state, }; diff --git a/client/dive-common/use/useRequest.ts b/client/dive-common/use/useRequest.ts index ec91c3c81..443fc57fe 100644 --- a/client/dive-common/use/useRequest.ts +++ b/client/dive-common/use/useRequest.ts @@ -1,4 +1,5 @@ -import { reactive, toRefs } from '@vue/composition-api'; +import { reactive, shallowRef, toRefs } from '@vue/composition-api'; +import { AxiosResponse } from 'axios'; import { getResponseError } from 'vue-media-annotator/utils'; export default function useRequest() { @@ -36,3 +37,43 @@ export default function useRequest() { reset, }; } + +export function usePaginatedRequest() { + const main = useRequest(); + const paginationParams = reactive({ + totalCount: 0, + offset: 0, + limit: 20, + }); + const allPages = shallowRef([] as T[]); + + function reset() { + paginationParams.totalCount = 0; + paginationParams.offset = 0; + paginationParams.limit = 20; + allPages.value = []; + main.reset(); + } + + async function loadNextPage( + func: (limit: number, offset: number) => Promise>, + ) { + const wrapped = () => main.request(() => func(paginationParams.limit, paginationParams.offset)); + const nextOffset = paginationParams.offset + paginationParams.limit; + const maxOffset = (paginationParams.totalCount + paginationParams.limit); + if (nextOffset < maxOffset || main.count.value === 0) { + const resp = await wrapped(); + paginationParams.offset = nextOffset; + paginationParams.totalCount = Number.parseInt(resp.headers['girder-total-count'], 10); + allPages.value = allPages.value.concat(resp.data); + } + } + + return { + ...main, + ...toRefs(paginationParams), + allPages, + reset, + loadNextPage, + }; +} diff --git a/client/platform/desktop/frontend/components/ViewerLoader.vue b/client/platform/desktop/frontend/components/ViewerLoader.vue index 73675a35c..b283a5cf3 100644 --- a/client/platform/desktop/frontend/components/ViewerLoader.vue +++ b/client/platform/desktop/frontend/components/ViewerLoader.vue @@ -6,7 +6,8 @@ import { import Viewer from 'dive-common/components/Viewer.vue'; import RunPipelineMenu from 'dive-common/components/RunPipelineMenu.vue'; import ImportAnnotations from 'dive-common//components/ImportAnnotations.vue'; - +import SidebarContext from 'dive-common/components/SidebarContext.vue'; +import context from 'dive-common/store/context'; import { usePrompt } from 'dive-common/vue-utilities/prompt-service'; import Export from './Export.vue'; import JobTab from './JobTab.vue'; @@ -33,8 +34,10 @@ export default defineComponent({ Export, JobTab, RunPipelineMenu, + SidebarContext, Viewer, ImportAnnotations, + ...context.getComponents(), }, props: { id: { // always the base ID @@ -143,5 +146,12 @@ export default defineComponent({ :button-options="buttonOptions" /> + diff --git a/client/platform/web-girder/api/annotation.service.ts b/client/platform/web-girder/api/annotation.service.ts index a9d5d9a91..5f50e9043 100644 --- a/client/platform/web-girder/api/annotation.service.ts +++ b/client/platform/web-girder/api/annotation.service.ts @@ -2,6 +2,17 @@ import { SaveDetectionsArgs } from 'dive-common/apispec'; import { TrackData } from 'vue-media-annotator/track'; import girderRest from 'platform/web-girder/plugins/girder'; +export interface Revision { + additions: Readonly; + deletions: Readonly; + author_id: Readonly; + author_name: Readonly; + created: Readonly; + dataset: Readonly; + description: Readonly; + revision: Readonly; +} + function loadDetections(folderId: string, revision?: number) { const params: Record = { folderId }; if (revision !== undefined) { @@ -10,6 +21,19 @@ function loadDetections(folderId: string, revision?: number) { return girderRest.get<{ [key: string]: TrackData }>('dive_annotation', { params }); } +function loadRevisions( + folderId: string, + limit?: number, + offset?: number, + sort?: string, +) { + return girderRest.get('dive_annotation/revision', { + params: { + folderId, sortdir: -1, limit, offset, sort, + }, + }); +} + function saveDetections(folderId: string, args: SaveDetectionsArgs) { return girderRest.patch('dive_annotation', { upsert: args.upsert, @@ -21,5 +45,6 @@ function saveDetections(folderId: string, args: SaveDetectionsArgs) { export { loadDetections, + loadRevisions, saveDetections, }; diff --git a/client/platform/web-girder/router.ts b/client/platform/web-girder/router.ts index 3fdf99a7a..8a1106b18 100644 --- a/client/platform/web-girder/router.ts +++ b/client/platform/web-girder/router.ts @@ -35,8 +35,8 @@ const router = new Router({ beforeEnter, }, { - path: '/viewer/:id/rev/:revision', - name: 'viewer', + path: '/viewer/:id/revision/:revision', + name: 'revision viewer', component: ViewerLoader, props: true, beforeEnter, diff --git a/client/platform/web-girder/views/Clone.vue b/client/platform/web-girder/views/Clone.vue index 377bb1e00..dca5199c1 100644 --- a/client/platform/web-girder/views/Clone.vue +++ b/client/platform/web-girder/views/Clone.vue @@ -150,6 +150,12 @@ export default defineComponent({ Create a new clone + + Revision {{ revision }} selected. + Promise.resolve(); let pendingSaveCount = ref(0); let checkedTypes = ref([] as readonly string[]); + let revisionId = ref(null as null | number); if (props.blockOnUnsaved) { save = useHandler().save; pendingSaveCount = usePendingSaveCount(); checkedTypes = useCheckedTypes(); + revisionId = useRevisionId(); } async function doExport({ forceSave = false, url }: { url?: string; forceSave?: boolean }) { @@ -103,7 +107,10 @@ export default defineComponent({ return { exportAllUrl: getUri({ url: 'dive_dataset/export', - params: { ...params, folderIds: JSON.stringify([singleDataSetId.value]) }, + params: { + ...params, + folderIds: JSON.stringify([singleDataSetId.value]), + }, }), exportMediaUrl: dataset.value?.type === 'video' ? datasetMedia.value?.video?.url @@ -118,7 +125,11 @@ export default defineComponent({ }), exportDetectionsUrl: getUri({ url: 'dive_annotation/export', - params: { ...params, folderId: singleDataSetId.value }, + params: { + ...params, + folderId: singleDataSetId.value, + revisionId: revisionId.value, + }, }), exportConfigurationUrl: getUri({ url: `dive_dataset/${singleDataSetId.value}/configuration`, @@ -153,6 +164,7 @@ export default defineComponent({ menuOpen, exportUrls, checkedTypes, + revisionId, savePrompt, singleDataSetId, doExport, @@ -207,6 +219,13 @@ export default defineComponent({ Download options + + Revision {{ revisionId }} selected + -
Get latest detections csv only
+
Get latest annotation csv only