Skip to content

Commit c5281e5

Browse files
Live Collab M2 - Automatically update annotation to newest changes (#8648)
From changelog: > When you are viewing an annotation and another user changes that annotation, these changes will be automatically shown. For some changes (e.g., when adding a new annotation layer), you will still need to reload the page, but most of the time WEBKNOSSOS will update the annotation automatically. Implementation details: - I extended the save saga so that it not only polls for the newest version, but also does try to incorporate newer version from the server, if they exist. I named this incorporation "update action application" - The update action application does not support all update actions yet (I skipped the ones that I deemed unimportant for this iteration). If an update action cannot be applied, the user is asked to reload the page (as before). - I refactored the tests and fixtures quite a bit to get the new tests up and running. Limitations: - adding/removing layers will still require a reload - changes to the active mapping will still require a reload <-- this also impacts the very first brush as this will make the null-mapping locked - meshes aren't refreshed automatically ### URL of deployed dev instance (used for testing): - https://___.webknossos.xyz ### Steps to test: - open wk in two different windows (one incognito) and login as sample and sample2 - create a new annotation as sample user and copy a sharing link - open the same annotation as sample2 (should be read only) - perform actions as sample and check that all changes are also shown for sample2 - support actions include - skeleton related actions - volume brushing as well as changing the segment list - proofreading operations ### TODOs: - [X] proof of concept - [x] skeleton - [x] volume - [x] proofreading - [x] bounding boxes - [x] write new tests - [X] skeleton - [x] proofreading - [x] volume - [x] final clean up ### Issues: - fixes #8664 ------ (Please delete unneeded items, merge only when none are left open) - [x] Updated [changelog](../blob/master/CHANGELOG.unreleased.md#unreleased) - [ ] Updated [migration guide](../blob/master/MIGRATIONS.unreleased.md#unreleased) if applicable - [ ] Updated [documentation](../blob/master/docs) if applicable - [ ] Adapted [wk-libs python client](https://github.com/scalableminds/webknossos-libs/tree/master/webknossos/webknossos/client) if relevant API parts change - [ ] Removed dev-only changes like prints and application.conf edits - [ ] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [ ] Needs datastore update after deployment --------- Co-authored-by: Michael Büßemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com>
1 parent 14dbb8a commit c5281e5

File tree

116 files changed

+3825
-1405
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+3825
-1405
lines changed

frontend/javascripts/admin/rest_api.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -777,8 +777,9 @@ export function getUpdateActionLog(
777777
annotationId: string,
778778
oldestVersion?: number,
779779
newestVersion?: number,
780+
sortAscending: boolean = false,
780781
): Promise<Array<APIUpdateActionBatch>> {
781-
return doWithToken((token) => {
782+
return doWithToken(async (token) => {
782783
const params = new URLSearchParams();
783784
params.set("token", token);
784785
if (oldestVersion != null) {
@@ -787,9 +788,14 @@ export function getUpdateActionLog(
787788
if (newestVersion != null) {
788789
params.set("newestVersion", newestVersion.toString());
789790
}
790-
return Request.receiveJSON(
791+
const log: APIUpdateActionBatch[] = await Request.receiveJSON(
791792
`${tracingStoreUrl}/tracings/annotation/${annotationId}/updateActionLog?${params}`,
792793
);
794+
795+
if (sortAscending) {
796+
log.reverse();
797+
}
798+
return log;
793799
});
794800
}
795801

@@ -1989,6 +1995,9 @@ export async function getAgglomeratesForSegmentsFromDatastore<T extends number |
19891995
mappingId: string,
19901996
segmentIds: Array<T>,
19911997
): Promise<Mapping> {
1998+
if (segmentIds.length === 0) {
1999+
return new Map();
2000+
}
19922001
const segmentIdBuffer = serializeProtoListOfLong<T>(segmentIds);
19932002
const listArrayBuffer: ArrayBuffer = await doWithToken((token) => {
19942003
const params = new URLSearchParams({ token });
@@ -2019,6 +2028,9 @@ export async function getAgglomeratesForSegmentsFromTracingstore<T extends numbe
20192028
annotationId: string,
20202029
version?: number | null | undefined,
20212030
): Promise<Mapping> {
2031+
if (segmentIds.length === 0) {
2032+
return new Map();
2033+
}
20222034
const params = new URLSearchParams({ annotationId });
20232035
if (version != null) {
20242036
params.set("version", version.toString());
@@ -2201,7 +2213,7 @@ export function getSynapseTypes(
22012213
);
22022214
}
22032215

2204-
type MinCutTargetEdge = {
2216+
export type MinCutTargetEdge = {
22052217
position1: Vector3;
22062218
position2: Vector3;
22072219
segmentId1: number;

frontend/javascripts/libs/utils.ts

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1+
import { Chalk } from "chalk";
12
import dayjs from "dayjs";
23
import naturalSort from "javascript-natural-sort";
34
import window, { document, location } from "libs/window";
45
import _ from "lodash";
56
import type { APIDataset, APIUser, MapEntries } from "types/api_types";
7+
import type { BoundingBoxMinMaxType } from "types/bounding_box";
68
import type { ArbitraryObject, Comparator } from "types/globals";
7-
import type {
8-
BoundingBoxType,
9-
ColorObject,
10-
Point3,
11-
TypedArray,
12-
Vector3,
13-
Vector4,
14-
Vector6,
15-
} from "viewer/constants";
9+
import type { ColorObject, Point3, TypedArray, Vector3, Vector4, Vector6 } from "viewer/constants";
1610
import type { TreeGroup } from "viewer/model/types/tree_types";
1711
import type { BoundingBoxObject, NumberLike, SegmentGroup } from "viewer/store";
1812

@@ -276,19 +270,23 @@ export function getRandomColor(): Vector3 {
276270
return randomColor as any as Vector3;
277271
}
278272

279-
export function computeBoundingBoxFromArray(bb: Vector6): BoundingBoxType {
273+
export function computeBoundingBoxFromArray(bb: Vector6): BoundingBoxMinMaxType {
280274
const [x, y, z, width, height, depth] = bb;
281275
return {
282276
min: [x, y, z],
283277
max: [x + width, y + height, z + depth],
284278
};
285279
}
286280

287-
export function computeBoundingBoxFromBoundingBoxObject(bb: BoundingBoxObject): BoundingBoxType {
281+
export function computeBoundingBoxFromBoundingBoxObject(
282+
bb: BoundingBoxObject,
283+
): BoundingBoxMinMaxType {
288284
return computeBoundingBoxFromArray([...bb.topLeft, bb.width, bb.height, bb.depth]);
289285
}
290286

291-
export function computeBoundingBoxObjectFromBoundingBox(bb: BoundingBoxType): BoundingBoxObject {
287+
export function computeBoundingBoxObjectFromBoundingBox(
288+
bb: BoundingBoxMinMaxType,
289+
): BoundingBoxObject {
292290
const boundingBoxArray = computeArrayFromBoundingBox(bb);
293291
return {
294292
topLeft: [boundingBoxArray[0], boundingBoxArray[1], boundingBoxArray[2]],
@@ -298,7 +296,7 @@ export function computeBoundingBoxObjectFromBoundingBox(bb: BoundingBoxType): Bo
298296
};
299297
}
300298

301-
export function computeArrayFromBoundingBox(bb: BoundingBoxType): Vector6 {
299+
export function computeArrayFromBoundingBox(bb: BoundingBoxMinMaxType): Vector6 {
302300
return [
303301
bb.min[0],
304302
bb.min[1],
@@ -309,11 +307,13 @@ export function computeArrayFromBoundingBox(bb: BoundingBoxType): Vector6 {
309307
];
310308
}
311309

312-
export function computeShapeFromBoundingBox(bb: BoundingBoxType): Vector3 {
310+
export function computeShapeFromBoundingBox(bb: BoundingBoxMinMaxType): Vector3 {
313311
return [bb.max[0] - bb.min[0], bb.max[1] - bb.min[1], bb.max[2] - bb.min[2]];
314312
}
315313

316-
export function aggregateBoundingBox(boundingBoxes: Array<BoundingBoxObject>): BoundingBoxType {
314+
export function aggregateBoundingBox(
315+
boundingBoxes: Array<BoundingBoxObject>,
316+
): BoundingBoxMinMaxType {
317317
if (boundingBoxes.length === 0) {
318318
return {
319319
min: [0, 0, 0],
@@ -344,8 +344,8 @@ export function aggregateBoundingBox(boundingBoxes: Array<BoundingBoxObject>): B
344344
}
345345

346346
export function areBoundingBoxesOverlappingOrTouching(
347-
firstBB: BoundingBoxType,
348-
secondBB: BoundingBoxType,
347+
firstBB: BoundingBoxMinMaxType,
348+
secondBB: BoundingBoxMinMaxType,
349349
) {
350350
let areOverlapping = true;
351351

@@ -425,10 +425,6 @@ export function stringToNumberArray(s: string): Array<number> {
425425
return result;
426426
}
427427

428-
export function concatVector3(a: Vector3, b: Vector3): Vector6 {
429-
return [a[0], a[1], a[2], b[0], b[1], b[2]];
430-
}
431-
432428
export function numberArrayToVector3(array: Array<number>): Vector3 {
433429
const output: Vector3 = [0, 0, 0];
434430

@@ -1262,7 +1258,11 @@ export function notEmpty<TValue>(value: TValue | null | undefined): value is TVa
12621258

12631259
export function isNumberMap(x: Map<NumberLike, NumberLike>): x is Map<number, number> {
12641260
const { value } = x.entries().next();
1265-
return Boolean(value && typeof value[0] === "number");
1261+
if (value === undefined) {
1262+
// Let's assume a number map when the map is empty.
1263+
return true;
1264+
}
1265+
return Boolean(typeof value[0] === "number");
12661266
}
12671267

12681268
export function isBigInt(x: NumberLike): x is bigint {
@@ -1368,3 +1368,26 @@ export function areSetsEqual<T>(setA: Set<T>, setB: Set<T>) {
13681368
}
13691369
return true;
13701370
}
1371+
1372+
// ColoredLogger can be used to make certain log outputs easier to find (especially useful
1373+
// when automatic logging of redux actions is enabled which makes the overall logging
1374+
// very verbose).
1375+
const chalk = new Chalk({ level: 3 });
1376+
export const ColoredLogger = {
1377+
log: (...args: unknown[]) => {
1378+
// Simple wrapper to allow easy switching from colored to non-colored logs
1379+
console.log(...args);
1380+
},
1381+
logRed: (str: string, ...args: unknown[]) => {
1382+
console.log(chalk.bgRed(str), ...args);
1383+
},
1384+
logGreen: (str: string, ...args: unknown[]) => {
1385+
console.log(chalk.bgGreen(str), ...args);
1386+
},
1387+
logYellow: (str: string, ...args: unknown[]) => {
1388+
console.log(chalk.bgYellow(str), ...args);
1389+
},
1390+
logBlue: (str: string, ...args: unknown[]) => {
1391+
console.log(chalk.bgBlue(str), ...args);
1392+
},
1393+
};

frontend/javascripts/libs/vector_input.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { InputProps } from "antd";
22
import * as Utils from "libs/utils";
33
import _ from "lodash";
44
import * as React from "react";
5-
import type { ServerBoundingBoxTypeTuple } from "types/api_types";
5+
import type { ServerBoundingBoxMinMaxTypeTuple } from "types/api_types";
66
import type { Vector3, Vector6 } from "viewer/constants";
77
import InputComponent from "viewer/view/components/input_component";
88

@@ -206,11 +206,11 @@ export class ArbitraryVectorInput extends BaseVector<number[]> {
206206
}
207207

208208
type BoundingBoxInputProps = Omit<InputProps, "value"> & {
209-
value: ServerBoundingBoxTypeTuple;
210-
onChange: (arg0: ServerBoundingBoxTypeTuple) => void;
209+
value: ServerBoundingBoxMinMaxTypeTuple;
210+
onChange: (arg0: ServerBoundingBoxMinMaxTypeTuple) => void;
211211
};
212212

213-
function boundingBoxToVector6(value: ServerBoundingBoxTypeTuple): Vector6 {
213+
function boundingBoxToVector6(value: ServerBoundingBoxMinMaxTypeTuple): Vector6 {
214214
const { topLeft, width, height, depth } = value;
215215
const [x, y, z] = topLeft;
216216
return [x, y, z, width, height, depth];

frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import { createTreeMapFromTreeArray } from "viewer/model/reducers/skeletontracin
1111
import { diffTrees } from "viewer/model/sagas/skeletontracing_saga";
1212
import { getNullableSkeletonTracing } from "viewer/model/accessors/skeletontracing_accessor";
1313
import { getServerVolumeTracings } from "viewer/model/accessors/volumetracing_accessor";
14-
import { addVersionNumbers } from "viewer/model/sagas/save_saga";
15-
import * as UpdateActions from "viewer/model/sagas/update_actions";
14+
import * as UpdateActions from "viewer/model/sagas/volume/update_actions";
1615
import * as api from "admin/rest_api";
1716
import generateDummyTrees from "viewer/model/helpers/generate_dummy_trees";
1817
import { describe, it, beforeAll, expect } from "vitest";
1918
import { createSaveQueueFromUpdateActions } from "../helpers/saveHelpers";
2019
import type { SaveQueueEntry } from "viewer/store";
2120
import DiffableMap from "libs/diffable_map";
21+
import { addVersionNumbers } from "viewer/model/sagas/saving/save_queue_draining";
2222

2323
const datasetId = "59e9cfbdba632ac2ab8b23b3";
2424

0 commit comments

Comments
 (0)