From a8c0969eabe3d62be8a90dd76e7a83305e629bfe Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 11 Nov 2024 14:56:23 +0100 Subject: [PATCH 01/10] Zarr streaming e2e test (#8137) --- .circleci/config.yml | 1 + conf/messages | 4 - docker-compose.yml | 2 +- .../backend-snapshot-tests/datasets.e2e.ts | 56 ++- frontend/javascripts/test/e2e-setup.ts | 3 +- .../backend-snapshot-tests/datasets.e2e.js.md | 406 ++++-------------- .../datasets.e2e.js.snap | Bin 4101 -> 2673 bytes test/dataset/.gitignore | 1 + test/dataset/test-dataset.zip | Bin 0 -> 71988 bytes test/e2e/End2EndSpec.scala | 42 ++ 10 files changed, 175 insertions(+), 340 deletions(-) create mode 100644 test/dataset/.gitignore create mode 100644 test/dataset/test-dataset.zip diff --git a/.circleci/config.yml b/.circleci/config.yml index f35317647f1..a70616eb2d6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -118,6 +118,7 @@ jobs: - run: name: Run end-to-end tests command: | + mkdir -p binaryData/Organization_X && chmod 777 binaryData/Organization_X for i in {1..3}; do # retry .circleci/not-on-master.sh docker-compose run e2e-tests && s=0 && break || s=$? done diff --git a/conf/messages b/conf/messages index 44eff4e660d..4f59c9f0e84 100644 --- a/conf/messages +++ b/conf/messages @@ -73,10 +73,6 @@ oidc.disabled=OIDC is disabled oidc.configuration.invalid=OIDC configuration is invalid oidc.authentication.failed=Failed to register / log in via Single-Sign-On (SSO with OIDC) -braintracing.new=An account on braintracing.org was created for you. You can use the same credentials as on WEBKNOSSOS to login. -braintracing.error=We could not automatically create an account for you on braintracing.org. Please do it on your own. -braintracing.exists=Great, you already have an account on braintracing.org. Please double check that you have uploaded all requested information. - dataset=Dataset dataset.notFound=Dataset {0} does not exist or could not be accessed dataset.notFoundConsiderLogin=Dataset {0} does not exist or could not be accessed. You may need to log in. diff --git a/docker-compose.yml b/docker-compose.yml index 76f802426bb..a1f0e549c0e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -201,7 +201,7 @@ services: -Ddatastore.redis.address=redis -Ddatastore.watchFileSystem.enabled=false" volumes: - - ./binaryData/Connectomics department:/home/${USER_NAME:-sbt-user}/webknossos/binaryData/Organization_X + - ./binaryData/Organization_X:/home/${USER_NAME:-sbt-user}/webknossos/binaryData/Organization_X screenshot-tests: image: scalableminds/puppeteer:master diff --git a/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.ts b/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.ts index ab2e5f0856f..a82653b9462 100644 --- a/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.ts +++ b/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.ts @@ -1,5 +1,11 @@ import _ from "lodash"; -import { tokenUserA, setCurrToken, resetDatabase, writeTypeCheckingFile } from "test/e2e-setup"; +import { + tokenUserA, + setCurrToken, + resetDatabase, + writeTypeCheckingFile, + replaceVolatileValues, +} from "test/e2e-setup"; import type { APIDataset } from "types/api_flow_types"; import * as api from "admin/admin_rest_api"; import test from "ava"; @@ -15,6 +21,7 @@ async function getFirstDataset(): Promise { test.before("Reset database and change token", async () => { resetDatabase(); setCurrToken(tokenUserA); + await api.triggerDatasetCheck("http://localhost:9000"); }); test.serial("getDatasets", async (t) => { let datasets = await api.getDatasets(); @@ -29,19 +36,19 @@ test.serial("getDatasets", async (t) => { writeTypeCheckingFile(datasets, "dataset", "APIDatasetCompact", { isArray: true, }); - t.snapshot(datasets); + t.snapshot(replaceVolatileValues(datasets)); }); test("getActiveDatasets", async (t) => { let datasets = await api.getActiveDatasetsOfMyOrganization(); datasets = _.sortBy(datasets, (d) => d.name); - t.snapshot(datasets); + t.snapshot(replaceVolatileValues(datasets)); }); test("getDatasetAccessList", async (t) => { const dataset = await getFirstDataset(); const accessList = _.sortBy(await api.getDatasetAccessList(dataset), (user) => user.id); - t.snapshot(accessList); + t.snapshot(replaceVolatileValues(accessList)); }); test("updateDatasetTeams", async (t) => { const [dataset, newTeams] = await Promise.all([getFirstDataset(), api.getEditableTeams()]); @@ -49,7 +56,7 @@ test("updateDatasetTeams", async (t) => { dataset, newTeams.map((team) => team.id), ); - t.snapshot(updatedDataset); + t.snapshot(replaceVolatileValues(updatedDataset)); // undo the Change await api.updateDatasetTeams( dataset, @@ -62,3 +69,42 @@ test("updateDatasetTeams", async (t) => { // await api.revokeDatasetSharingToken(dataset.name); // t.pass(); // }); + +test("Zarr streaming", async (t) => { + const zattrsResp = await fetch("/data/zarr/Organization_X/test-dataset/segmentation/.zattrs", { + headers: new Headers(), + }); + const zattrs = await zattrsResp.text(); + t.snapshot(zattrs); + + const rawDataResponse = await fetch( + "/data/zarr/Organization_X/test-dataset/segmentation/1/0.1.1.0", + { + headers: new Headers(), + }, + ); + const bytes = await rawDataResponse.arrayBuffer(); + const base64 = btoa(String.fromCharCode(...new Uint8Array(bytes.slice(-128)))); + t.snapshot(base64); +}); + +test("Zarr 3 streaming", async (t) => { + const zarrJsonResp = await fetch( + "/data/zarr3_experimental/Organization_X/test-dataset/segmentation/zarr.json", + { + headers: new Headers(), + }, + ); + const zarrJson = await zarrJsonResp.text(); + t.snapshot(zarrJson); + + const rawDataResponse = await fetch( + "/data/zarr3_experimental/Organization_X/test-dataset/segmentation/1/0.1.1.0", + { + headers: new Headers(), + }, + ); + const bytes = await rawDataResponse.arrayBuffer(); + const base64 = btoa(String.fromCharCode(...new Uint8Array(bytes.slice(-128)))); + t.snapshot(base64); +}); diff --git a/frontend/javascripts/test/e2e-setup.ts b/frontend/javascripts/test/e2e-setup.ts index 9e8dee48c81..b1330eb25e1 100644 --- a/frontend/javascripts/test/e2e-setup.ts +++ b/frontend/javascripts/test/e2e-setup.ts @@ -39,6 +39,7 @@ const volatileKeys: Array = [ "lastActivity", "tracingTime", "tracingId", + "sortingKey", ]; export function replaceVolatileValues(obj: ArbitraryObject | null | undefined) { if (obj == null) return obj; @@ -130,7 +131,7 @@ export async function writeTypeCheckingFile( const fullTypeAnnotation = options.isArray ? `Array<${typeString}>` : typeString; fs.writeFileSync( `frontend/javascripts/test/snapshots/type-check/test-type-checking-${name}.ts`, - ` + ` import type { ${typeString} } from "types/api_flow_types"; const a: ${fullTypeAnnotation} = ${JSON.stringify(object)}`, ); diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/datasets.e2e.js.md b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/datasets.e2e.js.md index 719d80174c7..f54066d7901 100644 --- a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/datasets.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/datasets.e2e.js.md @@ -11,10 +11,10 @@ Generated by [AVA](https://avajs.dev). [ { colorLayerNames: [], - created: 1460379470082, + created: 'created', displayName: null, folderId: '570b9f4e4bb848d0885ea917', - id: '570b9f4e4bb848d0885ee711', + id: 'id', isActive: false, isEditable: true, isUnreported: true, @@ -27,10 +27,10 @@ Generated by [AVA](https://avajs.dev). }, { colorLayerNames: [], - created: 1460379470080, + created: 'created', displayName: null, folderId: '570b9f4e4bb848d0885ea917', - id: '570b9f4e4bb848d0885ee713', + id: 'id', isActive: false, isEditable: true, isUnreported: true, @@ -43,10 +43,10 @@ Generated by [AVA](https://avajs.dev). }, { colorLayerNames: [], - created: 1460379470079, + created: 'created', displayName: null, folderId: '570b9f4e4bb848d0885ea917', - id: '570b9f4e4bb848d0885ee712', + id: 'id', isActive: false, isEditable: true, isUnreported: true, @@ -58,51 +58,43 @@ Generated by [AVA](https://avajs.dev). tags: [], }, { - colorLayerNames: [ - 'color_1', - 'color_2', - 'color_3', - ], - created: 1508495293763, + colorLayerNames: [], + created: 'created', displayName: null, folderId: '570b9f4e4bb848d0885ea917', - id: '59e9cfbdba632ac2ab8b23b3', - isActive: true, + id: 'id', + isActive: false, isEditable: true, - isUnreported: false, + isUnreported: true, lastUsedByUser: 0, name: 'confocal-multi_knossos', owningOrganization: 'Organization_X', segmentationLayerNames: [], - status: '', + status: 'No longer available on datastore.', tags: [], }, { - colorLayerNames: [ - 'color', - ], - created: 1508495293789, + colorLayerNames: [], + created: 'created', displayName: null, folderId: '570b9f4e4bb848d0885ea917', - id: '59e9cfbdba632ac2ab8b23b5', - isActive: true, + id: 'id', + isActive: false, isEditable: true, - isUnreported: false, + isUnreported: true, lastUsedByUser: 0, name: 'l4_sample', owningOrganization: 'Organization_X', - segmentationLayerNames: [ - 'segmentation', - ], - status: '', + segmentationLayerNames: [], + status: 'No longer available on datastore.', tags: [], }, { colorLayerNames: [], - created: 1460379603792, + created: 'created', displayName: null, folderId: '570b9f4e4bb848d0885ea917', - id: '570b9fd34bb848d0885ee716', + id: 'id', isActive: false, isEditable: true, isUnreported: true, @@ -113,276 +105,51 @@ Generated by [AVA](https://avajs.dev). status: 'No longer available on datastore.', tags: [], }, - ] - -## getActiveDatasets - -> Snapshot 1 - - [ { - allowedTeams: [ - { - id: '570b9f4b2a7c0e3b008da6ec', - name: 'team_X1', - organization: 'Organization_X', - }, - ], - allowedTeamsCumulative: [ - { - id: '570b9f4b2a7c0e3b008da6ec', - name: 'team_X1', - organization: 'Organization_X', - }, - ], - created: 1508495293763, - dataSource: { - dataLayers: [ - { - boundingBox: { - depth: 256, - height: 512, - topLeft: [ - 0, - 0, - 0, - ], - width: 512, - }, - category: 'color', - elementClass: 'uint8', - name: 'color_1', - resolutions: [ - [ - 1, - 1, - 1, - ], - [ - 2, - 2, - 2, - ], - [ - 4, - 4, - 4, - ], - [ - 8, - 8, - 8, - ], - [ - 16, - 16, - 16, - ], - ], - }, - { - boundingBox: { - depth: 256, - height: 512, - topLeft: [ - 0, - 0, - 0, - ], - width: 512, - }, - category: 'color', - elementClass: 'uint8', - name: 'color_2', - resolutions: [ - [ - 1, - 1, - 1, - ], - [ - 2, - 2, - 2, - ], - [ - 4, - 4, - 4, - ], - [ - 8, - 8, - 8, - ], - [ - 16, - 16, - 16, - ], - ], - }, - { - boundingBox: { - depth: 256, - height: 512, - topLeft: [ - 0, - 0, - 0, - ], - width: 512, - }, - category: 'color', - elementClass: 'uint8', - name: 'color_3', - resolutions: [ - [ - 1, - 1, - 1, - ], - [ - 2, - 2, - 2, - ], - [ - 4, - 4, - 4, - ], - [ - 8, - 8, - 8, - ], - [ - 16, - 16, - 16, - ], - ], - }, - ], - id: { - name: 'confocal-multi_knossos', - team: 'Organization_X', - }, - scale: { - factor: [ - 22, - 22, - 44.599998474121094, - ], - unit: 'nanometer', - }, - }, - dataStore: { - allowsUpload: true, - isScratch: false, - jobsEnabled: false, - jobsSupportedByAvailableWorkers: [], - name: 'localhost', - url: 'http://localhost:9000', - }, - description: null, + colorLayerNames: [], + created: 'created', displayName: null, folderId: '570b9f4e4bb848d0885ea917', + id: 'id', isActive: true, isEditable: true, - isPublic: false, isUnreported: false, lastUsedByUser: 0, - logoUrl: '/assets/images/mpi-logos.svg', - metadata: [ - { - key: 'key', - type: 'number', - value: 4, - }, - ], - name: 'confocal-multi_knossos', + name: 'test-dataset', owningOrganization: 'Organization_X', - publication: null, - sortingKey: 1508495293763, + segmentationLayerNames: [ + 'segmentation', + ], + status: '', tags: [], - usedStorageBytes: 0, }, + ] + +## getActiveDatasets + +> Snapshot 1 + + [ { - allowedTeams: [ - { - id: '570b9f4b2a7c0e3b008da6ec', - name: 'team_X1', - organization: 'Organization_X', - }, - ], - allowedTeamsCumulative: [ - { - id: '570b9f4b2a7c0e3b008da6ec', - name: 'team_X1', - organization: 'Organization_X', - }, - ], - created: 1508495293789, + allowedTeams: [], + allowedTeamsCumulative: [], + created: 'created', dataSource: { dataLayers: [ { boundingBox: { - depth: 1024, - height: 1024, + depth: 100, + height: 100, topLeft: [ - 3072, - 3072, - 512, + 50, + 50, + 25, ], - width: 1024, - }, - category: 'color', - elementClass: 'uint8', - name: 'color', - resolutions: [ - [ - 1, - 1, - 1, - ], - [ - 2, - 2, - 1, - ], - [ - 4, - 4, - 1, - ], - [ - 8, - 8, - 2, - ], - [ - 16, - 16, - 4, - ], - ], - }, - { - boundingBox: { - depth: 1024, - height: 1024, - topLeft: [ - 3072, - 3072, - 512, - ], - width: 1024, + width: 100, }, category: 'segmentation', elementClass: 'uint32', - largestSegmentId: 2504697, + largestSegmentId: 176, name: 'segmentation', resolutions: [ [ @@ -390,37 +157,14 @@ Generated by [AVA](https://avajs.dev). 1, 1, ], - [ - 2, - 2, - 1, - ], - [ - 4, - 4, - 1, - ], - [ - 8, - 8, - 2, - ], - [ - 16, - 16, - 4, - ], ], }, ], - id: { - name: 'l4_sample', - team: 'Organization_X', - }, + id: 'id', scale: { factor: [ - 11.239999771118164, - 11.239999771118164, + 11.24, + 11.24, 28, ], unit: 'nanometer', @@ -444,10 +188,10 @@ Generated by [AVA](https://avajs.dev). lastUsedByUser: 0, logoUrl: '/assets/images/mpi-logos.svg', metadata: [], - name: 'l4_sample', + name: 'test-dataset', owningOrganization: 'Organization_X', publication: null, - sortingKey: 1508495293789, + sortingKey: 'sortingKey', tags: [], usedStorageBytes: 0, }, @@ -461,24 +205,24 @@ Generated by [AVA](https://avajs.dev). { email: 'user_A@scalableminds.com', firstName: 'user_A', - id: '570b9f4d2a7c0e4d008da6ef', + id: 'id', isAdmin: true, isAnonymous: false, isDatasetManager: true, lastName: 'last_A', teams: [ { - id: '570b9f4b2a7c0e3b008da6ec', + id: 'id', isTeamManager: true, name: 'team_X1', }, { - id: '59882b370d889b84020efd3f', + id: 'id', isTeamManager: false, name: 'team_X3', }, { - id: '59882b370d889b84020efd6f', + id: 'id', isTeamManager: true, name: 'team_X4', }, @@ -487,35 +231,19 @@ Generated by [AVA](https://avajs.dev). { email: 'user_B@scalableminds.com', firstName: 'user_B', - id: '670b9f4d2a7c0e4d008da6ef', + id: 'id', isAdmin: false, isAnonymous: false, isDatasetManager: true, lastName: 'last_B', teams: [ { - id: '570b9f4b2a7c0e3b008da6ec', + id: 'id', isTeamManager: true, name: 'team_X1', }, ], }, - { - email: 'user_C@scalableminds.com', - firstName: 'user_C', - id: '770b9f4d2a7c0e4d008da6ef', - isAdmin: false, - isAnonymous: false, - isDatasetManager: false, - lastName: 'last_C', - teams: [ - { - id: '570b9f4b2a7c0e3b008da6ec', - isTeamManager: false, - name: 'team_X1', - }, - ], - }, ] ## updateDatasetTeams @@ -528,3 +256,23 @@ Generated by [AVA](https://avajs.dev). '59882b370d889b84020efd6f', '69882b370d889b84020efd4f', ] + +## Zarr streaming + +> Snapshot 1 + + '{"multiscales":[{"version":"0.4","name":"segmentation","axes":[{"name":"c","type":"channel"},{"name":"x","type":"space","unit":"nanometer"},{"name":"y","type":"space","unit":"nanometer"},{"name":"z","type":"space","unit":"nanometer"}],"datasets":[{"path":"1","coordinateTransformations":[{"type":"scale","scale":[1,11.24,11.24,28]}]}]}]}' + +> Snapshot 2 + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAA=' + +## Zarr 3 streaming + +> Snapshot 1 + + '{"zarr_format":3,"node_type":"group","attributes":{"ome":{"version":"0.5","multiscales":[{"name":"segmentation","axes":[{"name":"c","type":"channel"},{"name":"x","type":"space","unit":"nanometer"},{"name":"y","type":"space","unit":"nanometer"},{"name":"z","type":"space","unit":"nanometer"}],"datasets":[{"path":"1","coordinateTransformations":[{"type":"scale","scale":[1,11.24,11.24,28]}]}]}]}}}' + +> Snapshot 2 + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAA=' diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/datasets.e2e.js.snap b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/datasets.e2e.js.snap index 054bb7f37c25cad554474ffe3861f47dddde6865..a47da3e0e5c03ade5134263a6e34fb4e526de66c 100644 GIT binary patch literal 2673 zcmV-%3Xb(bRzVpP9**-NHWp1Mofo4+7{T zdN0u?Jde&3(rl$tDUcqJC`gQuqjhC5LjEPbNzmLz01^qB-xf_JXhtFcQG@0l(bR(G zk3~}#uaoLifQuDiTmh&898rLuD!?BV;J*S>fooKNs=y;E@JkhVTLrRO<5l%r@_Sjq zCy`b&olXa;KcfNLG@zmZk88k>G~g`_i0i;rI_3N0N*lzR}A3a2C&ftZZmBhe?nwbJrhwN|z~yO4^+1>EO=;ljG;l{6IFbgQO9QW^fir2~l3w8I zUf|AN;7Bj`IIX}gj7u;4M38_wV;94)~9m)<4t

=Ka`u^A~erQ(rerUj_s6g@k-RO z4^rQDomOw{H>Y<7?%LDlt^v+U73TN>zNFW5?o+?Uo0nJI?5-=jPKkNz=zeOKg+11} z&N>k+x$k;xL(tEBTR*B#OT3LTPh6nPN>XNRoibyBGHh;eWYctZbZ%&QwE6xj(dM@y zw0YwKZB~*t$*pr$=Gnrt)7k9c^7x|ltqxzV=v$gE*Y;iD%L?*kpx`<)Zh@B9S88S7 zp5EuUoV&7R&yhE~R*5&Y5Z*j`fj2A38?!tz&1t1tX3L|>3#&wxUxiTR^aZM{Bvllz zlwTf6NdM}P|F(rxY;PWAgOtf<+N!(}LUtg?m;4F_VQ_Exv0xbu-xnOi` z3@IHOL!Fo!f@vXGGz2q3Ff#;;g<$azED?gGLa=lQ)*FKLg<$<5*xC^66Cv0|A=t$s z*e5$N5;@urJk{U1Yj|mhg3II`MFJ8RbZqf0-p<;_{^s2!=GUj~(z~4I-`jRMBN+fg zXWPxJZ(Lr}d}!b}fD-^t0eBz4l?0e1z#anJOMqtx@FxQNj{uiMfUyX0M+CS(0{koj zoQeQYfH4KwtpHz9fTxx4eaZyL+F;JTtSrpAI4zgm1FX1%(FzaHzqX+R?dX;y2?3&#t!OQCIw4!3e+HAa2j9EMD`_=J{8(XsD zW7%vr2=F^};C8(;!0Qqpl%fvo)qw|e$)WKuho%ra?ur5rMu8thfj`TKPf?k+>boM%8Nh%6tTzx+ zxYVhUB;-0{;ay{Sx8@XWr!?uxJMl(=`mE%7ty$fXjAfFsG85RAGUctshF-HBe^}nC zO_!-xV%*;elJUuqWcGy{O@m*&=yODVj>ylEMos81oOw?a3XJn@Hut;BLOsz|7Ajc) zn*m&Zw$iYEkbP6@p!?y*LAOE7-#0461OYY?;0pw(65ufcoFKqo$k|S&&HoVKJpv3z zfZY*bE&?2l0ODNy^$4KLMPsz9Xf$N+vkJ9af%BpoXWsP0^&&-xfLyViBHvJOD{>Z? zu|4kJ+&1a-`jh9frjFWtqFAw=#S=};<`a(V%vapTBC{*iq;1}CyJ{0P zSI4W08{$>P{Aep^HNRdqe@H7$p0|}I&)-U2>2`&!G#Oaw#zm}T5Y<(HngZOb0AE!Q zR)3Mm6AJL8BxEcS4q}8nr!=R}t4iy4B8po7ok%M@4XMrWB!1%|O5iW4z%jXeQ@iru zZ&ct9D)5#Nl}om+YEK>2fNM42b`7{k7Vr9rhx)7r9Mgc8HQ)_d-0Yf<%DHeMVRNx~ z3w2~iDvSzeP4681Xu6WAwOCift2e5VRWx8Dd3d#Ik^0TugkT}vl{$8wZp$264#0H) zN&p@O@Y2~bh$?SvG*`FzAv#*fvf+F-n=8`Itk9YA0?DylZYV!Inl0vXWBJ@jb|}kc zio-LVxo~O8%`Hi#CAoP~$&s0cMV#BI=dI(u$7scNN(*7)v2)_&%3+jXpQE4yLD%2>hy*0|MP##tGQ&NajvaKXx0{(LoP&Qix=W$SRJ zh0ZOYyh;nq%2=YRw#KbiJ>4!oe{SLjKaBXEjMZ3S*WbHJ{aI_=8niN2!F9c&?NFcX z@TkLQT(1%gvo6@wEG%SY8sFo)2Qxth+V~vG?KxckZ_w_g4m&@OiIx7lReWp}ACt?x zzP3T8YCxvpP&z%?G0ca^A?qOZyy?0Lt?}WE<+w#Q-LPTFb8A&W9^d!ue9adWI%K)R fiJiRLWM#T|d4Y%L#KXfM2PXa>Bp>3sBP##^r!^Up literal 4101 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 g@gI2k;VXZO7YP1)A*E+H^6-f2eyIDBhvsnh4 Date: Mon, 11 Nov 2024 15:11:08 +0100 Subject: [PATCH 02/10] Fix layout persistence when switching between orthogonal, flight, and oblique mode (#8177) * Fix layout persistence when switching between orthogonal, flight, and oblique mode * update changelog * Apply suggestions from code review Co-authored-by: MichaelBuessemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com> --------- Co-authored-by: MichaelBuessemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com> --- CHANGELOG.unreleased.md | 1 + .../oxalis/view/layouting/flex_layout_wrapper.tsx | 5 +---- .../oxalis/view/layouting/tracing_layout_view.tsx | 10 ++++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index a0384b3cbc1..16136b2e699 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -43,6 +43,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Removed unnecessary scrollbars in skeleton tab that occurred especially after resizing. [#8148](https://github.com/scalableminds/webknossos/pull/8148) - Deleting a bounding box is now possible independently of a visible segmentation layer. [#8164](https://github.com/scalableminds/webknossos/pull/8164) - S3-compliant object storages can now be accessed via HTTPS. [#8167](https://github.com/scalableminds/webknossos/pull/8167) +- Fixed a layout persistence bug leading to empty viewports, triggered when switching between orthogonal, flight, or oblique mode. [#8177](https://github.com/scalableminds/webknossos/pull/8177) ### Removed diff --git a/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx b/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx index 6c79170cbcf..59b34a3300a 100644 --- a/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx +++ b/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx @@ -207,10 +207,7 @@ class FlexLayoutWrapper extends React.PureComponent { rebuildLayout() { const model = this.loadCurrentModel(); this.updateToModelStateAndAdjustIt(model); - this.setState({ - model, - }); - setTimeout(this.onLayoutChange, 1); + this.setState({ model }, () => this.onLayoutChange()); if (this.props.layoutName !== DEFAULT_LAYOUT_NAME) { sendAnalyticsEvent("load_custom_layout", { diff --git a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx index a1671264ae5..4a92943d240 100644 --- a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx +++ b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx @@ -194,14 +194,12 @@ class TracingLayoutView extends React.PureComponent { app.vent.emit("rerender"); if (model != null) { - this.setState({ - model, + this.setState({ model }, () => { + if (this.props.autoSaveLayouts) { + this.saveCurrentLayout(layoutName); + } }); } - - if (this.props.autoSaveLayouts) { - this.saveCurrentLayout(layoutName); - } }; debouncedOnLayoutChange = _.debounce(() => this.onLayoutChange(), Constants.RESIZE_THROTTLE_TIME); From 73e0be8bf8064c2e49de821f5acc56ad178da2d0 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Tue, 12 Nov 2024 10:04:14 +0100 Subject: [PATCH 03/10] Send right parameter to createNodeSkeletonAction (#8185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix that createNodeAction got wrong param * add changelog * undo edit of MutableNode type * fix typo * fix reducer tests --------- Co-authored-by: Michael Büßemeyer --- CHANGELOG.unreleased.md | 1 + frontend/javascripts/oxalis/model/helpers/nml_helpers.ts | 4 ++-- .../model/reducers/skeletontracing_reducer_helpers.ts | 4 ++-- frontend/javascripts/oxalis/model/sagas/proofread_saga.ts | 2 +- frontend/javascripts/oxalis/store.ts | 2 +- .../right-border-tabs/connectome_tab/connectome_view.tsx | 2 +- frontend/javascripts/test/libs/nml.spec.ts | 2 +- .../test/reducers/skeletontracing_reducer.spec.ts | 6 +++--- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 16136b2e699..2d0bec114f8 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -43,6 +43,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Removed unnecessary scrollbars in skeleton tab that occurred especially after resizing. [#8148](https://github.com/scalableminds/webknossos/pull/8148) - Deleting a bounding box is now possible independently of a visible segmentation layer. [#8164](https://github.com/scalableminds/webknossos/pull/8164) - S3-compliant object storages can now be accessed via HTTPS. [#8167](https://github.com/scalableminds/webknossos/pull/8167) +- Fixed that skeleton tree nodes were created with the wrong mag. [#8185](https://github.com/scalableminds/webknossos/pull/8185) - Fixed a layout persistence bug leading to empty viewports, triggered when switching between orthogonal, flight, or oblique mode. [#8177](https://github.com/scalableminds/webknossos/pull/8177) ### Removed diff --git a/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts b/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts index e594576d314..95400af562d 100644 --- a/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts +++ b/frontend/javascripts/oxalis/model/helpers/nml_helpers.ts @@ -412,7 +412,7 @@ function serializeNodes( rotY: node.rotation[1], rotZ: node.rotation[2], inVp: node.viewport, - inMag: node.mag, + inMag: node.resolution, bitDepth: node.bitDepth, interpolation: node.interpolation, time: node.timestamp, @@ -963,7 +963,7 @@ export function parseNml(nmlString: string): Promise<{ }), bitDepth: _parseInt(attr, "bitDepth", { defaultValue: DEFAULT_BITDEPTH }), viewport: _parseInt(attr, "inVp", { defaultValue: DEFAULT_VIEWPORT }), - mag: _parseInt(attr, "inMag", { defaultValue: DEFAULT_RESOLUTION }), + resolution: _parseInt(attr, "inMag", { defaultValue: DEFAULT_RESOLUTION }), radius: _parseFloat(attr, "radius", { defaultValue: Constants.DEFAULT_NODE_RADIUS }), timestamp: _parseTimestamp(attr, "time", { defaultValue: DEFAULT_TIMESTAMP }), }; diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts index 99b7398c644..4570c1d437b 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts @@ -146,7 +146,7 @@ export function createNode( radius, rotation, viewport, - mag: resolution, + resolution, id: nextNewId, timestamp, bitDepth: state.datasetConfiguration.fourBit ? 4 : 8, @@ -846,7 +846,7 @@ function serverNodeToMutableNode(n: ServerNode): MutableNode { rotation: Utils.point3ToVector3(n.rotation), bitDepth: n.bitDepth, viewport: n.viewport, - mag: n.resolution, + resolution: n.resolution, radius: n.radius, timestamp: n.createdTimestamp, interpolation: n.interpolation, diff --git a/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts b/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts index aa2727ae8af..05910411232 100644 --- a/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/proofread_saga.ts @@ -270,7 +270,7 @@ function* createEditableMapping(): Saga { // Get volume tracing again to make sure the version is up to date const upToDateVolumeTracing = yield* select((state) => getActiveSegmentationTracing(state)); if (upToDateVolumeTracing == null) { - throw new Error("No active segmentation tracing layer. Cannot create editble mapping."); + throw new Error("No active segmentation tracing layer. Cannot create editable mapping."); } const volumeTracingId = upToDateVolumeTracing.tracingId; diff --git a/frontend/javascripts/oxalis/store.ts b/frontend/javascripts/oxalis/store.ts index f36f1c95592..dc4847893e1 100644 --- a/frontend/javascripts/oxalis/store.ts +++ b/frontend/javascripts/oxalis/store.ts @@ -89,7 +89,7 @@ export type MutableNode = { rotation: Vector3; bitDepth: number; viewport: number; - mag: number; + resolution: number; radius: number; timestamp: number; interpolation: boolean; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/connectome_tab/connectome_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/connectome_tab/connectome_view.tsx index 75535fa47b8..1b22d003da9 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/connectome_tab/connectome_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/connectome_tab/connectome_view.tsx @@ -163,7 +163,7 @@ const synapseNodeCreator = (synapseId: number, synapsePosition: Vector3): Mutabl radius: Constants.DEFAULT_NODE_RADIUS, rotation: [0, 0, 0], viewport: 0, - mag: 0, + resolution: 0, id: synapseId, timestamp: Date.now(), bitDepth: 8, diff --git a/frontend/javascripts/test/libs/nml.spec.ts b/frontend/javascripts/test/libs/nml.spec.ts index e73937386f7..5506e01c012 100644 --- a/frontend/javascripts/test/libs/nml.spec.ts +++ b/frontend/javascripts/test/libs/nml.spec.ts @@ -34,7 +34,7 @@ const createDummyNode = (id: number): Node => ({ untransformedPosition: [id, id, id], additionalCoordinates: [], radius: id, - mag: 10, + resolution: 10, rotation: [id, id, id], timestamp: id, viewport: 1, diff --git a/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts b/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts index 5cf6b5767bd..2eb6a6858b4 100644 --- a/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts @@ -130,7 +130,7 @@ test("SkeletonTracing should add a new node", (t) => { untransformedPosition: position, rotation, viewport, - mag: resolution, + resolution, id: 1, radius: 1, }); @@ -295,7 +295,7 @@ test("SkeletonTracing should delete nodes and split the tree", (t) => { untransformedPosition: [0, 0, 0], additionalCoordinates: null, radius: 10, - mag: 10, + resolution: 10, rotation: [0, 0, 0], timestamp: 0, viewport: 1, @@ -453,7 +453,7 @@ test("SkeletonTracing should delete an edge and split the tree", (t) => { untransformedPosition: [0, 0, 0], additionalCoordinates: null, radius: 10, - mag: 10, + resolution: 10, rotation: [0, 0, 0], timestamp: 0, viewport: 1, From 8dec57826e253f3b1c2d17308e7d75d41fea26b1 Mon Sep 17 00:00:00 2001 From: MichaelBuessemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:47:13 +0100 Subject: [PATCH 04/10] Fix: Use forwardRef for scrollable virtualized tree (#8186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use forwardRef for scrollable virtualized tree * add changelog entry --------- Co-authored-by: Michael Büßemeyer --- CHANGELOG.unreleased.md | 1 + .../scrollable_virtualized_tree.tsx | 16 ++++++++++++---- .../segments_tab/segments_view.tsx | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 2d0bec114f8..9317f6f652a 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -35,6 +35,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Fix that scrolling in the trees and segments tab did not work while dragging. [#8162](https://github.com/scalableminds/webknossos/pull/8162) - Fixed that uploading a dataset which needs a conversion failed when the angstrom unit was configured for the conversion. [#8173](https://github.com/scalableminds/webknossos/pull/8173) - Fixed that the skeleton search did not automatically expand groups that contained the selected tree [#8129](https://github.com/scalableminds/webknossos/pull/8129) +- Fixed interactions in the trees and segments tab like the search due to a bug introduced by [#8162](https://github.com/scalableminds/webknossos/pull/8162). [#8186](https://github.com/scalableminds/webknossos/pull/8186) - Fixed a bug that zarr streaming version 3 returned the shape of mag (1, 1, 1) / the finest mag for all mags. [#8116](https://github.com/scalableminds/webknossos/pull/8116) - Fixed sorting of mags in outbound zarr streaming. [#8125](https://github.com/scalableminds/webknossos/pull/8125) - Fixed a bug where you could not create annotations for public datasets of other organizations. [#8107](https://github.com/scalableminds/webknossos/pull/8107) diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/scrollable_virtualized_tree.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/scrollable_virtualized_tree.tsx index f2a8a8439fe..aa02951bc02 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/scrollable_virtualized_tree.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/scrollable_virtualized_tree.tsx @@ -1,7 +1,7 @@ import { Tree as AntdTree, type TreeProps } from "antd"; import type { BasicDataNode } from "antd/es/tree"; import { throttle } from "lodash"; -import { useCallback, useRef } from "react"; +import { forwardRef, useCallback, useRef } from "react"; import type RcTree from "rc-tree"; const MIN_SCROLL_SPEED = 30; @@ -10,8 +10,10 @@ const MIN_SCROLL_AREA_HEIGHT = 60; const SCROLL_AREA_RATIO = 10; // 1/10th of the container height const THROTTLE_TIME = 25; -function ScrollableVirtualizedTree( - props: TreeProps & { ref: React.RefObject }, +// React.forwardRef does not support generic types, so we need to define the type of the ref separately. +function ScrollableVirtualizedTreeInner( + props: TreeProps, + ref: React.Ref, ) { const wrapperRef = useRef(null); // biome-ignore lint/correctness/useExhaustiveDependencies: biome is not smart enough to notice that the function needs to be re-created when wrapperRef changes. @@ -56,9 +58,15 @@ function ScrollableVirtualizedTree( return (

