diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml new file mode 100644 index 00000000000..599e2b22e00 --- /dev/null +++ b/.github/workflows/build_test_deploy.yml @@ -0,0 +1,13 @@ +name: CI Pipeline + +on: + workflow_dispatch: + +jobs: + foo: + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 5 \ No newline at end of file diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 516413a4895..4e0cb15ae5d 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -16,6 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - 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) @@ -24,14 +25,18 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - 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) - Fixed that the skeleton search did not automatically expand groups that contained the selected tree [#8129](https://github.com/scalableminds/webknossos/pull/8129) - 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 diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md index 0e50f676c9e..f6d640f469d 100644 --- a/MIGRATIONS.unreleased.md +++ b/MIGRATIONS.unreleased.md @@ -12,3 +12,4 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md). - [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) diff --git a/app/controllers/AiModelController.scala b/app/controllers/AiModelController.scala index 3a332504239..e09d8a4f534 100644 --- a/app/controllers/AiModelController.scala +++ b/app/controllers/AiModelController.scala @@ -57,6 +57,16 @@ object UpdateAiModelParameters { implicit val jsonFormat: OFormat[UpdateAiModelParameters] = Json.format[UpdateAiModelParameters] } +case class RegisterAiModelParameters(id: ObjectId, // must be a valid MongoDB ObjectId + dataStoreName: String, + name: String, + comment: Option[String], + category: Option[AiModelCategory]) + +object RegisterAiModelParameters { + implicit val jsonFormat: OFormat[RegisterAiModelParameters] = Json.format[RegisterAiModelParameters] +} + class AiModelController @Inject()( aiModelDAO: AiModelDAO, aiModelService: AiModelService, @@ -209,6 +219,28 @@ class AiModelController @Inject()( } yield Ok(jsResult) } + def registerAiModel: Action[RegisterAiModelParameters] = + sil.SecuredAction.async(validateJson[RegisterAiModelParameters]) { implicit request => + for { + _ <- userService.assertIsSuperUser(request.identity) + _ <- dataStoreDAO.findOneByName(request.body.dataStoreName) ?~> "dataStore.notFound" + _ <- aiModelDAO.findOne(request.body.id).reverse ?~> "aiModel.id.taken" + _ <- aiModelDAO.findOneByName(request.body.name).reverse ?~> "aiModel.name.taken" + _ <- aiModelDAO.insertOne( + AiModel( + request.body.id, + _organization = request.identity._organization, + request.body.dataStoreName, + request.identity._id, + None, + List.empty, + request.body.name, + request.body.comment, + request.body.category + )) + } yield Ok + } + def deleteAiModel(aiModelId: String): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { diff --git a/app/controllers/AnnotationIOController.scala b/app/controllers/AnnotationIOController.scala index 3fffaa23121..8f183d84494 100755 --- a/app/controllers/AnnotationIOController.scala +++ b/app/controllers/AnnotationIOController.scala @@ -457,7 +457,7 @@ class AnnotationIOController @Inject()( tracingStoreClient.getSkeletonTracing(skeletonAnnotationLayer, skeletonVersion) } ?~> "annotation.download.fetchSkeletonLayer.failed" user <- userService.findOneCached(annotation._user)(GlobalAccessContext) ?~> "annotation.download.findUser.failed" - taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne) + taskOpt <- Fox.runOptional(annotation._task)(taskDAO.findOne(_)(GlobalAccessContext)) ?~> "task.notFound" nmlStream = nmlWriter.toNmlStream( name, fetchedSkeletonLayers ::: fetchedVolumeLayers, diff --git a/app/models/aimodels/AiModel.scala b/app/models/aimodels/AiModel.scala index 053913b90e3..5857f85e63d 100644 --- a/app/models/aimodels/AiModel.scala +++ b/app/models/aimodels/AiModel.scala @@ -144,4 +144,11 @@ class AiModelDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext) q"UPDATE webknossos.aiModels SET name = ${a.name}, comment = ${a.comment}, modified = ${a.modified} WHERE _id = ${a._id}".asUpdate) } yield () + def findOneByName(name: String)(implicit ctx: DBAccessContext): Fox[AiModel] = + for { + accessQuery <- readAccessQuery + r <- run(q"SELECT $columns FROM $existingCollectionName WHERE name = $name AND $accessQuery".as[AimodelsRow]) + parsed <- parseFirst(r, name) + } yield parsed + } diff --git a/app/models/aimodels/AiModelCategory.scala b/app/models/aimodels/AiModelCategory.scala index 70f556a09b8..8f1ab9f861d 100644 --- a/app/models/aimodels/AiModelCategory.scala +++ b/app/models/aimodels/AiModelCategory.scala @@ -4,5 +4,5 @@ import com.scalableminds.util.enumeration.ExtendedEnumeration object AiModelCategory extends ExtendedEnumeration { type AiModelCategory = Value - val em_neurons, em_nuclei = Value + val em_neurons, em_nuclei, em_synapses, em_neuron_types, em_cell_organelles = Value } diff --git a/app/models/dataset/ThumbnailService.scala b/app/models/dataset/ThumbnailService.scala index 88e8385c0da..ff4e4e5ecc3 100644 --- a/app/models/dataset/ThumbnailService.scala +++ b/app/models/dataset/ThumbnailService.scala @@ -14,7 +14,7 @@ import models.configuration.DatasetConfigurationService import net.liftweb.common.Full import play.api.http.Status.NOT_FOUND import play.api.i18n.{Messages, MessagesProvider} -import play.api.libs.json.JsArray +import play.api.libs.json.{JsArray, JsObject} import utils.ObjectId import utils.sql.{SimpleSQLDAO, SqlClient} @@ -74,39 +74,41 @@ class ThumbnailService @Inject()(datasetService: DatasetService, viewConfiguration <- datasetConfigurationService.getDatasetViewConfigurationForDataset(List.empty, datasetName, organizationId)(ctx) - (mag1BoundingBox, mag, intensityRangeOpt, colorSettingsOpt) = selectParameters(viewConfiguration, - usableDataSource, - layerName, - layer, - width, - height) + (mag1BoundingBox, mag, intensityRangeOpt, colorSettingsOpt, mapping) = selectParameters(viewConfiguration, + usableDataSource, + layerName, + layer, + width, + height, + mappingName) client <- datasetService.clientFor(dataset) image <- client.getDataLayerThumbnail(organizationId, dataset, layerName, mag1BoundingBox, mag, - mappingName, + mapping, intensityRangeOpt, colorSettingsOpt) _ <- thumbnailDAO.upsertThumbnail(dataset._id, layerName, width, height, - mappingName, + mapping, image, jpegMimeType, mag, mag1BoundingBox) } yield image - private def selectParameters( - viewConfiguration: DatasetViewConfiguration, - usableDataSource: GenericDataSource[DataLayerLike], - layerName: String, - layer: DataLayerLike, - targetMagWidth: Int, - targetMagHeigt: Int): (BoundingBox, Vec3Int, Option[(Double, Double)], Option[ThumbnailColorSettings]) = { + private def selectParameters(viewConfiguration: DatasetViewConfiguration, + usableDataSource: GenericDataSource[DataLayerLike], + layerName: String, + layer: DataLayerLike, + targetMagWidth: Int, + targetMagHeigt: Int, + mappingName: Option[String]) + : (BoundingBox, Vec3Int, Option[(Double, Double)], Option[ThumbnailColorSettings], Option[String]) = { val configuredCenterOpt = viewConfiguration.get("position").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Vec3Int])) val centerOpt = @@ -124,7 +126,13 @@ class ThumbnailService @Inject()(datasetService: DatasetService, val x = center.x - mag1Width / 2 val y = center.y - mag1Height / 2 val z = center.z - (BoundingBox(Vec3Int(x, y, z), mag1Width, mag1Height, 1), mag, intensityRangeOpt, colorSettingsOpt) + + val mappingNameResult = mappingName.orElse(readMappingName(viewConfiguration, layerName)) + (BoundingBox(Vec3Int(x, y, z), mag1Width, mag1Height, 1), + mag, + intensityRangeOpt, + colorSettingsOpt, + mappingNameResult) } private def readIntensityRange(viewConfiguration: DatasetViewConfiguration, @@ -147,6 +155,13 @@ class ThumbnailService @Inject()(datasetService: DatasetService, b <- colorArray(2).validate[Int].asOpt } yield ThumbnailColorSettings(Color(r / 255d, g / 255d, b / 255d, 0), isInverted) + private def readMappingName(viewConfiguration: DatasetViewConfiguration, layerName: String): Option[String] = + for { + layersJsValue <- viewConfiguration.get("layers") + mapping <- (layersJsValue \ layerName \ "mapping").validate[JsObject].asOpt + mappingName <- mapping("name").validate[String].asOpt + } yield mappingName + private def magForZoom(dataLayer: DataLayerLike, zoom: Double): Vec3Int = dataLayer.resolutions.minBy(r => Math.abs(r.maxDim - zoom)) diff --git a/app/utils/sql/SQLDAO.scala b/app/utils/sql/SQLDAO.scala index 8ef7548d1ef..2cf9d7fe40a 100644 --- a/app/utils/sql/SQLDAO.scala +++ b/app/utils/sql/SQLDAO.scala @@ -47,7 +47,7 @@ abstract class SQLDAO[C, R, X <: AbstractTable[R]] @Inject()(sqlClient: SqlClien case Some(r) => parse(r) ?~> ("sql: could not parse database row for object" + id) case _ => - Fox.failure("sql: could not find object " + id) + Fox.empty }.flatten @nowarn // suppress warning about unused implicit ctx, as it is used in subclasses diff --git a/app/views/mail/jobFailedUploadConvert.scala.html b/app/views/mail/jobFailedUploadConvert.scala.html index 80c3d557e33..4c459fc303d 100644 --- a/app/views/mail/jobFailedUploadConvert.scala.html +++ b/app/views/mail/jobFailedUploadConvert.scala.html @@ -11,9 +11,9 @@
Here are some tips for uploading and converting datasets:
- Do you want to make corrections to the automated segmentation? Use the easy-to-use, built-in proof-reading tools in WEBKNOSSOS (requires Power plan). + Do you want to make corrections to the automated segmentation? Use the easy-to-use, built-in proof-reading tools in WEBKNOSSOS (requires Power plan).
An Auth Token is a series of symbols that serves to authenticate you. It is used in - communication with the backend API and sent with every request to verify your identity. + communication with the Python API and sent with every request to verify your identity.
You should revoke it if somebody else has acquired your token or you have the suspicion this has happened.{" "} - - Read more - + Read more
diff --git a/frontend/javascripts/admin/dataset/dataset_add_view.tsx b/frontend/javascripts/admin/dataset/dataset_add_view.tsx index c1513f1f7c7..c7677831751 100644 --- a/frontend/javascripts/admin/dataset/dataset_add_view.tsx +++ b/frontend/javascripts/admin/dataset/dataset_add_view.tsx @@ -183,7 +183,7 @@ const alignBanner = ( />
diff --git a/frontend/javascripts/admin/dataset/dataset_upload_view.tsx b/frontend/javascripts/admin/dataset/dataset_upload_view.tsx
index b4f235e9110..653d2ff1249 100644
--- a/frontend/javascripts/admin/dataset/dataset_upload_view.tsx
+++ b/frontend/javascripts/admin/dataset/dataset_upload_view.tsx
@@ -1243,7 +1243,7 @@ function FileUploadArea({
To learn more about the task system in WEBKNOSSOS,{" "}
diff --git a/frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx b/frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx
index ebd803d0dd2..33adc70da4f 100644
--- a/frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx
+++ b/frontend/javascripts/admin/user/permissions_and_teams_modal_view.tsx
@@ -243,7 +243,7 @@ function PermissionsAndTeamsModalView({
WEBKNOSSOS supports a variety of (remote){" "}
diff --git a/frontend/javascripts/libs/request.ts b/frontend/javascripts/libs/request.ts
index 1b1271e4846..25bf31657e5 100644
--- a/frontend/javascripts/libs/request.ts
+++ b/frontend/javascripts/libs/request.ts
@@ -311,7 +311,11 @@ class Request {
...message,
key: json.status.toString(),
}));
- if (showErrorToast) Toast.messages(messages);
+ if (showErrorToast) {
+ Toast.messages(messages); // Note: Toast.error internally logs to console
+ } else {
+ console.error(messages);
+ }
// Check whether the error chain mentions an url which belongs
// to a datastore. Then, ping the datastore
pingMentionedDataStores(text);
@@ -319,7 +323,11 @@ class Request {
/* eslint-disable-next-line prefer-promise-reject-errors */
return Promise.reject({ ...json, url: requestedUrl });
} catch (_jsonError) {
- if (showErrorToast) Toast.error(text);
+ if (showErrorToast) {
+ Toast.error(text); // Note: Toast.error internally logs to console
+ } else {
+ console.error(`Request failed for ${requestedUrl}:`, text);
+ }
/* eslint-disable-next-line prefer-promise-reject-errors */
return Promise.reject({
diff --git a/frontend/javascripts/oxalis/model/sagas/save_saga.ts b/frontend/javascripts/oxalis/model/sagas/save_saga.ts
index e9e09a12a32..d2acc8ca949 100644
--- a/frontend/javascripts/oxalis/model/sagas/save_saga.ts
+++ b/frontend/javascripts/oxalis/model/sagas/save_saga.ts
@@ -196,6 +196,9 @@ export function* sendRequestToServer(
method: "POST",
data: compactedSaveQueue,
compress: process.env.NODE_ENV === "production",
+ // Suppressing error toast, as the doWithToken retry with personal token functionality should not show an error.
+ // Instead the error is logged and toggleErrorHighlighting should take care of showing an error to the user.
+ showErrorToast: false,
},
);
const endTime = Date.now();
diff --git a/frontend/javascripts/oxalis/view/action-bar/default-predict-workflow-template.ts b/frontend/javascripts/oxalis/view/action-bar/default-predict-workflow-template.ts
index fdfcc186963..ffaffb19b0c 100644
--- a/frontend/javascripts/oxalis/view/action-bar/default-predict-workflow-template.ts
+++ b/frontend/javascripts/oxalis/view/action-bar/default-predict-workflow-template.ts
@@ -1,8 +1,9 @@
export default `predict:
task: PredictTask
distribution:
- default:
- processes: 2
+ step:
+ strategy: sequential
+ num_io_threads: 5
inputs:
model: TO_BE_SET_BY_WORKER
config:
@@ -19,6 +20,6 @@ publish_dataset_meshes:
config:
name: TO_BE_SET_BY_WORKER
public_directory: TO_BE_SET_BY_WORKER
- webknossos_organization: TO_BE_SET_BY_WORKER
use_symlinks: False
- move_dataset_symlink_artifact: True`;
+ move_dataset_symlink_artifact: True
+ keep_symlinks_to: TO_BE_SET_BY_WORKER`;
diff --git a/frontend/javascripts/oxalis/view/action-bar/download_modal_view.tsx b/frontend/javascripts/oxalis/view/action-bar/download_modal_view.tsx
index 9be46f578f4..a0ef1d37e31 100644
--- a/frontend/javascripts/oxalis/view/action-bar/download_modal_view.tsx
+++ b/frontend/javascripts/oxalis/view/action-bar/download_modal_view.tsx
@@ -433,7 +433,7 @@ function _DownloadModalView({
>
For more information on how to work with {typeDependentFileName} visit the{" "}
diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/advanced_search_popover.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/advanced_search_popover.tsx
index 76bcb9e0399..54852103229 100644
--- a/frontend/javascripts/oxalis/view/right-border-tabs/advanced_search_popover.tsx
+++ b/frontend/javascripts/oxalis/view/right-border-tabs/advanced_search_popover.tsx
@@ -1,5 +1,5 @@
import { Input, Tooltip, Popover, Space, type InputRef } from "antd";
-import { DownOutlined, UpOutlined } from "@ant-design/icons";
+import { CheckSquareOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
import * as React from "react";
import memoizeOne from "memoize-one";
import ButtonComponent from "oxalis/view/components/button_component";
@@ -7,10 +7,13 @@ import Shortcut from "libs/shortcut_component";
import DomVisibilityObserver from "oxalis/view/components/dom_visibility_observer";
import { mod } from "libs/utils";
+const PRIMARY_COLOR = "var(--ant-color-primary)";
+
type Props
Organization Permissions{" "}
diff --git a/frontend/javascripts/dashboard/dashboard_task_list_view.tsx b/frontend/javascripts/dashboard/dashboard_task_list_view.tsx
index 899a5957c87..cec4d65b369 100644
--- a/frontend/javascripts/dashboard/dashboard_task_list_view.tsx
+++ b/frontend/javascripts/dashboard/dashboard_task_list_view.tsx
@@ -414,7 +414,7 @@ class DashboardTaskListView extends React.PureComponent
= {
data: S[];
searchKey: keyof S | ((item: S) => string);
onSelect: (arg0: S) => void;
+ onSelectAllMatches?: (arg0: S[]) => void;
children: React.ReactNode;
provideShortcut?: boolean;
targetId: string;
@@ -20,6 +23,7 @@ type State = {
isVisible: boolean;
searchQuery: string;
currentPosition: number | null | undefined;
+ areAllMatchesSelected: boolean;
};
export default class AdvancedSearchPopover<
@@ -29,6 +33,7 @@ export default class AdvancedSearchPopover<
isVisible: false,
searchQuery: "",
currentPosition: null,
+ areAllMatchesSelected: false,
};
getAvailableOptions = memoizeOne(
@@ -69,6 +74,7 @@ export default class AdvancedSearchPopover<
currentPosition = mod(currentPosition + offset, numberOfAvailableOptions);
this.setState({
currentPosition,
+ areAllMatchesSelected: false,
});
this.props.onSelect(availableOptions[currentPosition]);
};
@@ -101,7 +107,7 @@ export default class AdvancedSearchPopover<
render() {
const { data, searchKey, provideShortcut, children, targetId } = this.props;
- const { searchQuery, isVisible } = this.state;
+ const { searchQuery, isVisible, areAllMatchesSelected } = this.state;
let { currentPosition } = this.state;
const availableOptions = this.getAvailableOptions(data, searchQuery, searchKey);
const numberOfAvailableOptions = availableOptions.length;
@@ -109,13 +115,17 @@ export default class AdvancedSearchPopover<
currentPosition =
currentPosition == null ? -1 : Math.min(currentPosition, numberOfAvailableOptions - 1);
const hasNoResults = numberOfAvailableOptions === 0;
- const hasMultipleResults = numberOfAvailableOptions > 1;
+ const availableOptionsToSelectAllMatches = availableOptions.filter(
+ (result) => result.type === "Tree" || result.type === "segment",
+ );
+ const isSelectAllMatchesDisabled = availableOptionsToSelectAllMatches.length < 2;
const additionalInputStyle =
hasNoResults && searchQuery !== ""
? {
color: "red",
}
: {};
+ const selectAllMatchesButtonColor = areAllMatchesSelected ? PRIMARY_COLOR : undefined;
return (