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
- detections
+ annotations
detections unavailable
diff --git a/client/platform/web-girder/views/RevisionHistory.vue b/client/platform/web-girder/views/RevisionHistory.vue
new file mode 100644
index 000000000..3e21d5a51
--- /dev/null
+++ b/client/platform/web-girder/views/RevisionHistory.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+ Inspecting revision {{ revisionId }}.
+ Past revisions are not editable.
+ Return to latest or clone this revision to edit.
+
+ Return to newest revision
+
+
+
+ Choose a previous revision to inspect in read-only mode.
+
+
+
+
+
+
+
+
+
+ {{ revision.description }}
+
+
+
+ by
+
+ {{ revision.author_name }}
+
+
+
+
+
+
+
+
+
+
+ Load More
+
+
+
+ No revision history yet. A revision is created each time you press save
+ mdi-content-save.
+
+
+
diff --git a/client/platform/web-girder/views/ViewerLoader.vue b/client/platform/web-girder/views/ViewerLoader.vue
index 7349a9e04..e2a8f70f4 100644
--- a/client/platform/web-girder/views/ViewerLoader.vue
+++ b/client/platform/web-girder/views/ViewerLoader.vue
@@ -1,19 +1,21 @@