diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 4a915a538a..7a080a50e3 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -16,6 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Added route for triggering the compute segment index worker job. [#7471](https://github.com/scalableminds/webknossos/pull/7471) ### Changed +- Improved loading speed of the annotation list. [#7410](https://github.com/scalableminds/webknossos/pull/7410) ### Fixed - Fixed several deprecation warning for using antd's Tabs.TabPane components. [#7469] diff --git a/app/controllers/AnnotationController.scala b/app/controllers/AnnotationController.scala index 505763b09a..933886f54f 100755 --- a/app/controllers/AnnotationController.scala +++ b/app/controllers/AnnotationController.scala @@ -595,31 +595,25 @@ class AnnotationController @Inject()( includeTotalCount: Option[Boolean] = None): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { - readableAnnotations <- annotationDAO.findAllListableExplorationals( + annotationInfos <- annotationDAO.findAllListableExplorationals( isFinished, + None, + AnnotationType.Explorational, limit.getOrElse(annotationService.DefaultAnnotationListLimit), pageNumber.getOrElse(0)) annotationCount <- Fox.runIf(includeTotalCount.getOrElse(false))( annotationDAO.countAllListableExplorationals(isFinished)) ?~> "annotation.countReadable.failed" - jsonList <- Fox.serialCombined(readableAnnotations)(annotationService.compactWrites) ?~> "annotation.compactWrites.failed" + annotationInfosJsons = annotationInfos.map(annotationService.writeCompactInfo) _ = userDAO.updateLastActivity(request.identity._id)(GlobalAccessContext) } yield { - val result = Ok(Json.toJson(jsonList)) + val result = Ok(Json.toJson(annotationInfosJsons)) annotationCount match { case Some(count) => result.withHeaders("X-Total-Count" -> count.toString) case None => result } } - } - @ApiOperation(hidden = true, value = "") - def sharedAnnotations: Action[AnyContent] = sil.SecuredAction.async { implicit request => - for { - userTeams <- userService.teamIdsFor(request.identity._id) - sharedAnnotations <- annotationService.sharedAnnotationsFor(userTeams) - json <- Fox.serialCombined(sharedAnnotations)(annotationService.compactWrites) - } yield Ok(Json.toJson(json)) - } + } @ApiOperation(hidden = true, value = "") def getSharedTeams(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index dad97e7ee3..49bd0c2df0 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -67,14 +67,16 @@ class UserController @Inject()(userService: UserService, includeTotalCount: Option[Boolean] = None): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { - annotations <- annotationDAO.findAllFor(request.identity._id, - isFinished, - AnnotationType.Explorational, - limit.getOrElse(annotationService.DefaultAnnotationListLimit), - pageNumber.getOrElse(0)) + annotations <- annotationDAO.findAllListableExplorationals( + isFinished, + Some(request.identity._id), + AnnotationType.Explorational, + limit.getOrElse(annotationService.DefaultAnnotationListLimit), + pageNumber.getOrElse(0) + ) annotationCount: Option[Int] <- Fox.runIf(includeTotalCount.getOrElse(false))( annotationDAO.countAllFor(request.identity._id, isFinished, AnnotationType.Explorational)) - jsonList <- Fox.serialCombined(annotations)(a => annotationService.compactWrites(a)) + jsonList = annotations.map(annotationService.writeCompactInfo) _ = userDAO.updateLastActivity(request.identity._id)(GlobalAccessContext) } yield { val result = Ok(Json.toJson(jsonList)) @@ -179,14 +181,15 @@ class UserController @Inject()(userService: UserService, userIdValidated <- ObjectId.fromString(userId) ?~> "user.id.invalid" user <- userDAO.findOne(userIdValidated) ?~> "user.notFound" ~> NOT_FOUND _ <- Fox.assertTrue(userService.isEditableBy(user, request.identity)) ?~> "notAllowed" ~> FORBIDDEN - annotations <- annotationDAO.findAllFor(userIdValidated, - isFinished, - AnnotationType.Explorational, - limit.getOrElse(annotationService.DefaultAnnotationListLimit), - pageNumber.getOrElse(0)) + annotations <- annotationDAO.findAllListableExplorationals( + isFinished, + Some(userIdValidated), + AnnotationType.Explorational, + limit.getOrElse(annotationService.DefaultAnnotationListLimit), + pageNumber.getOrElse(0)) annotationCount <- Fox.runIf(includeTotalCount.getOrElse(false))( annotationDAO.countAllFor(userIdValidated, isFinished, AnnotationType.Explorational)) - jsonList <- Fox.serialCombined(annotations)(annotationService.compactWrites) + jsonList = annotations.map(annotationService.writeCompactInfo) } yield { val result = Ok(Json.toJson(jsonList)) annotationCount match { diff --git a/app/models/annotation/Annotation.scala b/app/models/annotation/Annotation.scala index 7411944c5b..3226d033e9 100755 --- a/app/models/annotation/Annotation.scala +++ b/app/models/annotation/Annotation.scala @@ -71,6 +71,33 @@ case class Annotation( } +case class AnnotationCompactInfo(id: ObjectId, + typ: AnnotationType.Value, + name: String, + description: String, + ownerId: ObjectId, + ownerFirstName: String, + ownerLastName: String, + othersMayEdit: Boolean, + teamIds: Seq[ObjectId], + teamNames: Seq[String], + teamOrganizationIds: Seq[ObjectId], + modified: Instant, + stats: JsObject, + tags: Set[String], + state: AnnotationState.Value = Active, + dataSetName: String, + visibility: AnnotationVisibility.Value = AnnotationVisibility.Internal, + tracingTime: Option[Long] = None, + organizationName: String, + tracingIds: Seq[String], + annotationLayerNames: Seq[String], + annotationLayerTypes: Seq[String]) + +object AnnotationCompactInfo { + implicit val jsonFormat: Format[AnnotationCompactInfo] = Json.format[AnnotationCompactInfo] +} + class AnnotationLayerDAO @Inject()(SQLClient: SqlClient)(implicit ec: ExecutionContext) extends SimpleSQLDAO(SQLClient) { @@ -193,14 +220,14 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati override protected def anonymousReadAccessQ(sharingToken: Option[String]) = q"visibility = ${AnnotationVisibility.Public}" - private def listAccessQ(requestingUserId: ObjectId): SqlToken = + private def listAccessQ(requestingUserId: ObjectId, prefix: SqlToken): SqlToken = q""" ( _user = $requestingUserId or ( - (visibility = ${AnnotationVisibility.Public} or visibility = ${AnnotationVisibility.Internal}) + (${prefix}visibility = ${AnnotationVisibility.Public} or ${prefix}visibility = ${AnnotationVisibility.Internal}) and ( - _id in ( + ${prefix}_id in ( select distinct a._annotation from webknossos.annotation_sharedTeams a join webknossos.user_team_roles t @@ -208,7 +235,7 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati where t._user = $requestingUserId ) or - _id in ( + ${prefix}_id in ( select _annotation from webknossos.annotation_contributors where _user = $requestingUserId ) @@ -217,6 +244,9 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati ) """ + private def baseListAccessQ(implicit ctx: DBAccessContext): Fox[SqlToken] = + accessQueryFromAccessQWithPrefix(listAccessQ, q"")(ctx) + override protected def readAccessQ(requestingUserId: ObjectId): SqlToken = q"""( visibility = ${AnnotationVisibility.Public} @@ -268,22 +298,120 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati } yield parsed } - def findAllListableExplorationals(isFinished: Option[Boolean], limit: Int, pageNumber: Int = 0)( - implicit ctx: DBAccessContext): Fox[List[Annotation]] = { - val stateQuery = getStateQuery(isFinished) - for { - accessQuery <- accessQueryFromAccessQ(listAccessQ) - r <- run(q"""select $columns from $existingCollectionName - where typ = ${AnnotationType.Explorational} and $stateQuery and $accessQuery - order by _id desc limit $limit offset ${pageNumber * limit}""".as[AnnotationsRow]) - parsed <- parseAll(r) - } yield parsed - } + private def parseObjectIdArray(objectIdArray: String): Seq[ObjectId] = + Option(objectIdArray).map(_.split(",").map(id => ObjectId(id))).getOrElse(Array[ObjectId]()).toSeq + + def findAllListableExplorationals( + isFinished: Option[Boolean], + forUser: Option[ObjectId], + typ: AnnotationType, + limit: Int, + pageNumber: Int = 0)(implicit ctx: DBAccessContext): Fox[List[AnnotationCompactInfo]] = + for { + accessQuery <- accessQueryFromAccessQWithPrefix(listAccessQ, q"a.") + stateQuery = getStateQuery(isFinished) + userQuery = forUser.map(u => q"a._user = $u").getOrElse(q"true") + typQuery = q"a.typ = $typ" + + query = q""" + SELECT + a._id, + a.name, + a.description, + a._user, + u.firstname, + u.lastname, + a.othersmayedit, + STRING_AGG(t._id, ',') AS team_ids, + STRING_AGG(t.name, ',') AS team_names, + STRING_AGG(t._organization, ',') AS team_orgs, + a.modified, + a.statistics, + a.tags, + a.state, + d.name, + a.typ, + a.visibility, + a.tracingtime, + o.name, + STRING_AGG(al.tracingid, ',') AS tracing_ids, + STRING_AGG(al.name, ',') AS tracing_names, + STRING_AGG(al.typ :: varchar, ',') AS tracing_typs + FROM webknossos.annotations as a + LEFT JOIN webknossos.users_ u + ON u._id = a._user + LEFT JOIN webknossos.annotation_sharedteams ast + ON ast._annotation = a._id + LEFT JOIN webknossos.teams_ t + ON ast._team = t._id + LEFT JOIN webknossos.datasets_ d + ON d._id = a._dataset + LEFT JOIN webknossos.organizations_ as o + ON o._id = d._organization + LEFT JOIN webknossos.annotation_layers as al + ON al._annotation = a._id + WHERE $stateQuery AND $accessQuery AND $userQuery AND $typQuery + GROUP BY a._id, u.firstname, u.lastname, d.name, o.name + ORDER BY a._id DESC LIMIT $limit OFFSET ${pageNumber * limit} + """ + rows <- run( + query.as[ + (ObjectId, + String, + String, + ObjectId, + String, + String, + Boolean, + String, + String, + String, + Instant, + String, + String, + String, + String, + String, + String, + Long, + String, + String, + String, + String)]) + } yield + rows.toList.map( + r => { + AnnotationCompactInfo( + id = r._1, + name = r._2, + description = r._3, + ownerId = r._4, + ownerFirstName = r._5, + ownerLastName = r._6, + othersMayEdit = r._7, + teamIds = parseObjectIdArray(r._8), + teamNames = Option(r._9).map(_.split(",")).getOrElse(Array[String]()).toSeq, + teamOrganizationIds = parseObjectIdArray(r._10), + modified = r._11, + stats = Json.parse(r._12).validate[JsObject].getOrElse(Json.obj()), + tags = parseArrayLiteral(r._13).toSet, + state = AnnotationState.fromString(r._14).getOrElse(AnnotationState.Active), + dataSetName = r._15, + typ = AnnotationType.fromString(r._16).getOrElse(AnnotationType.Explorational), + visibility = AnnotationVisibility.fromString(r._17).getOrElse(AnnotationVisibility.Internal), + tracingTime = Option(r._18), + organizationName = r._19, + tracingIds = Option(r._20).map(_.split(",")).getOrElse(Array[String]()).toSeq, + annotationLayerNames = Option(r._21).map(_.split(",")).getOrElse(Array[String]()).toSeq, + annotationLayerTypes = Option(r._22).map(_.split(",")).getOrElse(Array[String]()).toSeq + ) + } + ) def countAllListableExplorationals(isFinished: Option[Boolean])(implicit ctx: DBAccessContext): Fox[Long] = { val stateQuery = getStateQuery(isFinished) for { - accessQuery <- accessQueryFromAccessQ(listAccessQ) + accessQuery <- baseListAccessQ rows <- run(q"""select count(_id) from $existingCollectionName where typ = ${AnnotationType.Explorational} and ($stateQuery) and ($accessQuery)""".as[Long]) count <- rows.headOption.toFox diff --git a/app/models/annotation/AnnotationService.scala b/app/models/annotation/AnnotationService.scala index a85204920e..0918d373ec 100755 --- a/app/models/annotation/AnnotationService.scala +++ b/app/models/annotation/AnnotationService.scala @@ -5,7 +5,6 @@ import akka.stream.Materializer import com.scalableminds.util.accesscontext.{AuthorizedAccessContext, DBAccessContext, GlobalAccessContext} import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} import com.scalableminds.util.io.{NamedStream, ZipIO} -import com.scalableminds.util.mvc.Formatter import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{BoxImplicits, Fox, FoxImplicits, TextUtils} import com.scalableminds.webknossos.datastore.SkeletonTracing._ @@ -899,7 +898,6 @@ class AnnotationService @Inject()( "task" -> taskJson, "stats" -> annotation.statistics, "restrictions" -> restrictionsJs, - "formattedHash" -> Formatter.formatHash(annotation._id.toString), "annotationLayers" -> Json.toJson(annotation.annotationLayers), "dataSetName" -> dataSet.name, "organization" -> organization.name, @@ -966,39 +964,58 @@ class AnnotationService @Inject()( } //for Explorative Annotations list - def compactWrites(annotation: Annotation): Fox[JsObject] = { - implicit val ctx: DBAccessContext = GlobalAccessContext - for { - dataSet <- datasetDAO.findOne(annotation._dataSet) ?~> "dataset.notFoundForAnnotation" - organization <- organizationDAO.findOne(dataSet._organization) ?~> "organization.notFound" - teams <- teamDAO.findSharedTeamsForAnnotation(annotation._id) ?~> s"fetching sharedTeams for annotation ${annotation._id} failed" - teamsJson <- Fox.serialCombined(teams)(teamService.publicWrites(_, Some(organization))) ?~> s"serializing sharedTeams for annotation ${annotation._id} failed" - user <- userDAO.findOne(annotation._user) ?~> s"fetching owner info for annotation ${annotation._id} failed" - userJson = Json.obj( - "id" -> user._id.toString, - "firstName" -> user.firstName, - "lastName" -> user.lastName - ) - } yield { - Json.obj( - "modified" -> annotation.modified, - "state" -> annotation.state, - "id" -> annotation._id.toString, - "name" -> annotation.name, - "description" -> annotation.description, - "typ" -> annotation.typ, - "stats" -> annotation.statistics, - "formattedHash" -> Formatter.formatHash(annotation._id.toString), - "annotationLayers" -> annotation.annotationLayers, - "dataSetName" -> dataSet.name, - "organization" -> organization.name, - "visibility" -> annotation.visibility, - "tracingTime" -> annotation.tracingTime, - "teams" -> teamsJson, - "tags" -> (annotation.tags ++ Set(dataSet.name, annotation.tracingType.toString)), - "owner" -> userJson, - "othersMayEdit" -> annotation.othersMayEdit + + def writeCompactInfo(annotationInfo: AnnotationCompactInfo): JsObject = { + val teamsJson = annotationInfo.teamNames.indices.map( + idx => + Json.obj( + "id" -> annotationInfo.teamIds(idx), + "name" -> annotationInfo.teamNames(idx), + "organizationId" -> annotationInfo.teamOrganizationIds(idx) + )) + + val annotationLayerJson = annotationInfo.tracingIds.indices.map( + idx => + Json.obj( + "tracingId" -> annotationInfo.tracingIds(idx), + "typ" -> annotationInfo.annotationLayerTypes(idx), + "name" -> annotationInfo.annotationLayerNames(idx) ) + ) + val tracingType: String = getAnnotationTypeForTag(annotationInfo) + Json.obj( + "modified" -> annotationInfo.modified, + "state" -> annotationInfo.state, + "id" -> annotationInfo.id, + "name" -> annotationInfo.name, + "description" -> annotationInfo.description, + "typ" -> annotationInfo.typ, + "stats" -> annotationInfo.stats, + "annotationLayers" -> annotationLayerJson, + "dataSetName" -> annotationInfo.dataSetName, + "organization" -> annotationInfo.organizationName, + "visibility" -> annotationInfo.visibility, + "tracingTime" -> annotationInfo.tracingTime, + "teams" -> teamsJson, + "tags" -> (annotationInfo.tags ++ Set(annotationInfo.dataSetName, tracingType)), + "owner" -> Json.obj( + "id" -> annotationInfo.ownerId.toString, + "firstName" -> annotationInfo.ownerFirstName, + "lastName" -> annotationInfo.ownerLastName + ), + "othersMayEdit" -> annotationInfo.othersMayEdit, + ) + } + + private def getAnnotationTypeForTag(annotationInfo: AnnotationCompactInfo): String = { + val skeletonPresent = annotationInfo.annotationLayerTypes.contains(AnnotationLayerType.Skeleton.toString) + val volumePresent = annotationInfo.annotationLayerTypes.contains(AnnotationLayerType.Volume.toString) + if (skeletonPresent && volumePresent) { + "hybrid" + } else if (skeletonPresent) { + "skeleton" + } else { + "volume" } } } diff --git a/app/models/task/TaskService.scala b/app/models/task/TaskService.scala index 0f85133335..9ecdc3b5f8 100644 --- a/app/models/task/TaskService.scala +++ b/app/models/task/TaskService.scala @@ -1,7 +1,6 @@ package models.task import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} -import com.scalableminds.util.mvc.Formatter import com.scalableminds.util.tools.{Fox, FoxImplicits} import javax.inject.Inject import models.annotation.{Annotation, AnnotationDAO, AnnotationType} @@ -41,7 +40,6 @@ class TaskService @Inject()(conf: WkConf, } yield { Json.obj( "id" -> task._id.toString, - "formattedHash" -> Formatter.formatHash(task._id.toString), "projectId" -> project._id.id, "projectName" -> project.name, "team" -> team.name, diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index 7e9df7dd33..b7ffa40ba5 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -167,7 +167,6 @@ GET /annotations/:typ/:id/loggedTime GET /annotations/source/:accessTokenOrId controllers.AnnotationPrivateLinkController.annotationSource(accessTokenOrId: String, userToken: Option[String]) -GET /annotations/shared controllers.AnnotationController.sharedAnnotations() GET /annotations/readable controllers.AnnotationController.listExplorationals(isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean]) GET /annotations/:typ/:id/sharedTeams controllers.AnnotationController.getSharedTeams(typ: String, id: String) PATCH /annotations/:typ/:id/sharedTeams controllers.AnnotationController.updateSharedTeams(typ: String, id: String) diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts index 4aadc172d7..abac7c54ca 100644 --- a/frontend/javascripts/admin/admin_rest_api.ts +++ b/frontend/javascripts/admin/admin_rest_api.ts @@ -4,7 +4,7 @@ import dayjs from "dayjs"; import type { APIActiveUser, APIAnnotation, - APIAnnotationCompact, + APIAnnotationInfo, APIAnnotationType, APIAnnotationVisibility, APIAnnotationWithTask, @@ -552,10 +552,10 @@ export function deletePrivateLink(linkId: string): Promise<{ } // ### Annotations -export function getCompactAnnotations( +export function getAnnotationInfos( isFinished: boolean, pageNumber: number = 0, -): Promise> { +): Promise> { return Request.receiveJSON( `/api/user/annotations?isFinished=${isFinished.toString()}&pageNumber=${pageNumber}`, ); @@ -565,20 +565,16 @@ export function getCompactAnnotationsForUser( userId: string, isFinished: boolean, pageNumber: number = 0, -): Promise> { +): Promise> { return Request.receiveJSON( `/api/users/${userId}/annotations?isFinished=${isFinished.toString()}&pageNumber=${pageNumber}`, ); } -export function getSharedAnnotations(): Promise> { - return Request.receiveJSON("/api/annotations/shared"); -} - export function getReadableAnnotations( isFinished: boolean, pageNumber: number = 0, -): Promise> { +): Promise> { return Request.receiveJSON( `/api/annotations/readable?isFinished=${isFinished.toString()}&pageNumber=${pageNumber}`, ); diff --git a/frontend/javascripts/dashboard/explorative_annotations_view.tsx b/frontend/javascripts/dashboard/explorative_annotations_view.tsx index 29be896230..4198a5704e 100644 --- a/frontend/javascripts/dashboard/explorative_annotations_view.tsx +++ b/frontend/javascripts/dashboard/explorative_annotations_view.tsx @@ -19,7 +19,7 @@ import update from "immutability-helper"; import { AsyncLink } from "components/async_clickables"; import { annotationToCompact, - APIAnnotationCompact, + APIAnnotationInfo, APIUser, APIUserCompact, } from "types/api_flow_types"; @@ -55,11 +55,11 @@ import { SearchProps } from "antd/lib/input"; const { Column } = Table; const { Search } = Input; -const typeHint: APIAnnotationCompact[] = []; +const typeHint: APIAnnotationInfo[] = []; const pageLength: number = 1000; type TracingModeState = { - tracings: Array; + tracings: Array; lastLoadedPage: number; loadedAllTracings: boolean; }; @@ -119,7 +119,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { // Other than that, the value should not be changed. It can be used to // retrieve the items of the currently rendered page (while respecting // the active search and filters). - currentPageData: Readonly = []; + currentPageData: Readonly = []; componentDidMount() { this.setState(persistence.load() as PartialState, () => { @@ -214,7 +214,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { ); }; - finishOrReopenAnnotation = async (type: "finish" | "reopen", tracing: APIAnnotationCompact) => { + finishOrReopenAnnotation = async (type: "finish" | "reopen", tracing: APIAnnotationInfo) => { const shouldFinish = type === "finish"; const newTracing = annotationToCompact( shouldFinish @@ -252,19 +252,18 @@ class ExplorativeAnnotationsView extends React.PureComponent { }; _updateAnnotationWithArchiveAction = ( - annotation: APIAnnotationCompact, + annotation: APIAnnotationInfo, type: "finish" | "reopen", - ): APIAnnotationCompact => ({ + ): APIAnnotationInfo => ({ ...annotation, state: type === "reopen" ? "Active" : "Finished", }); - renderActions = (tracing: APIAnnotationCompact) => { + renderActions = (tracing: APIAnnotationInfo) => { if (tracing.typ !== "Explorational") { return null; } - const hasVolumeTracing = getVolumeDescriptors(tracing).length > 0; const { typ, id, state } = tracing; if (state === "Active") { @@ -277,7 +276,10 @@ class ExplorativeAnnotationsView extends React.PureComponent {
downloadAnnotation(id, typ, hasVolumeTracing)} + onClick={() => { + const hasVolumeTracing = getVolumeDescriptors(tracing).length > 0; + return downloadAnnotation(id, typ, hasVolumeTracing); + }} icon={} > Download @@ -311,7 +313,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { } }; - getCurrentTracings(): Array { + getCurrentTracings(): Array { return this.getCurrentModeState().tracings; } @@ -321,7 +323,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { }); }; - renameTracing(tracing: APIAnnotationCompact, name: string) { + renameTracing(tracing: APIAnnotationInfo, name: string) { const tracings = this.getCurrentTracings(); const newTracings = tracings.map((currentTracing) => { if (currentTracing.id !== tracing.id) { @@ -349,7 +351,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { archiveAll = () => { const selectedAnnotations = this.currentPageData.filter( - (annotation: APIAnnotationCompact) => annotation.owner?.id === this.props.activeUser.id, + (annotation: APIAnnotationInfo) => annotation.owner?.id === this.props.activeUser.id, ); if (selectedAnnotations.length === 0) { @@ -392,7 +394,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { }; editTagFromAnnotation = ( - annotation: APIAnnotationCompact, + annotation: APIAnnotationInfo, shouldAddTag: boolean, tag: string, event: React.SyntheticEvent, @@ -494,7 +496,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { ); } - renderIdAndCopyButton(tracing: APIAnnotationCompact) { + renderIdAndCopyButton(tracing: APIAnnotationInfo) { const copyIdToClipboard = async () => { await navigator.clipboard.writeText(tracing.id); Toast.success("ID copied to clipboard"); @@ -518,7 +520,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { ); } - renderNameWithDescription(tracing: APIAnnotationCompact) { + renderNameWithDescription(tracing: APIAnnotationInfo) { return (
{ ); } - isTracingEditable(tracing: APIAnnotationCompact): boolean { + isTracingEditable(tracing: APIAnnotationInfo): boolean { return tracing.owner?.id === this.props.activeUser.id || tracing.othersMayEdit; } @@ -614,7 +616,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { title="ID" dataIndex="id" width={100} - render={(__, tracing: APIAnnotationCompact) => ( + render={(__, tracing: APIAnnotationInfo) => ( <>
{this.renderIdAndCopyButton(tracing)}
@@ -633,7 +635,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { width={280} dataIndex="name" sorter={Utils.localeCompareBy(typeHint, (annotation) => annotation.name)} - render={(_name: string, tracing: APIAnnotationCompact) => + render={(_name: string, tracing: APIAnnotationInfo) => this.renderNameWithDescription(tracing) } /> @@ -643,7 +645,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { width={300} filters={ownerAndTeamsFilters} filterMode="tree" - onFilter={(value: string | number | boolean, tracing: APIAnnotationCompact) => + onFilter={(value: string | number | boolean, tracing: APIAnnotationInfo) => (tracing.owner != null && tracing.owner.id === value.toString()) || tracing.teams.some((team) => team.id === value) } @@ -651,7 +653,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { typeHint, (annotation) => annotation.owner?.firstName || "", )} - render={(owner: APIUser | null, tracing: APIAnnotationCompact) => { + render={(owner: APIUser | null, tracing: APIAnnotationInfo) => { const ownerName = owner != null ? renderOwner(owner) : null; const teamTags = tracing.teams.map((t) => ( @@ -678,7 +680,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { + render={(__, annotation: APIAnnotationInfo) => "treeCount" in annotation.stats && "nodeCount" in annotation.stats && "edgeCount" in annotation.stats ? ( @@ -722,7 +724,7 @@ class ExplorativeAnnotationsView extends React.PureComponent { , annotation: APIAnnotationCompact) => ( + render={(tags: Array, annotation: APIAnnotationInfo) => (
{tags.map((tag) => ( { title="Actions" className="nowrap" key="action" - render={(__, tracing: APIAnnotationCompact) => this.renderActions(tracing)} + render={(__, tracing: APIAnnotationInfo) => this.renderActions(tracing)} /> ); diff --git a/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.ts b/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.ts index e6f816f015..7f6fcae64e 100644 --- a/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.ts @@ -1,7 +1,7 @@ import memoizeOne from "memoize-one"; import type { APIAnnotation, - APIAnnotationCompact, + APIAnnotationInfo, APIDataset, APISegmentationLayer, AnnotationLayerDescriptor, @@ -76,13 +76,13 @@ export function hasVolumeTracings(tracing: Tracing): boolean { } export function getVolumeDescriptors( - annotation: APIAnnotation | APIAnnotationCompact | HybridTracing, + annotation: APIAnnotation | HybridTracing | APIAnnotationInfo, ): Array { return annotation.annotationLayers.filter((layer) => layer.typ === "Volume"); } export function getVolumeDescriptorById( - annotation: APIAnnotation | APIAnnotationCompact | HybridTracing, + annotation: APIAnnotation | HybridTracing, tracingId: string, ): AnnotationLayerDescriptor { const descriptors = getVolumeDescriptors(annotation).filter( @@ -97,7 +97,7 @@ export function getVolumeDescriptorById( } export function getReadableNameByVolumeTracingId( - annotation: APIAnnotation | APIAnnotationCompact | HybridTracing, + annotation: APIAnnotation | HybridTracing, tracingId: string, ) { const volumeDescriptor = getVolumeDescriptorById(annotation, tracingId); diff --git a/frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts b/frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts index 8c7f37df09..115a208b90 100644 --- a/frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts +++ b/frontend/javascripts/test/fixtures/skeletontracing_server_objects.ts @@ -172,7 +172,6 @@ export const annotation: APIAnnotation = { allowDownload: true, allowSave: true, }, - formattedHash: "f043e7", annotationLayers: [ { name: "Skeleton", diff --git a/frontend/javascripts/test/fixtures/tasktracing_server_objects.ts b/frontend/javascripts/test/fixtures/tasktracing_server_objects.ts index a6cf3dad2c..649d481414 100644 --- a/frontend/javascripts/test/fixtures/tasktracing_server_objects.ts +++ b/frontend/javascripts/test/fixtures/tasktracing_server_objects.ts @@ -70,7 +70,6 @@ export const annotation: APIAnnotation = { typ: "Task", task: { id: "5b1fd1cb97000027049c67ec", - formattedHash: "9c67ec", projectName: "sampleProject", projectId: "dummy-project-id", team: "Connectomics department", @@ -116,7 +115,6 @@ export const annotation: APIAnnotation = { allowFinish: true, allowDownload: true, }, - formattedHash: "9c67ee", annotationLayers: [ { name: "Skeleton", diff --git a/frontend/javascripts/test/fixtures/volumetracing_server_objects.ts b/frontend/javascripts/test/fixtures/volumetracing_server_objects.ts index 15b02a9442..867b2b4ec9 100644 --- a/frontend/javascripts/test/fixtures/volumetracing_server_objects.ts +++ b/frontend/javascripts/test/fixtures/volumetracing_server_objects.ts @@ -83,7 +83,6 @@ export const annotation: APIAnnotation = { allowFinish: true, allowDownload: true, }, - formattedHash: "f043e7", annotationLayers: [ { name: "volume", diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.md b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.md index 093ed9bf7b..3f3e852934 100644 --- a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.md @@ -23,7 +23,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: '56fe9b', id: '570ba0092a7c0e980056fe9b', modified: 0, name: '', @@ -141,7 +140,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: '81c05d', id: '88135c192faeb34c0081c05d', modified: 0, name: '', @@ -259,7 +257,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: 'formattedHash', id: 'id', messages: [ { @@ -335,7 +332,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: 'formattedHash', id: 'id', neededExperience: { domain: 'abc', @@ -429,7 +425,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: 'formattedHash', id: 'id', messages: [ { @@ -505,7 +500,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: 'formattedHash', id: 'id', neededExperience: { domain: 'abc', @@ -599,7 +593,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: 'formattedHash', id: 'id', messages: [ { @@ -722,7 +715,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: 'formattedHash', id: 'id', messages: [ { @@ -845,7 +837,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: 'new description', - formattedHash: 'formattedHash', id: 'id', modified: 'modified', name: 'new name', @@ -963,7 +954,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: 'formattedHash', id: 'id', modified: 'modified', name: '', diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.snap b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.snap index 1484795a14..89c8bd419b 100644 Binary files a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.snap and b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/annotations.e2e.js.snap differ diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md index 7f937a048e..5ebe15362e 100644 --- a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md @@ -23,7 +23,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '8a5352', id: '581367a82faeb37a008a5352', neededExperience: { domain: 'abc', @@ -77,7 +76,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '8a5354', id: '581367a82faeb37a008a5354', neededExperience: { domain: 'abc', @@ -131,7 +129,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '81c058', id: '58135c192faeb34c0081c058', neededExperience: { domain: 'abc', @@ -190,7 +187,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '81c058', id: '58135c192faeb34c0081c058', neededExperience: { domain: 'abc', @@ -244,7 +240,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '8a5352', id: '581367a82faeb37a008a5352', neededExperience: { domain: 'abc', @@ -301,7 +296,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '81c058', id: '58135c192faeb34c0081c058', neededExperience: { domain: 'abc', @@ -358,7 +352,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '81c058', id: '58135c192faeb34c0081c058', neededExperience: { domain: 'abc', @@ -417,7 +410,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: '81c068', id: '58135c402faeb34e0081c068', modified: 0, name: '', @@ -488,7 +480,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '8a5352', id: '581367a82faeb37a008a5352', neededExperience: { domain: 'abc', @@ -582,7 +573,6 @@ Generated by [AVA](https://avajs.dev). 0, 0, ], - formattedHash: '81c058', id: '58135c192faeb34c0081c058', messages: [ { @@ -643,7 +633,6 @@ Generated by [AVA](https://avajs.dev). 5, 6, ], - formattedHash: 'formattedHash', id: 'id', neededExperience: { domain: 'abc', @@ -701,7 +690,6 @@ Generated by [AVA](https://avajs.dev). url: 'http://localhost:9000', }, description: '', - formattedHash: 'formattedHash', id: 'id', modified: 'modified', name: '', @@ -772,7 +760,6 @@ Generated by [AVA](https://avajs.dev). 5, 6, ], - formattedHash: 'formattedHash', id: 'id', neededExperience: { domain: 'abc', diff --git a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.snap b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.snap index c221fbff42..74e6407d8b 100644 Binary files a/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.snap and b/frontend/javascripts/test/snapshots/public-test/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.snap differ diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts index 4070f6781b..06a15afdf7 100644 --- a/frontend/javascripts/types/api_flow_types.ts +++ b/frontend/javascripts/types/api_flow_types.ts @@ -397,7 +397,6 @@ export type APITask = { readonly dataSet: string; readonly editPosition: Vector3; readonly editRotation: Vector3; - readonly formattedHash: string; readonly id: string; readonly neededExperience: { readonly domain: string; @@ -420,20 +419,17 @@ export type AnnotationLayerDescriptor = { export type EditableLayerProperties = Partial<{ name: string | null | undefined; }>; -export type APIAnnotationCompact = { +export type APIAnnotationInfo = { readonly annotationLayers: Array; readonly dataSetName: string; readonly organization: string; readonly description: string; - readonly formattedHash: string; readonly modified: number; readonly id: string; - readonly visibility: APIAnnotationVisibility; readonly name: string; readonly state: string; readonly stats: SkeletonTracingStats | {}; readonly tags: Array; - readonly tracingTime: number | null | undefined; readonly typ: APIAnnotationType; // The owner can be null (e.g., for a sandbox annotation // or due to missing permissions). @@ -442,25 +438,22 @@ export type APIAnnotationCompact = { readonly othersMayEdit: boolean; }; -export function annotationToCompact(annotation: APIAnnotation): APIAnnotationCompact { +export function annotationToCompact(annotation: APIAnnotation): APIAnnotationInfo { const { - annotationLayers, dataSetName, - organization, description, - formattedHash, modified, id, - visibility, name, - state, stats, + state, tags, - tracingTime, typ, owner, teams, othersMayEdit, + organization, + annotationLayers, } = annotation; return { @@ -468,15 +461,12 @@ export function annotationToCompact(annotation: APIAnnotation): APIAnnotationCom dataSetName, organization, description, - formattedHash, modified, id, - visibility, name, state, stats, tags, - tracingTime, typ, owner, teams, @@ -492,7 +482,10 @@ export type AnnotationViewConfiguration = { } >; }; -type APIAnnotationBase = APIAnnotationCompact & { +type APIAnnotationBase = APIAnnotationInfo & { + readonly visibility: APIAnnotationVisibility; + readonly tracingTime: number | null | undefined; + readonly dataStore: APIDataStore; readonly tracingStore: APITracingStore; readonly restrictions: APIRestrictions;