- +
); } +const ScrollableVirtualizedTree = forwardRef(ScrollableVirtualizedTreeInner) as < + T extends BasicDataNode, +>( + props: TreeProps & { ref?: React.Ref }, +) => ReturnType; + export default ScrollableVirtualizedTree; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index f37f3860e08..2188a8bd2c9 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -1904,7 +1904,7 @@ class SegmentsView extends React.Component { overflow: "hidden", }} > - + Date: Tue, 12 Nov 2024 15:11:42 +0100 Subject: [PATCH 05/10] Fix expected node type from server by renaming resolution to mag (#8187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix server node type by switching back to mag * add changelog entry --------- Co-authored-by: Michael Büßemeyer --- CHANGELOG.unreleased.md | 1 + .../oxalis/model/helpers/generate_dummy_trees.ts | 2 +- .../model/reducers/skeletontracing_reducer_helpers.ts | 2 +- .../test/fixtures/skeletontracing_server_objects.ts | 6 +++--- .../javascripts/test/fixtures/tasktracing_server_objects.ts | 2 +- frontend/javascripts/types/api_flow_types.ts | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 9317f6f652a..ffdd320c196 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -45,6 +45,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Deleting a bounding box is now possible independently of a visible segmentation layer. [#8164](https://github.com/scalableminds/webknossos/pull/8164) - S3-compliant object storages can now be accessed via HTTPS. [#8167](https://github.com/scalableminds/webknossos/pull/8167) - Fixed that skeleton tree nodes were created with the wrong mag. [#8185](https://github.com/scalableminds/webknossos/pull/8185) +- Fixed the expected type of a tree node received from the server. Fixes nml export to include the `inMag` field correctly. [#8187](https://github.com/scalableminds/webknossos/pull/8187) - Fixed a layout persistence bug leading to empty viewports, triggered when switching between orthogonal, flight, or oblique mode. [#8177](https://github.com/scalableminds/webknossos/pull/8177) ### Removed diff --git a/frontend/javascripts/oxalis/model/helpers/generate_dummy_trees.ts b/frontend/javascripts/oxalis/model/helpers/generate_dummy_trees.ts index 3d48d91c3f6..3ed8d9b4174 100644 --- a/frontend/javascripts/oxalis/model/helpers/generate_dummy_trees.ts +++ b/frontend/javascripts/oxalis/model/helpers/generate_dummy_trees.ts @@ -34,7 +34,7 @@ export default function generateDummyTrees( }, radius: 112.39999389648438, viewport: 1, - resolution: 1, + mag: 1, bitDepth: 4, interpolation: true, createdTimestamp: 1507550793899, diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts index 4570c1d437b..784a14a1346 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts @@ -846,7 +846,7 @@ function serverNodeToMutableNode(n: ServerNode): MutableNode { rotation: Utils.point3ToVector3(n.rotation), bitDepth: n.bitDepth, viewport: n.viewport, - resolution: n.resolution, + resolution: n.mag, radius: n.radius, timestamp: n.createdTimestamp, interpolation: n.interpolation, diff --git a/frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts b/frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts index 55a2c1fa71d..cbcec2723a7 100644 --- a/frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts +++ b/frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts @@ -22,7 +22,7 @@ export const tracing: ServerSkeletonTracing = { }, radius: 112.39999389648438, viewport: 0, - resolution: 1, + mag: 1, bitDepth: 4, interpolation: true, createdTimestamp: 1502302785450, @@ -70,7 +70,7 @@ export const tracing: ServerSkeletonTracing = { }, radius: 112.39999389648438, viewport: 0, - resolution: 1, + mag: 1, bitDepth: 4, interpolation: true, createdTimestamp: 1502302785447, @@ -90,7 +90,7 @@ export const tracing: ServerSkeletonTracing = { }, radius: 112.39999389648438, viewport: 0, - resolution: 1, + mag: 1, bitDepth: 4, interpolation: true, createdTimestamp: 1502302785448, diff --git a/frontend/javascripts/test/fixtures/tasktracing_server_objects.ts b/frontend/javascripts/test/fixtures/tasktracing_server_objects.ts index 8da34fd0361..cd1d0d08f28 100644 --- a/frontend/javascripts/test/fixtures/tasktracing_server_objects.ts +++ b/frontend/javascripts/test/fixtures/tasktracing_server_objects.ts @@ -20,7 +20,7 @@ export const tracing: ServerSkeletonTracing = { }, radius: 120, viewport: 1, - resolution: 1, + mag: 1, bitDepth: 0, interpolation: false, createdTimestamp: 1528811979356, diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts index c27d3356693..a4c6df351f5 100644 --- a/frontend/javascripts/types/api_flow_types.ts +++ b/frontend/javascripts/types/api_flow_types.ts @@ -756,7 +756,7 @@ export type ServerNode = { rotation: Point3; bitDepth: number; viewport: number; - resolution: number; + mag: number; radius: number; createdTimestamp: number; interpolation: boolean; From 825b1f8662fe46c0557f370d032be6ac1409b58d Mon Sep 17 00:00:00 2001 From: frcroth Date: Wed, 13 Nov 2024 11:48:59 +0100 Subject: [PATCH 06/10] Async reading in FileSystemDataVault (#8126) --- CHANGELOG.unreleased.md | 3 +- .../datavault/FileSystemDataVault.scala | 77 ++++++++++++------- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index ffdd320c196..a2c8e22ef8c 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -27,12 +27,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Migrated nightly screenshot tests from CircleCI to GitHub actions. [#8134](https://github.com/scalableminds/webknossos/pull/8134) - Migrated nightly screenshot tests for wk.org from CircleCI to GitHub actions. [#8135](https://github.com/scalableminds/webknossos/pull/8135) - Thumbnails for datasets now use the selected mapping from the view configuration if available. [#8157](https://github.com/scalableminds/webknossos/pull/8157) +- Reading image files on datastore filesystem is now done asynchronously. [#8126](https://github.com/scalableminds/webknossos/pull/8126) ### Fixed - Fixed a bug during dataset upload in case the configured `datastore.baseFolder` is an absolute path. [#8098](https://github.com/scalableminds/webknossos/pull/8098) [#8103](https://github.com/scalableminds/webknossos/pull/8103) - Fixed bbox export menu item [#8152](https://github.com/scalableminds/webknossos/pull/8152) - When trying to save an annotation opened via a link including a sharing token, the token is automatically discarded in case it is insufficient for update actions but the users token is. [#8139](https://github.com/scalableminds/webknossos/pull/8139) -- Fix that scrolling in the trees and segments tab did not work while dragging. [#8162](https://github.com/scalableminds/webknossos/pull/8162) +- Fix that scrolling in the trees and segments tab did not work while dragging. [#8162](https://github.com/scalableminds/webknossos/pull/8162) - Fixed that uploading a dataset which needs a conversion failed when the angstrom unit was configured for the conversion. [#8173](https://github.com/scalableminds/webknossos/pull/8173) - Fixed that the skeleton search did not automatically expand groups that contained the selected tree [#8129](https://github.com/scalableminds/webknossos/pull/8129) - Fixed interactions in the trees and segments tab like the search due to a bug introduced by [#8162](https://github.com/scalableminds/webknossos/pull/8162). [#8186](https://github.com/scalableminds/webknossos/pull/8186) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/datavault/FileSystemDataVault.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/datavault/FileSystemDataVault.scala index bb16d34cd00..0d42244c6a0 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/datavault/FileSystemDataVault.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/datavault/FileSystemDataVault.scala @@ -1,15 +1,16 @@ package com.scalableminds.webknossos.datastore.datavault import com.scalableminds.util.tools.Fox -import net.liftweb.common.Box.tryo -import com.scalableminds.util.tools.Fox.{bool2Fox, box2Fox} +import com.scalableminds.util.tools.Fox.bool2Fox import com.scalableminds.webknossos.datastore.storage.DataVaultService +import net.liftweb.common.{Box, Full} import org.apache.commons.lang3.builder.HashCodeBuilder import java.nio.ByteBuffer -import java.nio.file.{Files, Path, Paths} +import java.nio.channels.{AsynchronousFileChannel, CompletionHandler} +import java.nio.file.{Files, Path, Paths, StandardOpenOption} import java.util.stream.Collectors -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Promise} import scala.jdk.CollectionConverters._ class FileSystemDataVault extends DataVault { @@ -24,31 +25,55 @@ class FileSystemDataVault extends DataVault { private def readBytesLocal(localPath: Path, range: RangeSpecifier)(implicit ec: ExecutionContext): Fox[Array[Byte]] = if (Files.exists(localPath)) { range match { - case Complete() => tryo(Files.readAllBytes(localPath)).toFox + case Complete() => + readAsync(localPath, 0, Math.toIntExact(Files.size(localPath))) + case StartEnd(r) => - tryo { - val channel = Files.newByteChannel(localPath) - val buf = ByteBuffer.allocateDirect(r.length) - channel.position(r.start) - channel.read(buf) - buf.rewind() - val arr = new Array[Byte](r.length) - buf.get(arr) - arr - }.toFox + readAsync(localPath, r.start, r.length) + case SuffixLength(length) => - tryo { - val channel = Files.newByteChannel(localPath) - val buf = ByteBuffer.allocateDirect(length) - channel.position(channel.size() - length) - channel.read(buf) - buf.rewind() - val arr = new Array[Byte](length) - buf.get(arr) - arr - }.toFox + val fileSize = Files.size(localPath) + readAsync(localPath, fileSize - length, length) } - } else Fox.empty + } else { + Fox.empty + } + + private def readAsync(path: Path, position: Long, length: Int)(implicit ec: ExecutionContext): Fox[Array[Byte]] = { + val promise = Promise[Box[Array[Byte]]]() + val buffer = ByteBuffer.allocateDirect(length) + var channel: AsynchronousFileChannel = null + + try { + channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ) + + channel.read( + buffer, + position, + buffer, + new CompletionHandler[Integer, ByteBuffer] { + override def completed(result: Integer, buffer: ByteBuffer): Unit = { + buffer.rewind() + val arr = new Array[Byte](length) + buffer.get(arr) + promise.success(Full(arr)) + channel.close() + } + + override def failed(exc: Throwable, buffer: ByteBuffer): Unit = { + promise.failure(exc) + channel.close() + } + } + ) + } catch { + case e: Throwable => + promise.failure(e) + if (channel != null && channel.isOpen) channel.close() + } + + promise.future + } override def listDirectory(path: VaultPath, maxItems: Int)(implicit ec: ExecutionContext): Fox[List[VaultPath]] = vaultPathToLocalPath(path).map( From eba94476d7f9223b1512be8a2572f572e999ce33 Mon Sep 17 00:00:00 2001 From: Florian M Date: Wed, 13 Nov 2024 13:00:49 +0100 Subject: [PATCH 07/10] Release 24.11.1 (#8188) --- CHANGELOG.released.md | 50 ++++++++++++++++++++++++++++++++++++++++ CHANGELOG.unreleased.md | 34 +-------------------------- MIGRATIONS.released.md | 10 ++++++++ MIGRATIONS.unreleased.md | 6 +---- 4 files changed, 62 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.released.md b/CHANGELOG.released.md index 201055984b8..676a694171f 100644 --- a/CHANGELOG.released.md +++ b/CHANGELOG.released.md @@ -7,6 +7,56 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MICRO`. For upgrade instructions, please check the [migration guide](MIGRATIONS.released.md). +## [24.11.1](https://github.com/scalableminds/webknossos/releases/tag/24.11.1) - 2024-11-13 +[Commits](https://github.com/scalableminds/webknossos/compare/24.10.0...24.11.1) + +### Highlights +- It is now possible to add metadata in annotations to Trees and Segments. [#7875](https://github.com/scalableminds/webknossos/pull/7875) +- Added a button to the search popover in the skeleton and segment tab to select all matching non-group results. [#8123](https://github.com/scalableminds/webknossos/pull/8123) + +### Added +- It is now possible to add metadata in annotations to Trees and Segments. [#7875](https://github.com/scalableminds/webknossos/pull/7875) +- Added a summary row to the time tracking overview, where times and annotations/tasks are summed. [#8092](https://github.com/scalableminds/webknossos/pull/8092) +- Most sliders have been improved: Wheeling above a slider now changes its value and double-clicking its knob resets it to its default value. [#8095](https://github.com/scalableminds/webknossos/pull/8095) +- It is now possible to search for unnamed segments with the full default name instead of only their id. [#8133](https://github.com/scalableminds/webknossos/pull/8133) +- Increased loading speed for precomputed meshes. [#8110](https://github.com/scalableminds/webknossos/pull/8110) +- Added a button to the search popover in the skeleton and segment tab to select all matching non-group results. [#8123](https://github.com/scalableminds/webknossos/pull/8123) +- Unified wording in UI and code: “Magnification”/“mag” is now used in place of “Resolution“ most of the time, compare [https://docs.webknossos.org/webknossos/terminology.html](terminology document). [#8111](https://github.com/scalableminds/webknossos/pull/8111) +- Added support for adding remote OME-Zarr NGFF version 0.5 datasets. [#8122](https://github.com/scalableminds/webknossos/pull/8122) +- Workflow reports may be deleted by superusers. [#8156](https://github.com/scalableminds/webknossos/pull/8156) + +### Changed +- Some mesh-related actions were disabled in proofreading-mode when using meshfiles that were created for a mapping rather than an oversegmentation. [#8091](https://github.com/scalableminds/webknossos/pull/8091) +- Admins can now see and cancel all jobs. The owner of the job is shown in the job list. [#8112](https://github.com/scalableminds/webknossos/pull/8112) +- Migrated nightly screenshot tests from CircleCI to GitHub actions. [#8134](https://github.com/scalableminds/webknossos/pull/8134) +- Migrated nightly screenshot tests for wk.org from CircleCI to GitHub actions. [#8135](https://github.com/scalableminds/webknossos/pull/8135) +- Thumbnails for datasets now use the selected mapping from the view configuration if available. [#8157](https://github.com/scalableminds/webknossos/pull/8157) + +### Fixed +- Fixed a bug during dataset upload in case the configured `datastore.baseFolder` is an absolute path. [#8098](https://github.com/scalableminds/webknossos/pull/8098) [#8103](https://github.com/scalableminds/webknossos/pull/8103) +- Fixed bbox export menu item [#8152](https://github.com/scalableminds/webknossos/pull/8152) +- When trying to save an annotation opened via a link including a sharing token, the token is automatically discarded in case it is insufficient for update actions but the users token is. [#8139](https://github.com/scalableminds/webknossos/pull/8139) +- Fix that scrolling in the trees and segments tab did not work while dragging. [#8162](https://github.com/scalableminds/webknossos/pull/8162) +- Fixed that uploading a dataset which needs a conversion failed when the angstrom unit was configured for the conversion. [#8173](https://github.com/scalableminds/webknossos/pull/8173) +- Fixed that the skeleton search did not automatically expand groups that contained the selected tree [#8129](https://github.com/scalableminds/webknossos/pull/8129) +- Fixed interactions in the trees and segments tab like the search due to a bug introduced by [#8162](https://github.com/scalableminds/webknossos/pull/8162). [#8186](https://github.com/scalableminds/webknossos/pull/8186) +- Fixed a bug that zarr streaming version 3 returned the shape of mag (1, 1, 1) / the finest mag for all mags. [#8116](https://github.com/scalableminds/webknossos/pull/8116) +- Fixed sorting of mags in outbound zarr streaming. [#8125](https://github.com/scalableminds/webknossos/pull/8125) +- Fixed a bug where you could not create annotations for public datasets of other organizations. [#8107](https://github.com/scalableminds/webknossos/pull/8107) +- Users without edit permissions to a dataset can no longer delete sharing tokens via the API. [#8083](https://github.com/scalableminds/webknossos/issues/8083) +- Fixed downloading task annotations of teams you are not in, when accessing directly via URI. [#8155](https://github.com/scalableminds/webknossos/pull/8155) +- Removed unnecessary scrollbars in skeleton tab that occurred especially after resizing. [#8148](https://github.com/scalableminds/webknossos/pull/8148) +- Deleting a bounding box is now possible independently of a visible segmentation layer. [#8164](https://github.com/scalableminds/webknossos/pull/8164) +- S3-compliant object storages can now be accessed via HTTPS. [#8167](https://github.com/scalableminds/webknossos/pull/8167) +- Fixed that skeleton tree nodes were created with the wrong mag. [#8185](https://github.com/scalableminds/webknossos/pull/8185) +- Fixed the expected type of a tree node received from the server. Fixes nml export to include the `inMag` field correctly. [#8187](https://github.com/scalableminds/webknossos/pull/8187) +- Fixed a layout persistence bug leading to empty viewports, triggered when switching between orthogonal, flight, or oblique mode. [#8177](https://github.com/scalableminds/webknossos/pull/8177) + +### Removed + +### Breaking Changes + + ## [24.10.0](https://github.com/scalableminds/webknossos/releases/tag/24.10.0) - 2024-09-24 [Commits](https://github.com/scalableminds/webknossos/compare/24.08.1...24.10.0) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index a2c8e22ef8c..152e2d38b13 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -8,46 +8,14 @@ and this project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MIC For upgrade instructions, please check the [migration guide](MIGRATIONS.released.md). ## Unreleased -[Commits](https://github.com/scalableminds/webknossos/compare/24.10.0...HEAD) +[Commits](https://github.com/scalableminds/webknossos/compare/24.11.1...HEAD) ### Added -- It is now possible to add metadata in annotations to Trees and Segments. [#7875](https://github.com/scalableminds/webknossos/pull/7875) -- Added a summary row to the time tracking overview, where times and annotations/tasks are summed. [#8092](https://github.com/scalableminds/webknossos/pull/8092) -- Most sliders have been improved: Wheeling above a slider now changes its value and double-clicking its knob resets it to its default value. [#8095](https://github.com/scalableminds/webknossos/pull/8095) -- It is now possible to search for unnamed segments with the full default name instead of only their id. [#8133](https://github.com/scalableminds/webknossos/pull/8133) -- Increased loading speed for precomputed meshes. [#8110](https://github.com/scalableminds/webknossos/pull/8110) -- Added a button to the search popover in the skeleton and segment tab to select all matching non-group results. [#8123](https://github.com/scalableminds/webknossos/pull/8123) -- Unified wording in UI and code: “Magnification”/“mag” is now used in place of “Resolution“ most of the time, compare [https://docs.webknossos.org/webknossos/terminology.html](terminology document). [#8111](https://github.com/scalableminds/webknossos/pull/8111) -- Added support for adding remote OME-Zarr NGFF version 0.5 datasets. [#8122](https://github.com/scalableminds/webknossos/pull/8122) -- Workflow reports may be deleted by superusers. [#8156](https://github.com/scalableminds/webknossos/pull/8156) ### Changed -- Some mesh-related actions were disabled in proofreading-mode when using meshfiles that were created for a mapping rather than an oversegmentation. [#8091](https://github.com/scalableminds/webknossos/pull/8091) -- Admins can now see and cancel all jobs. The owner of the job is shown in the job list. [#8112](https://github.com/scalableminds/webknossos/pull/8112) -- Migrated nightly screenshot tests from CircleCI to GitHub actions. [#8134](https://github.com/scalableminds/webknossos/pull/8134) -- Migrated nightly screenshot tests for wk.org from CircleCI to GitHub actions. [#8135](https://github.com/scalableminds/webknossos/pull/8135) -- Thumbnails for datasets now use the selected mapping from the view configuration if available. [#8157](https://github.com/scalableminds/webknossos/pull/8157) - Reading image files on datastore filesystem is now done asynchronously. [#8126](https://github.com/scalableminds/webknossos/pull/8126) ### Fixed -- Fixed a bug during dataset upload in case the configured `datastore.baseFolder` is an absolute path. [#8098](https://github.com/scalableminds/webknossos/pull/8098) [#8103](https://github.com/scalableminds/webknossos/pull/8103) -- Fixed bbox export menu item [#8152](https://github.com/scalableminds/webknossos/pull/8152) -- When trying to save an annotation opened via a link including a sharing token, the token is automatically discarded in case it is insufficient for update actions but the users token is. [#8139](https://github.com/scalableminds/webknossos/pull/8139) -- Fix that scrolling in the trees and segments tab did not work while dragging. [#8162](https://github.com/scalableminds/webknossos/pull/8162) -- Fixed that uploading a dataset which needs a conversion failed when the angstrom unit was configured for the conversion. [#8173](https://github.com/scalableminds/webknossos/pull/8173) -- Fixed that the skeleton search did not automatically expand groups that contained the selected tree [#8129](https://github.com/scalableminds/webknossos/pull/8129) -- Fixed interactions in the trees and segments tab like the search due to a bug introduced by [#8162](https://github.com/scalableminds/webknossos/pull/8162). [#8186](https://github.com/scalableminds/webknossos/pull/8186) -- Fixed a bug that zarr streaming version 3 returned the shape of mag (1, 1, 1) / the finest mag for all mags. [#8116](https://github.com/scalableminds/webknossos/pull/8116) -- Fixed sorting of mags in outbound zarr streaming. [#8125](https://github.com/scalableminds/webknossos/pull/8125) -- Fixed a bug where you could not create annotations for public datasets of other organizations. [#8107](https://github.com/scalableminds/webknossos/pull/8107) -- Users without edit permissions to a dataset can no longer delete sharing tokens via the API. [#8083](https://github.com/scalableminds/webknossos/issues/8083) -- Fixed downloading task annotations of teams you are not in, when accessing directly via URI. [#8155](https://github.com/scalableminds/webknossos/pull/8155) -- Removed unnecessary scrollbars in skeleton tab that occurred especially after resizing. [#8148](https://github.com/scalableminds/webknossos/pull/8148) -- Deleting a bounding box is now possible independently of a visible segmentation layer. [#8164](https://github.com/scalableminds/webknossos/pull/8164) -- S3-compliant object storages can now be accessed via HTTPS. [#8167](https://github.com/scalableminds/webknossos/pull/8167) -- Fixed that skeleton tree nodes were created with the wrong mag. [#8185](https://github.com/scalableminds/webknossos/pull/8185) -- Fixed the expected type of a tree node received from the server. Fixes nml export to include the `inMag` field correctly. [#8187](https://github.com/scalableminds/webknossos/pull/8187) -- Fixed a layout persistence bug leading to empty viewports, triggered when switching between orthogonal, flight, or oblique mode. [#8177](https://github.com/scalableminds/webknossos/pull/8177) ### Removed diff --git a/MIGRATIONS.released.md b/MIGRATIONS.released.md index 2b490c94b58..d0db04bb6a0 100644 --- a/MIGRATIONS.released.md +++ b/MIGRATIONS.released.md @@ -6,6 +6,16 @@ See `MIGRATIONS.unreleased.md` for the changes which are not yet part of an offi This project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MICRO`. User-facing changes are documented in the [changelog](CHANGELOG.released.md). +## [24.11.1](https://github.com/scalableminds/webknossos/releases/tag/24.11.1) - 2024-11-13 +[Commits](https://github.com/scalableminds/webknossos/compare/24.10.0...24.11.1) + +### Postgres Evolutions: + +- [121-worker-name.sql](conf/evolutions/121-worker-name.sql) +- [122-resolution-to-mag.sql](conf/evolutions/122-resolution-to-mag.sql) +- [123-more-model-categories.sql](conf/evolutions/123-more-model-categories.sql) + + ## [24.10.0](https://github.com/scalableminds/webknossos/releases/tag/24.10.0) - 2024-09-24 [Commits](https://github.com/scalableminds/webknossos/compare/24.08.1...24.10.0) diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md index f6d640f469d..20414e596e6 100644 --- a/MIGRATIONS.unreleased.md +++ b/MIGRATIONS.unreleased.md @@ -6,10 +6,6 @@ This project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MICRO`. User-facing changes are documented in the [changelog](CHANGELOG.released.md). ## Unreleased -[Commits](https://github.com/scalableminds/webknossos/compare/24.10.0...HEAD) +[Commits](https://github.com/scalableminds/webknossos/compare/24.11.1...HEAD) ### Postgres Evolutions: - -- [121-worker-name.sql](conf/evolutions/121-worker-name.sql) -- [122-resolution-to-mag.sql](conf/evolutions/122-resolution-to-mag.sql) -- [123-more-model-categories.sql](conf/evolutions/123-more-model-categories.sql) From 3a18852c656c551ef7c95bc97bcf5cdd92d3a64a Mon Sep 17 00:00:00 2001 From: frcroth Date: Wed, 13 Nov 2024 13:27:23 +0100 Subject: [PATCH 08/10] Add dataset upload test (without using frontend) (#8184) --- .../backend-snapshot-tests/datasets.e2e.ts | 98 +++++++++++++++++++ frontend/javascripts/test/e2e-setup.ts | 6 +- test/e2e/End2EndSpec.scala | 8 +- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.ts b/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.ts index a82653b9462..9a091665301 100644 --- a/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.ts +++ b/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.ts @@ -9,6 +9,7 @@ import { import type { APIDataset } from "types/api_flow_types"; import * as api from "admin/admin_rest_api"; import test from "ava"; +import fs from "node:fs"; async function getFirstDataset(): Promise { const datasets = await api.getActiveDatasetsOfMyOrganization(); @@ -108,3 +109,100 @@ test("Zarr 3 streaming", async (t) => { const base64 = btoa(String.fromCharCode(...new Uint8Array(bytes.slice(-128)))); t.snapshot(base64); }); + +test("Dataset upload", async (t) => { + const uploadId = "test-dataset-upload-" + Date.now(); + + await fetch("/data/datasets/reserveUpload", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ + filePaths: ["test-dataset-upload.zip"], + folderId: "570b9f4e4bb848d0885ea917", + initialTeams: [], + layersToLink: [], + name: "test-dataset-upload", + organization: "Organization_X", + totalFileCount: 1, + uploadId: uploadId, + }), + }); + + const filePath = "test/dataset/test-dataset.zip"; + const testDataset = fs.readFileSync(filePath); + + let formData = new FormData(); + formData.append("resumableChunkNumber", "1"); + formData.append("resumableChunkSize", "10485760"); + formData.append("resumableCurrentChunkSize", "71988"); + formData.append("resumableTotalSize", "71988"); + formData.append("resumableType", "application/zip"); + formData.append("resumableIdentifier", uploadId + "/test-dataset.zip"); + formData.append("resumableFilename", "test-dataset.zip"); + formData.append("resumableRelativePath", "test-dataset.zip"); + formData.append("resumableTotalChunks", "1"); + + // Setting the correct content type header automatically does not work (the boundary is not included) + // We can not extract the boundary from the FormData object + // Thus we have to set the content type header ourselves and create the body manually + + const boundary = "----WebKitFormBoundaryAqTsFa4N9FW7zF7I"; + let bodyString = `--${boundary}\r\n`; + // @ts-ignore + for (const [key, value] of formData.entries()) { + bodyString += `Content-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`; + bodyString += `--${boundary}\r\n`; + } + bodyString += `Content-Disposition: form-data; name="file"; filename="test-dataset.zip"\r\n`; + bodyString += "Content-Type: application/octet-stream\r\n\r\n"; + + // We have to send the file as bytes, otherwise JS does some encoding, resulting in erroneous bytes + + const formBytes = new TextEncoder().encode(bodyString); + const fileBytes = new Uint8Array(testDataset); + const endBytes = new TextEncoder().encode(`\r\n--${boundary}--`); + const body = new Uint8Array(formBytes.length + fileBytes.length + endBytes.length); + body.set(formBytes, 0); + body.set(fileBytes, formBytes.length); + body.set(endBytes, formBytes.length + fileBytes.length); + + let content_type = `multipart/form-data; boundary=${boundary}`; + + const uploadResult = await fetch("/data/datasets", { + method: "POST", + headers: new Headers({ + "Content-Type": content_type, + }), + body: body, + }); + + if (uploadResult.status !== 200) { + t.fail("Dataset upload failed"); + } + + const finishResult = await fetch("/data/datasets/finishUpload", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + }), + body: JSON.stringify({ + uploadId: uploadId, + needsConversion: false, + }), + }); + + if (finishResult.status !== 200) { + t.fail("Dataset upload failed at finish"); + } + + const result = await fetch("/api/datasets/Organization_X/test-dataset-upload/health", { + headers: new Headers(), + }); + + if (result.status !== 200) { + t.fail("Dataset health check after upload failed"); + } + t.pass(); +}); diff --git a/frontend/javascripts/test/e2e-setup.ts b/frontend/javascripts/test/e2e-setup.ts index b1330eb25e1..e8d6ed720d8 100644 --- a/frontend/javascripts/test/e2e-setup.ts +++ b/frontend/javascripts/test/e2e-setup.ts @@ -3,7 +3,7 @@ import _ from "lodash"; // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'deep... Remove this comment to see the full error message import deepForEach from "deep-for-each"; // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'node... Remove this comment to see the full error message -import fetch, { Headers, Request, Response, FetchError } from "node-fetch"; +import fetch, { Headers, FormData, Request, Response, FetchError, File } from "node-fetch"; import fs from "node:fs"; // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'shel... Remove this comment to see the full error message import shell from "shelljs"; @@ -67,7 +67,7 @@ global.fetch = function fetchWrapper(url, options) { let newUrl = url; // @ts-expect-error ts-migrate(2339) FIXME: Property 'indexOf' does not exist on type 'Request... Remove this comment to see the full error message - if (url.indexOf("http:") === -1) { + if (url.indexOf("http:") === -1 && url.indexOf("https:") === -1) { newUrl = `http://localhost:9000${url}`; } @@ -84,6 +84,8 @@ global.Request = Request; global.Response = Response; // @ts-ignore FIXME: Element implicitly has an 'any' type because type ... Remove this comment to see the full error message global.FetchError = FetchError; +global.FormData = FormData; +global.File = File; const { JSDOM } = require("jsdom"); diff --git a/test/e2e/End2EndSpec.scala b/test/e2e/End2EndSpec.scala index 1de30f63de7..dc61e6c5d38 100644 --- a/test/e2e/End2EndSpec.scala +++ b/test/e2e/End2EndSpec.scala @@ -1,6 +1,6 @@ package e2e -import com.scalableminds.util.io.ZipIO +import com.scalableminds.util.io.{PathUtils, ZipIO} import com.typesafe.scalalogging.LazyLogging import org.scalatestplus.play.guice._ import org.specs2.main.Arguments @@ -51,9 +51,11 @@ class End2EndSpec(arguments: Arguments) extends Specification with GuiceFakeAppl private def ensureTestDataset(): Unit = { val testDatasetPath = "test/dataset/test-dataset.zip" val dataDirectory = new File("binaryData/Organization_X") - if (!dataDirectory.exists()) { - dataDirectory.mkdirs() + if (dataDirectory.exists()) { + println("Deleting existing data directory Organization_X") + PathUtils.deleteDirectoryRecursively(dataDirectory.toPath) } + dataDirectory.mkdirs() val testDatasetZip = new File(testDatasetPath) if (!testDatasetZip.exists()) { throw new Exception("Test dataset zip file does not exist.") From 1ec9b19d14e1e859e2770d71cf171c97e34f4ede Mon Sep 17 00:00:00 2001 From: frcroth Date: Wed, 13 Nov 2024 14:31:25 +0100 Subject: [PATCH 09/10] Update default security.txt for 2025 (#8192) --- conf/application.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/application.conf b/conf/application.conf index 07d8b5d2dd1..0ae8b6f25dd 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -125,7 +125,7 @@ webKnossos { securityTxt { enabled = true content ="""Contact: https://github.com/scalableminds/webknossos/security/advisories/new -Expires: 2024-07-03T10:00:00.000Z +Expires: 2025-07-03T10:00:00.000Z Preferred-Languages: en,de """ } From 8ff5b67b4bf8dc166550af4a9f5c7dff4b618095 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 Nov 2024 15:11:30 +0100 Subject: [PATCH 10/10] Fix performance bottleneck when deleting a lot of trees at once (#8176) * Fix performance bottleneck when deleting a lot of trees at once. Also fix a bug when importing an NML with groups when only groups but no trees exist in an annotation. Also fix a bug where trying to delete a non-existing node (via the API, for example) would delete the whole active tree. * update changelog * apply PR feedback * Better error message for non-existing key in diffable map and don't throw an error in getNodeAndTree if a node with nodeId does not exist. --- CHANGELOG.unreleased.md | 3 ++ frontend/javascripts/libs/diffable_map.ts | 2 +- .../accessors/skeletontracing_accessor.ts | 2 +- .../model/actions/skeletontracing_actions.tsx | 39 ++++++++++++++++--- .../model/reducers/skeletontracing_reducer.ts | 16 +++++--- .../skeletontracing_reducer_helpers.ts | 19 +++++---- .../model/sagas/skeletontracing_saga.ts | 1 + .../trees_tab/skeleton_tab_view.tsx | 17 ++++---- .../trees_tab/tree_hierarchy_renderers.tsx | 1 + .../reducers/skeletontracing_reducer.spec.ts | 14 +++++++ 10 files changed, 87 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 152e2d38b13..7cd632c60f3 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -16,6 +16,9 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Reading image files on datastore filesystem is now done asynchronously. [#8126](https://github.com/scalableminds/webknossos/pull/8126) ### Fixed +- Fix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.com/scalableminds/webknossos/pull/8176) +- Fix a bug when importing an NML with groups when only groups but no trees exist in an annotation. [#8176](https://github.com/scalableminds/webknossos/pull/8176) +- Fix a bug where trying to delete a non-existing node (via the API, for example) would delete the whole active tree. [#8176](https://github.com/scalableminds/webknossos/pull/8176) ### Removed diff --git a/frontend/javascripts/libs/diffable_map.ts b/frontend/javascripts/libs/diffable_map.ts index bdfd028d47c..eebd14d429a 100644 --- a/frontend/javascripts/libs/diffable_map.ts +++ b/frontend/javascripts/libs/diffable_map.ts @@ -54,7 +54,7 @@ class DiffableMap { if (value !== undefined) { return value; } else { - throw new Error("Get empty"); + throw new Error(`Key '${key}' does not exist in diffable map.`); } } diff --git a/frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts b/frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts index b30708bb6c9..657a18ca2e0 100644 --- a/frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts @@ -175,7 +175,7 @@ export function getNodeAndTree( let node = null; if (nodeId != null) { - node = tree.nodes.getOrThrow(nodeId); + node = tree.nodes.getNullable(nodeId); } else { const { activeNodeId } = skeletonTracing; diff --git a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.tsx b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.tsx index 6a8e9f4308a..54db3eef1c1 100644 --- a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.tsx +++ b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.tsx @@ -39,6 +39,7 @@ type CreateTreeAction = ReturnType; type SetEdgeVisibilityAction = ReturnType; type AddTreesAndGroupsAction = ReturnType; type DeleteTreeAction = ReturnType; +type DeleteTreesAction = ReturnType; type ResetSkeletonTracingAction = ReturnType; type SetActiveTreeAction = ReturnType; type SetActiveTreeByNameAction = ReturnType; @@ -65,7 +66,11 @@ type UpdateNavigationListAction = ReturnType; export type LoadAgglomerateSkeletonAction = ReturnType; type NoAction = ReturnType; -export type BatchableUpdateTreeAction = SetTreeGroupAction | DeleteTreeAction | SetTreeGroupsAction; +export type BatchableUpdateTreeAction = + | SetTreeGroupAction + | DeleteTreeAction + | DeleteTreesAction + | SetTreeGroupsAction; export type BatchUpdateGroupsAndTreesAction = { type: "BATCH_UPDATE_GROUPS_AND_TREES"; payload: BatchableUpdateTreeAction[]; @@ -93,6 +98,7 @@ export type SkeletonTracingAction = | SetEdgeVisibilityAction | AddTreesAndGroupsAction | DeleteTreeAction + | DeleteTreesAction | ResetSkeletonTracingAction | SetActiveTreeAction | SetActiveTreeByNameAction @@ -139,6 +145,7 @@ export const SkeletonTracingSaveRelevantActions = [ "SET_EDGES_ARE_VISIBLE", "ADD_TREES_AND_GROUPS", "DELETE_TREE", + "DELETE_TREES", "SET_ACTIVE_TREE", "SET_ACTIVE_TREE_BY_NAME", "SET_TREE_NAME", @@ -337,6 +344,19 @@ export const deleteTreeAction = (treeId?: number, suppressActivatingNextNode: bo suppressActivatingNextNode, }) as const; +export const deleteTreesAction = (treeIds: number[], suppressActivatingNextNode: boolean = false) => + // If suppressActivatingNextNode is true, the trees will be deleted without activating + // another node (nor tree). Use this in cases where you want to avoid changing + // the active position (due to the auto-centering). One could also suppress the auto-centering + // behavior, but the semantics of changing the active node might also be confusing to the user + // (e.g., when proofreading). So, it might be clearer to not have an active node in the first + // place. + ({ + type: "DELETE_TREES", + treeIds, + suppressActivatingNextNode, + }) as const; + export const resetSkeletonTracingAction = () => ({ type: "RESET_SKELETON_TRACING", @@ -555,11 +575,15 @@ export const deleteNodeAsUserAction = ( return deleteNodeAction(node.id, tree.treeId); }) // If the tree is empty, it will be deleted - .getOrElse(deleteTreeAction(treeId)); + .getOrElse( + getTree(skeletonTracing, treeId) + .map((tree) => (tree.nodes.size() === 0 ? deleteTreeAction(tree.treeId) : noAction())) + .getOrElse(noAction()), + ); }; // Let the user confirm the deletion of the initial node (node with id 1) of a task -function confirmDeletingInitialNode(treeId?: number) { +function confirmDeletingInitialNode(treeId: number) { Modal.confirm({ title: messages["tracing.delete_tree_with_initial_node"], onOk: () => { @@ -573,12 +597,15 @@ export const deleteTreeAsUserAction = (treeId?: number): NoAction => { const skeletonTracing = enforceSkeletonTracing(state.tracing); getTree(skeletonTracing, treeId).map((tree) => { if (state.task != null && tree.nodes.has(1)) { - confirmDeletingInitialNode(treeId); + confirmDeletingInitialNode(tree.treeId); } else if (state.userConfiguration.hideTreeRemovalWarning) { - Store.dispatch(deleteTreeAction(treeId)); + Store.dispatch(deleteTreeAction(tree.treeId)); } else { renderIndependently((destroy) => ( - Store.dispatch(deleteTreeAction(treeId))} destroy={destroy} /> + Store.dispatch(deleteTreeAction(tree.treeId))} + destroy={destroy} + /> )); } }); diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts index 03f739f5e97..473b07b3c8e 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts @@ -13,7 +13,7 @@ import { deleteBranchPoint, createNode, createTree, - deleteTree, + deleteTrees, deleteNode, deleteEdge, shuffleTreeColor, @@ -897,10 +897,16 @@ function SkeletonTracingReducer(state: OxalisState, action: Action): OxalisState .getOrElse(state); } - case "DELETE_TREE": { - const { treeId, suppressActivatingNextNode } = action; - return getTree(skeletonTracing, treeId) - .chain((tree) => deleteTree(skeletonTracing, tree, suppressActivatingNextNode)) + case "DELETE_TREE": + case "DELETE_TREES": { + const { suppressActivatingNextNode } = action; + const treeIds = + action.type === "DELETE_TREE" + ? getTree(skeletonTracing, action.treeId) // The treeId in a DELETE_TREE action can be undefined which will select the active tree + .map((tree) => [tree.treeId]) + .getOrElse([]) + : action.treeIds; + return deleteTrees(skeletonTracing, treeIds, suppressActivatingNextNode) .map(([trees, newActiveTreeId, newActiveNodeId, newMaxNodeId]) => update(state, { tracing: { diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts index 784a14a1346..a46db81a1ec 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts @@ -561,7 +561,10 @@ export function addTreesAndGroups( ); const hasInvalidNodeIds = getMinimumNodeId(trees) < Constants.MIN_NODE_ID; const needsReassignedIds = - Object.keys(skeletonTracing.trees).length > 0 || hasInvalidTreeIds || hasInvalidNodeIds; + Object.keys(skeletonTracing.trees).length > 0 || + skeletonTracing.treeGroups.length > 0 || + hasInvalidTreeIds || + hasInvalidNodeIds; if (!needsReassignedIds) { // Without reassigning ids, the code is considerably faster. @@ -631,20 +634,22 @@ export function addTreesAndGroups( return Maybe.Just([newTrees, treeGroups, newNodeId - 1]); } -export function deleteTree( +export function deleteTrees( skeletonTracing: SkeletonTracing, - tree: Tree, + treeIds: number[], suppressActivatingNextNode: boolean = false, ): Maybe<[TreeMap, number | null | undefined, number | null | undefined, number]> { - // Delete tree - const newTrees = _.omit(skeletonTracing.trees, tree.treeId); + if (treeIds.length === 0) return Maybe.Nothing(); + // Delete trees + const newTrees = _.omit(skeletonTracing.trees, treeIds); let newActiveTreeId = null; let newActiveNodeId = null; if (_.size(newTrees) > 0 && !suppressActivatingNextNode) { - // Setting the tree active whose id is the next highest compared to the id of the deleted tree. - newActiveTreeId = getNearestTreeId(tree.treeId, newTrees); + // Setting the tree active whose id is the next highest compared to the ids of the deleted trees. + const maximumTreeId = _.max(treeIds) || Constants.MIN_TREE_ID; + newActiveTreeId = getNearestTreeId(maximumTreeId, newTrees); // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'. newActiveNodeId = +_.first(Array.from(newTrees[newActiveTreeId].nodes.keys())) || null; } diff --git a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts index d2c3aa976e7..287f71350dd 100644 --- a/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts +++ b/frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts @@ -468,6 +468,7 @@ export function* watchSkeletonTracingAsync(): Saga { "DELETE_BRANCHPOINT", "SELECT_NEXT_TREE", "DELETE_TREE", + "DELETE_TREES", "BATCH_UPDATE_GROUPS_AND_TREES", "CENTER_ACTIVE_NODE", ], diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx index 2085d9f101d..f373160a65f 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx @@ -53,7 +53,7 @@ import { setDropzoneModalVisibilityAction } from "oxalis/model/actions/ui_action import { setTreeNameAction, createTreeAction, - deleteTreeAction, + deleteTreesAction, deleteTreeAsUserAction, shuffleAllTreeColorsAction, selectNextTreeAction, @@ -458,11 +458,11 @@ class SkeletonTabView extends React.PureComponent { }); checkAndConfirmDeletingInitialNode(treeIdsToDelete).then(() => { // Update the store at once - const deleteTreeActions: BatchableUpdateTreeAction[] = treeIdsToDelete.map((treeId) => - deleteTreeAction(treeId), - ); this.props.onBatchUpdateGroupsAndTreesAction( - updateTreeActions.concat(deleteTreeActions, [setTreeGroupsAction(newTreeGroups)]), + updateTreeActions.concat([ + deleteTreesAction(treeIdsToDelete), + setTreeGroupsAction(newTreeGroups), + ]), ); }); }; @@ -510,8 +510,7 @@ class SkeletonTabView extends React.PureComponent { if (selectedTreeCount > 0) { const deleteAllSelectedTrees = () => { checkAndConfirmDeletingInitialNode(selectedTreeIds).then(() => { - const deleteTreeActions = selectedTreeIds.map((treeId) => deleteTreeAction(treeId)); - this.props.onBatchActions(deleteTreeActions, "DELETE_TREE"); + this.props.onDeleteTrees(selectedTreeIds); this.setState({ selectedTreeIds: [], }); @@ -1033,6 +1032,10 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(deleteTreeAsUserAction()); }, + onDeleteTrees(treeIds: number[]) { + dispatch(deleteTreesAction(treeIds)); + }, + onBatchActions(actions: Array, actionName: string) { dispatch(batchActions(actions, actionName)); }, diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx index 08039bc4897..c43db01f878 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx @@ -140,6 +140,7 @@ const createMenuForTree = (tree: Tree, props: Props, hideContextMenu: () => void onClick: () => { props.deselectAllTrees(); Store.dispatch(deleteTreeAction(tree.treeId)); + hideContextMenu(); }, title: "Delete Tree", disabled: isEditingDisabled, diff --git a/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts b/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts index 2eb6a6858b4..de936e44f9e 100644 --- a/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts +++ b/frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts @@ -888,6 +888,20 @@ test("SkeletonTracing should delete several trees", (t) => { t.deepEqual(_.size(newSkeletonTracing.trees), 0); t.not(newSkeletonTracing.trees, initialSkeletonTracing.trees); }); +test("SkeletonTracing should delete several trees at once", (t) => { + const createTreeAction = SkeletonTracingActions.createTreeAction(); + const deleteTreesAction = SkeletonTracingActions.deleteTreesAction([1, 2, 3]); + // create trees and delete them + const newState = ChainReducer(initialState) + .apply(SkeletonTracingReducer, createTreeAction) + .apply(SkeletonTracingReducer, createTreeAction) + .apply(SkeletonTracingReducer, deleteTreesAction) + .unpack(); + t.not(newState, initialState); + const newSkeletonTracing = enforceSkeletonTracing(newState.tracing); + t.deepEqual(_.size(newSkeletonTracing.trees), 0); + t.not(newSkeletonTracing.trees, initialSkeletonTracing.trees); +}); test("SkeletonTracing should set a new active tree", (t) => { const createTreeAction = SkeletonTracingActions.createTreeAction(); const setActiveTreeAction = SkeletonTracingActions.setActiveTreeAction(2);