From 27af7317d7718b45830837272f1d28778cdfdf6f Mon Sep 17 00:00:00 2001 From: Gritta Weisheit Date: Wed, 30 Sep 2020 11:57:17 +0200 Subject: [PATCH 01/39] change getting of dataset configurations --- frontend/javascripts/admin/admin_rest_api.js | 18 +++++++++++++++--- frontend/javascripts/admin/api_flow_types.js | 5 +++++ .../javascripts/oxalis/model_initialization.js | 7 +++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index 1147e2b8a18..38a3c50df30 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -8,6 +8,7 @@ import { type APIAnnotationWithTask, type APIAnnotationVisibility, type APIBuildInfo, + type APIDataLayer, type APIDataSource, type APIDataSourceWithMessages, type APIDataStore, @@ -23,6 +24,7 @@ import { type APIProjectProgressReport, type APIProjectUpdater, type APIProjectWithAssignments, + type APIReducedDataLayer, type APISampleDataset, type APIScript, type APIScriptCreator, @@ -783,12 +785,22 @@ export function updateDataset(datasetId: APIDatasetId, dataset: APIDataset): Pro ); } -export function getDatasetConfiguration(datasetId: APIDatasetId): Promise { - return Request.receiveJSON( - `/api/dataSetConfigurations/${datasetId.owningOrganization}/${datasetId.name}`, +export function getDatasetConfiguration(dataset: APIDataset): Promise { + const layers = dataset.dataSource.dataLayers; + return Request.sendJSONReceiveJSON( + `/api/dataSetConfigurations/${dataset.owningOrganization}/${dataset.name}`, + { + data: layers.map(layer => transformLayer(layer)), + method: "POST", + }, ); } +function transformLayer(layer: APIDataLayer): APIReducedDataLayer { + const isSegmentationLayer = layer.category !== "color"; + return { name: layer.name, isSegmentationLayer }; +} + export function updateDatasetConfiguration( datasetId: APIDatasetId, datasetConfig: DatasetConfiguration, diff --git a/frontend/javascripts/admin/api_flow_types.js b/frontend/javascripts/admin/api_flow_types.js index 11c9b7aaa0b..4dc77b20ef0 100644 --- a/frontend/javascripts/admin/api_flow_types.js +++ b/frontend/javascripts/admin/api_flow_types.js @@ -62,6 +62,11 @@ export type APISegmentationLayer = {| export type APIDataLayer = APIColorLayer | APISegmentationLayer; +export type APIReducedDataLayer = {| + +name: string, + +isSegmentationLayer: boolean, +|}; + export type APIHistogramData = Array<{ numberOfElements: number, elementCounts: Array, diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index 532b19d2da9..15efd7fe841 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -109,12 +109,14 @@ export async function initialize( datasetId = { name, owningOrganization }; } - const [dataset, initialUserSettings, initialDatasetSettings, tracing] = await fetchParallel( + const [dataset, initialUserSettings, tracing] = await fetchParallel( annotation, datasetId, versions, ); + const initialDatasetSettings = await Promise.all([getDatasetConfiguration(dataset)]); + initializeDataset(initialFetch, dataset, tracing); initializeSettings(initialUserSettings, initialDatasetSettings); @@ -160,7 +162,8 @@ async function fetchParallel( return Promise.all([ getDataset(datasetId, getSharingToken()), getUserConfiguration(), - getDatasetConfiguration(datasetId), + // getDatasetConfiguration(datasetId), + // Fetch the actual tracing from the datastore, if there is an skeletonAnnotation // (Also see https://github.com/facebook/flow/issues/4936) // $FlowFixMe: Type inference with Promise.all seems to be a bit broken in flow From f98f2d1fe0926a6a98a1f23b7a84bb1219db7f62 Mon Sep 17 00:00:00 2001 From: Youri K Date: Wed, 30 Sep 2020 16:46:22 +0200 Subject: [PATCH 02/39] [WIP] backend split layer and dataset config handling --- app/controllers/ConfigurationController.scala | 64 ++++++++++++------- .../configuration/DataSetConfiguration.scala | 31 ++++++++- app/models/user/User.scala | 36 ++++++++++- app/models/user/UserService.scala | 16 ++++- conf/webknossos.latest.routes | 2 +- frontend/javascripts/oxalis/store.js | 1 - tools/postgres/schema.sql | 11 ++++ 7 files changed, 130 insertions(+), 31 deletions(-) diff --git a/app/controllers/ConfigurationController.scala b/app/controllers/ConfigurationController.scala index f25a4417775..1c91ccf89cf 100755 --- a/app/controllers/ConfigurationController.scala +++ b/app/controllers/ConfigurationController.scala @@ -4,23 +4,26 @@ import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContex import com.scalableminds.util.tools.Fox import javax.inject.Inject import models.binary.{DataSet, DataSetDAO, DataSetService} -import models.configuration.{DataSetConfiguration, DataSetConfigurationDefaults, UserConfiguration} -import models.user.{UserDataSetConfigurationDAO, UserService} +import models.configuration.{DataSetConfiguration, DataSetConfigurationDefaults, DataSetLayerId, UserConfiguration} +import models.user.{UserDataSetConfigurationDAO, UserDataSetLayerConfigurationDAO, UserService} import oxalis.security.WkEnv import com.mohiva.play.silhouette.api.Silhouette import com.mohiva.play.silhouette.api.actions.{SecuredRequest, UserAwareRequest} import play.api.i18n.{Messages, MessagesApi} import play.api.libs.json.{JsObject, JsValue} import play.api.libs.json.Json._ +import play.api.mvc.PlayBodyParsers import scala.concurrent.ExecutionContext -class ConfigurationController @Inject()(userService: UserService, - dataSetService: DataSetService, - dataSetDAO: DataSetDAO, - userDataSetConfigurationDAO: UserDataSetConfigurationDAO, - dataSetConfigurationDefaults: DataSetConfigurationDefaults, - sil: Silhouette[WkEnv])(implicit ec: ExecutionContext) +class ConfigurationController @Inject()( + userService: UserService, + dataSetService: DataSetService, + dataSetDAO: DataSetDAO, + userDataSetConfigurationDAO: UserDataSetConfigurationDAO, + userDataSetLayerConfigurationDAO: UserDataSetLayerConfigurationDAO, + dataSetConfigurationDefaults: DataSetConfigurationDefaults, + sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) extends Controller { def read = sil.UserAwareAction.async { implicit request => @@ -41,30 +44,45 @@ class ConfigurationController @Inject()(userService: UserService, } } - def readDataSet(organizationName: String, dataSetName: String) = sil.UserAwareAction.async { implicit request => - request.identity.toFox.flatMap { user => - for { - configurationJson: JsValue <- userDataSetConfigurationDAO.findOneForUserAndDataset(user._id, dataSetName) - } yield DataSetConfiguration(configurationJson.validate[Map[String, JsValue]].getOrElse(Map.empty)) - }.orElse( + def readDataSet(organizationName: String, dataSetName: String) = + sil.UserAwareAction.async(validateJson[List[DataSetLayerId]]) { implicit request => + request.identity.toFox.flatMap { user => for { + configurationJson: JsValue <- userDataSetConfigurationDAO.findOneForUserAndDataset(user._id, dataSetName) + initialConfigurationMap = configurationJson.validate[Map[String, JsValue]].getOrElse(Map.empty) + layerConfigJson <- userDataSetLayerConfigurationDAO.findAllByLayerNameForUserAndDataset( + request.body.map(_.name), + user._id, + dataSetName) dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) - config <- dataSetConfigurationDefaults.constructInitialDefault(dataSet) - } yield config - ) - .getOrElse(dataSetConfigurationDefaults.constructInitialDefault(List())) - .map(configuration => Ok(toJson(dataSetConfigurationDefaults.configurationOrDefaults(configuration)))) - } + layerSourceDefaultViewConfigs <- dataSetConfigurationDefaults.getAllLayerSourceDefaultViewConfigForDataSet( + dataSet) + layerSettings = dataSetConfigurationDefaults.layerConfigurationOrDefaults(request.body, + layerConfigJson, + layerSourceDefaultViewConfigs) + } yield dataSetConfigurationDefaults.buildCompleteConfig(initialConfigurationMap, layerSettings) + }.orElse( + for { + dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) + config <- dataSetConfigurationDefaults.constructInitialDefault(dataSet) + } yield config + ) + .getOrElse(dataSetConfigurationDefaults.constructInitialDefault(List())) + .map(configuration => Ok(toJson(configuration))) + } def updateDataSet(organizationName: String, dataSetName: String) = sil.SecuredAction.async(parse.json(maxLength = 20480)) { implicit request => for { jsConfiguration <- request.body.asOpt[JsObject] ?~> "user.configuration.dataset.invalid" conf = jsConfiguration.fields.toMap + dataSetConf = conf - "layers" + layerConf = conf.get("layers") _ <- userService.updateDataSetConfiguration(request.identity, dataSetName, organizationName, - DataSetConfiguration(conf)) + DataSetConfiguration(dataSetConf), + layerConf) } yield { JsonOk(Messages("user.configuration.dataset.updated")) } @@ -77,9 +95,7 @@ class ConfigurationController @Inject()(userService: UserService, Fox.successful( Ok(toJson(dataSetConfigurationDefaults.configurationOrDefaults(c, dataSet.sourceDefaultConfiguration)))) case _ => - dataSetConfigurationDefaults - .constructInitialDefault(dataSet) - .map(c => Ok(toJson(dataSetConfigurationDefaults.configurationOrDefaults(c)))) + dataSetConfigurationDefaults.constructInitialDefault(dataSet).map(c => Ok(toJson(c.configuration))) } } } diff --git a/app/models/configuration/DataSetConfiguration.scala b/app/models/configuration/DataSetConfiguration.scala index 7d70a0b443b..4a2dd3534c1 100644 --- a/app/models/configuration/DataSetConfiguration.scala +++ b/app/models/configuration/DataSetConfiguration.scala @@ -7,6 +7,9 @@ import javax.inject.Inject import models.binary.{DataSet, DataSetService} import play.api.libs.json._ +case class DataSetLayerId(name: String, isSegmentationLayer: Boolean) +object DataSetLayerId { implicit val dataSetLayerId = Json.format[DataSetLayerId] } + case class DataSetConfiguration(configuration: Map[String, JsValue]) object DataSetConfiguration { implicit val dataSetConfigurationFormat = Json.format[DataSetConfiguration] } @@ -40,7 +43,6 @@ class DataSetConfigurationDefaults @Inject()(dataSetService: DataSetService) { DataSetConfiguration( Map( "fourBit" -> JsBoolean(false), - "quality" -> JsNumber(0), "interpolation" -> JsBoolean(true), "highlightHoveredCellId" -> JsBoolean(true), "renderMissingDataBlack" -> JsBoolean(true), @@ -55,6 +57,33 @@ class DataSetConfigurationDefaults @Inject()(dataSetService: DataSetService) { sourceDefaultConfiguration.map(_.toMap).getOrElse(Map.empty) ++ configuration.configuration + def layerConfigurationOrDefaults(requestedLayer: List[DataSetLayerId], + existingLayerConfiguration: Map[String, JsValue], + sourceDefaultConfiguration: Map[String, JsValue]) = + requestedLayer.map { + case DataSetLayerId(name, isSegmentationLayer) => + (name, + existingLayerConfiguration.getOrElse( + name, + sourceDefaultConfiguration.getOrElse(name, + Json.toJson( + if (isSegmentationLayer) initialDefaultPerSegmentationLayer + else initialDefaultPerColorLayer)))) + }.toMap + + def buildCompleteConfig(initialConfiguration: Map[String, JsValue], layerConfigurations: Map[String, JsValue]) = + DataSetConfiguration(initialConfiguration + ("layers" -> Json.toJson(layerConfigurations))) + + def getAllLayerSourceDefaultViewConfigForDataSet(dataSet: DataSet)( + implicit ctx: DBAccessContext): Fox[Map[String, JsValue]] = + for { + + dataSource <- dataSetService.dataSourceFor(dataSet) + dataLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) + layerSourceDefaultViewConfig = dataLayers.flatMap(dl => + dl.defaultViewConfiguration.map(c => (dl.name, Json.toJson(c.toMap)))) + } yield layerSourceDefaultViewConfig.toMap + val initialDefaultPerColorLayer: Map[String, JsValue] = Map( "brightness" -> JsNumber(0), "contrast" -> JsNumber(1), diff --git a/app/models/user/User.scala b/app/models/user/User.scala index b2912e89fed..3cd42b9d9bc 100755 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -323,7 +323,7 @@ class UserDataSetConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserD where d.name = ${dataSetName} and c._user = ${userId} """.as[String]) - parsed = rows.map(Json.parse(_)) + parsed = rows.map(Json.parse) result <- parsed.headOption.toFox } yield result @@ -370,3 +370,37 @@ class UserDataSetConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserD } yield () } + +class UserDataSetLayerConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserDAO)(implicit ec: ExecutionContext) + extends SimpleSQLDAO(sqlClient) { + + def findAllByLayerNameForUserAndDataset(layerNames: List[String], userId: ObjectId, dataSetName: String)( + implicit ctx: DBAccessContext): Fox[Map[String, JsValue]] = + for { + rows <- run(sql"""select layerName, configuration + from webknossos.user_dataSetLayerConfigurations + where _dataset in (select _id from webknossos.dataSets_ where name = $dataSetName) + and _user = $userId + and layerName in #${writeStructTupleWithQuotes(layerNames)} + """.as[(String, String)]) + parsed = rows.map(t => (t._1, Json.parse(t._2))) + } yield parsed.toMap + + def updateDatasetConfigurationForUserAndDatasetAndLayer( + userId: ObjectId, + dataSetId: ObjectId, + layerName: String, + configuration: JsValue)(implicit ctx: DBAccessContext): Fox[Unit] = + for { + _ <- userDAO.assertUpdateAccess(userId) + deleteQuery = sqlu"""delete from webknossos.user_dataSetLayerConfigurations + where _user = $userId and _dataSet = $dataSetId and layerName = $layerName""" + insertQuery = sqlu"""insert into webknossos.user_dataSetLayerConfigurations(_user, _dataSet, layerName, configuration) + values($userId, $dataSetId, $layerName, '#${sanitize(configuration.toString)}')""" + _ <- run( + DBIO.sequence(List(deleteQuery, insertQuery)).transactionally.withTransactionIsolation(Serializable), + retryCount = 50, + retryIfErrorContains = List(transactionSerializationError) + ) + } yield () +} diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index fd6f6324c6c..408daa758f6 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -28,6 +28,7 @@ class UserService @Inject()(conf: WkConf, userTeamRolesDAO: UserTeamRolesDAO, userExperiencesDAO: UserExperiencesDAO, userDataSetConfigurationDAO: UserDataSetConfigurationDAO, + userDataSetLayerConfigurationDAO: UserDataSetLayerConfigurationDAO, organizationDAO: OrganizationDAO, teamDAO: TeamDAO, teamMembershipService: TeamMembershipService, @@ -146,15 +147,24 @@ class UserService @Inject()(conf: WkConf, user: User, dataSetName: String, organizationName: String, - configuration: DataSetConfiguration)(implicit ctx: DBAccessContext, m: MessagesProvider) = + dataSetConfiguration: DataSetConfiguration, + layerConfiguration: Option[JsValue])(implicit ctx: DBAccessContext, m: MessagesProvider) = for { dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName) ?~> Messages( "dataSet.notFound", dataSetName) + layerMap = layerConfiguration.map(_.validate[Map[String, JsValue]].getOrElse(Map.empty)).getOrElse(Map.empty) + _ <- Fox.serialCombined(layerMap.toList) { + case (name, config) => + userDataSetLayerConfigurationDAO.updateDatasetConfigurationForUserAndDatasetAndLayer(user._id, + dataSet._id, + name, + config) + } _ <- userDataSetConfigurationDAO.updateDatasetConfigurationForUserAndDataset(user._id, dataSet._id, - configuration.configuration) - _ = userCache.invalidateUser(user._id) + dataSetConfiguration.configuration) + _ = userCache.invalidateUser(user._id) // TODO Why do we need to invalidate the user here? } yield () def updateLastTaskTypeId(user: User, lastTaskTypeId: Option[String])(implicit ctx: DBAccessContext) = diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index a23ab132409..e3247067e2c 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -28,7 +28,7 @@ POST /auth/createOrganizationWithAdmin # Configurations GET /user/userConfiguration controllers.ConfigurationController.read PUT /user/userConfiguration controllers.ConfigurationController.update -GET /dataSetConfigurations/:organizationName/:dataSetName controllers.ConfigurationController.readDataSet(organizationName: String, dataSetName: String) +POST /dataSetConfigurations/:organizationName/:dataSetName controllers.ConfigurationController.readDataSet(organizationName: String, dataSetName: String) PUT /dataSetConfigurations/:organizationName/:dataSetName controllers.ConfigurationController.updateDataSet(organizationName: String, dataSetName: String) GET /dataSetConfigurations/default/:organizationName/:dataSetName controllers.ConfigurationController.readDataSetDefault(organizationName: String, dataSetName: String) PUT /dataSetConfigurations/default/:organizationName/:dataSetName controllers.ConfigurationController.updateDataSetDefault(organizationName: String, dataSetName: String) diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index a4ad4e1fcb5..eb44a0c1127 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -272,7 +272,6 @@ export type DatasetConfiguration = {| +layers: { [name: string]: DatasetLayerConfiguration, }, - +quality: 0 | 1 | 2, +highlightHoveredCellId: boolean, +renderIsosurfaces: boolean, +position?: Vector3, diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index a0841035f5d..505180f1cc4 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -316,6 +316,17 @@ CREATE TABLE webknossos.user_dataSetConfigurations( CONSTRAINT configurationIsJsonObject CHECK(jsonb_typeof(configuration) = 'object') ); +CREATE TABLE webknossos.user_dataSetLayerConfigurations( + _user CHAR(24) NOT NULL, + _dataSet CHAR(24) NOT NULL, + layerName VARCHAR(256) NOT NULL, + configuration JSONB NOT NULL, + PRIMARY KEY (_user, _dataSet, layerName), + CONSTRAINT configurationIsJsonObject CHECK(jsonb_typeof(configuration) = 'object') +); + + + CREATE TYPE webknossos.TOKEN_TYPES AS ENUM ('Authentication', 'DataStore', 'ResetPassword'); CREATE TABLE webknossos.tokens( _id CHAR(24) PRIMARY KEY DEFAULT '', From 31bbc1f22186c8ebb2d3b1f41b94cf4c104e6756 Mon Sep 17 00:00:00 2001 From: Gritta Weisheit Date: Thu, 1 Oct 2020 10:48:11 +0200 Subject: [PATCH 03/39] clean up frontend --- frontend/javascripts/admin/admin_rest_api.js | 10 ++++------ frontend/javascripts/oxalis/model_initialization.js | 3 +-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index 38a3c50df30..d2bf29835af 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -790,17 +790,15 @@ export function getDatasetConfiguration(dataset: APIDataset): Promise { return Request.sendJSONReceiveJSON( `/api/dataSetConfigurations/${dataset.owningOrganization}/${dataset.name}`, { - data: layers.map(layer => transformLayer(layer)), + data: layers.map(layer => ({ + name: layer.name, + isSegmentationLayer: layer.category !== "color", + })), method: "POST", }, ); } -function transformLayer(layer: APIDataLayer): APIReducedDataLayer { - const isSegmentationLayer = layer.category !== "color"; - return { name: layer.name, isSegmentationLayer }; -} - export function updateDatasetConfiguration( datasetId: APIDatasetId, datasetConfig: DatasetConfiguration, diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index 15efd7fe841..4bd697999e6 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -115,7 +115,7 @@ export async function initialize( versions, ); - const initialDatasetSettings = await Promise.all([getDatasetConfiguration(dataset)]); + const initialDatasetSettings = await getDatasetConfiguration(dataset); initializeDataset(initialFetch, dataset, tracing); initializeSettings(initialUserSettings, initialDatasetSettings); @@ -162,7 +162,6 @@ async function fetchParallel( return Promise.all([ getDataset(datasetId, getSharingToken()), getUserConfiguration(), - // getDatasetConfiguration(datasetId), // Fetch the actual tracing from the datastore, if there is an skeletonAnnotation // (Also see https://github.com/facebook/flow/issues/4936) From be446f6cb2648d2a9b05f59a4c13de930476272c Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 1 Oct 2020 16:19:00 +0200 Subject: [PATCH 04/39] add evolutions --- app/controllers/ConfigurationController.scala | 2 +- .../056-add-layer-specific-view-configs.sql | 36 +++++++++++++++++++ .../056-add-layer-specific-view-configs.sql | 17 +++++++++ frontend/javascripts/admin/admin_rest_api.js | 9 ++--- frontend/javascripts/oxalis/default_state.js | 1 - .../oxalis/model_initialization.js | 16 +++++++-- .../puppeteer/dataset_rendering.screenshot.js | 1 - tools/postgres/schema.sql | 9 +++-- 8 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 conf/evolutions/056-add-layer-specific-view-configs.sql create mode 100644 conf/evolutions/reversions/056-add-layer-specific-view-configs.sql diff --git a/app/controllers/ConfigurationController.scala b/app/controllers/ConfigurationController.scala index 1c91ccf89cf..0c25c768ab4 100755 --- a/app/controllers/ConfigurationController.scala +++ b/app/controllers/ConfigurationController.scala @@ -68,7 +68,7 @@ class ConfigurationController @Inject()( } yield config ) .getOrElse(dataSetConfigurationDefaults.constructInitialDefault(List())) - .map(configuration => Ok(toJson(configuration))) + .map(configuration => Ok(toJson(configuration.configuration))) } def updateDataSet(organizationName: String, dataSetName: String) = diff --git a/conf/evolutions/056-add-layer-specific-view-configs.sql b/conf/evolutions/056-add-layer-specific-view-configs.sql new file mode 100644 index 00000000000..2b81b35702c --- /dev/null +++ b/conf/evolutions/056-add-layer-specific-view-configs.sql @@ -0,0 +1,36 @@ +-- https://github.com/scalableminds/webknossos/pull/4XXX + +START TRANSACTION; + +CREATE TABLE webknossos.user_dataSetLayerConfigurations( + _user CHAR(24) NOT NULL, + _dataSet CHAR(24) NOT NULL, + layerName VARCHAR(256) NOT NULL, + configuration JSONB NOT NULL, + PRIMARY KEY (_user, _dataSet, layerName), + CONSTRAINT configurationIsJsonObject CHECK(jsonb_typeof(configuration) = 'object') +); + +ALTER TABLE webknossos.user_dataSetLayerConfigurations + ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, + ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; + +-- Insert the layer configs into the new table +INSERT INTO webknossos.user_dataSetLayerConfigurations(_user, _dataset, layerName, configuration) +SELECT _user, _dataset, (js).key AS layerName, (js).value AS config FROM (SELECT _user, _dataset, jsonb_each(configuration->'layers') AS js FROM webknossos.user_dataSetConfigurations WHERE configuration ? 'layers') AS sub_q; + + +-- Remove layers field from old table +UPDATE webknossos.user_dataSetConfigurations +SET configuration = configuration - 'layers' +WHERE configuration ? 'layers'; + +--remove unused field quality +UPDATE webknossos.user_dataSetConfigurations +SET configuration = configuration - 'quality' +WHERE configuration ? 'quality'; + + +UPDATE webknossos.releaseInformation SET schemaVersion = 56; + +COMMIT TRANSACTION; diff --git a/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql b/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql new file mode 100644 index 00000000000..40465afefe2 --- /dev/null +++ b/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql @@ -0,0 +1,17 @@ + +START TRANSACTION; + +-- pre-aggregate all layer jsons into one json object, set json object in configuration +UPDATE webknossos.user_dataSetConfigurations dsC +SET configuration = jsonb_set( + dsC.configuration, + array['layers'], + subQ.json) +FROM (select _user, _dataset, jsonb_object_agg(layerName, configuration) AS json FROM webknossos.user_dataSetLayerConfigurations dlC GROUP BY _user, _dataset) AS subQ +WHERE dsC._user = subQ._user and dsC._dataset = subQ._dataset; + +DROP TABLE webknossos.user_dataSetLayerConfigurations; + +UPDATE webknossos.releaseInformation SET schemaVersion = 55; + +COMMIT TRANSACTION; diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index d2bf29835af..b2c9e242463 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -8,7 +8,6 @@ import { type APIAnnotationWithTask, type APIAnnotationVisibility, type APIBuildInfo, - type APIDataLayer, type APIDataSource, type APIDataSourceWithMessages, type APIDataStore, @@ -785,15 +784,11 @@ export function updateDataset(datasetId: APIDatasetId, dataset: APIDataset): Pro ); } -export function getDatasetConfiguration(dataset: APIDataset): Promise { - const layers = dataset.dataSource.dataLayers; +export function getDatasetViewConfiguration(dataset: APIDataset, displayedLayers: Array): Promise { return Request.sendJSONReceiveJSON( `/api/dataSetConfigurations/${dataset.owningOrganization}/${dataset.name}`, { - data: layers.map(layer => ({ - name: layer.name, - isSegmentationLayer: layer.category !== "color", - })), + data: displayedLayers, method: "POST", }, ); diff --git a/frontend/javascripts/oxalis/default_state.js b/frontend/javascripts/oxalis/default_state.js index b05ad1f6d0a..0331bdf8803 100644 --- a/frontend/javascripts/oxalis/default_state.js +++ b/frontend/javascripts/oxalis/default_state.js @@ -40,7 +40,6 @@ const defaultState: OxalisState = { fourBit: false, interpolation: false, layers: {}, - quality: 0, loadingStrategy: "PROGRESSIVE_QUALITY", highlightHoveredCellId: true, segmentationPatternOpacity: 40, diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index 4bd697999e6..51473baf877 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -33,7 +33,7 @@ import { getDataset, getSharingToken, getUserConfiguration, - getDatasetConfiguration, + getDatasetViewConfiguration, } from "admin/admin_rest_api"; import { initializeAnnotationAction } from "oxalis/model/actions/annotation_actions"; import { @@ -115,7 +115,17 @@ export async function initialize( versions, ); - const initialDatasetSettings = await getDatasetConfiguration(dataset); + // TODO Can we know here if the fallback layer is needed? + const displayedLayers = dataset.dataSource.dataLayers.map(layer => ({ + name: layer.name, + isSegmentationLayer: layer.category !== "color", + })); + + if (annotation != null && annotation.tracing.volume != null) { + displayedLayers.push({ name: annotation.tracing.volume, isSegmentationLayer: true }); + } + + const initialDatasetSettings = await getDatasetViewConfiguration(dataset, displayedLayers); initializeDataset(initialFetch, dataset, tracing); initializeSettings(initialUserSettings, initialDatasetSettings); @@ -158,7 +168,7 @@ async function fetchParallel( annotation: ?APIAnnotation, datasetId: APIDatasetId, versions?: Versions, -): Promise<[APIDataset, *, *, ?HybridServerTracing]> { +): Promise<[APIDataset, *, ?HybridServerTracing]> { return Promise.all([ getDataset(datasetId, getSharingToken()), getUserConfiguration(), diff --git a/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.js b/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.js index 681610b3e38..54912879134 100644 --- a/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.js +++ b/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.js @@ -105,7 +105,6 @@ const datasetConfigOverrides: { [key: string]: DatasetConfiguration } = { isInEditMode: false, }, }, - quality: 0, highlightHoveredCellId: true, renderIsosurfaces: false, renderMissingDataBlack: false, diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index 505180f1cc4..d35a9a5b1a8 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -21,7 +21,7 @@ START TRANSACTION; CREATE TABLE webknossos.releaseInformation ( schemaVersion BIGINT NOT NULL ); -INSERT INTO webknossos.releaseInformation(schemaVersion) values(55); +INSERT INTO webknossos.releaseInformation(schemaVersion) values(56); COMMIT TRANSACTION; CREATE TABLE webknossos.analytics( @@ -435,8 +435,11 @@ ALTER TABLE webknossos.user_team_roles ALTER TABLE webknossos.user_experiences ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE; ALTER TABLE webknossos.user_dataSetConfigurations - ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, - ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; + ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, + ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; +ALTER TABLE webknossos.user_dataSetLayerConfigurations + ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, + ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; CREATE FUNCTION webknossos.countsAsTaskInstance(a webknossos.annotations) RETURNS BOOLEAN AS $$ BEGIN From aa6be99238f17e4abafac1e33cb7c09127bef021 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 1 Oct 2020 16:40:52 +0200 Subject: [PATCH 05/39] fix frontend linting --- frontend/javascripts/admin/admin_rest_api.js | 7 +++++-- frontend/javascripts/test/model/flycam_accessors.spec.js | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index 1344b8d9523..d1891bc065e 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -784,9 +784,12 @@ export function updateDataset(datasetId: APIDatasetId, dataset: APIDataset): Pro ); } -export function getDatasetViewConfiguration(dataset: APIDataset, displayedLayers: Array): Promise { +export function getDatasetViewConfiguration( + datasetId: APIDatasetId, + displayedLayers: Array, +): Promise { return Request.sendJSONReceiveJSON( - `/api/dataSetConfigurations/${dataset.owningOrganization}/${dataset.name}`, + `/api/dataSetConfigurations/${datasetId.owningOrganization}/${datasetId.name}`, { data: displayedLayers, method: "POST", diff --git a/frontend/javascripts/test/model/flycam_accessors.spec.js b/frontend/javascripts/test/model/flycam_accessors.spec.js index b28db824d9e..7061905b81e 100644 --- a/frontend/javascripts/test/model/flycam_accessors.spec.js +++ b/frontend/javascripts/test/model/flycam_accessors.spec.js @@ -46,7 +46,6 @@ const initialState: OxalisState = { }, datasetConfiguration: { ...defaultState.datasetConfiguration, - quality: 0, }, userConfiguration: { ...defaultState.userConfiguration, From fd7cf8904042d70f5692061c435b130449a5de63 Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 2 Oct 2020 10:20:28 +0200 Subject: [PATCH 06/39] fix frontend tests --- frontend/javascripts/test/helpers/apiHelpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/javascripts/test/helpers/apiHelpers.js b/frontend/javascripts/test/helpers/apiHelpers.js index 1168df1d1a3..97bb237c54e 100644 --- a/frontend/javascripts/test/helpers/apiHelpers.js +++ b/frontend/javascripts/test/helpers/apiHelpers.js @@ -157,6 +157,7 @@ export function __setupOxalis(t, mode, apiVersion) { }), ); Request.receiveJSON.returns(Promise.resolve({})); + Request.sendJSONReceiveJSON.returns(Promise.resolve({})); return Model.fetch( ANNOTATION_TYPE, From 0528ed1a87d412d9275290a21cfb60b93bfa29dc Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 2 Oct 2020 13:10:48 +0200 Subject: [PATCH 07/39] fix frontend tests 2 --- .../oxalis/model_initialization.js | 21 ++++++++++++------- frontend/javascripts/test/model/model.spec.js | 15 ++++++++++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index 533939faf06..c7f44572680 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -115,14 +115,19 @@ export async function initialize( versions, ); - // TODO Can we know here if the fallback layer is needed? - const displayedLayers = dataset.dataSource.dataLayers.map(layer => ({ - name: layer.name, - isSegmentationLayer: layer.category !== "color", - })); - - if (annotation != null && annotation.tracing.volume != null) { - displayedLayers.push({ name: annotation.tracing.volume, isSegmentationLayer: true }); + let displayedLayers = []; + if (dataset.dataSource.dataLayers != null) { + displayedLayers = dataset.dataSource.dataLayers.map(layer => ({ + name: layer.name, + isSegmentationLayer: layer.category !== "color", + })); + } + + if (tracing != null && tracing.volume != null) { + if (tracing.volume.fallbackLayer == null) { + displayedLayers = displayedLayers.filter(layer => !layer.isSegmentationLayer) + } + displayedLayers.push({ name: tracing.volume.id, isSegmentationLayer: true }); } const initialDatasetSettings = await getDatasetViewConfiguration(dataset, displayedLayers); diff --git a/frontend/javascripts/test/model/model.spec.js b/frontend/javascripts/test/model/model.spec.js index b1a382d4ff6..c7d0792ef57 100644 --- a/frontend/javascripts/test/model/model.spec.js +++ b/frontend/javascripts/test/model/model.spec.js @@ -5,6 +5,7 @@ import mockRequire from "mock-require"; import sinon from "sinon"; import test from "ava"; +import { ControlModeEnum } from "oxalis/constants"; import { tracing as TRACING, annotation as ANNOTATION, @@ -22,7 +23,7 @@ function makeModelMock() { const User = makeModelMock(); const DatasetConfiguration = makeModelMock(); -const Request = { receiveJSON: sinon.stub() }; +const Request = { receiveJSON: sinon.stub(), sendJSONReceiveJSON: sinon.stub() }; const ErrorHandling = { assertExtendContext: _.noop, }; @@ -91,11 +92,19 @@ test("Model Initialization: should throw a model.HANDLED_ERROR for missing data const datasetObject = _.clone(DATASET); delete datasetObject.dataSource.dataLayers; Request.receiveJSON - .withArgs(`/api/datasets/${ANNOTATION.dataSetName}`) + .withArgs(`/api/datasets/${ANNOTATION.organization}/${ANNOTATION.dataSetName}`) .returns(Promise.resolve(_.cloneDeep(datasetObject))); return model - .fetch(ANNOTATION_TYPE, ANNOTATION_ID, "VIEW", true) + .fetch( + ANNOTATION_TYPE, + { + type: ControlModeEnum.VIEW, + name: ANNOTATION.dataSetName || "", + owningOrganization: ANNOTATION.organization || "", + }, + true, + ) .then(() => { t.fail("Promise should not have been resolved."); }) From 37da2ae2f19af557d6b6e1f785b24929df685295 Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 2 Oct 2020 13:58:15 +0200 Subject: [PATCH 08/39] pretty frontend --- frontend/javascripts/oxalis/model_initialization.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index c7f44572680..cae413b2487 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -125,7 +125,7 @@ export async function initialize( if (tracing != null && tracing.volume != null) { if (tracing.volume.fallbackLayer == null) { - displayedLayers = displayedLayers.filter(layer => !layer.isSegmentationLayer) + displayedLayers = displayedLayers.filter(layer => !layer.isSegmentationLayer); } displayedLayers.push({ name: tracing.volume.id, isSegmentationLayer: true }); } From 4c2cdf4c88a36c6198308bdcd18d1a0c628440da Mon Sep 17 00:00:00 2001 From: Youri K Date: Mon, 5 Oct 2020 11:00:13 +0200 Subject: [PATCH 09/39] update changelog and migrations guide --- CHANGELOG.unreleased.md | 1 + MIGRATIONS.unreleased.md | 2 +- .../056-add-layer-specific-view-configs.sql | 16 ++++++++-------- tools/postgres/schema.sql | 8 ++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index e4bbe2e5656..be3412f8aa1 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -18,6 +18,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Fixed - Fixed failing histogram requests for float layers with NaN values. [#4834](https://github.com/scalableminds/webknossos/pull/4834) +- Fixed the disappearing of dataset settings after switching between view mode and trace mode. [#4845](https://github.com/scalableminds/webknossos/pull/4845) ### Removed - diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md index f4b6114cff5..f07fca7c39c 100644 --- a/MIGRATIONS.unreleased.md +++ b/MIGRATIONS.unreleased.md @@ -9,4 +9,4 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md). - ### Postgres Evolutions: -- +- [056-add-layer-specific-view-configs.sql](conf/evolutions/056-add-layer-specific-view-configs.sql) diff --git a/conf/evolutions/056-add-layer-specific-view-configs.sql b/conf/evolutions/056-add-layer-specific-view-configs.sql index 2b81b35702c..9bf5646c639 100644 --- a/conf/evolutions/056-add-layer-specific-view-configs.sql +++ b/conf/evolutions/056-add-layer-specific-view-configs.sql @@ -3,17 +3,17 @@ START TRANSACTION; CREATE TABLE webknossos.user_dataSetLayerConfigurations( - _user CHAR(24) NOT NULL, - _dataSet CHAR(24) NOT NULL, - layerName VARCHAR(256) NOT NULL, - configuration JSONB NOT NULL, - PRIMARY KEY (_user, _dataSet, layerName), - CONSTRAINT configurationIsJsonObject CHECK(jsonb_typeof(configuration) = 'object') + _user CHAR(24) NOT NULL, + _dataSet CHAR(24) NOT NULL, + layerName VARCHAR(256) NOT NULL, + configuration JSONB NOT NULL, + PRIMARY KEY (_user, _dataSet, layerName), + CONSTRAINT configurationIsJsonObject CHECK(jsonb_typeof(configuration) = 'object') ); ALTER TABLE webknossos.user_dataSetLayerConfigurations - ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, - ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; + ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, + ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; -- Insert the layer configs into the new table INSERT INTO webknossos.user_dataSetLayerConfigurations(_user, _dataset, layerName, configuration) diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index d35a9a5b1a8..48ba88815f9 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -435,11 +435,11 @@ ALTER TABLE webknossos.user_team_roles ALTER TABLE webknossos.user_experiences ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE; ALTER TABLE webknossos.user_dataSetConfigurations - ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, - ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; + ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, + ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; ALTER TABLE webknossos.user_dataSetLayerConfigurations - ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, - ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; + ADD CONSTRAINT user_ref FOREIGN KEY(_user) REFERENCES webknossos.users(_id) ON DELETE CASCADE DEFERRABLE, + ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; CREATE FUNCTION webknossos.countsAsTaskInstance(a webknossos.annotations) RETURNS BOOLEAN AS $$ BEGIN From 59f80a9f58eeeaafa0c621cb887bb9cce2cf25bd Mon Sep 17 00:00:00 2001 From: Youri K Date: Mon, 5 Oct 2020 14:47:44 +0200 Subject: [PATCH 10/39] Apply suggestions from code review Co-authored-by: Florian M --- CHANGELOG.unreleased.md | 2 +- conf/evolutions/056-add-layer-specific-view-configs.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index be3412f8aa1..372a3b01709 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -18,7 +18,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Fixed - Fixed failing histogram requests for float layers with NaN values. [#4834](https://github.com/scalableminds/webknossos/pull/4834) -- Fixed the disappearing of dataset settings after switching between view mode and trace mode. [#4845](https://github.com/scalableminds/webknossos/pull/4845) +- Fixed the disappearing of dataset settings after switching between view mode and annotation mode. [#4845](https://github.com/scalableminds/webknossos/pull/4845) ### Removed - diff --git a/conf/evolutions/056-add-layer-specific-view-configs.sql b/conf/evolutions/056-add-layer-specific-view-configs.sql index 9bf5646c639..313ed6d2dc4 100644 --- a/conf/evolutions/056-add-layer-specific-view-configs.sql +++ b/conf/evolutions/056-add-layer-specific-view-configs.sql @@ -1,4 +1,4 @@ --- https://github.com/scalableminds/webknossos/pull/4XXX +-- https://github.com/scalableminds/webknossos/pull/4845 START TRANSACTION; From 9f544743089993fcec7f1748255071244767368e Mon Sep 17 00:00:00 2001 From: Youri K Date: Mon, 5 Oct 2020 14:49:47 +0200 Subject: [PATCH 11/39] remove unnecessary invalidation of user --- app/models/user/UserService.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index 408daa758f6..e62bc5927b8 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -164,7 +164,6 @@ class UserService @Inject()(conf: WkConf, _ <- userDataSetConfigurationDAO.updateDatasetConfigurationForUserAndDataset(user._id, dataSet._id, dataSetConfiguration.configuration) - _ = userCache.invalidateUser(user._id) // TODO Why do we need to invalidate the user here? } yield () def updateLastTaskTypeId(user: User, lastTaskTypeId: Option[String])(implicit ctx: DBAccessContext) = From 065cf3774b60bd9cd3f0e2999138a94acc9685bb Mon Sep 17 00:00:00 2001 From: Youri K Date: Wed, 7 Oct 2020 11:34:17 +0200 Subject: [PATCH 12/39] apply pr feedback --- app/controllers/ConfigurationController.scala | 26 +++++++++++-------- .../configuration/DataSetConfiguration.scala | 13 +++------- frontend/javascripts/admin/admin_rest_api.js | 5 ++-- frontend/javascripts/admin/api_flow_types.js | 5 ---- .../oxalis/model_initialization.js | 19 +++++--------- 5 files changed, 26 insertions(+), 42 deletions(-) diff --git a/app/controllers/ConfigurationController.scala b/app/controllers/ConfigurationController.scala index 0c25c768ab4..6dddcd5b0fd 100755 --- a/app/controllers/ConfigurationController.scala +++ b/app/controllers/ConfigurationController.scala @@ -1,17 +1,17 @@ package controllers -import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} +import com.mohiva.play.silhouette.api.Silhouette +import com.scalableminds.util.accesscontext.GlobalAccessContext import com.scalableminds.util.tools.Fox +import com.scalableminds.webknossos.datastore.models.datasource.Category import javax.inject.Inject import models.binary.{DataSet, DataSetDAO, DataSetService} import models.configuration.{DataSetConfiguration, DataSetConfigurationDefaults, DataSetLayerId, UserConfiguration} import models.user.{UserDataSetConfigurationDAO, UserDataSetLayerConfigurationDAO, UserService} import oxalis.security.WkEnv -import com.mohiva.play.silhouette.api.Silhouette -import com.mohiva.play.silhouette.api.actions.{SecuredRequest, UserAwareRequest} -import play.api.i18n.{Messages, MessagesApi} -import play.api.libs.json.{JsObject, JsValue} +import play.api.i18n.Messages import play.api.libs.json.Json._ +import play.api.libs.json.{JsObject, JsValue} import play.api.mvc.PlayBodyParsers import scala.concurrent.ExecutionContext @@ -45,19 +45,23 @@ class ConfigurationController @Inject()( } def readDataSet(organizationName: String, dataSetName: String) = - sil.UserAwareAction.async(validateJson[List[DataSetLayerId]]) { implicit request => + sil.UserAwareAction.async(validateJson[List[String]]) { implicit request => request.identity.toFox.flatMap { user => for { configurationJson: JsValue <- userDataSetConfigurationDAO.findOneForUserAndDataset(user._id, dataSetName) initialConfigurationMap = configurationJson.validate[Map[String, JsValue]].getOrElse(Map.empty) + dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) + dataSource <- dataSetService.dataSourceFor(dataSet) + dataSetLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) + allLayerIds = dataSetLayers.map(dl => DataSetLayerId(dl.name, dl.category == Category.segmentation)) ++ request.body + .map(DataSetLayerId(_, true)) layerConfigJson <- userDataSetLayerConfigurationDAO.findAllByLayerNameForUserAndDataset( - request.body.map(_.name), + allLayerIds.map(_.name), user._id, dataSetName) - dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) - layerSourceDefaultViewConfigs <- dataSetConfigurationDefaults.getAllLayerSourceDefaultViewConfigForDataSet( - dataSet) - layerSettings = dataSetConfigurationDefaults.layerConfigurationOrDefaults(request.body, + layerSourceDefaultViewConfigs = dataSetConfigurationDefaults.getAllLayerSourceDefaultViewConfigForDataSet( + dataSetLayers) + layerSettings = dataSetConfigurationDefaults.layerConfigurationOrDefaults(allLayerIds, layerConfigJson, layerSourceDefaultViewConfigs) } yield dataSetConfigurationDefaults.buildCompleteConfig(initialConfigurationMap, layerSettings) diff --git a/app/models/configuration/DataSetConfiguration.scala b/app/models/configuration/DataSetConfiguration.scala index 4a2dd3534c1..7ae3b8c608e 100644 --- a/app/models/configuration/DataSetConfiguration.scala +++ b/app/models/configuration/DataSetConfiguration.scala @@ -2,7 +2,7 @@ package models.configuration import com.scalableminds.util.accesscontext.DBAccessContext import com.scalableminds.util.tools.Fox -import com.scalableminds.webknossos.datastore.models.datasource.{Category, ViewConfiguration} +import com.scalableminds.webknossos.datastore.models.datasource.{Category, DataLayerLike, ViewConfiguration} import javax.inject.Inject import models.binary.{DataSet, DataSetService} import play.api.libs.json._ @@ -74,15 +74,8 @@ class DataSetConfigurationDefaults @Inject()(dataSetService: DataSetService) { def buildCompleteConfig(initialConfiguration: Map[String, JsValue], layerConfigurations: Map[String, JsValue]) = DataSetConfiguration(initialConfiguration + ("layers" -> Json.toJson(layerConfigurations))) - def getAllLayerSourceDefaultViewConfigForDataSet(dataSet: DataSet)( - implicit ctx: DBAccessContext): Fox[Map[String, JsValue]] = - for { - - dataSource <- dataSetService.dataSourceFor(dataSet) - dataLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) - layerSourceDefaultViewConfig = dataLayers.flatMap(dl => - dl.defaultViewConfiguration.map(c => (dl.name, Json.toJson(c.toMap)))) - } yield layerSourceDefaultViewConfig.toMap + def getAllLayerSourceDefaultViewConfigForDataSet(dataLayers: List[DataLayerLike]): Map[String, JsValue] = + dataLayers.flatMap(dl => dl.defaultViewConfiguration.map(c => (dl.name, Json.toJson(c.toMap)))).toMap val initialDefaultPerColorLayer: Map[String, JsValue] = Map( "brightness" -> JsNumber(0), diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index d1891bc065e..e902c989a38 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -23,7 +23,6 @@ import { type APIProjectProgressReport, type APIProjectUpdater, type APIProjectWithAssignments, - type APIReducedDataLayer, type APISampleDataset, type APIScript, type APIScriptCreator, @@ -786,12 +785,12 @@ export function updateDataset(datasetId: APIDatasetId, dataset: APIDataset): Pro export function getDatasetViewConfiguration( datasetId: APIDatasetId, - displayedLayers: Array, + displayedVolumeTracings: Array, ): Promise { return Request.sendJSONReceiveJSON( `/api/dataSetConfigurations/${datasetId.owningOrganization}/${datasetId.name}`, { - data: displayedLayers, + data: displayedVolumeTracings, method: "POST", }, ); diff --git a/frontend/javascripts/admin/api_flow_types.js b/frontend/javascripts/admin/api_flow_types.js index 4dc77b20ef0..11c9b7aaa0b 100644 --- a/frontend/javascripts/admin/api_flow_types.js +++ b/frontend/javascripts/admin/api_flow_types.js @@ -62,11 +62,6 @@ export type APISegmentationLayer = {| export type APIDataLayer = APIColorLayer | APISegmentationLayer; -export type APIReducedDataLayer = {| - +name: string, - +isSegmentationLayer: boolean, -|}; - export type APIHistogramData = Array<{ numberOfElements: number, elementCounts: Array, diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index cae413b2487..b3f89de688f 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -115,22 +115,15 @@ export async function initialize( versions, ); - let displayedLayers = []; - if (dataset.dataSource.dataLayers != null) { - displayedLayers = dataset.dataSource.dataLayers.map(layer => ({ - name: layer.name, - isSegmentationLayer: layer.category !== "color", - })); - } - + const displayedVolumeTracings = []; if (tracing != null && tracing.volume != null) { - if (tracing.volume.fallbackLayer == null) { - displayedLayers = displayedLayers.filter(layer => !layer.isSegmentationLayer); - } - displayedLayers.push({ name: tracing.volume.id, isSegmentationLayer: true }); + displayedVolumeTracings.push(tracing.volume.id); } - const initialDatasetSettings = await getDatasetViewConfiguration(dataset, displayedLayers); + const initialDatasetSettings = await getDatasetViewConfiguration( + dataset, + displayedVolumeTracings, + ); initializeDataset(initialFetch, dataset, tracing); initializeSettings(initialUserSettings, initialDatasetSettings); From 58cafc5b2dba2d98383eca1749f014cbac3c1feb Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 9 Oct 2020 09:47:16 +0200 Subject: [PATCH 13/39] check for volume ids when no config is found --- app/controllers/ConfigurationController.scala | 6 +++--- .../configuration/DataSetConfiguration.scala | 16 ++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/controllers/ConfigurationController.scala b/app/controllers/ConfigurationController.scala index 6dddcd5b0fd..431bc5b7cc7 100755 --- a/app/controllers/ConfigurationController.scala +++ b/app/controllers/ConfigurationController.scala @@ -68,10 +68,10 @@ class ConfigurationController @Inject()( }.orElse( for { dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) - config <- dataSetConfigurationDefaults.constructInitialDefault(dataSet) + config <- dataSetConfigurationDefaults.constructInitialDefaultForDataset(dataSet, request.body) } yield config ) - .getOrElse(dataSetConfigurationDefaults.constructInitialDefault(List())) + .getOrElse(dataSetConfigurationDefaults.constructInitialDefaultForLayers(List())) .map(configuration => Ok(toJson(configuration.configuration))) } @@ -99,7 +99,7 @@ class ConfigurationController @Inject()( Fox.successful( Ok(toJson(dataSetConfigurationDefaults.configurationOrDefaults(c, dataSet.sourceDefaultConfiguration)))) case _ => - dataSetConfigurationDefaults.constructInitialDefault(dataSet).map(c => Ok(toJson(c.configuration))) + dataSetConfigurationDefaults.constructInitialDefaultForDataset(dataSet).map(c => Ok(toJson(c.configuration))) } } } diff --git a/app/models/configuration/DataSetConfiguration.scala b/app/models/configuration/DataSetConfiguration.scala index 7ae3b8c608e..f4b056551b5 100644 --- a/app/models/configuration/DataSetConfiguration.scala +++ b/app/models/configuration/DataSetConfiguration.scala @@ -16,18 +16,22 @@ object DataSetConfiguration { implicit val dataSetConfigurationFormat = Json.for class DataSetConfigurationDefaults @Inject()(dataSetService: DataSetService) { - def constructInitialDefault(dataSet: DataSet)(implicit ctx: DBAccessContext): Fox[DataSetConfiguration] = + def constructInitialDefaultForDataset(dataSet: DataSet, requestedVolumeIds: List[String] = List())( + implicit ctx: DBAccessContext): Fox[DataSetConfiguration] = for { dataSource <- dataSetService.dataSourceFor(dataSet) dataLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) - initialConfig = constructInitialDefault(dataLayers.map(dl => (dl.name, dl.category)), - dataLayers.map(_.defaultViewConfiguration.map(_.toMap))).configuration + initialConfig = constructInitialDefaultForLayers( + dataLayers.map(dl => (dl.name, dl.category)) ++ requestedVolumeIds.map((_, Category.segmentation)), + dataLayers.map(_.defaultViewConfiguration.map(_.toMap)) ++ requestedVolumeIds.map(_ => None) + ).configuration sourceDefaultConfig = dataSet.sourceDefaultConfiguration.map(_.toMap).getOrElse(Map.empty) defaultConfig = dataSet.defaultConfiguration.map(_.configuration).getOrElse(Map.empty) } yield DataSetConfiguration(initialConfig ++ sourceDefaultConfig ++ defaultConfig) - def constructInitialDefault(layers: List[(String, Category.Value)], - layerDefaults: List[Option[Map[String, JsValue]]] = List.empty): DataSetConfiguration = { + def constructInitialDefaultForLayers( + layers: List[(String, Category.Value)], + layerDefaults: List[Option[Map[String, JsValue]]] = List.empty): DataSetConfiguration = { val layerValues = Json.toJson( layers .zipAll(layerDefaults, ("", Category.color), None) @@ -53,7 +57,7 @@ class DataSetConfigurationDefaults @Inject()(dataSetService: DataSetService) { def configurationOrDefaults(configuration: DataSetConfiguration, sourceDefaultConfiguration: Option[ViewConfiguration] = None): Map[String, JsValue] = - constructInitialDefault(List()).configuration ++ + constructInitialDefaultForLayers(List()).configuration ++ sourceDefaultConfiguration.map(_.toMap).getOrElse(Map.empty) ++ configuration.configuration From c469d1b472449cc0eee78650465da30dbd9cebb3 Mon Sep 17 00:00:00 2001 From: Youri K Date: Mon, 12 Oct 2020 15:24:50 +0200 Subject: [PATCH 14/39] rename default view config to unify wk and datastore implementation --- app/controllers/ConfigurationController.scala | 48 +++------ app/controllers/DataSetController.scala | 8 +- app/models/binary/DataSet.scala | 48 ++++----- .../configuration/DataSetConfiguration.scala | 99 +++++++++++-------- app/models/user/User.scala | 38 +------ app/models/user/UserService.scala | 7 +- .../056-add-layer-specific-view-configs.sql | 4 + tools/postgres/schema.sql | 4 +- .../knossos/KnossosDataFormat.scala | 25 ++--- .../knossos/KnossosDataLayers.scala | 7 +- .../dataformats/wkw/WKWDataFormat.scala | 40 ++++---- .../dataformats/wkw/WKWDataLayers.scala | 7 +- .../models/datasource/DataLayer.scala | 79 +++++---------- .../models/datasource/DataSource.scala | 21 +--- .../models/datasource/InboxDataSource.scala | 5 +- .../volume/FallbackLayerAdapter.scala | 2 + .../tracings/volume/VolumeTracingLayer.scala | 4 +- 17 files changed, 189 insertions(+), 257 deletions(-) diff --git a/app/controllers/ConfigurationController.scala b/app/controllers/ConfigurationController.scala index 431bc5b7cc7..440caea6ea7 100755 --- a/app/controllers/ConfigurationController.scala +++ b/app/controllers/ConfigurationController.scala @@ -3,15 +3,14 @@ package controllers import com.mohiva.play.silhouette.api.Silhouette import com.scalableminds.util.accesscontext.GlobalAccessContext import com.scalableminds.util.tools.Fox -import com.scalableminds.webknossos.datastore.models.datasource.Category import javax.inject.Inject import models.binary.{DataSet, DataSetDAO, DataSetService} -import models.configuration.{DataSetConfiguration, DataSetConfigurationDefaults, DataSetLayerId, UserConfiguration} +import models.configuration.{DataSetConfigurationService, UserConfiguration} import models.user.{UserDataSetConfigurationDAO, UserDataSetLayerConfigurationDAO, UserService} import oxalis.security.WkEnv import play.api.i18n.Messages +import play.api.libs.json.JsObject import play.api.libs.json.Json._ -import play.api.libs.json.{JsObject, JsValue} import play.api.mvc.PlayBodyParsers import scala.concurrent.ExecutionContext @@ -22,7 +21,7 @@ class ConfigurationController @Inject()( dataSetDAO: DataSetDAO, userDataSetConfigurationDAO: UserDataSetConfigurationDAO, userDataSetLayerConfigurationDAO: UserDataSetLayerConfigurationDAO, - dataSetConfigurationDefaults: DataSetConfigurationDefaults, + dataSetConfigurationService: DataSetConfigurationService, sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) extends Controller { @@ -46,33 +45,18 @@ class ConfigurationController @Inject()( def readDataSet(organizationName: String, dataSetName: String) = sil.UserAwareAction.async(validateJson[List[String]]) { implicit request => - request.identity.toFox.flatMap { user => - for { - configurationJson: JsValue <- userDataSetConfigurationDAO.findOneForUserAndDataset(user._id, dataSetName) - initialConfigurationMap = configurationJson.validate[Map[String, JsValue]].getOrElse(Map.empty) - dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) - dataSource <- dataSetService.dataSourceFor(dataSet) - dataSetLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) - allLayerIds = dataSetLayers.map(dl => DataSetLayerId(dl.name, dl.category == Category.segmentation)) ++ request.body - .map(DataSetLayerId(_, true)) - layerConfigJson <- userDataSetLayerConfigurationDAO.findAllByLayerNameForUserAndDataset( - allLayerIds.map(_.name), - user._id, - dataSetName) - layerSourceDefaultViewConfigs = dataSetConfigurationDefaults.getAllLayerSourceDefaultViewConfigForDataSet( - dataSetLayers) - layerSettings = dataSetConfigurationDefaults.layerConfigurationOrDefaults(allLayerIds, - layerConfigJson, - layerSourceDefaultViewConfigs) - } yield dataSetConfigurationDefaults.buildCompleteConfig(initialConfigurationMap, layerSettings) - }.orElse( + request.identity.toFox + .flatMap(user => + dataSetConfigurationService + .getDataSetConfigurationForUserAndDataset(request.body, user, dataSetName, organizationName)) + .orElse( for { dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) - config <- dataSetConfigurationDefaults.constructInitialDefaultForDataset(dataSet, request.body) + config <- dataSetConfigurationService.constructInitialDefaultForDataset(dataSet, request.body) } yield config ) - .getOrElse(dataSetConfigurationDefaults.constructInitialDefaultForLayers(List())) - .map(configuration => Ok(toJson(configuration.configuration))) + .getOrElse(dataSetConfigurationService.constructInitialDefaultForLayers(List())) + .map(configuration => Ok(toJson(configuration))) } def updateDataSet(organizationName: String, dataSetName: String) = @@ -85,7 +69,7 @@ class ConfigurationController @Inject()( _ <- userService.updateDataSetConfiguration(request.identity, dataSetName, organizationName, - DataSetConfiguration(dataSetConf), + dataSetConf, layerConf) } yield { JsonOk(Messages("user.configuration.dataset.updated")) @@ -94,12 +78,12 @@ class ConfigurationController @Inject()( def readDataSetDefault(organizationName: String, dataSetName: String) = sil.SecuredAction.async { implicit request => dataSetDAO.findOneByNameAndOrganization(dataSetName, request.identity._organization).flatMap { dataSet: DataSet => - dataSet.defaultConfiguration match { + dataSet.adminDefaultViewConfiguration match { case Some(c) => Fox.successful( - Ok(toJson(dataSetConfigurationDefaults.configurationOrDefaults(c, dataSet.sourceDefaultConfiguration)))) + Ok(toJson(dataSetConfigurationService.configurationOrDefaults(c, dataSet.defaultViewConfiguration)))) case _ => - dataSetConfigurationDefaults.constructInitialDefaultForDataset(dataSet).map(c => Ok(toJson(c.configuration))) + dataSetConfigurationService.constructInitialDefaultForDataset(dataSet).map(c => Ok(toJson(c))) } } } @@ -111,7 +95,7 @@ class ConfigurationController @Inject()( _ <- dataSetService.isEditableBy(dataset, Some(request.identity)) ?~> "notAllowed" ~> FORBIDDEN jsConfiguration <- request.body.asOpt[JsObject] ?~> "user.configuration.dataset.invalid" conf = jsConfiguration.fields.toMap - _ <- dataSetDAO.updateDefaultConfigurationByName(dataSetName, DataSetConfiguration(conf)) + _ <- dataSetDAO.updateDefaultConfigurationByName(dataset._id, conf) } yield { JsonOk(Messages("user.configuration.dataset.updated")) } diff --git a/app/controllers/DataSetController.scala b/app/controllers/DataSetController.scala index 37391924028..39397ce43a3 100755 --- a/app/controllers/DataSetController.scala +++ b/app/controllers/DataSetController.scala @@ -67,10 +67,10 @@ class DataSetController @Inject()(userService: UserService, case Some(a) => Fox.successful(a) case _ => { - val defaultCenterOpt = dataSet.defaultConfiguration.flatMap(c => - c.configuration.get("position").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Point3D]))) - val defaultZoomOpt = dataSet.defaultConfiguration.flatMap(c => - c.configuration.get("zoom").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Double]))) + val defaultCenterOpt = dataSet.adminDefaultViewConfiguration.flatMap(c => + c.get("position").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Point3D]))) + val defaultZoomOpt = dataSet.adminDefaultViewConfiguration.flatMap(c => + c.get("zoom").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Double]))) dataSetService .clientFor(dataSet) .flatMap( diff --git a/app/models/binary/DataSet.scala b/app/models/binary/DataSet.scala index 7687ebda320..9394341dad5 100755 --- a/app/models/binary/DataSet.scala +++ b/app/models/binary/DataSet.scala @@ -1,36 +1,26 @@ package models.binary -import com.scalableminds.util.geometry.{BoundingBox, Point3D, Scale} import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} +import com.scalableminds.util.geometry.{BoundingBox, Point3D, Scale} import com.scalableminds.util.tools.{Fox, FoxImplicits, JsonHelper} +import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration +import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration +import com.scalableminds.webknossos.datastore.models.datasource.inbox.{InboxDataSourceLike => InboxDataSource} import com.scalableminds.webknossos.datastore.models.datasource.{ AbstractDataLayer, AbstractSegmentationLayer, Category, - ColorLayerViewConfiguration, - DataSourceId, ElementClass, - GenericDataSource, - LayerViewConfiguration, - SegmentationLayerViewConfiguration, - ViewConfiguration, DataLayerLike => DataLayer } -import com.scalableminds.webknossos.datastore.models.datasource.inbox.{ - UnusableDataSource, - InboxDataSourceLike => InboxDataSource -} import com.scalableminds.webknossos.schema.Tables._ import javax.inject.Inject -import models.configuration.DataSetConfiguration import models.team._ -import play.api.i18n.Messages import play.api.libs.json._ import play.utils.UriEncoding import slick.jdbc.PostgresProfile.api._ import slick.jdbc.TransactionIsolation.Serializable import slick.lifted.Rep -import slick.sql import utils.{ObjectId, SQLClient, SQLDAO, SimpleSQLDAO} import scala.concurrent.ExecutionContext @@ -41,8 +31,8 @@ case class DataSet( _organization: ObjectId, _publication: Option[ObjectId], inboxSourceHash: Option[Int], - sourceDefaultConfiguration: Option[ViewConfiguration] = None, - defaultConfiguration: Option[DataSetConfiguration] = None, + defaultViewConfiguration: Option[DataSetViewConfiguration] = None, + adminDefaultViewConfiguration: Option[DataSetViewConfiguration] = None, description: Option[String] = None, displayName: Option[String] = None, isPublic: Boolean, @@ -87,9 +77,9 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, for { scale <- parseScaleOpt(r.scale) sourceDefaultConfigurationOpt <- Fox.runOptional(r.sourcedefaultconfiguration)( - JsonHelper.parseJsonToFox[ViewConfiguration](_)) + JsonHelper.parseJsonToFox[DataSetViewConfiguration](_)) defaultConfigurationOpt <- Fox.runOptional(r.defaultconfiguration)( - JsonHelper.parseJsonToFox[DataSetConfiguration](_)) + JsonHelper.parseJsonToFox[DataSetViewConfiguration](_)) details <- Fox.runOptional(r.details)(JsonHelper.parseJsonToFox[JsObject](_)) } yield { DataSet( @@ -242,17 +232,17 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, } yield () } - def updateDefaultConfigurationByName(name: String, configuration: DataSetConfiguration)( + def updateDefaultConfigurationByName(datasetId: ObjectId, configuration: DataSetViewConfiguration)( implicit ctx: DBAccessContext): Fox[Unit] = for { _ <- run(sqlu"""update webknossos.dataSets set defaultConfiguration = '#${sanitize(Json.toJson(configuration).toString)}' - where name = ${name}""") + where _id = $datasetId""") } yield () def insertOne(d: DataSet)(implicit ctx: DBAccessContext): Fox[Unit] = { - val defaultConfiguration: Option[String] = d.defaultConfiguration.map(c => Json.toJson(c.configuration).toString) - val sourceDefaultConfiguration: Option[String] = d.sourceDefaultConfiguration.map(Json.toJson(_).toString) + val defaultConfiguration: Option[String] = d.adminDefaultViewConfiguration.map(Json.toJson(_).toString) + val sourceDefaultConfiguration: Option[String] = d.defaultViewConfiguration.map(Json.toJson(_).toString) val details: Option[String] = d.details.map(_.toString) for { _ <- run( @@ -365,7 +355,7 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: resolutions <- Fox.fillOption(standinResolutions)( dataSetResolutionsDAO.findDataResolutionForLayer(dataSetId, row.name) ?~> "Could not find resolution for layer") defaultViewConfigurationOpt <- Fox.runOptional(row.defaultviewconfiguration)( - JsonHelper.parseJsonToFox[ColorLayerViewConfiguration](_)) + JsonHelper.parseJsonToFox[LayerViewConfiguration](_)) } yield { (row.largestsegmentid, row.mappings) match { case (Some(segmentId), Some(mappings)) => @@ -379,7 +369,7 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: elementClass, segmentId, if (mappingsAsSet.isEmpty) None else Some(mappingsAsSet), - defaultViewConfigurationOpt.map(SegmentationLayerViewConfiguration.from) + defaultViewConfigurationOpt )) case (None, None) => Fox.successful( @@ -421,17 +411,19 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: layer match { case s: AbstractSegmentationLayer => { val mappings = s.mappings.getOrElse(Set()) - sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, largestSegmentId, mappings, defaultViewConfiguration) + sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, largestSegmentId, mappings, defaultViewConfiguration, inDBdefaultViewConfiguration) values(${_dataSet.id}, ${s.name}, '#${s.category.toString}', '#${s.elementClass.toString}', '#${writeStructTuple(s.boundingBox.toSql.map(_.toString))}', ${s.largestSegmentId}, '#${writeArrayTuple( mappings.map(sanitize(_)).toList)}', #${optionLiteral( - s.defaultViewConfiguration.map(d => Json.toJson(d).toString))})""" + s.defaultViewConfiguration.map(d => Json.toJson(d).toString))}, #${optionLiteral( + s.adminViewConfiguration.map(d => Json.toJson(d).toString))})""" } case d: AbstractDataLayer => { - sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, defaultViewConfiguration) + sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, defaultViewConfiguration, inDBdefaultViewConfiguration) values(${_dataSet.id}, ${d.name}, '#${d.category.toString}', '#${d.elementClass.toString}', '#${writeStructTuple(d.boundingBox.toSql.map(_.toString))}', #${optionLiteral( - d.defaultViewConfiguration.map(d => Json.toJson(d).toString))})""" + d.defaultViewConfiguration.map(d => Json.toJson(d).toString))}, #${optionLiteral( + d.adminViewConfiguration.map(d => Json.toJson(d).toString))})""" } case _ => throw new Exception("DataLayer type mismatch") } diff --git a/app/models/configuration/DataSetConfiguration.scala b/app/models/configuration/DataSetConfiguration.scala index f4b056551b5..a08e4260faf 100644 --- a/app/models/configuration/DataSetConfiguration.scala +++ b/app/models/configuration/DataSetConfiguration.scala @@ -1,37 +1,65 @@ package models.configuration -import com.scalableminds.util.accesscontext.DBAccessContext +import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} import com.scalableminds.util.tools.Fox -import com.scalableminds.webknossos.datastore.models.datasource.{Category, DataLayerLike, ViewConfiguration} +import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration +import com.scalableminds.webknossos.datastore.models.datasource.{Category, DataLayerLike} import javax.inject.Inject -import models.binary.{DataSet, DataSetService} +import models.binary.{DataSet, DataSetDAO, DataSetService} +import models.user.{User, UserDataSetConfigurationDAO, UserDataSetLayerConfigurationDAO} import play.api.libs.json._ +import utils.ObjectId case class DataSetLayerId(name: String, isSegmentationLayer: Boolean) object DataSetLayerId { implicit val dataSetLayerId = Json.format[DataSetLayerId] } -case class DataSetConfiguration(configuration: Map[String, JsValue]) +//case class DataSetConfiguration(configuration: Map[String, JsValue]) -object DataSetConfiguration { implicit val dataSetConfigurationFormat = Json.format[DataSetConfiguration] } +class DataSetConfigurationService @Inject()(dataSetService: DataSetService, + userDataSetConfigurationDAO: UserDataSetConfigurationDAO, + userDataSetLayerConfigurationDAO: UserDataSetLayerConfigurationDAO, + dataSetDAO: DataSetDAO) { -class DataSetConfigurationDefaults @Inject()(dataSetService: DataSetService) { + def getDataSetConfigurationForUserAndDataset( + requestedVolumeIds: List[String], + user: User, + dataSetName: String, + organizationName: String)(implicit ctx: DBAccessContext): Fox[DataSetViewConfiguration] = { + def getLayerConfigurations(dataSetLayers: List[DataLayerLike], requestedVolumeIds: List[String]) = { + val allLayerNames = dataSetLayers.map(_.name) ++ requestedVolumeIds + userDataSetLayerConfigurationDAO.findAllByLayerNameForUserAndDataset(allLayerNames, user._id, dataSetName).map { + layerConfigJson => + val layerSourceDefaultViewConfigs = getAllLayerSourceDefaultViewConfigForDataSet(dataSetLayers) + mergeLayerConfigurations(allLayerNames, layerConfigJson, layerSourceDefaultViewConfigs) + } + } + for { + configurationJson: JsValue <- userDataSetConfigurationDAO.findOneForUserAndDataset(user._id, dataSetName) + dataSetConfiguration = configurationJson.validate[Map[String, JsValue]].getOrElse(Map.empty) + + dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) + dataSource <- dataSetService.dataSourceFor(dataSet) + dataSetLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) + layerConfigurations <- getLayerConfigurations(dataSetLayers, requestedVolumeIds) + } yield buildCompleteDataSetConfiguration(dataSetConfiguration, layerConfigurations) + } def constructInitialDefaultForDataset(dataSet: DataSet, requestedVolumeIds: List[String] = List())( - implicit ctx: DBAccessContext): Fox[DataSetConfiguration] = + implicit ctx: DBAccessContext): Fox[DataSetViewConfiguration] = for { dataSource <- dataSetService.dataSourceFor(dataSet) dataLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) initialConfig = constructInitialDefaultForLayers( dataLayers.map(dl => (dl.name, dl.category)) ++ requestedVolumeIds.map((_, Category.segmentation)), dataLayers.map(_.defaultViewConfiguration.map(_.toMap)) ++ requestedVolumeIds.map(_ => None) - ).configuration - sourceDefaultConfig = dataSet.sourceDefaultConfiguration.map(_.toMap).getOrElse(Map.empty) - defaultConfig = dataSet.defaultConfiguration.map(_.configuration).getOrElse(Map.empty) - } yield DataSetConfiguration(initialConfig ++ sourceDefaultConfig ++ defaultConfig) + ) + sourceDefaultConfig = dataSet.defaultViewConfiguration.map(_.toMap).getOrElse(Map.empty) + defaultConfig = dataSet.adminDefaultViewConfiguration.getOrElse(Map.empty) + } yield initialConfig ++ sourceDefaultConfig ++ defaultConfig def constructInitialDefaultForLayers( layers: List[(String, Category.Value)], - layerDefaults: List[Option[Map[String, JsValue]]] = List.empty): DataSetConfiguration = { + layerDefaults: List[Option[Map[String, JsValue]]] = List.empty): DataSetViewConfiguration = { val layerValues = Json.toJson( layers .zipAll(layerDefaults, ("", Category.color), None) @@ -44,39 +72,30 @@ class DataSetConfigurationDefaults @Inject()(dataSetService: DataSetService) { } .toMap) - DataSetConfiguration( - Map( - "fourBit" -> JsBoolean(false), - "interpolation" -> JsBoolean(true), - "highlightHoveredCellId" -> JsBoolean(true), - "renderMissingDataBlack" -> JsBoolean(true), - "layers" -> layerValues - ) + Map( + "fourBit" -> JsBoolean(false), + "interpolation" -> JsBoolean(true), + "highlightHoveredCellId" -> JsBoolean(true), + "renderMissingDataBlack" -> JsBoolean(true), + "layers" -> layerValues ) } - def configurationOrDefaults(configuration: DataSetConfiguration, - sourceDefaultConfiguration: Option[ViewConfiguration] = None): Map[String, JsValue] = - constructInitialDefaultForLayers(List()).configuration ++ - sourceDefaultConfiguration.map(_.toMap).getOrElse(Map.empty) ++ - configuration.configuration - - def layerConfigurationOrDefaults(requestedLayer: List[DataSetLayerId], - existingLayerConfiguration: Map[String, JsValue], - sourceDefaultConfiguration: Map[String, JsValue]) = - requestedLayer.map { - case DataSetLayerId(name, isSegmentationLayer) => - (name, - existingLayerConfiguration.getOrElse( - name, - sourceDefaultConfiguration.getOrElse(name, - Json.toJson( - if (isSegmentationLayer) initialDefaultPerSegmentationLayer - else initialDefaultPerColorLayer)))) + def configurationOrDefaults( + configuration: DataSetViewConfiguration, + sourceDefaultConfiguration: Option[DataSetViewConfiguration] = None): Map[String, JsValue] = + constructInitialDefaultForLayers(List()) ++ sourceDefaultConfiguration.getOrElse(Map.empty) ++ configuration + + def mergeLayerConfigurations(allLayerNames: List[String], + existingLayerConfiguration: Map[String, JsValue], + sourceDefaultConfiguration: Map[String, JsValue]) = + allLayerNames.flatMap { name => + existingLayerConfiguration.get(name).orElse(sourceDefaultConfiguration.get(name)).map((name, _)) }.toMap - def buildCompleteConfig(initialConfiguration: Map[String, JsValue], layerConfigurations: Map[String, JsValue]) = - DataSetConfiguration(initialConfiguration + ("layers" -> Json.toJson(layerConfigurations))) + def buildCompleteDataSetConfiguration(dataSetConfiguration: Map[String, JsValue], + layerConfigurations: Map[String, JsValue]): DataSetViewConfiguration = + dataSetConfiguration + ("layers" -> Json.toJson(layerConfigurations)) def getAllLayerSourceDefaultViewConfigForDataSet(dataLayers: List[DataLayerLike]): Map[String, JsValue] = dataLayers.flatMap(dl => dl.defaultViewConfiguration.map(c => (dl.name, Json.toJson(c.toMap)))).toMap diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 3cd42b9d9bc..1c66aaa3d6a 100755 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -1,13 +1,13 @@ package models.user import com.mohiva.play.silhouette.api.util.PasswordInfo -import com.mohiva.play.silhouette.api.{AuthInfo, Identity, LoginInfo} +import com.mohiva.play.silhouette.api.{Identity, LoginInfo} import com.scalableminds.util.accesscontext._ import com.scalableminds.util.tools.{Fox, FoxImplicits, JsonHelper} import com.scalableminds.webknossos.schema.Tables._ import javax.inject.Inject import models.binary.DataSetDAO -import models.configuration.{DataSetConfiguration, UserConfiguration} +import models.configuration.UserConfiguration import models.team._ import play.api.libs.json._ import slick.jdbc.PostgresProfile.api._ @@ -307,14 +307,6 @@ class UserExperiencesDAO @Inject()(sqlClient: SQLClient, userDAO: UserDAO)(impli class UserDataSetConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserDAO, dataSetDAO: DataSetDAO)( implicit ec: ExecutionContext) extends SimpleSQLDAO(sqlClient) { - - def findAllForUser(userId: ObjectId)(implicit ctx: DBAccessContext): Fox[Map[ObjectId, JsValue]] = - for { - rows <- run(UserDatasetconfigurations.filter(_._User === userId.id).result) - } yield { - rows.map(r => (ObjectId(r._Dataset), Json.parse(r.configuration).as[JsValue])).toMap - } - def findOneForUserAndDataset(userId: ObjectId, dataSetName: String)(implicit ctx: DBAccessContext): Fox[JsValue] = for { rows <- run(sql"""select c.configuration @@ -343,32 +335,6 @@ class UserDataSetConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserD retryIfErrorContains = List(transactionSerializationError) ) } yield () - - def insertDatasetConfigurationsFor(userId: ObjectId, configurations: Map[String, DataSetConfiguration])( - implicit ctx: DBAccessContext): Fox[Unit] = - for { - _ <- Fox.combined(configurations.map { - case (dataSetName, configuration) => - insertDatasetConfiguration(userId, dataSetName, configuration.configuration) - }.toList) - } yield () - - private def insertDatasetConfiguration(userId: ObjectId, dataSetName: String, configuration: Map[String, JsValue])( - implicit ctx: DBAccessContext): Fox[Unit] = - for { - user <- userDAO.findOne(userId) - dataSet <- dataSetDAO.findOneByNameAndOrganization(dataSetName, user._organization) - _ <- insertDatasetConfiguration(userId, dataSet._id, configuration) - } yield () - - private def insertDatasetConfiguration(userId: ObjectId, dataSetId: ObjectId, configuration: Map[String, JsValue])( - implicit ctx: DBAccessContext): Fox[Unit] = - for { - _ <- userDAO.assertUpdateAccess(userId) - _ <- run(sqlu"""insert into webknossos.user_dataSetConfigurations(_user, _dataSet, configuration) - values ('#${sanitize(configuration.toString)}', ${userId} and _dataSet = ${dataSetId})""") - } yield () - } class UserDataSetLayerConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserDAO)(implicit ec: ExecutionContext) diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index e62bc5927b8..c7a3486e8e0 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -10,9 +10,10 @@ import com.scalableminds.util.mail.Send import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} import com.scalableminds.util.security.SCrypt import com.scalableminds.util.tools.{Fox, FoxImplicits} +import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration import javax.inject.Inject import models.binary.DataSetDAO -import models.configuration.{DataSetConfiguration, UserConfiguration} +import models.configuration.UserConfiguration import models.team._ import oxalis.mail.DefaultMails import oxalis.security.TokenDAO @@ -147,7 +148,7 @@ class UserService @Inject()(conf: WkConf, user: User, dataSetName: String, organizationName: String, - dataSetConfiguration: DataSetConfiguration, + dataSetConfiguration: DataSetViewConfiguration, layerConfiguration: Option[JsValue])(implicit ctx: DBAccessContext, m: MessagesProvider) = for { dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName) ?~> Messages( @@ -163,7 +164,7 @@ class UserService @Inject()(conf: WkConf, } _ <- userDataSetConfigurationDAO.updateDatasetConfigurationForUserAndDataset(user._id, dataSet._id, - dataSetConfiguration.configuration) + dataSetConfiguration) } yield () def updateLastTaskTypeId(user: User, lastTaskTypeId: Option[String])(implicit ctx: DBAccessContext) = diff --git a/conf/evolutions/056-add-layer-specific-view-configs.sql b/conf/evolutions/056-add-layer-specific-view-configs.sql index 313ed6d2dc4..da1b1ecb571 100644 --- a/conf/evolutions/056-add-layer-specific-view-configs.sql +++ b/conf/evolutions/056-add-layer-specific-view-configs.sql @@ -30,6 +30,10 @@ UPDATE webknossos.user_dataSetConfigurations SET configuration = configuration - 'quality' WHERE configuration ? 'quality'; +ALTER TABLE webknossos.dataSet_layers ADD COLUMN adminViewConfiguration JSONB; +ALTER TABLE webknossos.dataSet_layers ADD CONSTRAINT adminViewConfigurationIsJsonObject CHECK(jsonb_typeof(adminViewConfiguration) = 'object'); + +ALTER TABLE UPDATE webknossos.releaseInformation SET schemaVersion = 56; diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index 48ba88815f9..bd9b032e560 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -124,9 +124,11 @@ CREATE TABLE webknossos.dataSet_layers( boundingBox webknossos.BOUNDING_BOX NOT NULL, largestSegmentId BIGINT, mappings VARCHAR(256)[], + sourceDefaultViewConfiguration JSONB, defaultViewConfiguration JSONB, PRIMARY KEY(_dataSet, name), - CONSTRAINT defaultViewConfigurationIsJsonObject CHECK(jsonb_typeof(defaultViewConfiguration) = 'object') + CONSTRAINT defaultViewConfigurationIsJsonObject CHECK(jsonb_typeof(defaultViewConfiguration) = 'object'), + CONSTRAINT sourceDefaultViewConfigurationIsJsonObject CHECK(jsonb_typeof(sourceDefaultViewConfiguration) = 'object') ); CREATE TABLE webknossos.dataSet_allowedTeams( diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataFormat.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataFormat.scala index 0ab45a7db9a..c330ffb73ec 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataFormat.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataFormat.scala @@ -3,14 +3,7 @@ package com.scalableminds.webknossos.datastore.dataformats.knossos import java.io.File import java.nio.file.Path -import com.scalableminds.webknossos.datastore.models.datasource.{ - Category, - ColorLayerViewConfiguration, - DataLayer, - ElementClass, - SegmentationLayer, - SegmentationLayerViewConfiguration -} +import com.scalableminds.webknossos.datastore.models.datasource.{Category, DataLayer, ElementClass, SegmentationLayer} import com.scalableminds.util.geometry.{BoundingBox, Point3D} import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.ExtendedTypes._ @@ -30,7 +23,7 @@ object KnossosDataFormat extends DataSourceImporter { case _ => None } - val defaultViewConfiguration = previous.flatMap(_.defaultViewConfiguration) + val inDBdefaultViewConfiguration = previous.flatMap(_.adminViewConfiguration) (for { elementClass <- guessElementClass(baseDir) @@ -44,14 +37,14 @@ object KnossosDataFormat extends DataSourceImporter { case Some(l: SegmentationLayer) => l.largestSegmentId case _ => SegmentationLayer.defaultLargestSegmentId } - val defaultVC = defaultViewConfiguration.map(SegmentationLayerViewConfiguration.from) - KnossosSegmentationLayer(name, sections, elementClass, mappings, largestSegmentId, defaultVC) + KnossosSegmentationLayer(name, + sections, + elementClass, + mappings, + largestSegmentId, + inDBdefaultViewConfiguration) case _ => - KnossosDataLayer(name, - category, - sections, - elementClass, - defaultViewConfiguration.map(ColorLayerViewConfiguration.from)) + KnossosDataLayer(name, category, sections, elementClass, inDBdefaultViewConfiguration) } }).passFailure { f => report.error(layer => s"Error processing layer '$layer' - ${f.msg}") diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataLayers.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataLayers.scala index 503b367b857..d6e8662fffa 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataLayers.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataLayers.scala @@ -3,6 +3,7 @@ package com.scalableminds.webknossos.datastore.dataformats.knossos import com.scalableminds.webknossos.datastore.models.CubePosition import com.scalableminds.webknossos.datastore.models.datasource._ import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration import play.api.libs.json._ case class KnossosSection(name: String, resolutions: List[Either[Int, Point3D]], boundingBox: BoundingBox) { @@ -43,7 +44,8 @@ case class KnossosDataLayer( category: Category.Value, sections: List[KnossosSection], elementClass: ElementClass.Value, - defaultViewConfiguration: Option[ColorLayerViewConfiguration] = None + defaultViewConfiguration: Option[LayerViewConfiguration] = None, + adminViewConfiguration: Option[LayerViewConfiguration] = None ) extends KnossosLayer object KnossosDataLayer { @@ -56,7 +58,8 @@ case class KnossosSegmentationLayer( elementClass: ElementClass.Value, mappings: Option[Set[String]], largestSegmentId: Long, - defaultViewConfiguration: Option[SegmentationLayerViewConfiguration] = None + defaultViewConfiguration: Option[LayerViewConfiguration] = None, + adminViewConfiguration: Option[LayerViewConfiguration] = None ) extends SegmentationLayer with KnossosLayer diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala index 0e9e2845e57..7d6b96318d4 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala @@ -2,13 +2,7 @@ package com.scalableminds.webknossos.datastore.dataformats.wkw import java.nio.file.Path -import com.scalableminds.webknossos.datastore.models.datasource.{ - Category, - ColorLayerViewConfiguration, - DataLayer, - SegmentationLayer, - SegmentationLayerViewConfiguration -} +import com.scalableminds.webknossos.datastore.models.datasource.{Category, DataLayer, SegmentationLayer} import com.scalableminds.util.geometry.{BoundingBox, Point3D} import com.scalableminds.util.io.PathUtils import com.scalableminds.util.tools.ExtendedTypes._ @@ -30,7 +24,7 @@ object WKWDataFormat extends DataSourceImporter with WKWDataFormatHelper { .map(_.boundingBox) .orElse(guessBoundingBox(baseDir, wkwResolutions.headOption)) .getOrElse(BoundingBox.empty) - val defaultViewConfiguration = previous.flatMap(_.defaultViewConfiguration) + val inDBdefaultViewConfiguration = previous.flatMap(_.adminViewConfiguration) category match { case Category.segmentation => val mappings = exploreMappings(baseDir) @@ -38,20 +32,24 @@ object WKWDataFormat extends DataSourceImporter with WKWDataFormatHelper { case Some(l: SegmentationLayer) => l.largestSegmentId case _ => SegmentationLayer.defaultLargestSegmentId } - WKWSegmentationLayer(name, - boundingBox, - wkwResolutions, - elementClass, - mappings, - largestSegmentId, - defaultViewConfiguration.map(SegmentationLayerViewConfiguration.from)) + WKWSegmentationLayer( + name, + boundingBox, + wkwResolutions, + elementClass, + mappings, + largestSegmentId, + adminViewConfiguration = inDBdefaultViewConfiguration + ) case _ => - WKWDataLayer(name, - category, - boundingBox, - wkwResolutions, - elementClass, - defaultViewConfiguration.map(ColorLayerViewConfiguration.from)) + WKWDataLayer( + name, + category, + boundingBox, + wkwResolutions, + elementClass, + adminViewConfiguration = inDBdefaultViewConfiguration + ) } }).passFailure { f => report.error(layer => s"Error processing layer '$layer' - ${f.msg}") diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataLayers.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataLayers.scala index 1d3f96e5eff..698703ac5ef 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataLayers.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataLayers.scala @@ -1,6 +1,7 @@ package com.scalableminds.webknossos.datastore.dataformats.wkw import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration import com.scalableminds.webknossos.datastore.models.datasource._ import play.api.libs.json.Json @@ -38,7 +39,8 @@ case class WKWDataLayer( boundingBox: BoundingBox, wkwResolutions: List[WKWResolution], elementClass: ElementClass.Value, - defaultViewConfiguration: Option[ColorLayerViewConfiguration] = None + defaultViewConfiguration: Option[LayerViewConfiguration] = None, + adminViewConfiguration: Option[LayerViewConfiguration] = None ) extends WKWLayer object WKWDataLayer { @@ -52,7 +54,8 @@ case class WKWSegmentationLayer( elementClass: ElementClass.Value, mappings: Option[Set[String]], largestSegmentId: Long, - defaultViewConfiguration: Option[SegmentationLayerViewConfiguration] = None + defaultViewConfiguration: Option[LayerViewConfiguration] = None, + adminViewConfiguration: Option[LayerViewConfiguration] = None ) extends SegmentationLayer with WKWLayer diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala index 56e089850b7..00bc63033a7 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataLayer.scala @@ -5,6 +5,7 @@ import com.scalableminds.webknossos.datastore.dataformats.wkw.{WKWDataLayer, WKW import com.scalableminds.webknossos.datastore.dataformats.{BucketProvider, MappingProvider} import com.scalableminds.webknossos.datastore.models.BucketPosition import com.scalableminds.util.geometry.{BoundingBox, Point3D} +import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration import play.api.libs.json._ object DataFormat extends Enumeration { @@ -72,50 +73,10 @@ object ElementClass extends Enumeration { def fromString(s: String): Option[Value] = values.find(_.toString == s) } -trait LayerViewConfiguration { - def color: Option[Point3D] - def alpha: Option[Int] - def intensityRange: Option[(Int, Int)] +object LayerViewConfiguration { + type LayerViewConfiguration = Map[String, JsValue] - def toMap: Map[String, JsValue] - - protected def getSeqFromNameAndValue[T](name: String, value: Option[T])(implicit fmt: Format[T]) = - value.map(v => Seq(name -> Json.toJson(v))).getOrElse(Nil) -} - -case class ColorLayerViewConfiguration(color: Option[Point3D], alpha: Option[Int], intensityRange: Option[(Int, Int)]) - extends LayerViewConfiguration { - - def toMap: Map[String, JsValue] = { - val colorSeq = getSeqFromNameAndValue("color", color) - val alphaSeq = getSeqFromNameAndValue("alpha", alpha) - val intensityRangeSeq = getSeqFromNameAndValue("intensityRange", intensityRange) - (colorSeq ++ alphaSeq ++ intensityRangeSeq).toMap - } -} - -object ColorLayerViewConfiguration { - implicit val colorLayerViewConfigurationFormat: Format[ColorLayerViewConfiguration] = - Json.format[ColorLayerViewConfiguration] - - def from(vc: LayerViewConfiguration): ColorLayerViewConfiguration = - ColorLayerViewConfiguration(vc.color, vc.alpha, vc.intensityRange) -} - -case class SegmentationLayerViewConfiguration(alpha: Option[Int]) extends LayerViewConfiguration { - val color: Option[Point3D] = None - val intensityRange: Option[(Int, Int)] = None - - def toMap: Map[String, JsValue] = - getSeqFromNameAndValue("alpha", alpha).toMap -} - -object SegmentationLayerViewConfiguration { - implicit val segmentationLayerViewConfigurationFormat: Format[SegmentationLayerViewConfiguration] = - Json.format[SegmentationLayerViewConfiguration] - - def from(vc: LayerViewConfiguration): SegmentationLayerViewConfiguration = - SegmentationLayerViewConfiguration(vc.alpha) + implicit val layerViewConfigurationFormat = Format.of[LayerViewConfiguration] } trait DataLayerLike { @@ -137,7 +98,11 @@ trait DataLayerLike { def elementClass: ElementClass.Value + // This is the default from the DataSource JSON. def defaultViewConfiguration: Option[LayerViewConfiguration] + + // This is the default from the DataSet Edit View. + def adminViewConfiguration: Option[LayerViewConfiguration] } object DataLayerLike { @@ -162,7 +127,9 @@ trait SegmentationLayerLike extends DataLayerLike { def mappings: Option[Set[String]] - def defaultViewConfiguration: Option[SegmentationLayerViewConfiguration] + def defaultViewConfiguration: Option[LayerViewConfiguration] + + def adminViewConfiguration: Option[LayerViewConfiguration] } trait DataLayer extends DataLayerLike { @@ -238,18 +205,22 @@ case class AbstractDataLayer( boundingBox: BoundingBox, resolutions: List[Point3D], elementClass: ElementClass.Value, - defaultViewConfiguration: Option[ColorLayerViewConfiguration] = None + defaultViewConfiguration: Option[LayerViewConfiguration] = None, + adminViewConfiguration: Option[LayerViewConfiguration] = None ) extends DataLayerLike object AbstractDataLayer { def from(layer: DataLayerLike): AbstractDataLayer = - AbstractDataLayer(layer.name, - layer.category, - layer.boundingBox, - layer.resolutions, - layer.elementClass, - layer.defaultViewConfiguration.map(ColorLayerViewConfiguration.from)) + AbstractDataLayer( + layer.name, + layer.category, + layer.boundingBox, + layer.resolutions, + layer.elementClass, + layer.defaultViewConfiguration, + layer.adminViewConfiguration + ) implicit val abstractDataLayerFormat = Json.format[AbstractDataLayer] } @@ -262,7 +233,8 @@ case class AbstractSegmentationLayer( elementClass: ElementClass.Value, largestSegmentId: Long, mappings: Option[Set[String]], - defaultViewConfiguration: Option[SegmentationLayerViewConfiguration] = None + defaultViewConfiguration: Option[LayerViewConfiguration] = None, + adminViewConfiguration: Option[LayerViewConfiguration] = None ) extends SegmentationLayerLike object AbstractSegmentationLayer { @@ -276,7 +248,8 @@ object AbstractSegmentationLayer { layer.elementClass, layer.largestSegmentId, layer.mappings, - layer.defaultViewConfiguration + layer.defaultViewConfiguration, + layer.adminViewConfiguration ) implicit val abstractSegmentationLayerFormat = Json.format[AbstractSegmentationLayer] diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataSource.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataSource.scala index dbcc3deb26a..18178ee8ff4 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataSource.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/DataSource.scala @@ -2,6 +2,7 @@ package com.scalableminds.webknossos.datastore.models import com.scalableminds.webknossos.datastore.models.datasource.inbox.GenericInboxDataSource import com.scalableminds.util.geometry.{BoundingBox, Point3D, Scale} +import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration import play.api.libs.json._ package object datasource { @@ -12,27 +13,15 @@ package object datasource { implicit val dataSourceIdFormat: Format[DataSourceId] = Json.format[DataSourceId] } - case class ViewConfiguration(position: Option[Point3D], zoom: Option[Point3D], interpolation: Option[Boolean]) { - - def toMap: Map[String, JsValue] = { - def getSeqFromNameAndValue[T](name: String, value: Option[T])(implicit fmt: Format[T]) = - value.map(v => Seq(name -> Json.toJson(v))).getOrElse(Nil) - - val positionSeq = getSeqFromNameAndValue("position", position) - val zoomSeq = getSeqFromNameAndValue("zoom", zoom) - val interpolationSeq = getSeqFromNameAndValue("interpolation", interpolation) - (positionSeq ++ zoomSeq ++ interpolationSeq).toMap - } - } - - object ViewConfiguration { - implicit val viewConfigurationFormat: Format[ViewConfiguration] = Json.format[ViewConfiguration] + object DataSetViewConfiguration { + type DataSetViewConfiguration = Map[String, JsValue] + implicit val dataSetViewConfigurationFormat: Format[DataSetViewConfiguration] = Format.of[DataSetViewConfiguration] } case class GenericDataSource[+T <: DataLayerLike](id: DataSourceId, dataLayers: List[T], scale: Scale, - defaultViewConfiguration: Option[ViewConfiguration] = None) + defaultViewConfiguration: Option[DataSetViewConfiguration] = None) extends GenericInboxDataSource[T] { val toUsable: Option[GenericDataSource[T]] = Some(this) diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/InboxDataSource.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/InboxDataSource.scala index 44852b78a86..a9cea3e0e2d 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/InboxDataSource.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/datasource/InboxDataSource.scala @@ -1,6 +1,7 @@ package com.scalableminds.webknossos.datastore.models.datasource import com.scalableminds.util.geometry.Scale +import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration import play.api.libs.json.{Format, JsResult, JsValue, Json} package object inbox { @@ -17,7 +18,7 @@ package object inbox { def statusOpt: Option[String] - def defaultViewConfiguration: Option[ViewConfiguration] + def defaultViewConfiguration: Option[DataSetViewConfiguration] } object GenericInboxDataSource { @@ -42,7 +43,7 @@ package object inbox { val statusOpt: Option[String] = Some(status) - val defaultViewConfiguration: Option[ViewConfiguration] = None + val defaultViewConfiguration: Option[DataSetViewConfiguration] = None } object UnusableDataSource { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/FallbackLayerAdapter.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/FallbackLayerAdapter.scala index 3495912c248..0aee49a69dc 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/FallbackLayerAdapter.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/FallbackLayerAdapter.scala @@ -51,4 +51,6 @@ class FallbackLayerAdapter(primary: SegmentationLayer, fallback: SegmentationLay override lazy val mappingProvider: MappingProvider = fallback.mappingProvider val defaultViewConfiguration = primary.defaultViewConfiguration + + val adminViewConfiguration = primary.adminViewConfiguration } diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala index c6ccef6fa5e..8a2c4d26f97 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala @@ -4,6 +4,7 @@ import com.scalableminds.util.geometry.{BoundingBox, Point3D} import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.dataformats.BucketProvider import com.scalableminds.webknossos.datastore.models.BucketPosition +import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration import com.scalableminds.webknossos.datastore.models.datasource._ import com.scalableminds.webknossos.datastore.models.requests.DataReadInstruction import com.scalableminds.webknossos.datastore.storage.DataCubeCache @@ -73,7 +74,8 @@ case class VolumeTracingLayer( elementClass: ElementClass.Value, largestSegmentId: Long, isTemporaryTracing: Boolean = false, - defaultViewConfiguration: Option[SegmentationLayerViewConfiguration] = None + defaultViewConfiguration: Option[LayerViewConfiguration] = None, + adminViewConfiguration: Option[LayerViewConfiguration] = None )(implicit val volumeDataStore: FossilDBClient, implicit val volumeDataCache: TemporaryVolumeDataStore, implicit val temporaryTracingStore: TemporaryTracingStore[VolumeTracing]) From 7818a065e03f1f797c9973c9c7b0aaa8161aaa89 Mon Sep 17 00:00:00 2001 From: Youri K Date: Tue, 13 Oct 2020 17:23:44 +0200 Subject: [PATCH 15/39] rename backend types to match each other --- app/controllers/ConfigurationController.scala | 32 ++--- app/controllers/DataSetController.scala | 4 +- app/models/binary/DataSet.scala | 52 +++++--- app/models/binary/DataSetService.scala | 27 ++-- .../configuration/DataSetConfiguration.scala | 112 ----------------- .../DataSetConfigurationService.scala | 118 ++++++++++++++++++ app/models/user/User.scala | 40 +++--- app/models/user/UserService.scala | 15 ++- .../056-add-layer-specific-view-configs.sql | 43 ++++++- .../libs/dataset_view_settings.schema.js | 38 ++++++ tools/postgres/schema.sql | 20 +-- .../knossos/KnossosDataFormat.scala | 11 +- .../dataformats/wkw/WKWDataFormat.scala | 6 +- 13 files changed, 303 insertions(+), 215 deletions(-) delete mode 100644 app/models/configuration/DataSetConfiguration.scala create mode 100644 app/models/configuration/DataSetConfigurationService.scala create mode 100644 frontend/javascripts/libs/dataset_view_settings.schema.js diff --git a/app/controllers/ConfigurationController.scala b/app/controllers/ConfigurationController.scala index 440caea6ea7..a6570a55122 100755 --- a/app/controllers/ConfigurationController.scala +++ b/app/controllers/ConfigurationController.scala @@ -9,7 +9,7 @@ import models.configuration.{DataSetConfigurationService, UserConfiguration} import models.user.{UserDataSetConfigurationDAO, UserDataSetLayerConfigurationDAO, UserService} import oxalis.security.WkEnv import play.api.i18n.Messages -import play.api.libs.json.JsObject +import play.api.libs.json.{JsObject} import play.api.libs.json.Json._ import play.api.mvc.PlayBodyParsers @@ -19,8 +19,6 @@ class ConfigurationController @Inject()( userService: UserService, dataSetService: DataSetService, dataSetDAO: DataSetDAO, - userDataSetConfigurationDAO: UserDataSetConfigurationDAO, - userDataSetLayerConfigurationDAO: UserDataSetLayerConfigurationDAO, dataSetConfigurationService: DataSetConfigurationService, sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) extends Controller { @@ -48,14 +46,11 @@ class ConfigurationController @Inject()( request.identity.toFox .flatMap(user => dataSetConfigurationService - .getDataSetConfigurationForUserAndDataset(request.body, user, dataSetName, organizationName)) + .getDataSetViewConfigurationForUserAndDataset(request.body, user, dataSetName, organizationName)) .orElse( - for { - dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) - config <- dataSetConfigurationService.constructInitialDefaultForDataset(dataSet, request.body) - } yield config + dataSetConfigurationService.getDataSetViewConfigurationForDataset(request.body, dataSetName, organizationName) ) - .getOrElse(dataSetConfigurationService.constructInitialDefaultForLayers(List())) + .getOrElse(Map.empty) .map(configuration => Ok(toJson(configuration))) } @@ -77,25 +72,18 @@ class ConfigurationController @Inject()( } def readDataSetDefault(organizationName: String, dataSetName: String) = sil.SecuredAction.async { implicit request => - dataSetDAO.findOneByNameAndOrganization(dataSetName, request.identity._organization).flatMap { dataSet: DataSet => - dataSet.adminDefaultViewConfiguration match { - case Some(c) => - Fox.successful( - Ok(toJson(dataSetConfigurationService.configurationOrDefaults(c, dataSet.defaultViewConfiguration)))) - case _ => - dataSetConfigurationService.constructInitialDefaultForDataset(dataSet).map(c => Ok(toJson(c))) - } - } + dataSetConfigurationService + .getCompleteAdminViewConfiguration(dataSetName, organizationName) + .map(configuration => Ok(toJson(configuration))) } def updateDataSetDefault(organizationName: String, dataSetName: String) = sil.SecuredAction.async(parse.json(maxLength = 20480)) { implicit request => for { - dataset <- dataSetDAO.findOneByNameAndOrganization(dataSetName, request.identity._organization) ?~> "dataset.notFound" ~> NOT_FOUND + dataset <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName) ?~> "dataset.notFound" ~> NOT_FOUND _ <- dataSetService.isEditableBy(dataset, Some(request.identity)) ?~> "notAllowed" ~> FORBIDDEN - jsConfiguration <- request.body.asOpt[JsObject] ?~> "user.configuration.dataset.invalid" - conf = jsConfiguration.fields.toMap - _ <- dataSetDAO.updateDefaultConfigurationByName(dataset._id, conf) + jsObject <- request.body.asOpt[JsObject].toFox ?~> "user.configuration.dataset.invalid" + _ <- dataSetConfigurationService.updateAdminViewConfigurationFor(dataset, jsObject.fields.toMap) } yield { JsonOk(Messages("user.configuration.dataset.updated")) } diff --git a/app/controllers/DataSetController.scala b/app/controllers/DataSetController.scala index 39397ce43a3..edc4a20765c 100755 --- a/app/controllers/DataSetController.scala +++ b/app/controllers/DataSetController.scala @@ -67,9 +67,9 @@ class DataSetController @Inject()(userService: UserService, case Some(a) => Fox.successful(a) case _ => { - val defaultCenterOpt = dataSet.adminDefaultViewConfiguration.flatMap(c => + val defaultCenterOpt = dataSet.adminViewConfiguration.flatMap(c => c.get("position").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Point3D]))) - val defaultZoomOpt = dataSet.adminDefaultViewConfiguration.flatMap(c => + val defaultZoomOpt = dataSet.adminViewConfiguration.flatMap(c => c.get("zoom").flatMap(jsValue => JsonHelper.jsResultToOpt(jsValue.validate[Double]))) dataSetService .clientFor(dataSet) diff --git a/app/models/binary/DataSet.scala b/app/models/binary/DataSet.scala index 9394341dad5..ce607da8bb6 100755 --- a/app/models/binary/DataSet.scala +++ b/app/models/binary/DataSet.scala @@ -32,7 +32,7 @@ case class DataSet( _publication: Option[ObjectId], inboxSourceHash: Option[Int], defaultViewConfiguration: Option[DataSetViewConfiguration] = None, - adminDefaultViewConfiguration: Option[DataSetViewConfiguration] = None, + adminViewConfiguration: Option[DataSetViewConfiguration] = None, description: Option[String] = None, displayName: Option[String] = None, isPublic: Boolean, @@ -76,9 +76,9 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, def parse(r: DatasetsRow): Fox[DataSet] = for { scale <- parseScaleOpt(r.scale) - sourceDefaultConfigurationOpt <- Fox.runOptional(r.sourcedefaultconfiguration)( + defaultViewConfigurationOpt <- Fox.runOptional(r.defaultviewconfiguration)( JsonHelper.parseJsonToFox[DataSetViewConfiguration](_)) - defaultConfigurationOpt <- Fox.runOptional(r.defaultconfiguration)( + adminViewConfigurationOpt <- Fox.runOptional(r.adminviewconfiguration)( JsonHelper.parseJsonToFox[DataSetViewConfiguration](_)) details <- Fox.runOptional(r.details)(JsonHelper.parseJsonToFox[JsObject](_)) } yield { @@ -88,8 +88,8 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, ObjectId(r._Organization), r._Publication.map(ObjectId(_)), r.inboxsourcehash, - sourceDefaultConfigurationOpt, - defaultConfigurationOpt, + defaultViewConfigurationOpt, + adminViewConfigurationOpt, r.description, r.displayname, r.ispublic, @@ -232,25 +232,25 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, } yield () } - def updateDefaultConfigurationByName(datasetId: ObjectId, configuration: DataSetViewConfiguration)( + def updateAdminViewConfiguration(datasetId: ObjectId, configuration: DataSetViewConfiguration)( implicit ctx: DBAccessContext): Fox[Unit] = for { _ <- run(sqlu"""update webknossos.dataSets - set defaultConfiguration = '#${sanitize(Json.toJson(configuration).toString)}' + set adminViewConfiguration = '#${sanitize(Json.toJson(configuration).toString)}' where _id = $datasetId""") } yield () def insertOne(d: DataSet)(implicit ctx: DBAccessContext): Fox[Unit] = { - val defaultConfiguration: Option[String] = d.adminDefaultViewConfiguration.map(Json.toJson(_).toString) - val sourceDefaultConfiguration: Option[String] = d.defaultViewConfiguration.map(Json.toJson(_).toString) + val adminViewConfiguration: Option[String] = d.adminViewConfiguration.map(Json.toJson(_).toString) + val defaultViewConfiguration: Option[String] = d.defaultViewConfiguration.map(Json.toJson(_).toString) val details: Option[String] = d.details.map(_.toString) for { _ <- run( - sqlu"""insert into webknossos.dataSets(_id, _dataStore, _organization, _publication, inboxSourceHash, sourceDefaultConfiguration, defaultConfiguration, description, displayName, + sqlu"""insert into webknossos.dataSets(_id, _dataStore, _organization, _publication, inboxSourceHash, defaultViewConfiguration, adminViewConfiguration, description, displayName, isPublic, isUsable, name, scale, status, sharingToken, sortingKey, details, created, isDeleted) values(${d._id.id}, ${d._dataStore}, ${d._organization.id}, #${optionLiteral(d._publication.map(_.id))}, #${optionLiteral(d.inboxSourceHash.map(_.toString))}, #${optionLiteral( - sourceDefaultConfiguration.map(sanitize))}, #${optionLiteral(defaultConfiguration.map(sanitize))}, + defaultViewConfiguration.map(sanitize))}, #${optionLiteral(adminViewConfiguration.map(sanitize))}, ${d.description}, ${d.displayName}, ${d.isPublic}, ${d.isUsable}, ${d.name}, #${optionLiteral(d.scale.map(s => writeScaleLiteral(s)))}, ${d.status .take(1024)}, ${d.sharingToken}, ${new java.sql.Timestamp(d.sortingKey)}, #${optionLiteral( @@ -266,12 +266,12 @@ class DataSetDAO @Inject()(sqlClient: SQLClient, isUsable: Boolean)(implicit ctx: DBAccessContext): Fox[Unit] = for { organization <- organizationDAO.findOneByName(source.id.team) - sourceDefaultConfig: Option[String] = source.defaultViewConfiguration.map(Json.toJson(_).toString) + defaultViewConfiguration: Option[String] = source.defaultViewConfiguration.map(Json.toJson(_).toString) q = sqlu"""update webknossos.dataSets set _dataStore = ${dataStoreName}, _organization = ${organization._id.id}, inboxSourceHash = #${optionLiteral(Some(inboxSourceHash.toString))}, - sourceDefaultConfiguration = #${optionLiteral(sourceDefaultConfig)}, + defaultViewConfiguration = #${optionLiteral(defaultViewConfiguration)}, isUsable = ${isUsable}, scale = #${optionLiteral(source.scaleOpt.map(s => writeScaleLiteral(s)))}, status = ${source.statusOpt.getOrElse("").take(1024)} @@ -351,11 +351,13 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: .fromSQL(parseArrayTuple(row.boundingbox).map(_.toInt)) .toFox ?~> "Could not parse boundingbox" elementClass <- ElementClass.fromString(row.elementclass).toFox ?~> "Could not parse Layer ElementClass" - standinResolutions: Option[List[Point3D]] = if (skipResolutions) Some(List.empty) else None + standinResolutions: Option[List[Point3D]] = if (skipResolutions) Some(List.empty[Point3D]) else None resolutions <- Fox.fillOption(standinResolutions)( dataSetResolutionsDAO.findDataResolutionForLayer(dataSetId, row.name) ?~> "Could not find resolution for layer") defaultViewConfigurationOpt <- Fox.runOptional(row.defaultviewconfiguration)( JsonHelper.parseJsonToFox[LayerViewConfiguration](_)) + adminViewConfigurationOpt <- Fox.runOptional(row.adminviewconfiguration)( + JsonHelper.parseJsonToFox[LayerViewConfiguration](_)) } yield { (row.largestsegmentid, row.mappings) match { case (Some(segmentId), Some(mappings)) => @@ -369,7 +371,8 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: elementClass, segmentId, if (mappingsAsSet.isEmpty) None else Some(mappingsAsSet), - defaultViewConfigurationOpt + defaultViewConfigurationOpt, + adminViewConfigurationOpt )) case (None, None) => Fox.successful( @@ -379,7 +382,8 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: boundingBox, resolutions.sortBy(_.maxDim), elementClass, - defaultViewConfigurationOpt + defaultViewConfigurationOpt, + adminViewConfigurationOpt )) case _ => Fox.failure("Could not match Dataset Layer") } @@ -411,7 +415,7 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: layer match { case s: AbstractSegmentationLayer => { val mappings = s.mappings.getOrElse(Set()) - sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, largestSegmentId, mappings, defaultViewConfiguration, inDBdefaultViewConfiguration) + sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, largestSegmentId, mappings, defaultViewConfiguration, adminViewConfiguration) values(${_dataSet.id}, ${s.name}, '#${s.category.toString}', '#${s.elementClass.toString}', '#${writeStructTuple(s.boundingBox.toSql.map(_.toString))}', ${s.largestSegmentId}, '#${writeArrayTuple( mappings.map(sanitize(_)).toList)}', #${optionLiteral( @@ -419,7 +423,7 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: s.adminViewConfiguration.map(d => Json.toJson(d).toString))})""" } case d: AbstractDataLayer => { - sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, defaultViewConfiguration, inDBdefaultViewConfiguration) + sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, defaultViewConfiguration, adminViewConfiguration) values(${_dataSet.id}, ${d.name}, '#${d.category.toString}', '#${d.elementClass.toString}', '#${writeStructTuple(d.boundingBox.toSql.map(_.toString))}', #${optionLiteral( d.defaultViewConfiguration.map(d => Json.toJson(d).toString))}, #${optionLiteral( @@ -430,7 +434,7 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: def updateLayers(_dataSet: ObjectId, source: InboxDataSource)(implicit ctx: DBAccessContext): Fox[Unit] = { val clearQuery = - sqlu"delete from webknossos.dataset_layers where _dataSet = (select _id from webknossos.dataSets where _id = ${_dataSet.id})" + sqlu"delete from webknossos.dataset_layers where _dataSet = ${_dataSet}" val insertQueries = source.toUsable match { case Some(usable) => usable.dataLayers.map(insertLayerQuery(_dataSet, _)) case None => List() @@ -440,6 +444,16 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: _ <- dataSetResolutionsDAO.updateResolutions(_dataSet, source.toUsable.map(_.dataLayers)) } yield () } + + def updateLayerAdminViewConfiguration(_dataSet: ObjectId, + layerName: String, + adminViewConfiguration: LayerViewConfiguration): Fox[Unit] = { + val q = + sqlu"""update webknossos.dataset_layers + set adminViewConfiguration = '#${sanitize(Json.toJson(adminViewConfiguration).toString)}' + where _dataSet = ${_dataSet} and name = $layerName""" + run(q).map(_ => ()) + } } class DataSetAllowedTeamsDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionContext) diff --git a/app/models/binary/DataSetService.scala b/app/models/binary/DataSetService.scala index 0fa9fff21b4..b78cae67fbe 100644 --- a/app/models/binary/DataSetService.scala +++ b/app/models/binary/DataSetService.scala @@ -80,16 +80,16 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, publication, Some(dataSource.hashCode()), dataSource.defaultViewConfiguration, - None, - None, - None, - false, - dataSource.toUsable.isDefined, - dataSource.id.name, - dataSource.scaleOpt, - None, - dataSource.statusOpt.getOrElse(""), - None, + adminViewConfiguration = None, + description = None, + displayName = None, + isPublic = false, + isUsable = dataSource.toUsable.isDefined, + name = dataSource.id.name, + scale = dataSource.scaleOpt, + sharingToken = None, + status = dataSource.statusOpt.getOrElse(""), + logoUrl = None, details = publication.map(_ => details) )) _ <- dataSetDataLayerDAO.updateLayers(newId, dataSource) @@ -171,6 +171,7 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, dataSource.hashCode, dataSource, dataSource.isUsable)(GlobalAccessContext) + // TODO should we preserve the adminViewConfiguration here? _ <- dataSetDataLayerDAO.updateLayers(foundDataSet._id, dataSource) } yield foundDataSet._id @@ -293,6 +294,12 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, teamsFiltered = teams.filter(team => requestingUser.map(_._organization).contains(team._organization)) } yield teamsFiltered + def allLayersFor(dataSet: DataSet)(implicit ctx: DBAccessContext): Fox[List[DataLayer]] = + for { + dataSource <- dataSourceFor(dataSet) + dataSetLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) + } yield dataSetLayers + def isEditableBy( dataSet: DataSet, userOpt: Option[User], diff --git a/app/models/configuration/DataSetConfiguration.scala b/app/models/configuration/DataSetConfiguration.scala deleted file mode 100644 index a08e4260faf..00000000000 --- a/app/models/configuration/DataSetConfiguration.scala +++ /dev/null @@ -1,112 +0,0 @@ -package models.configuration - -import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} -import com.scalableminds.util.tools.Fox -import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration -import com.scalableminds.webknossos.datastore.models.datasource.{Category, DataLayerLike} -import javax.inject.Inject -import models.binary.{DataSet, DataSetDAO, DataSetService} -import models.user.{User, UserDataSetConfigurationDAO, UserDataSetLayerConfigurationDAO} -import play.api.libs.json._ -import utils.ObjectId - -case class DataSetLayerId(name: String, isSegmentationLayer: Boolean) -object DataSetLayerId { implicit val dataSetLayerId = Json.format[DataSetLayerId] } - -//case class DataSetConfiguration(configuration: Map[String, JsValue]) - -class DataSetConfigurationService @Inject()(dataSetService: DataSetService, - userDataSetConfigurationDAO: UserDataSetConfigurationDAO, - userDataSetLayerConfigurationDAO: UserDataSetLayerConfigurationDAO, - dataSetDAO: DataSetDAO) { - - def getDataSetConfigurationForUserAndDataset( - requestedVolumeIds: List[String], - user: User, - dataSetName: String, - organizationName: String)(implicit ctx: DBAccessContext): Fox[DataSetViewConfiguration] = { - def getLayerConfigurations(dataSetLayers: List[DataLayerLike], requestedVolumeIds: List[String]) = { - val allLayerNames = dataSetLayers.map(_.name) ++ requestedVolumeIds - userDataSetLayerConfigurationDAO.findAllByLayerNameForUserAndDataset(allLayerNames, user._id, dataSetName).map { - layerConfigJson => - val layerSourceDefaultViewConfigs = getAllLayerSourceDefaultViewConfigForDataSet(dataSetLayers) - mergeLayerConfigurations(allLayerNames, layerConfigJson, layerSourceDefaultViewConfigs) - } - } - for { - configurationJson: JsValue <- userDataSetConfigurationDAO.findOneForUserAndDataset(user._id, dataSetName) - dataSetConfiguration = configurationJson.validate[Map[String, JsValue]].getOrElse(Map.empty) - - dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName)(GlobalAccessContext) - dataSource <- dataSetService.dataSourceFor(dataSet) - dataSetLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) - layerConfigurations <- getLayerConfigurations(dataSetLayers, requestedVolumeIds) - } yield buildCompleteDataSetConfiguration(dataSetConfiguration, layerConfigurations) - } - - def constructInitialDefaultForDataset(dataSet: DataSet, requestedVolumeIds: List[String] = List())( - implicit ctx: DBAccessContext): Fox[DataSetViewConfiguration] = - for { - dataSource <- dataSetService.dataSourceFor(dataSet) - dataLayers = dataSource.toUsable.map(d => d.dataLayers).getOrElse(List()) - initialConfig = constructInitialDefaultForLayers( - dataLayers.map(dl => (dl.name, dl.category)) ++ requestedVolumeIds.map((_, Category.segmentation)), - dataLayers.map(_.defaultViewConfiguration.map(_.toMap)) ++ requestedVolumeIds.map(_ => None) - ) - sourceDefaultConfig = dataSet.defaultViewConfiguration.map(_.toMap).getOrElse(Map.empty) - defaultConfig = dataSet.adminDefaultViewConfiguration.getOrElse(Map.empty) - } yield initialConfig ++ sourceDefaultConfig ++ defaultConfig - - def constructInitialDefaultForLayers( - layers: List[(String, Category.Value)], - layerDefaults: List[Option[Map[String, JsValue]]] = List.empty): DataSetViewConfiguration = { - val layerValues = Json.toJson( - layers - .zipAll(layerDefaults, ("", Category.color), None) - .map { - case ((name, category), default) => - category match { - case Category.color => name -> (initialDefaultPerColorLayer ++ default.getOrElse(Map.empty)) - case Category.segmentation => name -> (initialDefaultPerSegmentationLayer ++ default.getOrElse(Map.empty)) - } - } - .toMap) - - Map( - "fourBit" -> JsBoolean(false), - "interpolation" -> JsBoolean(true), - "highlightHoveredCellId" -> JsBoolean(true), - "renderMissingDataBlack" -> JsBoolean(true), - "layers" -> layerValues - ) - } - - def configurationOrDefaults( - configuration: DataSetViewConfiguration, - sourceDefaultConfiguration: Option[DataSetViewConfiguration] = None): Map[String, JsValue] = - constructInitialDefaultForLayers(List()) ++ sourceDefaultConfiguration.getOrElse(Map.empty) ++ configuration - - def mergeLayerConfigurations(allLayerNames: List[String], - existingLayerConfiguration: Map[String, JsValue], - sourceDefaultConfiguration: Map[String, JsValue]) = - allLayerNames.flatMap { name => - existingLayerConfiguration.get(name).orElse(sourceDefaultConfiguration.get(name)).map((name, _)) - }.toMap - - def buildCompleteDataSetConfiguration(dataSetConfiguration: Map[String, JsValue], - layerConfigurations: Map[String, JsValue]): DataSetViewConfiguration = - dataSetConfiguration + ("layers" -> Json.toJson(layerConfigurations)) - - def getAllLayerSourceDefaultViewConfigForDataSet(dataLayers: List[DataLayerLike]): Map[String, JsValue] = - dataLayers.flatMap(dl => dl.defaultViewConfiguration.map(c => (dl.name, Json.toJson(c.toMap)))).toMap - - val initialDefaultPerColorLayer: Map[String, JsValue] = Map( - "brightness" -> JsNumber(0), - "contrast" -> JsNumber(1), - "color" -> Json.arr(255, 255, 255), - "alpha" -> JsNumber(100) - ) - - val initialDefaultPerSegmentationLayer: Map[String, JsValue] = Map("alpha" -> JsNumber(20)) - -} diff --git a/app/models/configuration/DataSetConfigurationService.scala b/app/models/configuration/DataSetConfigurationService.scala new file mode 100644 index 00000000000..a6f11ef4317 --- /dev/null +++ b/app/models/configuration/DataSetConfigurationService.scala @@ -0,0 +1,118 @@ +package models.configuration + +import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} +import com.scalableminds.util.tools.Fox +import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration +import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration +import com.scalableminds.webknossos.datastore.models.datasource.{Category, DataLayerLike} +import javax.inject.Inject +import models.binary.{DataSet, DataSetDAO, DataSetDataLayerDAO, DataSetService} +import models.user.{User, UserDataSetConfigurationDAO, UserDataSetLayerConfigurationDAO} +import play.api.libs.json._ + +import scala.concurrent.ExecutionContext + +class DataSetConfigurationService @Inject()(dataSetService: DataSetService, + userDataSetConfigurationDAO: UserDataSetConfigurationDAO, + userDataSetLayerConfigurationDAO: UserDataSetLayerConfigurationDAO, + dataSetDAO: DataSetDAO, + dataSetDataLayerDAO: DataSetDataLayerDAO)(implicit ec: ExecutionContext) { + def getDataSetViewConfigurationForUserAndDataset( + requestedVolumeIds: List[String], + user: User, + dataSetName: String, + organizationName: String)(implicit ctx: DBAccessContext): Fox[DataSetViewConfiguration] = + for { + dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName) + + dataSetViewConfiguration <- userDataSetConfigurationDAO.findOneForUserAndDataset(user._id, dataSet._id) + + dataSetLayers <- dataSetService.allLayersFor(dataSet) + layerConfigurations <- getLayerConfigurations(dataSetLayers, requestedVolumeIds, dataSet, Some(user)) + } yield buildCompleteDataSetConfiguration(dataSetViewConfiguration, layerConfigurations) + + def getDataSetViewConfigurationForDataset( + requestedVolumeIds: List[String], + dataSetName: String, + organizationName: String)(implicit ctx: DBAccessContext): Fox[DataSetViewConfiguration] = + for { + dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName) + + dataSetViewConfiguration = getDataSetViewConfigurationFromDefaultAndAdmin(dataSet) + + dataSetLayers <- dataSetService.allLayersFor(dataSet) + layerConfigurations <- getLayerConfigurations(dataSetLayers, requestedVolumeIds, dataSet) + } yield buildCompleteDataSetConfiguration(dataSetViewConfiguration, layerConfigurations) + + def getDataSetViewConfigurationFromDefaultAndAdmin(dataSet: DataSet): DataSetViewConfiguration = { + val defaultVC = dataSet.defaultViewConfiguration.getOrElse(Map.empty) + val adminVC = dataSet.adminViewConfiguration.getOrElse(Map.empty) + defaultVC ++ adminVC + } + + def getCompleteAdminViewConfiguration(dataSetName: String, organizationName: String)(implicit ctx: DBAccessContext) = + for { + dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName) + dataSetViewConfiguration = getDataSetViewConfigurationFromDefaultAndAdmin(dataSet) + + dataSetLayers <- dataSetService.allLayersFor(dataSet) + layerConfigurations = getAllLayerAdminViewConfigForDataSet(dataSetLayers).mapValues(Json.toJson(_)) + } yield buildCompleteDataSetConfiguration(dataSetViewConfiguration, layerConfigurations) + + private def mergeLayerConfigurations(allLayerNames: List[String], + existingLayerVCs: Map[String, LayerViewConfiguration], + adminLayerVCs: Map[String, LayerViewConfiguration], + defaultVCs: Map[String, LayerViewConfiguration]): Map[String, JsValue] = + allLayerNames.map { name => + val defaultVC = defaultVCs.getOrElse(name, Map.empty) + val adminVC = adminLayerVCs.getOrElse(name, Map.empty) + val existingVC = existingLayerVCs.getOrElse(name, Map.empty) + (name, Json.toJson(defaultVC ++ adminVC ++ existingVC)) + }.toMap + + private def buildCompleteDataSetConfiguration(dataSetConfiguration: Map[String, JsValue], + layerConfigurations: Map[String, JsValue]): DataSetViewConfiguration = + dataSetConfiguration + ("layers" -> Json.toJson(layerConfigurations)) + + private def getAllLayerDefaultViewConfigForDataSet( + dataLayers: List[DataLayerLike]): Map[String, LayerViewConfiguration] = + dataLayers.flatMap(dl => dl.defaultViewConfiguration.map(c => (dl.name, c))).toMap + + private def getAllLayerAdminViewConfigForDataSet( + dataLayers: List[DataLayerLike]): Map[String, LayerViewConfiguration] = + dataLayers.flatMap(dl => dl.adminViewConfiguration.map(c => (dl.name, c))).toMap + + def getLayerConfigurations(dataSetLayers: List[DataLayerLike], + requestedVolumeIds: List[String], + dataSet: DataSet, + userOpt: Option[User] = None)(implicit ctx: DBAccessContext): Fox[Map[String, JsValue]] = { + val allLayerNames = dataSetLayers.map(_.name) ++ requestedVolumeIds + (userOpt match { + case Some(user) => + userDataSetLayerConfigurationDAO.findAllByLayerNameForUserAndDataset(allLayerNames, user._id, dataSet._id) + case None => Fox.successful(Map.empty[String, LayerViewConfiguration]) + }).map { existingLayerViewConfigs => + val layerDefaultViewConfigs = getAllLayerDefaultViewConfigForDataSet(dataSetLayers) + val layerAdminViewConfigs = getAllLayerAdminViewConfigForDataSet(dataSetLayers) + mergeLayerConfigurations(allLayerNames, existingLayerViewConfigs, layerAdminViewConfigs, layerDefaultViewConfigs) + } + } + + def updateAdminViewConfigurationFor(dataSet: DataSet, rawAdminViewConfiguration: DataSetViewConfiguration)( + implicit ctx: DBAccessContext): Fox[Unit] = { + val dataSetViewConfiguration = rawAdminViewConfiguration - "layers" + val layerViewConfigurations = + rawAdminViewConfiguration + .get("layers") + .flatMap(lVC => lVC.asOpt[Map[String, LayerViewConfiguration]]) + .getOrElse(Map.empty) + + for { + _ <- dataSetDAO.updateAdminViewConfiguration(dataSet._id, dataSetViewConfiguration) + _ <- Fox.serialCombined(layerViewConfigurations.toList) { + case (name, adminViewConfiguration) => + dataSetDataLayerDAO.updateLayerAdminViewConfiguration(dataSet._id, name, adminViewConfiguration) + } + } yield () + } +} diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 1c66aaa3d6a..062aefb1da8 100755 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -4,6 +4,8 @@ import com.mohiva.play.silhouette.api.util.PasswordInfo import com.mohiva.play.silhouette.api.{Identity, LoginInfo} import com.scalableminds.util.accesscontext._ import com.scalableminds.util.tools.{Fox, FoxImplicits, JsonHelper} +import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration +import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration import com.scalableminds.webknossos.schema.Tables._ import javax.inject.Inject import models.binary.DataSetDAO @@ -307,28 +309,28 @@ class UserExperiencesDAO @Inject()(sqlClient: SQLClient, userDAO: UserDAO)(impli class UserDataSetConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserDAO, dataSetDAO: DataSetDAO)( implicit ec: ExecutionContext) extends SimpleSQLDAO(sqlClient) { - def findOneForUserAndDataset(userId: ObjectId, dataSetName: String)(implicit ctx: DBAccessContext): Fox[JsValue] = + def findOneForUserAndDataset(userId: ObjectId, dataSetId: ObjectId)( + implicit ctx: DBAccessContext): Fox[DataSetViewConfiguration] = for { - rows <- run(sql"""select c.configuration - from webknossos.user_dataSetConfigurations c - join webknossos.dataSets_ d on c._dataSet = d._id - where d.name = ${dataSetName} - and c._user = ${userId} + rows <- run(sql"""select viewConfiguration + from webknossos.user_dataSetConfigurations + where _dataSet = $dataSetId + and _user = $userId """.as[String]) parsed = rows.map(Json.parse) - result <- parsed.headOption.toFox + result <- parsed.headOption.map(_.validate[DataSetViewConfiguration].getOrElse(Map.empty)).toFox } yield result def updateDatasetConfigurationForUserAndDataset( userId: ObjectId, dataSetId: ObjectId, - configuration: Map[String, JsValue])(implicit ctx: DBAccessContext): Fox[Unit] = + configuration: DataSetViewConfiguration)(implicit ctx: DBAccessContext): Fox[Unit] = for { _ <- userDAO.assertUpdateAccess(userId) deleteQuery = sqlu"""delete from webknossos.user_dataSetConfigurations - where _user = ${userId} and _dataSet = ${dataSetId}""" - insertQuery = sqlu"""insert into webknossos.user_dataSetConfigurations(_user, _dataSet, configuration) - values(${userId}, ${dataSetId}, '#${sanitize(Json.toJson(configuration).toString)}')""" + where _user = $userId and _dataSet = $dataSetId""" + insertQuery = sqlu"""insert into webknossos.user_dataSetConfigurations(_user, _dataSet, viewConfiguration) + values($userId, $dataSetId, '#${sanitize(Json.toJson(configuration).toString)}')""" _ <- run( DBIO.sequence(List(deleteQuery, insertQuery)).transactionally.withTransactionIsolation(Serializable), retryCount = 50, @@ -340,29 +342,29 @@ class UserDataSetConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserD class UserDataSetLayerConfigurationDAO @Inject()(sqlClient: SQLClient, userDAO: UserDAO)(implicit ec: ExecutionContext) extends SimpleSQLDAO(sqlClient) { - def findAllByLayerNameForUserAndDataset(layerNames: List[String], userId: ObjectId, dataSetName: String)( - implicit ctx: DBAccessContext): Fox[Map[String, JsValue]] = + def findAllByLayerNameForUserAndDataset(layerNames: List[String], userId: ObjectId, dataSetId: ObjectId)( + implicit ctx: DBAccessContext): Fox[Map[String, LayerViewConfiguration]] = for { - rows <- run(sql"""select layerName, configuration + rows <- run(sql"""select layerName, viewConfiguration from webknossos.user_dataSetLayerConfigurations - where _dataset in (select _id from webknossos.dataSets_ where name = $dataSetName) + where _dataset = $dataSetId and _user = $userId and layerName in #${writeStructTupleWithQuotes(layerNames)} """.as[(String, String)]) - parsed = rows.map(t => (t._1, Json.parse(t._2))) + parsed = rows.flatMap(t => Json.parse(t._2).asOpt[LayerViewConfiguration].map((t._1, _))) } yield parsed.toMap def updateDatasetConfigurationForUserAndDatasetAndLayer( userId: ObjectId, dataSetId: ObjectId, layerName: String, - configuration: JsValue)(implicit ctx: DBAccessContext): Fox[Unit] = + viewConfiguration: LayerViewConfiguration)(implicit ctx: DBAccessContext): Fox[Unit] = for { _ <- userDAO.assertUpdateAccess(userId) deleteQuery = sqlu"""delete from webknossos.user_dataSetLayerConfigurations where _user = $userId and _dataSet = $dataSetId and layerName = $layerName""" - insertQuery = sqlu"""insert into webknossos.user_dataSetLayerConfigurations(_user, _dataSet, layerName, configuration) - values($userId, $dataSetId, $layerName, '#${sanitize(configuration.toString)}')""" + insertQuery = sqlu"""insert into webknossos.user_dataSetLayerConfigurations(_user, _dataSet, layerName, viewConfiguration) + values($userId, $dataSetId, $layerName, '#${sanitize(Json.toJson(viewConfiguration).toString)}')""" _ <- run( DBIO.sequence(List(deleteQuery, insertQuery)).transactionally.withTransactionIsolation(Serializable), retryCount = 50, diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index c7a3486e8e0..53069f71774 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -11,6 +11,7 @@ import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContex import com.scalableminds.util.security.SCrypt import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.models.datasource.DataSetViewConfiguration.DataSetViewConfiguration +import com.scalableminds.webknossos.datastore.models.datasource.LayerViewConfiguration.LayerViewConfiguration import javax.inject.Inject import models.binary.DataSetDAO import models.configuration.UserConfiguration @@ -154,13 +155,17 @@ class UserService @Inject()(conf: WkConf, dataSet <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName) ?~> Messages( "dataSet.notFound", dataSetName) - layerMap = layerConfiguration.map(_.validate[Map[String, JsValue]].getOrElse(Map.empty)).getOrElse(Map.empty) + layerMap = layerConfiguration.flatMap(_.asOpt[Map[String, JsValue]]).getOrElse(Map.empty) _ <- Fox.serialCombined(layerMap.toList) { case (name, config) => - userDataSetLayerConfigurationDAO.updateDatasetConfigurationForUserAndDatasetAndLayer(user._id, - dataSet._id, - name, - config) + config.asOpt[LayerViewConfiguration] match { + case Some(viewConfiguration) => + userDataSetLayerConfigurationDAO.updateDatasetConfigurationForUserAndDatasetAndLayer(user._id, + dataSet._id, + name, + viewConfiguration) + case None => Fox.successful(()) + } } _ <- userDataSetConfigurationDAO.updateDatasetConfigurationForUserAndDataset(user._id, dataSet._id, diff --git a/conf/evolutions/056-add-layer-specific-view-configs.sql b/conf/evolutions/056-add-layer-specific-view-configs.sql index da1b1ecb571..70cc5a9e541 100644 --- a/conf/evolutions/056-add-layer-specific-view-configs.sql +++ b/conf/evolutions/056-add-layer-specific-view-configs.sql @@ -6,9 +6,9 @@ CREATE TABLE webknossos.user_dataSetLayerConfigurations( _user CHAR(24) NOT NULL, _dataSet CHAR(24) NOT NULL, layerName VARCHAR(256) NOT NULL, - configuration JSONB NOT NULL, + viewConfiguration JSONB NOT NULL, PRIMARY KEY (_user, _dataSet, layerName), - CONSTRAINT configurationIsJsonObject CHECK(jsonb_typeof(configuration) = 'object') + CONSTRAINT viewConfigurationIsJsonObject CHECK(jsonb_typeof(viewConfiguration) = 'object') ); ALTER TABLE webknossos.user_dataSetLayerConfigurations @@ -16,10 +16,9 @@ ALTER TABLE webknossos.user_dataSetLayerConfigurations ADD CONSTRAINT dataSet_ref FOREIGN KEY(_dataSet) REFERENCES webknossos.dataSets(_id) ON DELETE CASCADE DEFERRABLE; -- Insert the layer configs into the new table -INSERT INTO webknossos.user_dataSetLayerConfigurations(_user, _dataset, layerName, configuration) +INSERT INTO webknossos.user_dataSetLayerConfigurations(_user, _dataset, layerName, viewConfiguration) SELECT _user, _dataset, (js).key AS layerName, (js).value AS config FROM (SELECT _user, _dataset, jsonb_each(configuration->'layers') AS js FROM webknossos.user_dataSetConfigurations WHERE configuration ? 'layers') AS sub_q; - -- Remove layers field from old table UPDATE webknossos.user_dataSetConfigurations SET configuration = configuration - 'layers' @@ -30,10 +29,44 @@ UPDATE webknossos.user_dataSetConfigurations SET configuration = configuration - 'quality' WHERE configuration ? 'quality'; +--Adapt naming of dataset view configuration to match layer naming +DROP VIEW webknossos.dataSets_; + +ALTER TABLE webknossos.dataSets RENAME COLUMN sourceDefaultConfiguration TO defaultViewConfiguration; +ALTER TABLE webknossos.dataSets RENAME COLUMN defaultConfiguration TO adminViewConfiguration; +ALTER TABLE webknossos.dataSets RENAME CONSTRAINT sourceDefaultConfigurationIsJsonObject TO defaultViewConfigurationIsJsonObject; +ALTER TABLE webknossos.dataSets RENAME CONSTRAINT defaultConfigurationIsJsonObject TO adminViewConfigurationIsJsonObject; + +CREATE VIEW webknossos.dataSets_ AS SELECT * FROM webknossos.dataSets WHERE NOT isDeleted; + +--remove unnecessary configuration container from defaultConfig +UPDATE webknossos.dataSets +SET adminViewConfiguration = adminViewConfiguration->'configuration' +WHERE adminViewConfiguration ? 'configuration'; + +--ADD admin view configuration for layers ALTER TABLE webknossos.dataSet_layers ADD COLUMN adminViewConfiguration JSONB; ALTER TABLE webknossos.dataSet_layers ADD CONSTRAINT adminViewConfigurationIsJsonObject CHECK(jsonb_typeof(adminViewConfiguration) = 'object'); -ALTER TABLE +--split default configuration as well +UPDATE webknossos.dataSet_layers dl +SET adminViewConfiguration = dS.defaultConfiguration->'layers'->dl.name +FROM webknossos.dataSets dS +WHERE dl._dataSet = dS._id AND dS.defaultConfiguration ? 'layers' AND dS.defaultConfiguration->'layers' ? dl.name; + +-- Remove layers field from old table +UPDATE webknossos.dataSets +SET adminViewConfiguration = adminViewConfiguration - 'layers' +WHERE adminViewConfiguration ? 'layers'; + +--remove unused field quality +UPDATE webknossos.dataSets +SET adminViewConfiguration = adminViewConfiguration - 'quality' +WHERE adminViewConfiguration ? 'quality'; + +--Rename view configuration to reflect new naming standard +ALTER TABLE webknossos.user_dataSetConfigurations RENAME COLUMN configuration TO viewConfiguration; +ALTER TABLE webknossos.user_dataSetConfigurations RENAME CONSTRAINT configurationIsJsonObject TO viewConfigurationIsJsonObject; UPDATE webknossos.releaseInformation SET schemaVersion = 56; diff --git a/frontend/javascripts/libs/dataset_view_settings.schema.js b/frontend/javascripts/libs/dataset_view_settings.schema.js new file mode 100644 index 00000000000..b874094242c --- /dev/null +++ b/frontend/javascripts/libs/dataset_view_settings.schema.js @@ -0,0 +1,38 @@ +// @flow + +export const defaultDatasetViewConfiguration = { + fourBit: false, + interpolation: true, + highlightHoveredCellId: true, + renderIsosurfaces: false, + renderMissingDataBlack: true, + loadingStrategy: "PROGRESSIVE_QUALITY", + segmentationPatternOpacity: 40, +}; + +export const datasetViewConfiguration = { + fourBit: { type: "boolean" }, + interpolation: { type: "boolean" }, + highlightHoveredCellId: { type: "boolean" }, + renderIsosurfaces: { type: "boolean" }, + zoom: { type: "number", minimum: 0.005 }, + renderMissingDataBlack: { type: "boolean" }, + loadingStrategy: { enum: ["BEST_QUALITY_FIRST", "PROGRESSIVE_QUALITY"] }, + segmentationPatternOpacity: { type: "number", minimum: 0, maximum: 100 }, + + // TODO + position: { type: "number", minimum: 0, maximum: 100 }, + rotation: { type: "number", minimum: 0, maximum: 100 }, + layers: { type: "number", minimum: 0, maximum: 100 }, +}; + +export default { + $schema: "http://json-schema.org/draft-06/schema#", + definitions: { + "types::DatasetViewConfiguration": { + type: ["object", "null"], + properties: datasetViewConfiguration, + additionalProperties: false, + }, + }, +}; diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index bd9b032e560..8f6fb999e23 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -93,8 +93,8 @@ CREATE TABLE webknossos.dataSets( _organization CHAR(24) NOT NULL, _publication CHAR(24), inboxSourceHash INT, - sourceDefaultConfiguration JSONB, - defaultConfiguration JSONB, + defaultViewConfiguration JSONB, + adminViewConfiguration JSONB, description TEXT, displayName VARCHAR(256), isPublic BOOLEAN NOT NULL DEFAULT false, @@ -109,8 +109,8 @@ CREATE TABLE webknossos.dataSets( created TIMESTAMPTZ NOT NULL DEFAULT NOW(), isDeleted BOOLEAN NOT NULL DEFAULT false, UNIQUE (name, _organization), - CONSTRAINT sourceDefaultConfigurationIsJsonObject CHECK(jsonb_typeof(sourceDefaultConfiguration) = 'object'), - CONSTRAINT defaultConfigurationIsJsonObject CHECK(jsonb_typeof(defaultConfiguration) = 'object'), + CONSTRAINT defaultViewConfigurationIsJsonObject CHECK(jsonb_typeof(defaultViewConfiguration) = 'object'), + CONSTRAINT adminViewConfigurationIsJsonObject CHECK(jsonb_typeof(adminViewConfiguration) = 'object'), CONSTRAINT detailsIsJsonObject CHECK(jsonb_typeof(details) = 'object') ); @@ -124,11 +124,11 @@ CREATE TABLE webknossos.dataSet_layers( boundingBox webknossos.BOUNDING_BOX NOT NULL, largestSegmentId BIGINT, mappings VARCHAR(256)[], - sourceDefaultViewConfiguration JSONB, defaultViewConfiguration JSONB, + adminViewConfiguration JSONB, PRIMARY KEY(_dataSet, name), CONSTRAINT defaultViewConfigurationIsJsonObject CHECK(jsonb_typeof(defaultViewConfiguration) = 'object'), - CONSTRAINT sourceDefaultViewConfigurationIsJsonObject CHECK(jsonb_typeof(sourceDefaultViewConfiguration) = 'object') + CONSTRAINT adminViewConfigurationIsJsonObject CHECK(jsonb_typeof(adminViewConfiguration) = 'object') ); CREATE TABLE webknossos.dataSet_allowedTeams( @@ -313,18 +313,18 @@ CREATE TABLE webknossos.user_experiences( CREATE TABLE webknossos.user_dataSetConfigurations( _user CHAR(24) NOT NULL, _dataSet CHAR(24) NOT NULL, - configuration JSONB NOT NULL, + viewConfiguration JSONB NOT NULL, PRIMARY KEY (_user, _dataSet), - CONSTRAINT configurationIsJsonObject CHECK(jsonb_typeof(configuration) = 'object') + CONSTRAINT viewConfigurationIsJsonObject CHECK(jsonb_typeof(viewConfiguration) = 'object') ); CREATE TABLE webknossos.user_dataSetLayerConfigurations( _user CHAR(24) NOT NULL, _dataSet CHAR(24) NOT NULL, layerName VARCHAR(256) NOT NULL, - configuration JSONB NOT NULL, + viewConfiguration JSONB NOT NULL, PRIMARY KEY (_user, _dataSet, layerName), - CONSTRAINT configurationIsJsonObject CHECK(jsonb_typeof(configuration) = 'object') + CONSTRAINT viewConfigurationIsJsonObject CHECK(jsonb_typeof(viewConfiguration) = 'object') ); diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataFormat.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataFormat.scala index c330ffb73ec..b9037184ea9 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataFormat.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/knossos/KnossosDataFormat.scala @@ -23,7 +23,7 @@ object KnossosDataFormat extends DataSourceImporter { case _ => None } - val inDBdefaultViewConfiguration = previous.flatMap(_.adminViewConfiguration) + val adminViewConfiguration = previous.flatMap(_.adminViewConfiguration) (for { elementClass <- guessElementClass(baseDir) @@ -37,14 +37,9 @@ object KnossosDataFormat extends DataSourceImporter { case Some(l: SegmentationLayer) => l.largestSegmentId case _ => SegmentationLayer.defaultLargestSegmentId } - KnossosSegmentationLayer(name, - sections, - elementClass, - mappings, - largestSegmentId, - inDBdefaultViewConfiguration) + KnossosSegmentationLayer(name, sections, elementClass, mappings, largestSegmentId, adminViewConfiguration) case _ => - KnossosDataLayer(name, category, sections, elementClass, inDBdefaultViewConfiguration) + KnossosDataLayer(name, category, sections, elementClass, adminViewConfiguration) } }).passFailure { f => report.error(layer => s"Error processing layer '$layer' - ${f.msg}") diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala index 7d6b96318d4..ba66259258c 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/dataformats/wkw/WKWDataFormat.scala @@ -24,7 +24,7 @@ object WKWDataFormat extends DataSourceImporter with WKWDataFormatHelper { .map(_.boundingBox) .orElse(guessBoundingBox(baseDir, wkwResolutions.headOption)) .getOrElse(BoundingBox.empty) - val inDBdefaultViewConfiguration = previous.flatMap(_.adminViewConfiguration) + val adminViewConfiguration = previous.flatMap(_.adminViewConfiguration) category match { case Category.segmentation => val mappings = exploreMappings(baseDir) @@ -39,7 +39,7 @@ object WKWDataFormat extends DataSourceImporter with WKWDataFormatHelper { elementClass, mappings, largestSegmentId, - adminViewConfiguration = inDBdefaultViewConfiguration + adminViewConfiguration = adminViewConfiguration ) case _ => WKWDataLayer( @@ -48,7 +48,7 @@ object WKWDataFormat extends DataSourceImporter with WKWDataFormatHelper { boundingBox, wkwResolutions, elementClass, - adminViewConfiguration = inDBdefaultViewConfiguration + adminViewConfiguration = adminViewConfiguration ) } }).passFailure { f => From 4d100a4bac12a6a166f9bd4b59af2328fb6b1e8b Mon Sep 17 00:00:00 2001 From: Youri K Date: Wed, 14 Oct 2020 14:38:11 +0200 Subject: [PATCH 16/39] add frontend part to validate DatasetConfiguration --- .../libs/dataset_view_configuration.schema.js | 98 +++++++++++++++++++ .../dataset_view_configuration_validation.js | 78 +++++++++++++++ .../libs/dataset_view_settings.schema.js | 38 ------- .../oxalis/model_initialization.js | 5 + 4 files changed, 181 insertions(+), 38 deletions(-) create mode 100644 frontend/javascripts/libs/dataset_view_configuration.schema.js create mode 100644 frontend/javascripts/libs/dataset_view_configuration_validation.js delete mode 100644 frontend/javascripts/libs/dataset_view_settings.schema.js diff --git a/frontend/javascripts/libs/dataset_view_configuration.schema.js b/frontend/javascripts/libs/dataset_view_configuration.schema.js new file mode 100644 index 00000000000..d89fd5c921d --- /dev/null +++ b/frontend/javascripts/libs/dataset_view_configuration.schema.js @@ -0,0 +1,98 @@ +// @flow + +export function getDefaultLayerViewConfiguration(dynamicDefault: Object) { + const defaultLayerViewConfiguration = { + color: [255, 255, 255], + brightness: null, + contrast: null, + alpha: 100, + intensityRange: [0, 255], + min: null, + max: null, + isDisabled: false, + isInverted: false, + isInEditMode: false, + }; + return { + ...defaultLayerViewConfiguration, + ...dynamicDefault, + }; +} + +export const layerViewConfiguration = { + color: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 }, + brightness: { type: ["number", "null"], minimum: 0.005 }, // TODO min max + contrast: { type: ["number", "null"], minimum: 0.005 }, // TODO min max + alpha: { type: "number", minimum: 0, maximum: 100 }, + intensityRange: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 }, + min: { type: ["number", "null"], minimum: 0.005 }, // TODO min max + max: { type: ["number", "null"], minimum: 0.005 }, // TODO min max + isDisabled: { type: "boolean" }, + isInverted: { type: "boolean" }, + isInEditMode: { type: "boolean" }, +}; + +export const defaultDatasetViewConfiguration = { + fourBit: false, + interpolation: true, + highlightHoveredCellId: true, + renderIsosurfaces: false, + renderMissingDataBlack: true, + loadingStrategy: "PROGRESSIVE_QUALITY", + segmentationPatternOpacity: 40, + zoom: null, + position: null, + rotation: null, + layers: {}, +}; + +export const datasetViewConfiguration = { + fourBit: { type: "boolean" }, + interpolation: { type: "boolean" }, + highlightHoveredCellId: { type: "boolean" }, + renderIsosurfaces: { type: "boolean" }, + zoom: { type: ["number", "null"], minimum: 0.005 }, // TODO zoom max value? + renderMissingDataBlack: { type: "boolean" }, + loadingStrategy: { enum: ["BEST_QUALITY_FIRST", "PROGRESSIVE_QUALITY"] }, + segmentationPatternOpacity: { type: "number", minimum: 0, maximum: 100 }, + position: { type: ["array", "null"], items: { type: "number" }, minItems: 3, maxItems: 3 }, + rotation: { type: ["array", "null"], items: { type: "number" }, minItems: 3, maxItems: 3 }, + layers: { type: "object" }, +}; + +export default { + $schema: "http://json-schema.org/draft-06/schema#", + definitions: { + "types::OptionalDatasetViewConfiguration": { + type: ["object", "null"], + properties: datasetViewConfiguration, + additionalProperties: false, + }, + "types::DatasetViewConfiguration": { + type: "object", + properties: datasetViewConfiguration, + additionalProperties: false, + required: [ + "fourBit", + "interpolation", + "highlightHoveredCellId", + "renderIsosurfaces", + "renderMissingDataBlack", + "loadingStrategy", + "segmentationPatternOpacity", + "layers", + ], + }, + "types::OptionalLayerViewConfiguration": { + type: ["object", "null"], + properties: layerViewConfiguration, + additionalProperties: false, + }, + "types::LayerViewConfiguration": { + type: "object", + properties: layerViewConfiguration, + additionalProperties: false, + required: ["color", "alpha", "intensityRange", "isDisabled", "isInverted", "isInEditMode"], + }, + }, +}; diff --git a/frontend/javascripts/libs/dataset_view_configuration_validation.js b/frontend/javascripts/libs/dataset_view_configuration_validation.js new file mode 100644 index 00000000000..262518d6ffa --- /dev/null +++ b/frontend/javascripts/libs/dataset_view_configuration_validation.js @@ -0,0 +1,78 @@ +// @flow + +import jsonschema from "jsonschema"; + +import ViewConfigurationSchema, { + getDefaultLayerViewConfiguration, + defaultDatasetViewConfiguration, +} from "libs/dataset_view_configuration.schema.js"; +import Store, { type DatasetConfiguration } from "oxalis/store"; +import { type APIDataset, type APIDataLayer } from "admin/api_flow_types"; +import { getDefaultIntensityRangeOfLayer } from "oxalis/model/accessors/dataset_accessor"; + +const validator = new jsonschema.Validator(); +validator.addSchema(ViewConfigurationSchema, "/"); + +const validateWithSchema = (type: string, json: Object) => { + const result = validator.validate(json, { + $ref: `#/definitions/${type}`, + }); + if (result.valid) { + return []; + } else { + return result.errors; + } +}; + +const eliminateErrors = (instance: Object, errors: Array<*>, defaults: Object) => { + errors.forEach(error => { + if (error.name === "required") { + instance[error.argument] = defaults[error.argument]; + } else if (error.name === "additionalProperties") { + delete instance[error.argument]; + } else { + const [invalidFieldName] = error.property.split(".").slice(-1); + instance[invalidFieldName] = defaults[invalidFieldName]; + } + }); + return instance; +}; + +const getSpecificDefaultsForLayers = (dataset: APIDataset, layer: APIDataLayer) => ({ + intensityRange: getDefaultIntensityRangeOfLayer(dataset, layer.name), + alpha: layer.category === "color" ? 100 : 20, +}); + +export const getValidatedDatasetViewConfiguration = (json: Object): DatasetConfiguration => { + const validationErrors = validateWithSchema("types::DatasetViewConfiguration", json); + let datasetViewConfiguration = json; + if (validationErrors.length) { + datasetViewConfiguration = eliminateErrors( + datasetViewConfiguration, + validationErrors, + defaultDatasetViewConfiguration, + ); + } + + const { layers } = datasetViewConfiguration; + const { dataset } = Store.getState(); + dataset.dataSource.dataLayers.forEach(layer => { + const layerConfigDefault = getDefaultLayerViewConfiguration( + getSpecificDefaultsForLayers(dataset, layer), + ); + + const layerConfig = layers[layer.name]; + if (layerConfig) { + const layerErrors = validateWithSchema("types::LayerViewConfiguration", layerConfig); + + console.log(layerErrors); + layers[layer.name] = eliminateErrors(layerConfig, layerErrors, layerConfigDefault); + } else { + layers[layer.name] = layerConfigDefault; + } + }); + + return { ...datasetViewConfiguration, layers }; +}; + +export default validator; diff --git a/frontend/javascripts/libs/dataset_view_settings.schema.js b/frontend/javascripts/libs/dataset_view_settings.schema.js deleted file mode 100644 index b874094242c..00000000000 --- a/frontend/javascripts/libs/dataset_view_settings.schema.js +++ /dev/null @@ -1,38 +0,0 @@ -// @flow - -export const defaultDatasetViewConfiguration = { - fourBit: false, - interpolation: true, - highlightHoveredCellId: true, - renderIsosurfaces: false, - renderMissingDataBlack: true, - loadingStrategy: "PROGRESSIVE_QUALITY", - segmentationPatternOpacity: 40, -}; - -export const datasetViewConfiguration = { - fourBit: { type: "boolean" }, - interpolation: { type: "boolean" }, - highlightHoveredCellId: { type: "boolean" }, - renderIsosurfaces: { type: "boolean" }, - zoom: { type: "number", minimum: 0.005 }, - renderMissingDataBlack: { type: "boolean" }, - loadingStrategy: { enum: ["BEST_QUALITY_FIRST", "PROGRESSIVE_QUALITY"] }, - segmentationPatternOpacity: { type: "number", minimum: 0, maximum: 100 }, - - // TODO - position: { type: "number", minimum: 0, maximum: 100 }, - rotation: { type: "number", minimum: 0, maximum: 100 }, - layers: { type: "number", minimum: 0, maximum: 100 }, -}; - -export default { - $schema: "http://json-schema.org/draft-06/schema#", - definitions: { - "types::DatasetViewConfiguration": { - type: ["object", "null"], - properties: datasetViewConfiguration, - additionalProperties: false, - }, - }, -}; diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index b3f89de688f..b91d2d93024 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -67,6 +67,7 @@ import * as Utils from "libs/utils"; import constants, { ControlModeEnum, type Vector3 } from "oxalis/constants"; import messages from "messages"; import window from "libs/window"; +import { getValidatedDatasetViewConfiguration } from "libs/dataset_view_configuration_validation"; export const HANDLED_ERROR = "error_was_handled"; @@ -128,6 +129,10 @@ export async function initialize( initializeDataset(initialFetch, dataset, tracing); initializeSettings(initialUserSettings, initialDatasetSettings); + const test = getValidatedDatasetViewConfiguration(initialDatasetSettings); + + console.log(test); + let initializationInformation = null; // There is no need to reinstantiate the DataLayers if the dataset didn't change. if (initialFetch) { From 667c8be7c1db227237dc3ebff63b11c0d037524c Mon Sep 17 00:00:00 2001 From: Youri K Date: Wed, 14 Oct 2020 14:47:09 +0200 Subject: [PATCH 17/39] fix evolution after renaming --- conf/evolutions/056-add-layer-specific-view-configs.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/evolutions/056-add-layer-specific-view-configs.sql b/conf/evolutions/056-add-layer-specific-view-configs.sql index 70cc5a9e541..792d00ed133 100644 --- a/conf/evolutions/056-add-layer-specific-view-configs.sql +++ b/conf/evolutions/056-add-layer-specific-view-configs.sql @@ -50,9 +50,9 @@ ALTER TABLE webknossos.dataSet_layers ADD CONSTRAINT adminViewConfigurationIsJso --split default configuration as well UPDATE webknossos.dataSet_layers dl -SET adminViewConfiguration = dS.defaultConfiguration->'layers'->dl.name +SET adminViewConfiguration = dS.adminViewConfiguration->'layers'->dl.name FROM webknossos.dataSets dS -WHERE dl._dataSet = dS._id AND dS.defaultConfiguration ? 'layers' AND dS.defaultConfiguration->'layers' ? dl.name; +WHERE dl._dataSet = dS._id AND dS.adminViewConfiguration ? 'layers' AND dS.adminViewConfiguration->'layers' ? dl.name; -- Remove layers field from old table UPDATE webknossos.dataSets From 1b07231a920d863c8da9c12db4eaa1810f649d89 Mon Sep 17 00:00:00 2001 From: Youri K Date: Wed, 14 Oct 2020 16:16:20 +0200 Subject: [PATCH 18/39] move type and schema definitions --- frontend/javascripts/admin/admin_rest_api.js | 2 +- .../admin/auth/registration_form.js | 2 +- .../admin/dataset/dataset_add_boss_view.js | 2 +- .../dataset/dataset_add_neuroglancer_view.js | 2 +- .../admin/dataset/dataset_add_view.js | 2 +- .../admin/dataset/dataset_components.js | 2 +- .../admin/dataset/dataset_upload_view.js | 2 +- frontend/javascripts/admin/onboarding.js | 2 +- .../admin/project/project_create_view.js | 2 +- .../admin/project/project_list_view.js | 2 +- .../admin/project/transfer_all_tasks_modal.js | 2 +- .../admin/scripts/script_create_view.js | 2 +- .../admin/scripts/script_list_view.js | 2 +- .../admin/statistic/open_tasks_report_view.js | 2 +- .../statistic/project_progress_report_view.js | 2 +- .../admin/statistic/team_selection_form.js | 2 +- .../admin/task/task_annotation_view.js | 2 +- .../admin/task/task_create_bulk_view.js | 2 +- .../admin/task/task_create_form_view.js | 2 +- .../javascripts/admin/task/task_list_view.js | 2 +- .../admin/task/task_search_form.js | 2 +- .../recommended_configuration_view.js | 2 +- .../admin/tasktype/task_type_create_view.js | 2 +- .../admin/tasktype/task_type_list_view.js | 2 +- .../javascripts/admin/team/team_list_view.js | 2 +- .../javascripts/admin/time/time_line_view.js | 2 +- .../admin/user/experience_modal_view.js | 2 +- .../user/permissions_and_teams_modal_view.js | 2 +- .../javascripts/admin/user/user_list_view.js | 2 +- .../admin/user/user_selection_component.js | 2 +- frontend/javascripts/components/legal.js | 2 +- .../components/select_experience_domain.js | 2 +- .../dataset_access_list_view.js | 2 +- .../advanced_dataset/dataset_action_view.js | 2 +- .../advanced_dataset/dataset_table.js | 2 +- .../dashboard/dashboard_task_list_view.js | 2 +- .../javascripts/dashboard/dashboard_view.js | 2 +- .../dataset/dataset_cache_provider.js | 2 +- .../dashboard/dataset/dataset_import_view.js | 2 +- .../dataset/default_config_component.js | 2 +- .../dataset/import_delete_component.js | 2 +- .../dataset/import_sharing_component.js | 2 +- .../dataset/sample_datasets_modal.js | 2 +- .../dataset/simple_advanced_data_form.js | 2 +- .../dataset/team_selection_component.js | 2 +- .../javascripts/dashboard/dataset_view.js | 2 +- .../dashboard/explorative_annotations_view.js | 2 +- .../javascripts/dashboard/publication_card.js | 2 +- .../dashboard/publication_details_view.js | 2 +- .../javascripts/dashboard/publication_view.js | 2 +- .../dashboard/shared_annotations_view.js | 2 +- .../javascripts/dashboard/spotlight_view.js | 2 +- .../dashboard/transfer_task_modal.js | 2 +- .../dataset_view_configuration_validation.js | 78 ------------------- frontend/javascripts/libs/error_handling.js | 2 +- frontend/javascripts/libs/utils.js | 2 +- frontend/javascripts/libs/vector_input.js | 2 +- frontend/javascripts/navbar.js | 2 +- frontend/javascripts/oxalis/api/api_latest.js | 2 +- frontend/javascripts/oxalis/controller.js | 2 +- .../oxalis/controller/scene_controller.js | 2 +- .../materials/plane_material_factory.js | 2 +- .../model/accessors/dataset_accessor.js | 2 +- .../oxalis/model/accessors/flycam_accessor.js | 2 +- .../accessors/skeletontracing_accessor.js | 2 +- .../model/accessors/tracing_accessor.js | 2 +- .../oxalis/model/accessors/user_accessor.js | 2 +- .../model/accessors/volumetracing_accessor.js | 2 +- .../model/actions/annotation_actions.js | 2 +- .../oxalis/model/actions/dataset_actions.js | 2 +- .../model/actions/skeletontracing_actions.js | 2 +- .../oxalis/model/actions/user_actions.js | 2 +- .../model/actions/volumetracing_actions.js | 2 +- .../model/bucket_data_handling/bucket.js | 2 +- .../model/bucket_data_handling/data_cube.js | 2 +- .../data_rendering_logic.js | 2 +- .../model/bucket_data_handling/mappings.js | 2 +- .../texture_bucket_manager.js | 2 +- .../model/helpers/generate_dummy_trees.js | 2 +- .../oxalis/model/helpers/nml_helpers.js | 2 +- .../oxalis/model/helpers/proto_helpers.js | 2 +- .../oxalis/model/reducers/reducer_helpers.js | 2 +- .../oxalis/model/reducers/settings_reducer.js | 41 +--------- .../model/reducers/skeletontracing_reducer.js | 2 +- .../skeletontracing_reducer_helpers.js | 2 +- .../model/sagas/automatic_brush_saga.js | 2 +- .../oxalis/model/sagas/handle_mesh_changes.js | 2 +- .../oxalis/model/sagas/isosurface_saga.js | 2 +- .../model/sagas/load_histogram_data_saga.js | 2 +- .../oxalis/model/sagas/task_saga.js | 2 +- .../oxalis/model_initialization.js | 9 +-- frontend/javascripts/oxalis/store.js | 2 +- .../view/action-bar/dataset_position_view.js | 2 +- .../view/action-bar/merge_modal_view.js | 2 +- .../view/action-bar/share_modal_view.js | 2 +- .../view/action-bar/tracing_actions_view.js | 2 +- .../oxalis/view/action_bar_view.js | 2 +- .../view/right-menu/dataset_info_tab_view.js | 2 +- .../view/right-menu/mapping_info_view.js | 2 +- .../oxalis/view/right-menu/meshes_view.js | 2 +- frontend/javascripts/oxalis/view/scalebar.js | 2 +- .../view/settings/dataset_settings_view.js | 2 +- .../oxalis/view/settings/histogram_view.js | 3 +- .../view/settings/user_settings_view.js | 4 +- .../oxalis/view/version_entry_group.js | 2 +- .../javascripts/oxalis/view/version_list.js | 2 +- frontend/javascripts/router.js | 2 +- .../test/api/api_skeleton_latest.spec.js | 2 +- .../backend-snapshot-tests/annotations.e2e.js | 2 +- .../backend-snapshot-tests/datasets.e2e.js | 2 +- .../backend-snapshot-tests/projects.e2e.js | 2 +- frontend/javascripts/test/enzyme/e2e-setup.js | 2 +- .../skeletontracing_server_objects.js | 2 +- .../fixtures/tasktracing_server_objects.js | 2 +- .../fixtures/volumetracing_server_objects.js | 2 +- frontend/javascripts/test/libs/schema.spec.js | 2 +- .../puppeteer/dataset_rendering_helpers.js | 2 +- .../{admin => types}/api_flow_types.js | 0 .../dataset_view_configuration.schema.js | 8 +- .../dataset_view_configuration_defaults.js | 61 +++++++++++++++ .../schemas}/datasource.schema.json | 0 .../schemas}/user_settings.schema.js | 0 .../dataset => types}/validation.js | 17 +++- 123 files changed, 200 insertions(+), 245 deletions(-) delete mode 100644 frontend/javascripts/libs/dataset_view_configuration_validation.js rename frontend/javascripts/{admin => types}/api_flow_types.js (100%) rename frontend/javascripts/{libs => types/schemas}/dataset_view_configuration.schema.js (88%) create mode 100644 frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js rename frontend/javascripts/{libs => types/schemas}/datasource.schema.json (100%) rename frontend/javascripts/{libs => types/schemas}/user_settings.schema.js (100%) rename frontend/javascripts/{dashboard/dataset => types}/validation.js (72%) diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index b59f2a3ecef..638dcefea24 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -48,7 +48,7 @@ import { type ServerVolumeTracing, type TracingType, type WkConnectDatasetConfig, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import type { DatasetConfiguration, Tracing } from "oxalis/store"; import type { NewTask, TaskCreationResponseContainer } from "admin/task/task_create_bulk_view"; import type { QueryObject } from "admin/task/task_search_form"; diff --git a/frontend/javascripts/admin/auth/registration_form.js b/frontend/javascripts/admin/auth/registration_form.js index 693554c9f1f..e5d8bf1c084 100644 --- a/frontend/javascripts/admin/auth/registration_form.js +++ b/frontend/javascripts/admin/auth/registration_form.js @@ -2,7 +2,7 @@ import { Form, Input, Button, Row, Col, Icon, Checkbox } from "antd"; import React from "react"; -import { type APIOrganization } from "admin/api_flow_types"; +import { type APIOrganization } from "types/api_flow_types"; import { loginUser, getOrganization } from "admin/admin_rest_api"; import { setActiveUserAction } from "oxalis/model/actions/user_actions"; import Request from "libs/request"; diff --git a/frontend/javascripts/admin/dataset/dataset_add_boss_view.js b/frontend/javascripts/admin/dataset/dataset_add_boss_view.js index 97661f65eae..464291fbebc 100644 --- a/frontend/javascripts/admin/dataset/dataset_add_boss_view.js +++ b/frontend/javascripts/admin/dataset/dataset_add_boss_view.js @@ -4,7 +4,7 @@ import { connect } from "react-redux"; import React from "react"; import _ from "lodash"; -import type { APIDataStore, APIUser } from "admin/api_flow_types"; +import type { APIDataStore, APIUser } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { addWkConnectDataset } from "admin/admin_rest_api"; import messages from "messages"; diff --git a/frontend/javascripts/admin/dataset/dataset_add_neuroglancer_view.js b/frontend/javascripts/admin/dataset/dataset_add_neuroglancer_view.js index 6464dd94819..4193b49b79d 100644 --- a/frontend/javascripts/admin/dataset/dataset_add_neuroglancer_view.js +++ b/frontend/javascripts/admin/dataset/dataset_add_neuroglancer_view.js @@ -4,7 +4,7 @@ import { connect } from "react-redux"; import React from "react"; import _ from "lodash"; -import type { APIDataStore, APIUser } from "admin/api_flow_types"; +import type { APIDataStore, APIUser } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { addWkConnectDataset } from "admin/admin_rest_api"; import messages from "messages"; diff --git a/frontend/javascripts/admin/dataset/dataset_add_view.js b/frontend/javascripts/admin/dataset/dataset_add_view.js index 5401fa9f644..0c897873f5f 100644 --- a/frontend/javascripts/admin/dataset/dataset_add_view.js +++ b/frontend/javascripts/admin/dataset/dataset_add_view.js @@ -5,7 +5,7 @@ import React from "react"; import { connect } from "react-redux"; import _ from "lodash"; -import type { APIUser, APIDataStore } from "admin/api_flow_types"; +import type { APIUser, APIDataStore } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; import DatasetAddForeignView from "admin/dataset/dataset_add_foreign_view"; diff --git a/frontend/javascripts/admin/dataset/dataset_components.js b/frontend/javascripts/admin/dataset/dataset_components.js index 2f9afcde6a9..47fca81b069 100644 --- a/frontend/javascripts/admin/dataset/dataset_components.js +++ b/frontend/javascripts/admin/dataset/dataset_components.js @@ -4,7 +4,7 @@ import * as React from "react"; import { Form, Input, Select, Card } from "antd"; import messages from "messages"; import { isDatasetNameValid } from "admin/admin_rest_api"; -import type { APIDataStore, APIUser } from "admin/api_flow_types"; +import type { APIDataStore, APIUser } from "types/api_flow_types"; const FormItem = Form.Item; const { Option } = Select; diff --git a/frontend/javascripts/admin/dataset/dataset_upload_view.js b/frontend/javascripts/admin/dataset/dataset_upload_view.js index febc3bc7e88..1bc91a5a17b 100644 --- a/frontend/javascripts/admin/dataset/dataset_upload_view.js +++ b/frontend/javascripts/admin/dataset/dataset_upload_view.js @@ -3,7 +3,7 @@ import { Form, Button, Spin, Upload, Icon, Col, Row, Tooltip } from "antd"; import { connect } from "react-redux"; import React from "react"; -import type { APIDataStore, APIUser, DatasetConfig } from "admin/api_flow_types"; +import type { APIDataStore, APIUser, DatasetConfig } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { addDataset } from "admin/admin_rest_api"; import Toast from "libs/toast"; diff --git a/frontend/javascripts/admin/onboarding.js b/frontend/javascripts/admin/onboarding.js index 6f1727f785a..21c5524eca2 100644 --- a/frontend/javascripts/admin/onboarding.js +++ b/frontend/javascripts/admin/onboarding.js @@ -19,7 +19,7 @@ import { connect } from "react-redux"; import Clipboard from "clipboard-js"; import React, { type Node } from "react"; -import type { APIUser, APIDataStore } from "admin/api_flow_types"; +import type { APIUser, APIDataStore } from "types/api_flow_types"; import Store, { type OxalisState } from "oxalis/store"; import { getDatastores } from "admin/admin_rest_api"; import { location } from "libs/window"; diff --git a/frontend/javascripts/admin/project/project_create_view.js b/frontend/javascripts/admin/project/project_create_view.js index 6a675632c3e..8d0fcdd88c8 100644 --- a/frontend/javascripts/admin/project/project_create_view.js +++ b/frontend/javascripts/admin/project/project_create_view.js @@ -4,7 +4,7 @@ import { type RouterHistory, withRouter } from "react-router-dom"; import { connect } from "react-redux"; import React from "react"; -import type { APIUser, APITeam } from "admin/api_flow_types"; +import type { APIUser, APITeam } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; import { diff --git a/frontend/javascripts/admin/project/project_list_view.js b/frontend/javascripts/admin/project/project_list_view.js index 885105f2d9e..19c66e5ea3d 100644 --- a/frontend/javascripts/admin/project/project_list_view.js +++ b/frontend/javascripts/admin/project/project_list_view.js @@ -8,7 +8,7 @@ import * as React from "react"; import _ from "lodash"; import { AsyncLink } from "components/async_clickables"; -import type { APIProjectWithAssignments, APIUser } from "admin/api_flow_types"; +import type { APIProjectWithAssignments, APIUser } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; import { diff --git a/frontend/javascripts/admin/project/transfer_all_tasks_modal.js b/frontend/javascripts/admin/project/transfer_all_tasks_modal.js index f3a02df0bfd..6420cba374b 100644 --- a/frontend/javascripts/admin/project/transfer_all_tasks_modal.js +++ b/frontend/javascripts/admin/project/transfer_all_tasks_modal.js @@ -4,7 +4,7 @@ import { Modal, Button, Table, Spin } from "antd"; import * as React from "react"; import _ from "lodash"; -import type { APIUser, APIProject, APIActiveUser } from "admin/api_flow_types"; +import type { APIUser, APIProject, APIActiveUser } from "types/api_flow_types"; import { getUsers, getUsersWithActiveTasks, diff --git a/frontend/javascripts/admin/scripts/script_create_view.js b/frontend/javascripts/admin/scripts/script_create_view.js index 2f0b4033630..fb328f4e83f 100644 --- a/frontend/javascripts/admin/scripts/script_create_view.js +++ b/frontend/javascripts/admin/scripts/script_create_view.js @@ -4,7 +4,7 @@ import { type RouterHistory, withRouter } from "react-router-dom"; import { connect } from "react-redux"; import React from "react"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; import { diff --git a/frontend/javascripts/admin/scripts/script_list_view.js b/frontend/javascripts/admin/scripts/script_list_view.js index 1c1ee395272..37f9fbc0fb7 100644 --- a/frontend/javascripts/admin/scripts/script_list_view.js +++ b/frontend/javascripts/admin/scripts/script_list_view.js @@ -6,7 +6,7 @@ import { Table, Icon, Spin, Button, Input, Modal } from "antd"; import * as React from "react"; import _ from "lodash"; -import type { APIScript, APIUser } from "admin/api_flow_types"; +import type { APIScript, APIUser } from "types/api_flow_types"; import { getScripts, deleteScript } from "admin/admin_rest_api"; import { handleGenericError } from "libs/error_handling"; import Persistence from "libs/persistence"; diff --git a/frontend/javascripts/admin/statistic/open_tasks_report_view.js b/frontend/javascripts/admin/statistic/open_tasks_report_view.js index 070f8b50bc2..6e556239bb8 100644 --- a/frontend/javascripts/admin/statistic/open_tasks_report_view.js +++ b/frontend/javascripts/admin/statistic/open_tasks_report_view.js @@ -2,7 +2,7 @@ import { Spin, Table, Card } from "antd"; import * as React from "react"; -import type { APIOpenTasksReport } from "admin/api_flow_types"; +import type { APIOpenTasksReport } from "types/api_flow_types"; import { getOpenTasksReport } from "admin/admin_rest_api"; import { handleGenericError } from "libs/error_handling"; import * as Utils from "libs/utils"; diff --git a/frontend/javascripts/admin/statistic/project_progress_report_view.js b/frontend/javascripts/admin/statistic/project_progress_report_view.js index 7d38085659c..035d56f0dc7 100644 --- a/frontend/javascripts/admin/statistic/project_progress_report_view.js +++ b/frontend/javascripts/admin/statistic/project_progress_report_view.js @@ -2,7 +2,7 @@ import { Badge, Icon, Spin, Table, Card } from "antd"; import * as React from "react"; -import type { APIProjectProgressReport, APITeam } from "admin/api_flow_types"; +import type { APIProjectProgressReport, APITeam } from "types/api_flow_types"; import { getProjectProgressReport } from "admin/admin_rest_api"; import FormattedDate from "components/formatted_date"; import Loop from "components/loop"; diff --git a/frontend/javascripts/admin/statistic/team_selection_form.js b/frontend/javascripts/admin/statistic/team_selection_form.js index f5604f721d5..1306f1a7bc6 100644 --- a/frontend/javascripts/admin/statistic/team_selection_form.js +++ b/frontend/javascripts/admin/statistic/team_selection_form.js @@ -2,7 +2,7 @@ import { Row, Col, Form, Button } from "antd"; import * as React from "react"; -import type { APITeam } from "admin/api_flow_types"; +import type { APITeam } from "types/api_flow_types"; import TeamSelectionComponent from "dashboard/dataset/team_selection_component"; const FormItem = Form.Item; diff --git a/frontend/javascripts/admin/task/task_annotation_view.js b/frontend/javascripts/admin/task/task_annotation_view.js index 653a6c4d582..dc678acd3b1 100644 --- a/frontend/javascripts/admin/task/task_annotation_view.js +++ b/frontend/javascripts/admin/task/task_annotation_view.js @@ -3,7 +3,7 @@ import { Dropdown, Menu, Icon, Modal } from "antd"; import { connect } from "react-redux"; import React from "react"; -import type { APIUser, APITask, APIAnnotation } from "admin/api_flow_types"; +import type { APIUser, APITask, APIAnnotation } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { formatSeconds } from "libs/format_utils"; import { AsyncLink } from "components/async_clickables"; diff --git a/frontend/javascripts/admin/task/task_create_bulk_view.js b/frontend/javascripts/admin/task/task_create_bulk_view.js index a95464fe8cf..affb57269bc 100644 --- a/frontend/javascripts/admin/task/task_create_bulk_view.js +++ b/frontend/javascripts/admin/task/task_create_bulk_view.js @@ -3,7 +3,7 @@ import { Form, Input, Button, Card, Upload, Icon, Spin, Progress, Divider } from import React from "react"; import _ from "lodash"; -import type { APITask } from "admin/api_flow_types"; +import type { APITask } from "types/api_flow_types"; import type { BoundingBoxObject } from "oxalis/store"; import type { Vector3 } from "oxalis/constants"; import { createTasks } from "admin/admin_rest_api"; diff --git a/frontend/javascripts/admin/task/task_create_form_view.js b/frontend/javascripts/admin/task/task_create_form_view.js index a37c0645a53..c5ff278032f 100644 --- a/frontend/javascripts/admin/task/task_create_form_view.js +++ b/frontend/javascripts/admin/task/task_create_form_view.js @@ -19,7 +19,7 @@ import { import React from "react"; import _ from "lodash"; -import type { APIDataset, APITaskType, APIProject, APIScript, APITask } from "admin/api_flow_types"; +import type { APIDataset, APITaskType, APIProject, APIScript, APITask } from "types/api_flow_types"; import type { BoundingBoxObject } from "oxalis/store"; import type { TaskCreationResponse, diff --git a/frontend/javascripts/admin/task/task_list_view.js b/frontend/javascripts/admin/task/task_list_view.js index ac049e8e288..8b26f5ffbb6 100644 --- a/frontend/javascripts/admin/task/task_list_view.js +++ b/frontend/javascripts/admin/task/task_list_view.js @@ -7,7 +7,7 @@ import React from "react"; import _ from "lodash"; import { AsyncLink } from "components/async_clickables"; -import type { APITask, APITaskType } from "admin/api_flow_types"; +import type { APITask, APITaskType } from "types/api_flow_types"; import { deleteTask, getTasks, downloadNml } from "admin/admin_rest_api"; import { formatTuple, formatSeconds } from "libs/format_utils"; import { handleGenericError } from "libs/error_handling"; diff --git a/frontend/javascripts/admin/task/task_search_form.js b/frontend/javascripts/admin/task/task_search_form.js index 21149ad2f65..d16e8852609 100644 --- a/frontend/javascripts/admin/task/task_search_form.js +++ b/frontend/javascripts/admin/task/task_search_form.js @@ -5,7 +5,7 @@ import { type RouterHistory, withRouter } from "react-router-dom"; import React from "react"; import _ from "lodash"; -import type { APIUser, APIProject, APITaskType } from "admin/api_flow_types"; +import type { APIUser, APIProject, APITaskType } from "types/api_flow_types"; import { getEditableUsers, getProjects, getTaskTypes } from "admin/admin_rest_api"; import Persistence from "libs/persistence"; diff --git a/frontend/javascripts/admin/tasktype/recommended_configuration_view.js b/frontend/javascripts/admin/tasktype/recommended_configuration_view.js index e08357968be..f33b7576ae0 100644 --- a/frontend/javascripts/admin/tasktype/recommended_configuration_view.js +++ b/frontend/javascripts/admin/tasktype/recommended_configuration_view.js @@ -7,7 +7,7 @@ import type { DatasetConfiguration, UserConfiguration } from "oxalis/store"; import { jsonEditStyle } from "dashboard/dataset/helper_components"; import { jsonStringify } from "libs/utils"; import { settings } from "messages"; -import { validateUserSettingsJSON } from "dashboard/dataset/validation"; +import { validateUserSettingsJSON } from "types/validation"; const FormItem = Form.Item; const { Panel } = Collapse; diff --git a/frontend/javascripts/admin/tasktype/task_type_create_view.js b/frontend/javascripts/admin/tasktype/task_type_create_view.js index b7881bb1b10..c3203003860 100644 --- a/frontend/javascripts/admin/tasktype/task_type_create_view.js +++ b/frontend/javascripts/admin/tasktype/task_type_create_view.js @@ -16,7 +16,7 @@ import { type RouterHistory, withRouter } from "react-router-dom"; import React from "react"; import _ from "lodash"; -import type { APITeam } from "admin/api_flow_types"; +import type { APITeam } from "types/api_flow_types"; import { getEditableTeams, createTaskType, diff --git a/frontend/javascripts/admin/tasktype/task_type_list_view.js b/frontend/javascripts/admin/tasktype/task_type_list_view.js index 2bec1394b37..370413d4cc2 100644 --- a/frontend/javascripts/admin/tasktype/task_type_list_view.js +++ b/frontend/javascripts/admin/tasktype/task_type_list_view.js @@ -7,7 +7,7 @@ import * as React from "react"; import _ from "lodash"; import { AsyncLink } from "components/async_clickables"; -import type { APITaskType } from "admin/api_flow_types"; +import type { APITaskType } from "types/api_flow_types"; import { getTaskTypes, deleteTaskType, downloadNml } from "admin/admin_rest_api"; import { handleGenericError } from "libs/error_handling"; import Persistence from "libs/persistence"; diff --git a/frontend/javascripts/admin/team/team_list_view.js b/frontend/javascripts/admin/team/team_list_view.js index 71d04d638c5..7dfaa656a52 100644 --- a/frontend/javascripts/admin/team/team_list_view.js +++ b/frontend/javascripts/admin/team/team_list_view.js @@ -5,7 +5,7 @@ import { Table, Icon, Spin, Button, Input, Modal, Alert } from "antd"; import * as React from "react"; import _ from "lodash"; -import type { APITeam } from "admin/api_flow_types"; +import type { APITeam } from "types/api_flow_types"; import { getEditableTeams, deleteTeam } from "admin/admin_rest_api"; import { handleGenericError } from "libs/error_handling"; import CreateTeamModal from "admin/team/create_team_modal_view"; diff --git a/frontend/javascripts/admin/time/time_line_view.js b/frontend/javascripts/admin/time/time_line_view.js index a3ade64bba4..b018a0ba661 100644 --- a/frontend/javascripts/admin/time/time_line_view.js +++ b/frontend/javascripts/admin/time/time_line_view.js @@ -7,7 +7,7 @@ import _ from "lodash"; import moment from "moment"; import FormattedDate from "components/formatted_date"; import { type OxalisState } from "oxalis/store"; -import type { APIUser, APITimeTracking } from "admin/api_flow_types"; +import type { APIUser, APITimeTracking } from "types/api_flow_types"; import { formatMilliseconds, formatDurationToMinutesAndSeconds } from "libs/format_utils"; import { isUserAdminOrTeamManager } from "libs/utils"; import { getEditableUsers, getTimeTrackingForUser } from "admin/admin_rest_api"; diff --git a/frontend/javascripts/admin/user/experience_modal_view.js b/frontend/javascripts/admin/user/experience_modal_view.js index e614763fb82..113c10d2fa5 100644 --- a/frontend/javascripts/admin/user/experience_modal_view.js +++ b/frontend/javascripts/admin/user/experience_modal_view.js @@ -4,7 +4,7 @@ import { Modal, Button, Tooltip, Icon, Table, InputNumber, Tag, Badge } from "an import * as React from "react"; import _ from "lodash"; -import type { APIUser, ExperienceDomainList } from "admin/api_flow_types"; +import type { APIUser, ExperienceDomainList } from "types/api_flow_types"; import { handleGenericError } from "libs/error_handling"; import { updateUser } from "admin/admin_rest_api"; import HighlightableRow from "components/highlightable_row"; diff --git a/frontend/javascripts/admin/user/permissions_and_teams_modal_view.js b/frontend/javascripts/admin/user/permissions_and_teams_modal_view.js index 69c84854683..a68e461132c 100644 --- a/frontend/javascripts/admin/user/permissions_and_teams_modal_view.js +++ b/frontend/javascripts/admin/user/permissions_and_teams_modal_view.js @@ -4,7 +4,7 @@ import * as React from "react"; import _ from "lodash"; import update from "immutability-helper"; -import type { APIUser, APITeam, APITeamMembership } from "admin/api_flow_types"; +import type { APIUser, APITeam, APITeamMembership } from "types/api_flow_types"; import { updateUser, getEditableTeams } from "admin/admin_rest_api"; import messages from "messages"; diff --git a/frontend/javascripts/admin/user/user_list_view.js b/frontend/javascripts/admin/user/user_list_view.js index 48d94ad8132..8df7efc4740 100644 --- a/frontend/javascripts/admin/user/user_list_view.js +++ b/frontend/javascripts/admin/user/user_list_view.js @@ -8,7 +8,7 @@ import * as React from "react"; import _ from "lodash"; import moment from "moment"; -import type { APIUser, APITeamMembership, ExperienceMap } from "admin/api_flow_types"; +import type { APIUser, APITeamMembership, ExperienceMap } from "types/api_flow_types"; import { InviteUsersPopover } from "admin/onboarding"; import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; diff --git a/frontend/javascripts/admin/user/user_selection_component.js b/frontend/javascripts/admin/user/user_selection_component.js index a9b63d76212..3b205940a63 100644 --- a/frontend/javascripts/admin/user/user_selection_component.js +++ b/frontend/javascripts/admin/user/user_selection_component.js @@ -4,7 +4,7 @@ import { Spin, Select } from "antd"; import * as React from "react"; import _ from "lodash"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import { getUsers } from "admin/admin_rest_api"; import { handleGenericError } from "libs/error_handling"; diff --git a/frontend/javascripts/components/legal.js b/frontend/javascripts/components/legal.js index da2959c68c3..7edc60cf4fa 100644 --- a/frontend/javascripts/components/legal.js +++ b/frontend/javascripts/components/legal.js @@ -3,7 +3,7 @@ import { Row, Col, Card } from "antd"; import Markdown from "react-remarkable"; import React from "react"; -import type { APIOrganization } from "admin/api_flow_types"; +import type { APIOrganization } from "types/api_flow_types"; import { getOperatorData, getDefaultOrganization } from "admin/admin_rest_api"; type Props = {}; diff --git a/frontend/javascripts/components/select_experience_domain.js b/frontend/javascripts/components/select_experience_domain.js index d5c3cc38ce1..557e60f785b 100644 --- a/frontend/javascripts/components/select_experience_domain.js +++ b/frontend/javascripts/components/select_experience_domain.js @@ -3,7 +3,7 @@ import { Select } from "antd"; import * as React from "react"; -import type { ExperienceDomainList } from "admin/api_flow_types"; +import type { ExperienceDomainList } from "types/api_flow_types"; import { getExistingExperienceDomains } from "admin/admin_rest_api"; const { Option } = Select; diff --git a/frontend/javascripts/dashboard/advanced_dataset/dataset_access_list_view.js b/frontend/javascripts/dashboard/advanced_dataset/dataset_access_list_view.js index 6bff1f4a1f3..ec509c10f28 100644 --- a/frontend/javascripts/dashboard/advanced_dataset/dataset_access_list_view.js +++ b/frontend/javascripts/dashboard/advanced_dataset/dataset_access_list_view.js @@ -3,7 +3,7 @@ import { Spin, Tag } from "antd"; import * as React from "react"; -import type { APIDataset, APIUser } from "admin/api_flow_types"; +import type { APIDataset, APIUser } from "types/api_flow_types"; import { getDatasetAccessList } from "admin/admin_rest_api"; import { handleGenericError } from "libs/error_handling"; import { stringToColor } from "libs/format_utils"; diff --git a/frontend/javascripts/dashboard/advanced_dataset/dataset_action_view.js b/frontend/javascripts/dashboard/advanced_dataset/dataset_action_view.js index b9839ce6692..d866643a31c 100644 --- a/frontend/javascripts/dashboard/advanced_dataset/dataset_action_view.js +++ b/frontend/javascripts/dashboard/advanced_dataset/dataset_action_view.js @@ -3,7 +3,7 @@ import { Dropdown, Menu, Icon, Tooltip } from "antd"; import { Link, withRouter } from "react-router-dom"; import * as React from "react"; -import type { APIMaybeUnimportedDataset, TracingType } from "admin/api_flow_types"; +import type { APIMaybeUnimportedDataset, TracingType } from "types/api_flow_types"; import { clearCache } from "admin/admin_rest_api"; import Toast from "libs/toast"; import messages from "messages"; diff --git a/frontend/javascripts/dashboard/advanced_dataset/dataset_table.js b/frontend/javascripts/dashboard/advanced_dataset/dataset_table.js index 8fd43c23a0f..dcb8ff69488 100644 --- a/frontend/javascripts/dashboard/advanced_dataset/dataset_table.js +++ b/frontend/javascripts/dashboard/advanced_dataset/dataset_table.js @@ -6,7 +6,7 @@ import _ from "lodash"; import { Link } from "react-router-dom"; import dice from "dice-coefficient"; -import type { APITeam, APIMaybeUnimportedDataset } from "admin/api_flow_types"; +import type { APITeam, APIMaybeUnimportedDataset } from "types/api_flow_types"; import { stringToColor, formatScale } from "libs/format_utils"; import type { DatasetFilteringMode } from "dashboard/dataset_view"; import DatasetAccessListView from "dashboard/advanced_dataset/dataset_access_list_view"; diff --git a/frontend/javascripts/dashboard/dashboard_task_list_view.js b/frontend/javascripts/dashboard/dashboard_task_list_view.js index 267ffb75781..555a3cb77a3 100644 --- a/frontend/javascripts/dashboard/dashboard_task_list_view.js +++ b/frontend/javascripts/dashboard/dashboard_task_list_view.js @@ -7,7 +7,7 @@ import Markdown from "react-remarkable"; import * as React from "react"; import classNames from "classnames"; -import type { APITaskWithAnnotation, APIUser, APIAnnotation } from "admin/api_flow_types"; +import type { APITaskWithAnnotation, APIUser, APIAnnotation } from "types/api_flow_types"; import { AsyncButton, AsyncLink } from "components/async_clickables"; import type { OxalisState } from "oxalis/store"; import { diff --git a/frontend/javascripts/dashboard/dashboard_view.js b/frontend/javascripts/dashboard/dashboard_view.js index a123b227a4a..c00c5e8c8fd 100644 --- a/frontend/javascripts/dashboard/dashboard_view.js +++ b/frontend/javascripts/dashboard/dashboard_view.js @@ -6,7 +6,7 @@ import React, { PureComponent } from "react"; import _ from "lodash"; import * as Utils from "libs/utils"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { enforceActiveUser } from "oxalis/model/accessors/user_accessor"; import { getUser } from "admin/admin_rest_api"; diff --git a/frontend/javascripts/dashboard/dataset/dataset_cache_provider.js b/frontend/javascripts/dashboard/dataset/dataset_cache_provider.js index f596225a6c1..9a66388d522 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_cache_provider.js +++ b/frontend/javascripts/dashboard/dataset/dataset_cache_provider.js @@ -2,7 +2,7 @@ import React, { createContext, useState, type Node } from "react"; import type { DatasetFilteringMode } from "dashboard/dataset_view"; -import type { APIMaybeUnimportedDataset } from "admin/api_flow_types"; +import type { APIMaybeUnimportedDataset } from "types/api_flow_types"; import { getDatastores, triggerDatasetCheck, getDatasets } from "admin/admin_rest_api"; import { handleGenericError } from "libs/error_handling"; import UserLocalStorage from "libs/user_local_storage"; diff --git a/frontend/javascripts/dashboard/dataset/dataset_import_view.js b/frontend/javascripts/dashboard/dataset/dataset_import_view.js index d85c638d2ee..5f4624fb573 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_import_view.js +++ b/frontend/javascripts/dashboard/dataset/dataset_import_view.js @@ -6,7 +6,7 @@ import _ from "lodash"; import moment from "moment"; import { connect } from "react-redux"; -import type { APIDataSource, APIDataset, APIDatasetId, APIMessage } from "admin/api_flow_types"; +import type { APIDataSource, APIDataset, APIDatasetId, APIMessage } from "types/api_flow_types"; import type { DatasetConfiguration, OxalisState } from "oxalis/store"; import DatasetCacheProvider, { datasetCache } from "dashboard/dataset/dataset_cache_provider"; import { diffObjects, jsonStringify } from "libs/utils"; diff --git a/frontend/javascripts/dashboard/dataset/default_config_component.js b/frontend/javascripts/dashboard/dataset/default_config_component.js index 0dad05c90ca..84ff071650f 100644 --- a/frontend/javascripts/dashboard/dataset/default_config_component.js +++ b/frontend/javascripts/dashboard/dataset/default_config_component.js @@ -4,9 +4,9 @@ import { Icon, Input, Checkbox, Alert, Form, InputNumber, Col, Row, Tooltip } fr import * as React from "react"; import { Vector3Input } from "libs/vector_input"; +import { validateLayerConfigurationJSON, syncValidator } from "types/validation"; import { FormItemWithInfo, jsonEditStyle } from "./helper_components"; -import { validateLayerConfigurationJSON, syncValidator } from "./validation"; const FormItem = Form.Item; diff --git a/frontend/javascripts/dashboard/dataset/import_delete_component.js b/frontend/javascripts/dashboard/dataset/import_delete_component.js index 042b5583af9..2cb87b7e570 100644 --- a/frontend/javascripts/dashboard/dataset/import_delete_component.js +++ b/frontend/javascripts/dashboard/dataset/import_delete_component.js @@ -4,7 +4,7 @@ import { Button } from "antd"; import React, { useState, useEffect, useContext } from "react"; import * as Utils from "libs/utils"; -import type { APIDataset, APIDatasetId } from "admin/api_flow_types"; +import type { APIDataset, APIDatasetId } from "types/api_flow_types"; import { getDataset, deleteDatasetOnDisk } from "admin/admin_rest_api"; import Toast from "libs/toast"; import messages from "messages"; diff --git a/frontend/javascripts/dashboard/dataset/import_sharing_component.js b/frontend/javascripts/dashboard/dataset/import_sharing_component.js index d9c6b65b851..e30930c523b 100644 --- a/frontend/javascripts/dashboard/dataset/import_sharing_component.js +++ b/frontend/javascripts/dashboard/dataset/import_sharing_component.js @@ -4,7 +4,7 @@ import { Button, Input, Checkbox, Tooltip } from "antd"; import Clipboard from "clipboard-js"; import React, { useState, useEffect } from "react"; -import type { APIDataset, APIDatasetId } from "admin/api_flow_types"; +import type { APIDataset, APIDatasetId } from "types/api_flow_types"; import { AsyncButton } from "components/async_clickables"; import { getDatasetSharingToken, diff --git a/frontend/javascripts/dashboard/dataset/sample_datasets_modal.js b/frontend/javascripts/dashboard/dataset/sample_datasets_modal.js index 0e94f045fd7..6256f6de6da 100644 --- a/frontend/javascripts/dashboard/dataset/sample_datasets_modal.js +++ b/frontend/javascripts/dashboard/dataset/sample_datasets_modal.js @@ -9,7 +9,7 @@ import { } from "admin/admin_rest_api"; import { useInterval, useFetch } from "libs/react_helpers"; import { handleGenericError } from "libs/error_handling"; -import type { APISampleDataset } from "admin/api_flow_types"; +import type { APISampleDataset } from "types/api_flow_types"; type Props = { destroy: () => void, diff --git a/frontend/javascripts/dashboard/dataset/simple_advanced_data_form.js b/frontend/javascripts/dashboard/dataset/simple_advanced_data_form.js index 529c8a59589..9efcf99631c 100644 --- a/frontend/javascripts/dashboard/dataset/simple_advanced_data_form.js +++ b/frontend/javascripts/dashboard/dataset/simple_advanced_data_form.js @@ -4,6 +4,7 @@ import * as React from "react"; import { Vector3Input, BoundingBoxInput } from "libs/vector_input"; import { getBitDepth } from "oxalis/model/accessors/dataset_accessor"; +import { validateDatasourceJSON, isValidJSON, syncValidator } from "types/validation"; import { Hideable, @@ -11,7 +12,6 @@ import { RetryingErrorBoundary, jsonEditStyle, } from "./helper_components"; -import { validateDatasourceJSON, isValidJSON, syncValidator } from "./validation"; const FormItem = Form.Item; diff --git a/frontend/javascripts/dashboard/dataset/team_selection_component.js b/frontend/javascripts/dashboard/dataset/team_selection_component.js index 415ce4c701d..ce07f6b675a 100644 --- a/frontend/javascripts/dashboard/dataset/team_selection_component.js +++ b/frontend/javascripts/dashboard/dataset/team_selection_component.js @@ -3,7 +3,7 @@ import { Select, Spin } from "antd"; import * as React from "react"; import _ from "lodash"; -import type { APITeam } from "admin/api_flow_types"; +import type { APITeam } from "types/api_flow_types"; import { getEditableTeams, getTeams } from "admin/admin_rest_api"; const { Option } = Select; diff --git a/frontend/javascripts/dashboard/dataset_view.js b/frontend/javascripts/dashboard/dataset_view.js index 512a5c65ad2..1feac7400c7 100644 --- a/frontend/javascripts/dashboard/dataset_view.js +++ b/frontend/javascripts/dashboard/dataset_view.js @@ -5,7 +5,7 @@ import { Link, useHistory } from "react-router-dom"; import { Badge, Button, Radio, Col, Dropdown, Icon, Input, Menu, Row, Spin } from "antd"; import { PropTypes } from "@scalableminds/prop-types"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import { OptionCard } from "admin/onboarding"; import DatasetTable from "dashboard/advanced_dataset/dataset_table"; import SampleDatasetsModal from "dashboard/dataset/sample_datasets_modal"; diff --git a/frontend/javascripts/dashboard/explorative_annotations_view.js b/frontend/javascripts/dashboard/explorative_annotations_view.js index 9bcb3121f37..e7f0df9329a 100644 --- a/frontend/javascripts/dashboard/explorative_annotations_view.js +++ b/frontend/javascripts/dashboard/explorative_annotations_view.js @@ -7,7 +7,7 @@ import _ from "lodash"; import update from "immutability-helper"; import { AsyncLink } from "components/async_clickables"; -import type { APIAnnotationCompact } from "admin/api_flow_types"; +import type { APIAnnotationCompact } from "types/api_flow_types"; import { AnnotationContentTypes } from "oxalis/constants"; import { diff --git a/frontend/javascripts/dashboard/publication_card.js b/frontend/javascripts/dashboard/publication_card.js index 36ea6126ef5..f7901692328 100644 --- a/frontend/javascripts/dashboard/publication_card.js +++ b/frontend/javascripts/dashboard/publication_card.js @@ -5,7 +5,7 @@ import React, { useState } from "react"; import classNames from "classnames"; import { Link } from "react-router-dom"; -import type { APIDataset, APIDatasetDetails } from "admin/api_flow_types"; +import type { APIDataset, APIDatasetDetails } from "types/api_flow_types"; import { formatScale } from "libs/format_utils"; import { getThumbnailURL, diff --git a/frontend/javascripts/dashboard/publication_details_view.js b/frontend/javascripts/dashboard/publication_details_view.js index 0f614eb84ac..844bb2fc8d5 100644 --- a/frontend/javascripts/dashboard/publication_details_view.js +++ b/frontend/javascripts/dashboard/publication_details_view.js @@ -3,7 +3,7 @@ import * as React from "react"; import { Layout, Icon, Spin, Tooltip } from "antd"; import { getDatasets } from "admin/admin_rest_api"; -import type { APIDataset, APIMaybeUnimportedDataset } from "admin/api_flow_types"; +import type { APIDataset, APIMaybeUnimportedDataset } from "types/api_flow_types"; import PublicationCard from "dashboard/publication_card"; import { handleGenericError } from "libs/error_handling"; import { SimpleHeader } from "dashboard/spotlight_view"; diff --git a/frontend/javascripts/dashboard/publication_view.js b/frontend/javascripts/dashboard/publication_view.js index e0810eaa162..93c1ebbd7f4 100644 --- a/frontend/javascripts/dashboard/publication_view.js +++ b/frontend/javascripts/dashboard/publication_view.js @@ -3,7 +3,7 @@ import React, { memo, useContext, useState, useEffect } from "react"; import _ from "lodash"; import { List, Input, Spin } from "antd"; -import type { APIDataset, APIMaybeUnimportedDataset } from "admin/api_flow_types"; +import type { APIDataset, APIMaybeUnimportedDataset } from "types/api_flow_types"; import PublicationCard from "dashboard/publication_card"; import { DatasetCacheContext } from "dashboard/dataset/dataset_cache_provider"; import * as Utils from "libs/utils"; diff --git a/frontend/javascripts/dashboard/shared_annotations_view.js b/frontend/javascripts/dashboard/shared_annotations_view.js index 14c426c4629..75374dc958e 100644 --- a/frontend/javascripts/dashboard/shared_annotations_view.js +++ b/frontend/javascripts/dashboard/shared_annotations_view.js @@ -2,7 +2,7 @@ import { Link, type RouterHistory, withRouter } from "react-router-dom"; import { Spin, Table, Tag, Icon, Tooltip } from "antd"; import * as React from "react"; -import type { APIAnnotationCompact } from "admin/api_flow_types"; +import type { APIAnnotationCompact } from "types/api_flow_types"; import FormattedDate from "components/formatted_date"; import { getSharedAnnotations } from "admin/admin_rest_api"; import { formatHash, stringToColor } from "libs/format_utils"; diff --git a/frontend/javascripts/dashboard/spotlight_view.js b/frontend/javascripts/dashboard/spotlight_view.js index e6bca6222bc..0e17d7974a3 100644 --- a/frontend/javascripts/dashboard/spotlight_view.js +++ b/frontend/javascripts/dashboard/spotlight_view.js @@ -4,7 +4,7 @@ import { Spin, Layout, Row, Col, Card, Input } from "antd"; import { connect } from "react-redux"; import * as React from "react"; -import type { APIMaybeUnimportedDataset, APIUser } from "admin/api_flow_types"; +import type { APIMaybeUnimportedDataset, APIUser } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { checkAnyOrganizationExists, getDatasets } from "admin/admin_rest_api"; import { handleGenericError } from "libs/error_handling"; diff --git a/frontend/javascripts/dashboard/transfer_task_modal.js b/frontend/javascripts/dashboard/transfer_task_modal.js index 5425bd03622..847d5b6ef61 100644 --- a/frontend/javascripts/dashboard/transfer_task_modal.js +++ b/frontend/javascripts/dashboard/transfer_task_modal.js @@ -3,7 +3,7 @@ import { Modal, Button } from "antd"; import * as React from "react"; -import type { APIAnnotation } from "admin/api_flow_types"; +import type { APIAnnotation } from "types/api_flow_types"; import { handleGenericError } from "libs/error_handling"; import { transferTask } from "admin/admin_rest_api"; import UserSelectionComponent from "admin/user/user_selection_component"; diff --git a/frontend/javascripts/libs/dataset_view_configuration_validation.js b/frontend/javascripts/libs/dataset_view_configuration_validation.js deleted file mode 100644 index 262518d6ffa..00000000000 --- a/frontend/javascripts/libs/dataset_view_configuration_validation.js +++ /dev/null @@ -1,78 +0,0 @@ -// @flow - -import jsonschema from "jsonschema"; - -import ViewConfigurationSchema, { - getDefaultLayerViewConfiguration, - defaultDatasetViewConfiguration, -} from "libs/dataset_view_configuration.schema.js"; -import Store, { type DatasetConfiguration } from "oxalis/store"; -import { type APIDataset, type APIDataLayer } from "admin/api_flow_types"; -import { getDefaultIntensityRangeOfLayer } from "oxalis/model/accessors/dataset_accessor"; - -const validator = new jsonschema.Validator(); -validator.addSchema(ViewConfigurationSchema, "/"); - -const validateWithSchema = (type: string, json: Object) => { - const result = validator.validate(json, { - $ref: `#/definitions/${type}`, - }); - if (result.valid) { - return []; - } else { - return result.errors; - } -}; - -const eliminateErrors = (instance: Object, errors: Array<*>, defaults: Object) => { - errors.forEach(error => { - if (error.name === "required") { - instance[error.argument] = defaults[error.argument]; - } else if (error.name === "additionalProperties") { - delete instance[error.argument]; - } else { - const [invalidFieldName] = error.property.split(".").slice(-1); - instance[invalidFieldName] = defaults[invalidFieldName]; - } - }); - return instance; -}; - -const getSpecificDefaultsForLayers = (dataset: APIDataset, layer: APIDataLayer) => ({ - intensityRange: getDefaultIntensityRangeOfLayer(dataset, layer.name), - alpha: layer.category === "color" ? 100 : 20, -}); - -export const getValidatedDatasetViewConfiguration = (json: Object): DatasetConfiguration => { - const validationErrors = validateWithSchema("types::DatasetViewConfiguration", json); - let datasetViewConfiguration = json; - if (validationErrors.length) { - datasetViewConfiguration = eliminateErrors( - datasetViewConfiguration, - validationErrors, - defaultDatasetViewConfiguration, - ); - } - - const { layers } = datasetViewConfiguration; - const { dataset } = Store.getState(); - dataset.dataSource.dataLayers.forEach(layer => { - const layerConfigDefault = getDefaultLayerViewConfiguration( - getSpecificDefaultsForLayers(dataset, layer), - ); - - const layerConfig = layers[layer.name]; - if (layerConfig) { - const layerErrors = validateWithSchema("types::LayerViewConfiguration", layerConfig); - - console.log(layerErrors); - layers[layer.name] = eliminateErrors(layerConfig, layerErrors, layerConfigDefault); - } else { - layers[layer.name] = layerConfigDefault; - } - }); - - return { ...datasetViewConfiguration, layers }; -}; - -export default validator; diff --git a/frontend/javascripts/libs/error_handling.js b/frontend/javascripts/libs/error_handling.js index 1b448747d2f..dea4126955c 100644 --- a/frontend/javascripts/libs/error_handling.js +++ b/frontend/javascripts/libs/error_handling.js @@ -6,7 +6,7 @@ import AirbrakeClient from "airbrake-js"; import _ from "lodash"; import { getActionLog } from "oxalis/model/helpers/action_logger_middleware"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import Toast from "libs/toast"; import messages from "messages"; import window, { document, location } from "libs/window"; diff --git a/frontend/javascripts/libs/utils.js b/frontend/javascripts/libs/utils.js index 01151641320..fe0fb00f951 100644 --- a/frontend/javascripts/libs/utils.js +++ b/frontend/javascripts/libs/utils.js @@ -3,7 +3,7 @@ import Maybe from "data.maybe"; import _ from "lodash"; import naturalSort from "javascript-natural-sort"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import type { BoundingBoxObject } from "oxalis/store"; import type { Vector3, diff --git a/frontend/javascripts/libs/vector_input.js b/frontend/javascripts/libs/vector_input.js index d60c1f61c7f..5dd1de4e33d 100644 --- a/frontend/javascripts/libs/vector_input.js +++ b/frontend/javascripts/libs/vector_input.js @@ -2,7 +2,7 @@ import * as React from "react"; import _ from "lodash"; -import type { ServerBoundingBoxTypeTuple } from "admin/api_flow_types"; +import type { ServerBoundingBoxTypeTuple } from "types/api_flow_types"; import type { Vector3, Vector6 } from "oxalis/constants"; import InputComponent from "oxalis/view/components/input_component"; import * as Utils from "libs/utils"; diff --git a/frontend/javascripts/navbar.js b/frontend/javascripts/navbar.js index 685f14a510f..ee6af2d8c05 100644 --- a/frontend/javascripts/navbar.js +++ b/frontend/javascripts/navbar.js @@ -4,7 +4,7 @@ import { Link, withRouter, type RouterHistory } from "react-router-dom"; import { connect } from "react-redux"; import React from "react"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import { PortalTarget } from "oxalis/view/layouting/portal_utils"; import { getBuildInfo } from "admin/admin_rest_api"; import { logoutUserAction } from "oxalis/model/actions/user_actions"; diff --git a/frontend/javascripts/oxalis/api/api_latest.js b/frontend/javascripts/oxalis/api/api_latest.js index d9a088c8ad2..c32ec3c2957 100644 --- a/frontend/javascripts/oxalis/api/api_latest.js +++ b/frontend/javascripts/oxalis/api/api_latest.js @@ -101,7 +101,7 @@ import * as Utils from "libs/utils"; import dimensions from "oxalis/model/dimensions"; import messages from "messages"; import window, { location } from "libs/window"; -import { type ElementClass } from "admin/api_flow_types"; +import { type ElementClass } from "types/api_flow_types"; import UserLocalStorage from "libs/user_local_storage"; type OutdatedDatasetConfigurationKeys = "segmentationOpacity" | "isSegmentationDisabled"; diff --git a/frontend/javascripts/oxalis/controller.js b/frontend/javascripts/oxalis/controller.js index 91edc18beec..957dd57e2c2 100644 --- a/frontend/javascripts/oxalis/controller.js +++ b/frontend/javascripts/oxalis/controller.js @@ -36,7 +36,7 @@ import Store, { import Toast from "libs/toast"; import UrlManager from "oxalis/controller/url_manager"; import * as Utils from "libs/utils"; -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import app from "app"; import constants, { ControlModeEnum, type ViewMode } from "oxalis/constants"; diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index a799d5642cf..976bbcbc392 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -8,7 +8,7 @@ import * as THREE from "three"; import TWEEN from "tween.js"; import _ from "lodash"; -import type { MeshMetaData } from "admin/api_flow_types"; +import type { MeshMetaData } from "types/api_flow_types"; import { V3 } from "libs/mjs"; import { getBoundaries } from "oxalis/model/accessors/dataset_accessor"; import { diff --git a/frontend/javascripts/oxalis/geometries/materials/plane_material_factory.js b/frontend/javascripts/oxalis/geometries/materials/plane_material_factory.js index e7079b1b2fc..83481cbe0fc 100644 --- a/frontend/javascripts/oxalis/geometries/materials/plane_material_factory.js +++ b/frontend/javascripts/oxalis/geometries/materials/plane_material_factory.js @@ -34,7 +34,7 @@ import * as Utils from "libs/utils"; import app from "app"; import getMainFragmentShader from "oxalis/shaders/main_data_fragment.glsl"; import shaderEditor from "oxalis/model/helpers/shader_editor"; -import { type ElementClass } from "admin/api_flow_types"; +import { type ElementClass } from "types/api_flow_types"; type ShaderMaterialOptions = { polygonOffset?: boolean, diff --git a/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js b/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js index b6f1a423d93..cde58245f1e 100644 --- a/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/dataset_accessor.js @@ -9,7 +9,7 @@ import type { APIMaybeUnimportedDataset, APISegmentationLayer, ElementClass, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import type { Settings, DataLayerType, diff --git a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.js b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.js index ea0819b2493..7da2b5e7972 100644 --- a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.js @@ -14,7 +14,7 @@ import { getResolutions, } from "oxalis/model/accessors/dataset_accessor"; import { map3, mod } from "libs/utils"; -import { userSettings } from "libs/user_settings.schema"; +import { userSettings } from "types/schemas/user_settings.schema"; import Dimensions from "oxalis/model/dimensions"; import constants, { type OrthoView, diff --git a/frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.js b/frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.js index 16624e06fa2..b2a6c28b8da 100644 --- a/frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.js @@ -2,7 +2,7 @@ import Maybe from "data.maybe"; import _ from "lodash"; -import type { HybridServerTracing, ServerSkeletonTracing } from "admin/api_flow_types"; +import type { HybridServerTracing, ServerSkeletonTracing } from "types/api_flow_types"; import type { Tracing, SkeletonTracing, diff --git a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js index 7a7413ca4d6..acebf0c926a 100644 --- a/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/tracing_accessor.js @@ -4,7 +4,7 @@ import type { HybridServerTracing, ServerSkeletonTracing, ServerVolumeTracing, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import type { Tracing, VolumeTracing, SkeletonTracing, ReadOnlyTracing } from "oxalis/store"; export function getSomeTracing( diff --git a/frontend/javascripts/oxalis/model/accessors/user_accessor.js b/frontend/javascripts/oxalis/model/accessors/user_accessor.js index 896c92f9ba8..7185074d66c 100644 --- a/frontend/javascripts/oxalis/model/accessors/user_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/user_accessor.js @@ -1,6 +1,6 @@ // @flow -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; import messages from "messages"; export function enforceActiveUser(activeUser: ?APIUser): APIUser { diff --git a/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.js b/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.js index fc85b8f4a05..fad118b32d1 100644 --- a/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/volumetracing_accessor.js @@ -6,7 +6,7 @@ import Maybe from "data.maybe"; import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; import type { Tracing, VolumeTracing, OxalisState } from "oxalis/store"; import type { VolumeTool, ContourMode } from "oxalis/constants"; -import type { HybridServerTracing, ServerVolumeTracing } from "admin/api_flow_types"; +import type { HybridServerTracing, ServerVolumeTracing } from "types/api_flow_types"; export function getVolumeTracing(tracing: Tracing): Maybe { if (tracing.volume != null) { diff --git a/frontend/javascripts/oxalis/model/actions/annotation_actions.js b/frontend/javascripts/oxalis/model/actions/annotation_actions.js index e8865a3bbd5..f6fafe26da2 100644 --- a/frontend/javascripts/oxalis/model/actions/annotation_actions.js +++ b/frontend/javascripts/oxalis/model/actions/annotation_actions.js @@ -5,7 +5,7 @@ import type { MeshMetaData, RemoteMeshMetaData, APIAnnotationVisibility, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import type { UserBoundingBox } from "oxalis/store"; type InitializeAnnotationAction = { diff --git a/frontend/javascripts/oxalis/model/actions/dataset_actions.js b/frontend/javascripts/oxalis/model/actions/dataset_actions.js index c44f0eb7039..7bcf57a4874 100644 --- a/frontend/javascripts/oxalis/model/actions/dataset_actions.js +++ b/frontend/javascripts/oxalis/model/actions/dataset_actions.js @@ -1,5 +1,5 @@ // @flow -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; type SetDatasetAction = { type: "SET_DATASET", dataset: APIDataset }; type SetLayerMappingsAction = { diff --git a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js index 330077c2519..7a418fc3435 100644 --- a/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/skeletontracing_actions.js @@ -2,7 +2,7 @@ import { Modal } from "antd"; import React from "react"; -import type { ServerSkeletonTracing } from "admin/api_flow_types"; +import type { ServerSkeletonTracing } from "types/api_flow_types"; import type { Vector3 } from "oxalis/constants"; import { enforceSkeletonTracing, diff --git a/frontend/javascripts/oxalis/model/actions/user_actions.js b/frontend/javascripts/oxalis/model/actions/user_actions.js index 0dea5be714b..1ebb5240db6 100644 --- a/frontend/javascripts/oxalis/model/actions/user_actions.js +++ b/frontend/javascripts/oxalis/model/actions/user_actions.js @@ -1,5 +1,5 @@ // @flow -import type { APIUser } from "admin/api_flow_types"; +import type { APIUser } from "types/api_flow_types"; type SetActiveUser = { type: "SET_ACTIVE_USER", diff --git a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js index c4500383416..b336a7f7636 100644 --- a/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js +++ b/frontend/javascripts/oxalis/model/actions/volumetracing_actions.js @@ -2,7 +2,7 @@ * volumetracing_actions.js * @flow */ -import type { ServerVolumeTracing } from "admin/api_flow_types"; +import type { ServerVolumeTracing } from "types/api_flow_types"; import type { Vector2, Vector3, diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index deff603fd2f..847f05f4c1e 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -19,7 +19,7 @@ import TemporalBucketManager from "oxalis/model/bucket_data_handling/temporal_bu import Constants, { type Vector2, type Vector4, type BoundingBoxType } from "oxalis/constants"; import type { DimensionMap } from "oxalis/model/dimensions"; import window from "libs/window"; -import { type ElementClass } from "admin/api_flow_types"; +import { type ElementClass } from "types/api_flow_types"; import { addBucketToUndoAction } from "oxalis/model/actions/volumetracing_actions"; export const BucketStateEnum = { diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js index 6996ae47e7d..76d021b8057 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js @@ -33,7 +33,7 @@ import constants, { type Vector4, type BoundingBoxType, } from "oxalis/constants"; -import { type ElementClass } from "admin/api_flow_types"; +import { type ElementClass } from "types/api_flow_types"; import { areBoundingBoxesOverlappingOrTouching } from "libs/utils"; class CubeEntry { data: Map; diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_rendering_logic.js b/frontend/javascripts/oxalis/model/bucket_data_handling/data_rendering_logic.js index 8c3eb6a8e61..f0b745dd55b 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_rendering_logic.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_rendering_logic.js @@ -3,7 +3,7 @@ import _ from "lodash"; import { document } from "libs/window"; import constants, { type Vector3 } from "oxalis/constants"; -import { type ElementClass } from "admin/api_flow_types"; +import { type ElementClass } from "types/api_flow_types"; import Toast from "libs/toast"; type GpuSpecs = { diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/mappings.js b/frontend/javascripts/oxalis/model/bucket_data_handling/mappings.js index 90d62fe1290..3bf9f40b83d 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/mappings.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/mappings.js @@ -6,7 +6,7 @@ import * as THREE from "three"; import _ from "lodash"; -import type { APIMapping } from "admin/api_flow_types"; +import type { APIMapping } from "types/api_flow_types"; import type { ProgressCallback } from "libs/progress_callback"; import { createUpdatableTexture } from "oxalis/geometries/materials/plane_material_factory_helpers"; import { doWithToken } from "admin/admin_rest_api"; diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/texture_bucket_manager.js b/frontend/javascripts/oxalis/model/bucket_data_handling/texture_bucket_manager.js index 4f520269ae5..898591f9ba2 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/texture_bucket_manager.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/texture_bucket_manager.js @@ -20,7 +20,7 @@ import Store from "oxalis/store"; import UpdatableTexture from "libs/UpdatableTexture"; import constants, { type Vector3, type Vector4 } from "oxalis/constants"; import window from "libs/window"; -import type { ElementClass } from "admin/api_flow_types"; +import type { ElementClass } from "types/api_flow_types"; // A TextureBucketManager instance is responsible for making buckets available // to the GPU. diff --git a/frontend/javascripts/oxalis/model/helpers/generate_dummy_trees.js b/frontend/javascripts/oxalis/model/helpers/generate_dummy_trees.js index 5d0d7848727..5f0e48b2a8b 100644 --- a/frontend/javascripts/oxalis/model/helpers/generate_dummy_trees.js +++ b/frontend/javascripts/oxalis/model/helpers/generate_dummy_trees.js @@ -1,7 +1,7 @@ // @flow import _ from "lodash"; -import type { ServerSkeletonTracingTree } from "admin/api_flow_types"; +import type { ServerSkeletonTracingTree } from "types/api_flow_types"; // This is a quick'n'dirty code to generate a huge amount of mocked trees. // Since the server cannot handle such big tracings at the moment, we'll diff --git a/frontend/javascripts/oxalis/model/helpers/nml_helpers.js b/frontend/javascripts/oxalis/model/helpers/nml_helpers.js index a36c3fbeb52..8755d8852f2 100644 --- a/frontend/javascripts/oxalis/model/helpers/nml_helpers.js +++ b/frontend/javascripts/oxalis/model/helpers/nml_helpers.js @@ -3,7 +3,7 @@ import Saxophone from "@scalableminds/saxophone"; import _ from "lodash"; -import type { APIBuildInfo } from "admin/api_flow_types"; +import type { APIBuildInfo } from "types/api_flow_types"; import { getMaximumGroupId, getMaximumTreeId, diff --git a/frontend/javascripts/oxalis/model/helpers/proto_helpers.js b/frontend/javascripts/oxalis/model/helpers/proto_helpers.js index d33461f4c0e..23cd93b1fa1 100644 --- a/frontend/javascripts/oxalis/model/helpers/proto_helpers.js +++ b/frontend/javascripts/oxalis/model/helpers/proto_helpers.js @@ -2,7 +2,7 @@ import { Root } from "protobufjs/light"; -import type { ServerTracing } from "admin/api_flow_types"; +import type { ServerTracing } from "types/api_flow_types"; import SkeletonTracingProto from "SkeletonTracing.proto"; import VolumeTracingProto from "VolumeTracing.proto"; diff --git a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js index 3b6574009f1..5a470780245 100644 --- a/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js +++ b/frontend/javascripts/oxalis/model/reducers/reducer_helpers.js @@ -5,7 +5,7 @@ import type { APIAnnotation, ServerBoundingBox, UserBoundingBoxFromServer, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import type { Annotation, BoundingBoxObject, diff --git a/frontend/javascripts/oxalis/model/reducers/settings_reducer.js b/frontend/javascripts/oxalis/model/reducers/settings_reducer.js index 8ed573ca3e6..9fe3d322286 100644 --- a/frontend/javascripts/oxalis/model/reducers/settings_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/settings_reducer.js @@ -1,8 +1,6 @@ // @flow -import _ from "lodash"; - import type { Action } from "oxalis/model/actions/actions"; -import type { OxalisState, DatasetConfiguration } from "oxalis/store"; +import type { OxalisState } from "oxalis/store"; import { updateKey, updateKey2, @@ -11,8 +9,7 @@ import { type StateShape2, } from "oxalis/model/helpers/deep_update"; import { clamp } from "libs/utils"; -import { getDefaultIntensityRangeOfLayer } from "oxalis/model/accessors/dataset_accessor"; -import { userSettings } from "libs/user_settings.schema"; +import { userSettings } from "types/schemas/user_settings.schema"; // // Update helpers @@ -87,43 +84,11 @@ function SettingsReducer(state: OxalisState, action: Action): OxalisState { } case "INITIALIZE_SETTINGS": { - const initialLayerSettings = action.initialDatasetSettings.layers || {}; - const layerSettingsDefaults = _.transform( - state.dataset.dataSource.dataLayers, - (result, layer) => { - const intensityRange = getDefaultIntensityRangeOfLayer(state.dataset, layer.name); - result[layer.name] = Object.assign( - {}, - { - brightness: 0, - contrast: 1, - color: [255, 255, 255], - alpha: layer.category === "color" ? 100 : 20, - intensityRange, - isDisabled: false, - isInverted: false, - isInEditMode: false, - }, - initialLayerSettings[layer.name], - ); - }, - {}, - ); - - // $FlowIssue[incompatible-exact] Flow has problems with exactness for empty objects, see https://github.com/facebook/flow/issues/2977 - const initialDatasetSettingsWithDefaults: DatasetConfiguration = Object.assign( - {}, - action.initialDatasetSettings, - { - layers: layerSettingsDefaults, - }, - ); - return { ...state, datasetConfiguration: { ...state.datasetConfiguration, - ...initialDatasetSettingsWithDefaults, + ...action.initialDatasetSettings, }, userConfiguration: { ...state.userConfiguration, diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.js b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.js index 5014ed8d3b0..ce9a94683b7 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.js +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.js @@ -43,7 +43,7 @@ import ColorGenerator from "libs/color_generator"; import Constants from "oxalis/constants"; import Toast from "libs/toast"; import * as Utils from "libs/utils"; -import { userSettings } from "libs/user_settings.schema"; +import { userSettings } from "types/schemas/user_settings.schema"; function SkeletonTracingReducer(state: OxalisState, action: Action): OxalisState { const { restrictions } = state.tracing; diff --git a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.js b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.js index 01faf73f3b1..3693683b1a9 100644 --- a/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.js +++ b/frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.js @@ -31,7 +31,7 @@ import type { ServerSkeletonTracingTree, ServerNode, ServerBranchPoint, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import { getBaseVoxel } from "oxalis/model/scaleinfo"; import { getSkeletonTracing, diff --git a/frontend/javascripts/oxalis/model/sagas/automatic_brush_saga.js b/frontend/javascripts/oxalis/model/sagas/automatic_brush_saga.js index c89f1d74f7e..9cb990c655f 100644 --- a/frontend/javascripts/oxalis/model/sagas/automatic_brush_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/automatic_brush_saga.js @@ -9,7 +9,7 @@ import { type InferSegmentationInViewportAction } from "oxalis/model/actions/vol import { sleep } from "libs/utils"; import { type Vector2, type Vector3, type OrthoView, OrthoViews } from "oxalis/constants"; import { getMeanAndStdDevFromDataset } from "admin/admin_rest_api"; -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; import { createWorker } from "oxalis/workers/comlink_wrapper"; import TensorFlowWorker from "oxalis/workers/tensorflow.worker"; import mainThreadPredict from "oxalis/workers/tensorflow.impl"; diff --git a/frontend/javascripts/oxalis/model/sagas/handle_mesh_changes.js b/frontend/javascripts/oxalis/model/sagas/handle_mesh_changes.js index 26a9a362128..2ed8a65b567 100644 --- a/frontend/javascripts/oxalis/model/sagas/handle_mesh_changes.js +++ b/frontend/javascripts/oxalis/model/sagas/handle_mesh_changes.js @@ -8,7 +8,7 @@ import { addMeshMetaDataAction, updateLocalMeshMetaDataAction, } from "oxalis/model/actions/annotation_actions"; -import type { MeshMetaData } from "admin/api_flow_types"; +import type { MeshMetaData } from "types/api_flow_types"; import { type Saga, _all, diff --git a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js index 490dd79de61..987a801bd0c 100644 --- a/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/isosurface_saga.js @@ -1,7 +1,7 @@ // @flow import { saveAs } from "file-saver"; -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; import { changeActiveIsosurfaceCellAction, type ChangeActiveIsosurfaceCellAction, diff --git a/frontend/javascripts/oxalis/model/sagas/load_histogram_data_saga.js b/frontend/javascripts/oxalis/model/sagas/load_histogram_data_saga.js index 57f9453cb03..0999904a23a 100644 --- a/frontend/javascripts/oxalis/model/sagas/load_histogram_data_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/load_histogram_data_saga.js @@ -8,7 +8,7 @@ import { import { getHistogramForLayer } from "admin/admin_rest_api"; import DataLayer from "oxalis/model/data_layer"; import Model from "oxalis/model"; -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; async function fetchAllHistogramsForLayers( dataLayers: Array, diff --git a/frontend/javascripts/oxalis/model/sagas/task_saga.js b/frontend/javascripts/oxalis/model/sagas/task_saga.js index b07122e19c3..04d05390de6 100644 --- a/frontend/javascripts/oxalis/model/sagas/task_saga.js +++ b/frontend/javascripts/oxalis/model/sagas/task_saga.js @@ -2,7 +2,7 @@ import React from "react"; import _ from "lodash"; -import type { APITaskType } from "admin/api_flow_types"; +import type { APITaskType } from "types/api_flow_types"; import { type Saga, call, put, select, take } from "oxalis/model/sagas/effect-generators"; import { setZoomStepAction } from "oxalis/model/actions/flycam_actions"; import { diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index b91d2d93024..a0e7bc10009 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -8,7 +8,7 @@ import type { APIDataLayer, HybridServerTracing, ServerVolumeTracing, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import { computeDataTexturesSetup, getSupportedTextureSpecs, @@ -67,7 +67,7 @@ import * as Utils from "libs/utils"; import constants, { ControlModeEnum, type Vector3 } from "oxalis/constants"; import messages from "messages"; import window from "libs/window"; -import { getValidatedDatasetViewConfiguration } from "libs/dataset_view_configuration_validation"; +import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults"; export const HANDLED_ERROR = "error_was_handled"; @@ -127,12 +127,9 @@ export async function initialize( ); initializeDataset(initialFetch, dataset, tracing); + enforceValidatedDatasetViewConfiguration(initialDatasetSettings, dataset); initializeSettings(initialUserSettings, initialDatasetSettings); - const test = getValidatedDatasetViewConfiguration(initialDatasetSettings); - - console.log(test); - let initializationInformation = null; // There is no need to reinstantiate the DataLayers if the dataset didn't change. if (initialFetch) { diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index 4aca1ead2f8..d5418af1bce 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -24,7 +24,7 @@ import type { APIUser, APIUserBase, MeshMetaData, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import type { Action } from "oxalis/model/actions/actions"; import type { Matrix4x4 } from "libs/mjs"; import type { SkeletonTracingStats } from "oxalis/model/accessors/skeletontracing_accessor"; diff --git a/frontend/javascripts/oxalis/view/action-bar/dataset_position_view.js b/frontend/javascripts/oxalis/view/action-bar/dataset_position_view.js index 2bd31b47da3..8e4e31a2196 100644 --- a/frontend/javascripts/oxalis/view/action-bar/dataset_position_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/dataset_position_view.js @@ -3,7 +3,7 @@ import { Input, Tooltip, Icon } from "antd"; import { connect } from "react-redux"; import Clipboard from "clipboard-js"; import React, { PureComponent } from "react"; -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; import { V3 } from "libs/mjs"; import { Vector3Input } from "libs/vector_input"; import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor"; diff --git a/frontend/javascripts/oxalis/view/action-bar/merge_modal_view.js b/frontend/javascripts/oxalis/view/action-bar/merge_modal_view.js index f11945bf401..0730df2016f 100644 --- a/frontend/javascripts/oxalis/view/action-bar/merge_modal_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/merge_modal_view.js @@ -4,7 +4,7 @@ import { connect } from "react-redux"; import React, { PureComponent } from "react"; import type { Dispatch } from "redux"; -import type { APIAnnotation } from "admin/api_flow_types"; +import type { APIAnnotation } from "types/api_flow_types"; import { addTreesAndGroupsAction } from "oxalis/model/actions/skeletontracing_actions"; import { createMutableTreeMapFromTreeArray } from "oxalis/model/reducers/skeletontracing_reducer_helpers"; import { getAnnotationInformation, getTracingForAnnotationType } from "admin/admin_rest_api"; diff --git a/frontend/javascripts/oxalis/view/action-bar/share_modal_view.js b/frontend/javascripts/oxalis/view/action-bar/share_modal_view.js index 1fd5d379f34..a5d74536a14 100644 --- a/frontend/javascripts/oxalis/view/action-bar/share_modal_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/share_modal_view.js @@ -3,7 +3,7 @@ import { Alert, Divider, Radio, Modal, Input, Button, Row, Col, Icon } from "ant import { useSelector } from "react-redux"; import Clipboard from "clipboard-js"; import React, { useState, useEffect } from "react"; -import type { APIDataset, APIAnnotationVisibility, APIAnnotationType } from "admin/api_flow_types"; +import type { APIDataset, APIAnnotationVisibility, APIAnnotationType } from "types/api_flow_types"; import { getDatasetSharingToken, getTeamsForSharedAnnotation, diff --git a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js index 2b80fff98b8..4b5f5bfa4bd 100644 --- a/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js +++ b/frontend/javascripts/oxalis/view/action-bar/tracing_actions_view.js @@ -4,7 +4,7 @@ import { Button, Dropdown, Icon, Menu, Modal, Tooltip } from "antd"; import { connect } from "react-redux"; import * as React from "react"; -import { APIAnnotationTypeEnum, type APIAnnotationType, type APIUser } from "admin/api_flow_types"; +import { APIAnnotationTypeEnum, type APIAnnotationType, type APIUser } from "types/api_flow_types"; import { AsyncButton } from "components/async_clickables"; import { type LayoutKeys, diff --git a/frontend/javascripts/oxalis/view/action_bar_view.js b/frontend/javascripts/oxalis/view/action_bar_view.js index 091e5b441c6..3b5593602ce 100644 --- a/frontend/javascripts/oxalis/view/action_bar_view.js +++ b/frontend/javascripts/oxalis/view/action_bar_view.js @@ -8,7 +8,7 @@ import type { APIUser, APIMaybeUnimportedDataset, TracingType, -} from "admin/api_flow_types"; +} from "types/api_flow_types"; import { createExplorational } from "admin/admin_rest_api"; import { layoutEmitter, diff --git a/frontend/javascripts/oxalis/view/right-menu/dataset_info_tab_view.js b/frontend/javascripts/oxalis/view/right-menu/dataset_info_tab_view.js index d61f75979fe..b412fdadf7e 100644 --- a/frontend/javascripts/oxalis/view/right-menu/dataset_info_tab_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/dataset_info_tab_view.js @@ -8,7 +8,7 @@ import { connect } from "react-redux"; import Markdown from "react-remarkable"; import React from "react"; -import { APIAnnotationTypeEnum, type APIDataset, type APIUser } from "admin/api_flow_types"; +import { APIAnnotationTypeEnum, type APIDataset, type APIUser } from "types/api_flow_types"; import { ControlModeEnum } from "oxalis/constants"; import { convertToHybridTracing } from "admin/admin_rest_api"; import { formatScale } from "libs/format_utils"; diff --git a/frontend/javascripts/oxalis/view/right-menu/mapping_info_view.js b/frontend/javascripts/oxalis/view/right-menu/mapping_info_view.js index 3c33e82e016..2d215be8d27 100644 --- a/frontend/javascripts/oxalis/view/right-menu/mapping_info_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/mapping_info_view.js @@ -10,7 +10,7 @@ import _ from "lodash"; import debounceRender from "react-debounce-render"; import createProgressCallback from "libs/progress_callback"; -import type { APIDataset, APISegmentationLayer } from "admin/api_flow_types"; +import type { APIDataset, APISegmentationLayer } from "types/api_flow_types"; import { type OrthoView, OrthoViews, type Vector2, type Vector3 } from "oxalis/constants"; import type { OxalisState, Mapping, MappingType } from "oxalis/store"; import { calculateGlobalPos } from "oxalis/controller/viewmodes/plane_controller"; diff --git a/frontend/javascripts/oxalis/view/right-menu/meshes_view.js b/frontend/javascripts/oxalis/view/right-menu/meshes_view.js index a572c6cbd24..9be397b3c69 100644 --- a/frontend/javascripts/oxalis/view/right-menu/meshes_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/meshes_view.js @@ -6,7 +6,7 @@ import { connect } from "react-redux"; import React from "react"; import type { ExtractReturn } from "libs/type_helpers"; -import type { MeshMetaData, RemoteMeshMetaData } from "admin/api_flow_types"; +import type { MeshMetaData, RemoteMeshMetaData } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import type { Vector3 } from "oxalis/constants"; import { Vector3Input } from "libs/vector_input"; diff --git a/frontend/javascripts/oxalis/view/scalebar.js b/frontend/javascripts/oxalis/view/scalebar.js index f5090bb6360..e69dab81779 100644 --- a/frontend/javascripts/oxalis/view/scalebar.js +++ b/frontend/javascripts/oxalis/view/scalebar.js @@ -4,7 +4,7 @@ import { connect } from "react-redux"; import * as React from "react"; import { Tooltip } from "antd"; -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { convertPixelsToNm } from "oxalis/view/right-menu/dataset_info_tab_view"; import { formatNumberToLength } from "libs/format_utils"; diff --git a/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js b/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js index 404577f04c3..b4498bb365f 100644 --- a/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js +++ b/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js @@ -12,7 +12,7 @@ import { V3 } from "libs/mjs"; import api from "oxalis/api/internal_api"; import messages, { settings } from "messages"; -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; import { AsyncIconButton } from "components/async_clickables"; import { SwitchSetting, diff --git a/frontend/javascripts/oxalis/view/settings/histogram_view.js b/frontend/javascripts/oxalis/view/settings/histogram_view.js index 8697cd9ce37..f2a59b147d3 100644 --- a/frontend/javascripts/oxalis/view/settings/histogram_view.js +++ b/frontend/javascripts/oxalis/view/settings/histogram_view.js @@ -7,8 +7,7 @@ import type { Dispatch } from "redux"; import { connect } from "react-redux"; import { type DatasetLayerConfiguration } from "oxalis/store"; import { updateLayerSettingAction } from "oxalis/model/actions/settings_actions"; -import { type ElementClass } from "admin/api_flow_types"; -import type { APIHistogramData } from "admin/api_flow_types"; +import type { APIHistogramData, ElementClass } from "types/api_flow_types"; import type { Vector3, Vector2 } from "oxalis/constants"; import { roundTo } from "libs/utils"; diff --git a/frontend/javascripts/oxalis/view/settings/user_settings_view.js b/frontend/javascripts/oxalis/view/settings/user_settings_view.js index 91b1c7b967e..823d0634db9 100644 --- a/frontend/javascripts/oxalis/view/settings/user_settings_view.js +++ b/frontend/javascripts/oxalis/view/settings/user_settings_view.js @@ -10,7 +10,7 @@ import { connect } from "react-redux"; import React, { PureComponent } from "react"; import _ from "lodash"; -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; import { LogSliderSetting, NumberInputSetting, @@ -42,7 +42,7 @@ import { updateTemporarySettingAction, updateUserSettingAction, } from "oxalis/model/actions/settings_actions"; -import { userSettings } from "libs/user_settings.schema"; +import { userSettings } from "types/schemas/user_settings.schema"; import Constants, { type ControlMode, ControlModeEnum, type ViewMode } from "oxalis/constants"; import Toast from "libs/toast"; import * as Utils from "libs/utils"; diff --git a/frontend/javascripts/oxalis/view/version_entry_group.js b/frontend/javascripts/oxalis/view/version_entry_group.js index 10f4b0c7f9c..32273b41809 100644 --- a/frontend/javascripts/oxalis/view/version_entry_group.js +++ b/frontend/javascripts/oxalis/view/version_entry_group.js @@ -3,7 +3,7 @@ import { Avatar, List } from "antd"; import * as React from "react"; import _ from "lodash"; -import type { APIUpdateActionBatch } from "admin/api_flow_types"; +import type { APIUpdateActionBatch } from "types/api_flow_types"; import FormattedDate from "components/formatted_date"; import VersionEntry from "oxalis/view/version_entry"; diff --git a/frontend/javascripts/oxalis/view/version_list.js b/frontend/javascripts/oxalis/view/version_list.js index 0280337b83e..c45b8c2a1d0 100644 --- a/frontend/javascripts/oxalis/view/version_list.js +++ b/frontend/javascripts/oxalis/view/version_list.js @@ -4,7 +4,7 @@ import * as React from "react"; import _ from "lodash"; import moment from "moment"; -import type { APIUpdateActionBatch } from "admin/api_flow_types"; +import type { APIUpdateActionBatch } from "types/api_flow_types"; import { ControlModeEnum } from "oxalis/constants"; import type { Versions } from "oxalis/view/version_view"; import { chunkIntoTimeWindows } from "libs/utils"; diff --git a/frontend/javascripts/router.js b/frontend/javascripts/router.js index daac5251ee4..5ccefdc05f0 100644 --- a/frontend/javascripts/router.js +++ b/frontend/javascripts/router.js @@ -6,7 +6,7 @@ import Enum from "Enumjs"; import React from "react"; import { createBrowserHistory } from "history"; -import { APIAnnotationTypeEnum, type APIUser, TracingTypeEnum } from "admin/api_flow_types"; +import { APIAnnotationTypeEnum, type APIUser, TracingTypeEnum } from "types/api_flow_types"; import { ControlModeEnum } from "oxalis/constants"; import { Imprint, Privacy } from "components/legal"; import type { OxalisState } from "oxalis/store"; diff --git a/frontend/javascripts/test/api/api_skeleton_latest.spec.js b/frontend/javascripts/test/api/api_skeleton_latest.spec.js index 599b4d07ce2..96e1a1a1d84 100644 --- a/frontend/javascripts/test/api/api_skeleton_latest.spec.js +++ b/frontend/javascripts/test/api/api_skeleton_latest.spec.js @@ -3,7 +3,7 @@ import { __setupOxalis, KeyboardJS } from "test/helpers/apiHelpers"; import { makeBasicGroupObject } from "oxalis/view/right-menu/tree_hierarchy_view_helpers"; import { setMappingEnabledAction } from "oxalis/model/actions/settings_actions"; import { setTreeGroupsAction } from "oxalis/model/actions/skeletontracing_actions"; -import { userSettings } from "libs/user_settings.schema"; +import { userSettings } from "types/schemas/user_settings.schema"; import Store from "oxalis/store"; import sinon from "sinon"; import test from "ava"; diff --git a/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.js b/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.js index 3d6ee72153f..b04b4411edc 100644 --- a/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.js +++ b/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.js @@ -6,7 +6,7 @@ import { tokenUserA, writeFlowCheckingFile, } from "test/enzyme/e2e-setup"; -import { APIAnnotationTypeEnum } from "admin/api_flow_types"; +import { APIAnnotationTypeEnum } from "types/api_flow_types"; import { createTreeMapFromTreeArray } from "oxalis/model/reducers/skeletontracing_reducer_helpers"; import { diffTrees } from "oxalis/model/sagas/skeletontracing_saga"; import { sendRequestWithToken, addVersionNumbers } from "oxalis/model/sagas/save_saga"; diff --git a/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.js b/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.js index 668d57a89cc..34e4c2a2e86 100644 --- a/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.js +++ b/frontend/javascripts/test/backend-snapshot-tests/datasets.e2e.js @@ -1,7 +1,7 @@ // @flow import _ from "lodash"; -import type { APIDataset } from "admin/api_flow_types"; +import type { APIDataset } from "types/api_flow_types"; import { tokenUserA, setCurrToken, diff --git a/frontend/javascripts/test/backend-snapshot-tests/projects.e2e.js b/frontend/javascripts/test/backend-snapshot-tests/projects.e2e.js index 493fa7a5bb1..359b9433402 100644 --- a/frontend/javascripts/test/backend-snapshot-tests/projects.e2e.js +++ b/frontend/javascripts/test/backend-snapshot-tests/projects.e2e.js @@ -1,7 +1,7 @@ // @flow import _ from "lodash"; -import type { APIProject, APIProjectUpdater } from "admin/api_flow_types"; +import type { APIProject, APIProjectUpdater } from "types/api_flow_types"; import { tokenUserA, tokenUserD, diff --git a/frontend/javascripts/test/enzyme/e2e-setup.js b/frontend/javascripts/test/enzyme/e2e-setup.js index bd68fb0e97c..de9989486b8 100644 --- a/frontend/javascripts/test/enzyme/e2e-setup.js +++ b/frontend/javascripts/test/enzyme/e2e-setup.js @@ -105,7 +105,7 @@ export async function writeFlowCheckingFile( fs.writeFileSync( `frontend/javascripts/test/snapshots/flow-check/test-flow-checking-${name}.js`, `// @flow -import type { ${flowTypeString} } from "admin/api_flow_types"; +import type { ${flowTypeString} } from "types/api_flow_types"; const a: ${fullFlowType} = ${JSON.stringify(object)}`, ); } diff --git a/frontend/javascripts/test/fixtures/skeletontracing_server_objects.js b/frontend/javascripts/test/fixtures/skeletontracing_server_objects.js index e57ceb8edc0..68ee5b94094 100644 --- a/frontend/javascripts/test/fixtures/skeletontracing_server_objects.js +++ b/frontend/javascripts/test/fixtures/skeletontracing_server_objects.js @@ -1,5 +1,5 @@ // @flow -import type { ServerSkeletonTracing, APIAnnotation } from "admin/api_flow_types"; +import type { ServerSkeletonTracing, APIAnnotation } from "types/api_flow_types"; export const tracing: ServerSkeletonTracing = { id: "47e37793-d0be-4240-a371-87ce68561a13", diff --git a/frontend/javascripts/test/fixtures/tasktracing_server_objects.js b/frontend/javascripts/test/fixtures/tasktracing_server_objects.js index 98c1bfebada..c8931a4c948 100644 --- a/frontend/javascripts/test/fixtures/tasktracing_server_objects.js +++ b/frontend/javascripts/test/fixtures/tasktracing_server_objects.js @@ -1,5 +1,5 @@ // @flow -import type { ServerSkeletonTracing, APIAnnotation } from "admin/api_flow_types"; +import type { ServerSkeletonTracing, APIAnnotation } from "types/api_flow_types"; export const tracing: ServerSkeletonTracing = { trees: [ diff --git a/frontend/javascripts/test/fixtures/volumetracing_server_objects.js b/frontend/javascripts/test/fixtures/volumetracing_server_objects.js index 262dbfa0ee4..32b5ae421ff 100644 --- a/frontend/javascripts/test/fixtures/volumetracing_server_objects.js +++ b/frontend/javascripts/test/fixtures/volumetracing_server_objects.js @@ -1,5 +1,5 @@ // @flow -import type { ServerVolumeTracing, APIAnnotation } from "admin/api_flow_types"; +import type { ServerVolumeTracing, APIAnnotation } from "types/api_flow_types"; export const tracing: ServerVolumeTracing = { activeSegmentId: 10000, diff --git a/frontend/javascripts/test/libs/schema.spec.js b/frontend/javascripts/test/libs/schema.spec.js index cc1b63da27e..552b5b49c6e 100644 --- a/frontend/javascripts/test/libs/schema.spec.js +++ b/frontend/javascripts/test/libs/schema.spec.js @@ -3,7 +3,7 @@ import test from "ava"; import { DEFAULT_RECOMMENDED_CONFIGURATION } from "admin/tasktype/recommended_configuration_view"; -import { validateUserSettingsJSON } from "dashboard/dataset/validation"; +import { validateUserSettingsJSON } from "types/validation"; test("The default recommended task type settings should be valid according to the schema", async t => { validateUserSettingsJSON({}, JSON.stringify(DEFAULT_RECOMMENDED_CONFIGURATION), error => diff --git a/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js b/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js index 4698958076d..7c785307594 100644 --- a/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js +++ b/frontend/javascripts/test/puppeteer/dataset_rendering_helpers.js @@ -7,7 +7,7 @@ import type { Page } from "puppeteer"; import mergeImg from "merge-img"; import pixelmatch from "pixelmatch"; -import type { APIDatasetId } from "../../admin/api_flow_types"; +import type { APIDatasetId } from "../../types/api_flow_types"; import { createExplorational, updateDatasetConfiguration } from "../../admin/admin_rest_api"; export const DEV_AUTH_TOKEN = "secretScmBoyToken"; diff --git a/frontend/javascripts/admin/api_flow_types.js b/frontend/javascripts/types/api_flow_types.js similarity index 100% rename from frontend/javascripts/admin/api_flow_types.js rename to frontend/javascripts/types/api_flow_types.js diff --git a/frontend/javascripts/libs/dataset_view_configuration.schema.js b/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js similarity index 88% rename from frontend/javascripts/libs/dataset_view_configuration.schema.js rename to frontend/javascripts/types/schemas/dataset_view_configuration.schema.js index d89fd5c921d..5cadbd6d85a 100644 --- a/frontend/javascripts/libs/dataset_view_configuration.schema.js +++ b/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js @@ -21,12 +21,10 @@ export function getDefaultLayerViewConfiguration(dynamicDefault: Object) { export const layerViewConfiguration = { color: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 }, - brightness: { type: ["number", "null"], minimum: 0.005 }, // TODO min max - contrast: { type: ["number", "null"], minimum: 0.005 }, // TODO min max alpha: { type: "number", minimum: 0, maximum: 100 }, intensityRange: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 }, - min: { type: ["number", "null"], minimum: 0.005 }, // TODO min max - max: { type: ["number", "null"], minimum: 0.005 }, // TODO min max + min: { type: ["number", "null"] }, + max: { type: ["number", "null"] }, isDisabled: { type: "boolean" }, isInverted: { type: "boolean" }, isInEditMode: { type: "boolean" }, @@ -51,7 +49,7 @@ export const datasetViewConfiguration = { interpolation: { type: "boolean" }, highlightHoveredCellId: { type: "boolean" }, renderIsosurfaces: { type: "boolean" }, - zoom: { type: ["number", "null"], minimum: 0.005 }, // TODO zoom max value? + zoom: { type: ["number", "null"] }, renderMissingDataBlack: { type: "boolean" }, loadingStrategy: { enum: ["BEST_QUALITY_FIRST", "PROGRESSIVE_QUALITY"] }, segmentationPatternOpacity: { type: "number", minimum: 0, maximum: 100 }, diff --git a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js new file mode 100644 index 00000000000..c555868048f --- /dev/null +++ b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js @@ -0,0 +1,61 @@ +// @flow + +import { + getDefaultLayerViewConfiguration, + defaultDatasetViewConfiguration, +} from "types/schemas/dataset_view_configuration.schema.js"; +import { type APIDataset, type APIDataLayer } from "types/api_flow_types"; +import { getDefaultIntensityRangeOfLayer } from "oxalis/model/accessors/dataset_accessor"; +import { validateObjectWithType } from "types/validation"; + +const eliminateErrors = (instance: Object, errors: Array<*>, defaults: Object) => { + errors.forEach(error => { + if (error.name === "required") { + instance[error.argument] = defaults[error.argument]; + } else if (error.name === "additionalProperties") { + delete instance[error.argument]; + } else { + const [invalidFieldName] = error.property.split(".").slice(-1); + instance[invalidFieldName] = defaults[invalidFieldName]; + } + }); +}; + +export const getSpecificDefaultsForLayers = (dataset: APIDataset, layer: APIDataLayer) => ({ + intensityRange: getDefaultIntensityRangeOfLayer(dataset, layer.name), + alpha: layer.category === "color" ? 100 : 20, +}); + +export const enforceValidatedDatasetViewConfiguration = ( + datasetViewConfiguration: Object, + dataset: APIDataset, +) => { + const validationErrors = validateObjectWithType( + "types::DatasetViewConfiguration", + datasetViewConfiguration, + ); + if (validationErrors.length) { + eliminateErrors(datasetViewConfiguration, validationErrors, defaultDatasetViewConfiguration); + } + + const { layers } = datasetViewConfiguration; + const newLayerConfig = {}; + dataset.dataSource.dataLayers.forEach(layer => { + const layerConfigDefault = getDefaultLayerViewConfiguration( + getSpecificDefaultsForLayers(dataset, layer), + ); + + const existingLayerConfig = layers[layer.name]; + if (existingLayerConfig) { + const layerErrors = validateObjectWithType( + "types::LayerViewConfiguration", + existingLayerConfig, + ); + eliminateErrors(existingLayerConfig, layerErrors, layerConfigDefault); + newLayerConfig[layer.name] = existingLayerConfig; + } else { + newLayerConfig[layer.name] = layerConfigDefault; + } + }); + datasetViewConfiguration.layers = newLayerConfig; +}; diff --git a/frontend/javascripts/libs/datasource.schema.json b/frontend/javascripts/types/schemas/datasource.schema.json similarity index 100% rename from frontend/javascripts/libs/datasource.schema.json rename to frontend/javascripts/types/schemas/datasource.schema.json diff --git a/frontend/javascripts/libs/user_settings.schema.js b/frontend/javascripts/types/schemas/user_settings.schema.js similarity index 100% rename from frontend/javascripts/libs/user_settings.schema.js rename to frontend/javascripts/types/schemas/user_settings.schema.js diff --git a/frontend/javascripts/dashboard/dataset/validation.js b/frontend/javascripts/types/validation.js similarity index 72% rename from frontend/javascripts/dashboard/dataset/validation.js rename to frontend/javascripts/types/validation.js index bf2f296c331..d07c6d5d8ff 100644 --- a/frontend/javascripts/dashboard/dataset/validation.js +++ b/frontend/javascripts/types/validation.js @@ -2,12 +2,14 @@ import jsonschema from "jsonschema"; -import DatasourceSchema from "libs/datasource.schema.json"; -import UserSettingsSchema from "libs/user_settings.schema"; +import DatasourceSchema from "types/schemas/datasource.schema"; +import UserSettingsSchema from "types/schemas/user_settings.schema"; +import ViewConfigurationSchema from "types/schemas/dataset_view_configuration.schema"; const validator = new jsonschema.Validator(); validator.addSchema(DatasourceSchema, "/"); validator.addSchema(UserSettingsSchema, "/"); +validator.addSchema(ViewConfigurationSchema, "/"); const validateWithSchema = (type: string) => (rule: Object, value: string, callback: Function) => { try { @@ -29,6 +31,17 @@ const validateWithSchema = (type: string) => (rule: Object, value: string, callb } }; +export const validateObjectWithType = (type: string, json: Object) => { + const result = validator.validate(json, { + $ref: `#/definitions/${type}`, + }); + if (result.valid) { + return []; + } else { + return result.errors; + } +}; + export const validateDatasourceJSON = validateWithSchema("types::DatasourceConfiguration"); export const validateLayerConfigurationJSON = validateWithSchema("types::LayerUserConfiguration"); export const validateUserSettingsJSON = validateWithSchema("types::UserSettings"); From 6f36e65c94be38a20edf2d43b2d12f3c2ba28471 Mon Sep 17 00:00:00 2001 From: Youri K Date: Wed, 14 Oct 2020 16:29:16 +0200 Subject: [PATCH 19/39] fix e2e tests --- .../backend-snapshot-tests/tasks.e2e.js.md | 28 ++--- .../backend-snapshot-tests/tasks.e2e.js.snap | Bin 4583 -> 4586 bytes .../timetracking.e2e.js.md | 104 +++++++++--------- .../timetracking.e2e.js.snap | Bin 1971 -> 1972 bytes test/db/dataSet_layers.csv | 12 +- test/db/dataSets.csv | 2 +- 6 files changed, 73 insertions(+), 73 deletions(-) diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md index 72d3fcdc809..e4daa6968a8 100644 --- a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md @@ -542,9 +542,9 @@ Generated by [AVA](https://ava.li). { boundingBox: null, boundingBoxVec6: undefined, - created: 1477666728000, + created: 1477663769000, creationInfo: null, - dataSet: '2012-06-28_Cortex', + dataSet: 'confocal-multi_knossos', editPosition: [ 0, 0, @@ -555,8 +555,8 @@ Generated by [AVA](https://ava.li). 0, 0, ], - formattedHash: '8a5352', - id: '581367a82faeb37a008a5352', + formattedHash: '81c058', + id: '58135c192faeb34c0081c058', neededExperience: { domain: 'abc', value: 0, @@ -564,9 +564,9 @@ Generated by [AVA](https://ava.li). projectName: 'Test_Project', script: null, status: { - active: 1, - finished: -1, - open: 10, + active: 0, + finished: 1, + open: 9, }, team: 'team_X1', tracingTime: 0, @@ -593,9 +593,9 @@ Generated by [AVA](https://ava.li). { boundingBox: null, boundingBoxVec6: undefined, - created: 1477663769000, + created: 1477666728000, creationInfo: null, - dataSet: 'confocal-multi_knossos', + dataSet: '2012-06-28_Cortex', editPosition: [ 0, 0, @@ -606,8 +606,8 @@ Generated by [AVA](https://ava.li). 0, 0, ], - formattedHash: '81c058', - id: '58135c192faeb34c0081c058', + formattedHash: '8a5352', + id: '581367a82faeb37a008a5352', neededExperience: { domain: 'abc', value: 0, @@ -615,9 +615,9 @@ Generated by [AVA](https://ava.li). projectName: 'Test_Project', script: null, status: { - active: 0, - finished: 1, - open: 9, + active: 1, + finished: -1, + open: 10, }, team: 'team_X1', tracingTime: 0, diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.snap b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.snap index aa8f9a5dcfb3ee597e7740995519ca155763ba63..5f9a41d05ea82ca22e6c4679eb4a43d73649a4f5 100644 GIT binary patch literal 4586 zcmVg0=BSDlF9D4NoF^bfGZcRNGnt= zeN=2wsE=(${AdfJRHRlwD{8@_j}=P=^?E_H74Jp72v^Jf&t#HK&SWzzOG0*)=lPLj z=07uMX3jb9dEYtn?wf=V3%T<-=kO_i`uNsMYUT+ok9=xoi1e?{Ldc&RW_`&YSpR>| zw0OdwZ290{jJ)*5LPB&8>sSAM!mIC=zqoF4=c>oCMp{}ll#pvjKJA%#&8jW`vF;=1 z3&TEnjFFeV&_sx@>hOpyPyKAklRJyf_C4`RgprpXok&P@k?y)V!)83c_PTjD4~`tT zkCB&NF_n-=jsBv4Y<_k6JI`!kuNbvr4GL zCVIiWt)r z&-I))!Z3-iE|-fiPQnxsrVtukDWfNhv52vIglNdehg!!9H^|HMPD)Xjr1&1^t(TXZ zX_eU<=GX}5lYbjbYqvgNpUqGjhpgd+(4>%GNPjT1kBzX?xk&tXxmSSJ%LXcg(LjW6 zX%7jaD2Rk9V3^l%wuxPHK0Y#45P50$^4fpXYp)R^Y;0}H5uJV^9AqOX`b1W2i!U`b zR(aiZjg(f$`{G{**ELqv)Oo9#s{JhIsj2n4-SJIu@L~GygIj`}D6*|wtk6@F*0@3( z=i_{r-O#~>c`oGT=zG%<7NmQ3H5=p-r!s>RBP~;7D=585^oIG4NaD)ka0`EfyRfA0!bYTP<&dr{NnA$xC&N&`b&21n zx)B?R;FUMx)gxI)rg;oVUFq(k6&pg_jEi#0nuAIWJQ~ zZE$;<{I%X{w!!P>p8ly4Gwc&AsFOBXp!U4>F}-5Fq=5C(wrVQCADjji$u^U z=mupLqro5>?u!2|<=R`^b@eqZWBCptz=p?3g{Cj0)b1v!%It}ixyGZEnZ837X|v?s zL96|uiFOyioeOXgsXs)^dIu-ApRT?}x&;`|fxQ|Enn63b!%S^rRT&)mw&U`6z334W z=Q@KG+bfiX{%9c3RU!4(oKN1`AYb;4=A^!y4FrS*obM{Z$I+Lu#1m#i-nOX%ABu?0 zF`c|lS4b&sK`z|Ng{7Uu1TK14BAUmz0Pk(bb)$EMuH$_a=|f<^FykCcYL{e7>5^0p zSUS8AMh$KiLTn(lF-sRn@C5kID3{u*rF-EI@U3l;q(OtrkIw-3DR>IJ1qv`Y7!Ixg zivY$b)wb}i>{xhenS7|xY0dp&EL{qI0Upt7nEPiS*aCKe_w<>~z5Xx+pM$e# z(`-;#a%uQ*DYzDN(Q;KkW?ccv{orrlFgOnud?XOS8t|$y^VmUcl%;+y#VSh%H^nSV z|E1Z^KyPQHw=SjGoqiONXMjN!J<`~Q+oqTu8ioQ#rRwbwt>C!LR?`m6|{lH;4!cr90!BZ z!&igvfgre5@xGq`yTA$XEp%ITdQAgcu4*wcqnBJ$G8w+>l6qvTylq4XtE`N=8PaX} z^q9W;lT8u4-ZsVO{X=?hIJJQU1t0I{yGIU*1$4I)>b^GgX*h&%D;wf(WRrWGf+vnDT}ITO`sk;^K=F+7V4|w>v2cv)3Mv!Wih& zkUi>B7V65j*Kec)-Zo0b=<%=D*_90N6l@MyrzU?tcI3@$>3yQp2bR7ac*$vn^o zL|5vWl&74@NyK}L=Lpe?Rv}WsR)n|(6|phz>2~i1XoN;}aeEP>(dAu;<|fxb4AI0Y z5C(ngUGkt0-`>SHWh7WA`K&tG-57Rn(E}m8Mw_EjZcd0?yeQ5<4_r$ z59LrbxjU;`xguPKdpa5T!ObP9{e5w+!g7XMe2xI)!4#5O3MNe{(7LWA@h0~yNi?|% zCf?+VZpGJSN;K~^@R<_LyBY+*Pryd-A+YNS83|^C4sZ|HNJsSOzy(vZ1CrOl5#_nX zD8Pcdz&d5B!EsP^ux!7A`1cuSc}ka7r{>JTmyA?2VE%ma(TL*Pwt1Qg>Mjsl(F zZtwzl4;(Yc9#xLEP=_d1wQagu_TyPXQY@us&uTHfHkY%6esAfUqt(l~Kzl4%@w1e< zKvruy*D6O9Y0SYLr==nlRlBRIE8X>#)r~FV(2sF9$me#YHB%kxqSyAa4zYDLb=4Ym zsJ?-1jMbqA7IjG31P5PZ&1oHa&6>?RL~H}M4vn*AszV+0+Eb`Qbg1Pd>QJqG@YS}{ z81SKMa+*r?lI>I~5xwJ{D$!AOCBkSzicXIKQ$UM7bu>XgVK4{~M8Ohpk3I8~2K{d! zcogiQXk1l3>$9fEMG3ef?{d>}RNf$KmA6|pFxPy2@u{1~hN4^iS$`m~>dU^CcD7otp6h+~kP z0B6Hn8%aBN<*L)31<73SbMOi{CT#*YYy!9mtkH1zayPp)-)ECr`^jw5*NX>k0A!%l zL#>b-b0R}UwDpsUXe{}Q%lg5AEx z#wJf=t-IRI`F*u?nR}{+Jq|>BoU_-C6YB)a!6vY$gfit8dNg1(loDbC7l3PkSgQ4y z#<&`iUxTgSpTJR;Ipmv&;ntVsT?{u?FmJvV>}PpaFpM^zMUkEd`@qq%)KRRJ9`Y8J z6H*4;U{ZOixQG_B5VV5D;C@Q{jjV=)`@?>Iz{b=ooNYax+lj!HB4*8(YK{Rya0 z;?+L@OTlKa7dVZCTm;4fKUk^}qaIiR)5ZRzw-oOJ4}$e(jo9B-2)2Va!2z>I?C*ae z_yU|uKT=c2{zgDD2FwMEXt_G}w-S;Ez~6zxO2|ks6D$K;z(IQ2uR8WuY9r(~9Dp7XS}f0bT(6K(PY@ z70^jXO{v)5SIXxb5J9ljj;~`6`Cbpd5<#$C1F|AY@3lAyv4Zo#G!UjA8>yoYt04Im z*aAKRcKWG}OEuz;ImUGb`Q;cV3g*ePg26N{N0C;5b>QU!4Uhdj2tEMVmVC1?lgHk# zT;`Mv22X&$0Xy8>3&8{+D(>$~;B!z0H@6Z@17g-Q805yoT$V9?!NxP0$msn(W;5ue zcTkX>WCqIKFHM`j;D=cHW3U1|q?NzmDF`-$SHK>v`~@FD@Zb8RBuG8yFZeDbwO|_P z)XraUCnR@+oq!k#xfo1RvJy!%3E%E@{(}BIJozdNlPv=@^N?{QPOKTUfMwv(lg(%F zM*ih9U|`qGU2v%#G z)d3XV-H_4vzI!H(uQRu~0vofME3h@K@s%vmu!W7AjeRG>q>bAbA!XHN+l^%mE^J80-TkjZ_0)8{htBe5(hl@#S!0?TX>u z0NyxR?RkpoAoif`B1t%M5!$(CEJjC^3>_bF|;*fj-exZ?JlL~97E@)&oOja+8jfn zv^j=$_LO7j^Q0BM$}yxGhNVv{G_gFavaoiT`huBQ=q)l0AJEdVRcnZtpu4G?Ss{{SZ>kHdno zJ?I3>!Des>xU7VX0dqkVtOT3sM~A9c4E54$KD)u$^aH+Pl~j7NI2v%8$%U->zW&s*0)5-v;P<&bzJ^MF16yBNTkor?WLdVcvaXS> zX{_?GwZ1wxeQrq^gQTMm*i1ojU&g2SUp~c`p(k0N`TRjglJ0(Y{vgA7$`jME$$5kn zb8My~6Cwm%U@7>8)@ycam2^YTf}NCDHPvP>1n&c*_G@GXMYp literal 4583 zcmV=lphCSQm-cLuYjX)$8KS5YrjC6S z+*H`+obPS}Q6@4cC=+$y(2oN!I5BcGZXBK=ouA>c&7SZl z8{YpXBQL$SkPzKN`jvkf`|7)8FRq%{vEp&8k(L$>Cgk$rPkW|cx?;okR(<5$G~|=V z7PI&{xwYt2-xIGy7od1eDUVZ`zsjJ$O73_`a2ed6|l$hgydf4uONhxSgL&B#l~v=O2w zX88;l3sW4z-w3!1{0_Ve^f|uI9WE zhDm&Mxm<*C5~heSh0y5wF?zxnix|5{h=zQ8sAZIJjl4|nq!fioipw}}oxI#ktIXao z$3{4x{M}%Bc;nCPGZ{+bkTtvz8Xxit=?`Z1u@QDE7m0r_^9s;<*+4}w8i?@CZ6QGv z1(7fX4D%Y!HnD5Y$44d$A}{S;Ui&|K?WID5jjc^NqSG&ggKPv%AIFNV@ujAQO0T=N zfzs-DU;N{M+J?&NT5n}zm7nE2)iqwXJH81HK1APja7&OAMYe^DHF|Q=8dr$pe4Ous zYudRm&xO1k{oAyM1?jiDgbi|uQ<=tzk><&<6_nm2dc%BsBynYNxcNzkGl{qWQIXon z5aHNh;_^#hnrBwh4T*$VFDzghuQbF!0#~@h^UIqJT zxtW$*ZANrEFaaz8kAoM%VNhuzWR{8Ag(Yashje90;xfu#8HW0NAOndw5rNJUq2bKGgYX-TfjgT?~E#9?@&K`)4570JeeM`poWLzaN6n!6~$BHmE$g zG<`S^Tn;*ExoRArExCBD@zVH#Vt$!OS7GU z-p)vGXF3l4TWVe}9XP!@*?;S^=fCBC)pJiR&B^+rr#4o=OIL#_U_Mwudu_977>_y8 z7fpHRL_bB74oS^|N2@TWjs})oInn-$@OLqI9_*AC;tGSwpcO0vkAcnLFc^Rlz6x9h zg5X9a`hEgz14qE=7`AHlnhv&1)nj5#FWFKuIlilsMr5nJZA1vGyo|aV(tY{(xW0Rn zT@j+*Rwd^BZTe_9wSxo&AMfY8W)6u3^sp1^x;FG_ID~Ku8{)5JlSiC_Nl6QvXUZ*l zJE=vEFdS}}_e!f{?JYEDj~zzI)h2!;Q6MUpz+zh3Pc?^H2g!5bGceFj$S^Py zTnp|4FWRYHl&Xe56WxDhw( z5wYroow8z#FfWwhqR5&H<>^``#q@Sxe9)2dDL%0@1F+DPPm>^Z1P_ zX#y=HhfpxF;Q+~3H#dzGu^sCPa6vxg6Tgd)i5>|3d{|8Rvn`#&(bT-Mq=~>RC`IclGqC88O zj>T6vwK4&Q+ab9V{2FX_W(q+deg}d*;4@$)}3DNQvAyUqkhq!s=u{rPY4)6MChDLR9dyt~hgbVPSP6U3iuq1W7FV0n3&QOED!@w9YiKMoINz)4SuFFZh%RNgHU9Oyoce$b) z@o|Zg&3g@ereyOj0Riv>uoip>?0Q0mgPEWmECXxlj2@l1V2ZXt@;W%6+_xA3Sa1he zr95hI7*rT>R$w7m0iFbJDbEt59KxnLgv&%o8E61=z#{MfcoQ4|#khteKnJ)JYy!K% zA#?0j<@pxs5XGv#O`GLO{FIQCOX)tcT12nSfy6MJ1kj=vy^9ntk!g) zRgMg4=XQ}blMQv!YkO!zY;ARIm4*$~)w2yT z8>(kvL&_#N_=DD*+R$s(Y}yd9^<5hpZOdds?eyAXupv6tauhaHBOiRF?KmcUXiH9G zL@(Kn#faz~cV|Qg)kcKbgp{2g2_}JNd+Kb0zQbS;Ac%sSz%qO0I}Q3@LhvZqLg%~H z$%YRg*$3j1h|Bx9)!SMbb3Se$fDtSNL~Wl!TVHt!%m;} zK?uGCjsikX)ktsXheA*VrqL+~r>YU{kjw>A=Hf}s1c!ASByR#&At9qd7~BRnfP>(4 zI%AKmbfG>AXm*)G^q}G$%Ks4D0G^dSZrkjSUS|m<79i4GoQ+h8lO3 zoAdi>=r(s}hTTp?yM1P_4JXzCmV$L)M+xQ1E%a=_Xy`|X4V(oo1!6y~+cd_NklYV8 zf`0% zsp2A9%tFut7J+*y@i(%X4n7(7n3S^CyYaUUjMPe3>*UPz#6-IK*GjtDB3*q#`YZ2! zHUlYV{UF$`Vp1c7sD;pdRbAbJWuzxdJQz_k+#gBT%m7tFHu$!FsS0 zIE{py4MqV!Sgetw?lU=?pBgjeZ2m~E&1K&2b7M~PeuGWf%==wxI)Qn=T-=0MZpvgx z&(mvjc@yFjQ%()(6mvEWX}mdI?oWD4@eXhwSZ&tG{cVI`Gk6p1HEZPl{sX}m;57P{ znmYG443d#x4!Dk%t8;(%K=N}S-L!F92{{*B27UlGf)DA(e$~0ZQX3)P1ZRU`HjUih z1PCUBW+2!!a(@dUxEVY|KP9Zr{k;Im%U~}s(HT;8?r#tzX8{jb4mN>ZpxA+l3h1D- zrc~~)Z^e9HiR8md$Jfb+d^g0eLN<0=pR_~iy%r}SR&XYm0>bodBX#y+1th-!8^A}v zPT#e0sb>7Kz__*`zXIb#!#sJ`Fu2C0Xwq`93cOsP5wY)v;C+B?$u|o#MeHY4$edEa z;0f?&U`Lqy9WWM%O8EN{_#BiX%&h=ZfSC0P2Dyn2mSiknu=Ye2GJ3C<)eL$V9Ta4z zn1QnQ^U~HYxE@Qt50--mwCWc;1;Kjo3fQ4lzd*{V{7auy1gZP_1>c3F2224R+Vu-= zhvZJM6%ZpK=Ya7_RU+w0!nb=|zu@cHNq)~xEPb@{ka0LptO+!OCE(Gct!MB?{?#*J z;MdGx5Ey=<>KPPWjlb>S4)8R18yu19B*J}81sz}|*ar49v4TKSMtKI94_0cs)jpJ( zTa(fGzI!5_uQRu`0vobgE3hT4^OY(Ht^&`2KWRDNcOclKJX>$js1>O1XNWz3f3CLk zoe9YtFkh((q~Uyj3(2$Kpdof!WEK#?Ltqy$X_OlH+WDSr&bO+sI$sVa)}}b#HQ~q5<@@0G$gSXh@I2TB zir|gT0ppd*LpOnYz$)-Ecpq3XIDY3S?gQheELC+pb@icc?u=fY)xtxv->lR2n>B95 zZ*noeSr_x052UL@l-h$*stu_ho;n^ihBjubF?2w$J*0GBW9ZEEHHI!oTVp7cw#Lxb z?rIEup0uJzHHK8vu=Hhx2KTnCkn{*b04xQo!E4~(pg%o$=_%JuGTqBFj>G1cfOfD9 ztko`u`W7U+z!%`#7z4)A6@%#edK!6Gz4Xf7JaD%;b2`wq27-0qui%Ijaab_72OVH3 zSP%9Amz9u_U^a+?yTN+;)}iVZL%sBx&vtMseS@!9C2fS{e86cI7qZ?3$qMi~I1DQ6 zgiHiMqpsv)b7R}9MA3PIcK`Q`7^RR~Jws_OeQD1Wnx5UOqCmZT z{AflmYXtQ$G+mUP5<$w|S07tdpl`YB{XVzHS6|_;XX`3z>U@B9kaYAuTPY|W%lMS|%csOL^h4GszJAbwq+h?Qevsh|<&Np7ovQFm2yMRf~}NT^{CBG2;Kum?bqzifaFYY0pPS>v%3M3Tfjze01Pf9 zdFv-?8&_f=ekI6!~u&AHNC-;_M5vnXGQCw&I>?Senk69s>6>MHoN z#+4}e^EZ6_0GJR{y1^9solteZG Rvio?-{{d_#gWVcz0007rt4;s_ diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.md b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.md index e9f2c3b2279..16c33863686 100644 --- a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.md @@ -35,64 +35,64 @@ Generated by [AVA](https://ava.li). [ { - _id: '5b72df70940000eb0000f113', + _id: '5b717cb9790000830123e1f6', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M10.826S', - timestamp: 1534254947940, + time: 'PT0H0M1.809S', + timestamp: 1534164150423, }, { - _id: '5b72df4b9400009e0000f110', + _id: '5b71a028790000380423e22e', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M33.636S', - timestamp: 1534254914304, + time: 'PT0H0M1.591S', + timestamp: 1534173221886, }, { - _id: '5b72df3d9400009e0000f10f', + _id: '5b71aa19780000f800061b11', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M17.551S', - timestamp: 1534254896753, + time: 'PT0H0M31.107S', + timestamp: 1534175738759, }, { - _id: '5b72a927960000e60005fb2a', + _id: '5b71aa31780000f900061b12', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M46.076S', - timestamp: 1534241050172, + time: 'PT0H0M28.959S', + timestamp: 1534175769866, }, { - _id: '5b71ac7c7800007e01061b20', + _id: '5b71aa38780000d300061b13', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M4.622S', - timestamp: 1534176365769, + time: 'PT0H0M4.79S', + timestamp: 1534175798825, }, { - _id: '5b71ac507800005901061b1e', + _id: '5b71aa40780000f900061b14', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M59.521S', - timestamp: 1534176306248, + time: 'PT0H0M0.275S', + timestamp: 1534175803615, }, { _id: '5b71aab67800001101061b17', @@ -105,64 +105,64 @@ Generated by [AVA](https://ava.li). timestamp: 1534175895929, }, { - _id: '5b71aa40780000f900061b14', + _id: '5b71ac507800005901061b1e', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M0.275S', - timestamp: 1534175803615, + time: 'PT0H0M59.521S', + timestamp: 1534176306248, }, { - _id: '5b71aa38780000d300061b13', + _id: '5b71ac7c7800007e01061b20', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M4.79S', - timestamp: 1534175798825, + time: 'PT0H0M4.622S', + timestamp: 1534176365769, }, { - _id: '5b71aa31780000f900061b12', + _id: '5b72a927960000e60005fb2a', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M28.959S', - timestamp: 1534175769866, + time: 'PT0H0M46.076S', + timestamp: 1534241050172, }, { - _id: '5b71aa19780000f800061b11', + _id: '5b72df3d9400009e0000f10f', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M31.107S', - timestamp: 1534175738759, + time: 'PT0H0M17.551S', + timestamp: 1534254896753, }, { - _id: '5b71a028790000380423e22e', + _id: '5b72df4b9400009e0000f110', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M1.591S', - timestamp: 1534173221886, + time: 'PT0H0M33.636S', + timestamp: 1534254914304, }, { - _id: '5b717cb9790000830123e1f6', + _id: '5b72df70940000eb0000f113', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M1.809S', - timestamp: 1534164150423, + time: 'PT0H0M10.826S', + timestamp: 1534254947940, }, ] @@ -170,24 +170,24 @@ Generated by [AVA](https://ava.li). [ { - _id: '570ba00b2a7c0e4f0056ff4e', + _id: '570ba00b2a7c0e4f0056fa9e', annotation: '78135c192faeb34c0081c05e', project_name: 'Test_Project3(for_annotation_mutations)', task_id: '681367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460725259501, + timestamp: 1460379659501, }, { - _id: '570ba00b2a7c0e4f0056fd3e', + _id: '570ba00b2a7c0e4f0056fb1e', annotation: '78135c192faeb34c0081c05e', project_name: 'Test_Project3(for_annotation_mutations)', task_id: '681367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460638859501, + timestamp: 1460466059501, }, { _id: '570ba00b2a7c0e4f0056fc2e', @@ -200,24 +200,24 @@ Generated by [AVA](https://ava.li). timestamp: 1460552459501, }, { - _id: '570ba00b2a7c0e4f0056fb1e', + _id: '570ba00b2a7c0e4f0056fd3e', annotation: '78135c192faeb34c0081c05e', project_name: 'Test_Project3(for_annotation_mutations)', task_id: '681367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460466059501, + timestamp: 1460638859501, }, { - _id: '570ba00b2a7c0e4f0056fa9e', + _id: '570ba00b2a7c0e4f0056ff4e', annotation: '78135c192faeb34c0081c05e', project_name: 'Test_Project3(for_annotation_mutations)', task_id: '681367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460379659501, + timestamp: 1460725259501, }, ] @@ -225,24 +225,24 @@ Generated by [AVA](https://ava.li). [ { - _id: '570ba00b2a7c0e4f0056fe4e', + _id: '570ba00b2a7c0e4f0056fe9e', annotation: '58135c192faeb34c0081c05d', project_name: 'Test_Project2', task_id: '581367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460725259501, + timestamp: 1460379659501, }, { - _id: '570ba00b2a7c0e4f0056fe3e', + _id: '570ba00b2a7c0e4f0056fe1e', annotation: '58135c192faeb34c0081c05d', project_name: 'Test_Project2', task_id: '581367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460638859501, + timestamp: 1460466059501, }, { _id: '570ba00b2a7c0e4f0056fe2e', @@ -255,23 +255,23 @@ Generated by [AVA](https://ava.li). timestamp: 1460552459501, }, { - _id: '570ba00b2a7c0e4f0056fe1e', + _id: '570ba00b2a7c0e4f0056fe3e', annotation: '58135c192faeb34c0081c05d', project_name: 'Test_Project2', task_id: '581367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460466059501, + timestamp: 1460638859501, }, { - _id: '570ba00b2a7c0e4f0056fe9e', + _id: '570ba00b2a7c0e4f0056fe4e', annotation: '58135c192faeb34c0081c05d', project_name: 'Test_Project2', task_id: '581367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460379659501, + timestamp: 1460725259501, }, ] diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.snap b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.snap index dbb044ab46b28ef183b28c02c58cb7470cd429f5..5b043be3dd94bcd7646195806277184fdc9425e3 100644 GIT binary patch literal 1972 zcmV;l2TS-tRzV~6Q+quo{t1qo;(C`Pi*v9q&dnqZ4jK#{7AM}pbR?#{wO_h5I6H1RAL zjv$~!0YfB)V!}TJV?2mKQ6XwPfIk2^su58`jETfVQ7*r8w6t%L*=9YcX0Aaj@sUnPzVU`#{V7;$x8Qr|7nGW4-!I?2m-CU-bF0Dh)r!c=_bSKgb zGT;qx65QY6lc{?l4Zqk zQk7MzjaSKJvPGqs%4RM%jc|e(=VU&qs4*cFXW0lBXT=27)St^|wZn;Kl(gEnv$kC= z6r*j%FFe(vx~LQu4mwEm6qb~T(UXMZjVPiZhNyNvtzEGzr_v`VCqr@b1hu0~DB)z1 zYFppmw$PbQEBUUz9~#w#Q8p}vqOGZ1wnNEJS35ciRAW~$HQ?0R+SwWGY>o>`viK0? z>Crq3MJ1in_qg1@PEA#{GegyhvvnFNomQ;%4qYeS)@h`4I`wv7=sF3uP9vq$ohKTH zu2aa?X{2=8?6|(28Bh+^%{5EF4)6r%1}}h(U@O=Gc7eTMKR5<{v?>>C^&0TE{&G=Z z`pd<|OnvFkx-YgekPD5E})|=g$>DJACnpv%zHBO@6Wiap6Opk7^(M+#y9@I>q zZZ@JYWc+&R^$5(z!HeKaZ~=%Y02hHB;7FBa8F~ROP85F<$b+rmIB3GGXiW#}xpw1H)2&G;Lbf@wnnybiwbT9%<_;PNN9&4(%$ zYykU#&riq-?3`K}n;IbJU0KQsO_MixMf?vUyIx0kUNr+~+w1UOpO>h!iA0Xsm zupaCVP$6ncLi_-i(_lh9A&-KU;3IGW+|ht*(m;jql!RCXmvvw-xCGei2$>Ih!1usA ziVEQ^3Bkf83>JWG;4r9bBxDv?4&H2}LikET9Dz$OXc&!|8F&i34bFhz7@XV~Dullz z#M5wD2Hpd|gGo(<%mo|4A#kaQ3Q=1UV)9tr6Ces+1z!N?I6|g@C&3%zs1S7(PETSI zC&gp3B(wNL1eW6kl}m=pN&k9cO>#zJl_SQJBSI(EE2e+{!1O=F>`y;6D#-pS{dl{2 z_7}rK+1cN3X8&M>l`WJ1hADFdwVa1ER8m+zVyGlU*br3W%PV?qm9D0eD07x7U0i>( ze3cYVmLi%;N$oq##W>DZX^1MBm6X5*IaaciY5BQJ704uTrcAOa6MYG6nJSctj|64W zI_G&iNvHyuB8E%}!IVj`Wg4PPK0~GurX`jl-TG4{B86CLEYTA)TcipfOl**stmls3 zovlQrSlCdB<5*5tNwQTMX2uHzc`U#7Td4kl4HWwNWhVag=RL6NsE^Jeta zR3MWSH)N7jLnfZJWg1@23kAbGZ>iGft3Bna#49o{$ze?;Rr?l`FcEG?}icr9CUa9&jGWPD0|=OM6zr zWi9vwTm-XRgmi-sz&S9Pel1h_PUT*6SqG;2E&pdTGyoxEul=4|)gdz-PeaC8Px`q+iSY7fXATvX=HpSAKO$H@)I$yJg^L z6Q(4y`RL^yqUlb&=L*;I+F@C{flzAzQ&~ zcL`CltR+OYf+M@Q$T1aL-l_mKYR%@pM&6~^t^_;5QKJ|oH-qUQ4%P#0>Cn{*%UXR| z(4fN&DDh%yu{tw@L(at2f^pDV?}OZ~i={d`)WmFjrwVF3n@O0SgmP?1rZTC*d^JHu zr2T5MIWVrkA|=%=`8JGABuOMclR>^dU9U z14#F3$F0=o;TW#8RatC1!De$Y*&oDGMocv77-k~$?mMv2N}-UNmr1LcVxhImJVp98 zb_^Ick}+%&=CDba^SIJsqn&We<=|J=(*>dO{g+<*BRc)Y)@;{wEpPq%OywVFo~rQ+ GG5`ROhq%Z9 literal 1971 zcmV;k2Tb@uRzV%YYgc#bCXUUhghhgn>am1$9a^=)AUfSn0=XJ9Q?C4izvU zDlvc>i4ir%gqRRTGzxwYG*OWl6c8jaQ)7$?N+b{v{olvN_7=EyGYc`dkn(aZ_OQl&4vxTp54q4<1Z2UdWhxGf_fLeEsmpyF>H#HaBeK+P^-yhcWkF z&*IygE?mEF;r>yt-t*hIabqSQW6ZrvIYPXo*6fq1WBkq7P6czo0+0Y%FoN+9V`^Fo z1+|l5daOt!LYN?7Y6w%$Fb`NkV7+f_8Phyvm>$z?!I?2$(|pV@eMX0YoiM*-OefL| za^N-aBe=**$V{)@xqn{b9xYj7JiTUPW1|@&50?(`2IvMOeT3Wqo&-C=A3iFCw=Be{ z8bZc_8^KEOAvg+nKOuRr-cN<_m4(;_mmT0Zh~a|j;3cphjH)A~rH%?wQx;+oT(aP0 zZ~!y}2$>GLz&7w}fC}O7A0mt>go(h%(_~y}^x$nvr+VzsqQoXtmQC=glw`G-&az@$ z*J2vgCaC6e`I1^n=X0l<%A6o3Ifd6%Eg{5`EGu(KR!mV%L+ypUaX4)`HLLaP47X~< zQrmp%8=h)WTT+XQ`yC{Dic6|&^(5hVWmOc!7}c)cXjkfN*XR>e^jOk9LE|V>YFyW; zwvD}QiyhglTIlTgqDfoa#>S;s+thS>KBE?o&jHgzk!GX@LsS9?5%MuLr=lQhvH8LMX(VZ0Hbj^Zvjhc9m~*H z;qoRp1jeEOoDG(OZQvxBKo@}JW#}BZ+ytHgAAvu?RhTyD;1%#?(6J0X0+-|9>Uu)% z2J69Y5DpPC3lu}=E<+JwJzO?`Z$LwskjdbFuo)Z%V;iUtwPhiuz-2mk5WEe32IC`y z+z!@)k0Mlvy0Q?zz~y&vNh2W(!78u=FvAGB7AzV@g$R^|SOu3gU?=blCqw{?!Rw$K zgho&yf@L9ixX2&_UI%+YV-u!WUup@CzMp7X{ zWg#Ac%M;)|@E4dcijX_O^WZx`MpGfeWg#ZOWfHgxya2uczA=PM0S|%K#!w*|s+^uA zBtE4}tP*4KrzMO}o#TW`(!ZHlbc+LLNqQo%S0}$MkXQEUnZ8m z)~-y|jwUH8ikzcN_kO#jVwqHhmz21f^o&nYM+NN4G&q@J@hB@fkEZ6Qx}^$b;?$&+ zlw?CDNn<%S&Lwy^=?zq-kS$X*&hw5ceZDdft6U|KwNw%nOQp&t`mRc6A55YW6?x~J zH)CU6xFr)x2TLZ&m1%G?@v$f`I_JDj)l-E`F?%%W3O)>(=wCWqnX1frY?POr ze~dYFj8vhLAX_S>1Y0F{)-+I+>U*9{lHw@R#Gk7YiKB)ReF0pNs?B(ER1uwJ3U*W@ z6Q|fR$+k?KE7Ra)61XVGN{%uuJMoWDu}my4OU5{21(}WUg2q?&-p!Ti><=a`Dk{!Z z-l;YFDppBKCKO90SztM+#OZNYrNOEsv(71R?UnPO(s{MCXF1poP6EYCNYZ&}&kDG# z2A_aaV3v=NF7P2Z0WPOs%ap%UnQbrYc^qs9AwMD4fk(gx;1}nmJz?X`4Y(e3fv~Ha4EJcz*}ISRg97eU>Zn*HNaRpbgsfOY%UAx zceoKXSxVoh&CKDDb4jge9kkJVKeyqDbS9&v=HfG5)ROsJ%Jw9b<1js)OBe6fQdC6R zFPv}Ja&)gFC?|ir+ELUjr9F+*mv!xhbiR--b=r;TC2#fi{dz-%bEJk6ZrcofNKNzr zGQC=HEA@DI1}kk%9@`G^qP>{xC~=e#6O9Ij84taC4(tN8SWGX diff --git a/test/db/dataSet_layers.csv b/test/db/dataSet_layers.csv index 50d0c5b0265..22d9d68283e 100644 --- a/test/db/dataSet_layers.csv +++ b/test/db/dataSet_layers.csv @@ -1,6 +1,6 @@ -_dataSet,name,category,elementClass,boundingBox,largestSegmentId,mappings,defaultViewConfiguration -'59e9cfbdba632ac2ab8b23b3','color_1','color','uint8','(0,0,0,512,512,256)',,, -'59e9cfbdba632ac2ab8b23b3','color_2','color','uint8','(0,0,0,512,512,256)',,, -'59e9cfbdba632ac2ab8b23b3','color_3','color','uint8','(0,0,0,512,512,256)',,, -'59e9cfbdba632ac2ab8b23b5','color','color','uint8','(4480,4736,1920,512,512,512)',,, -'59e9cfbdba632ac2ab8b23b5','segmentation','segmentation','uint16','(4480,4736,1920,512,512,512)',10000,'{}', +_dataSet,name,category,elementClass,boundingBox,largestSegmentId,mappings,defaultViewConfiguration,adminViewConfiguration +'59e9cfbdba632ac2ab8b23b3','color_1','color','uint8','(0,0,0,512,512,256)',,,, +'59e9cfbdba632ac2ab8b23b3','color_2','color','uint8','(0,0,0,512,512,256)',,,, +'59e9cfbdba632ac2ab8b23b3','color_3','color','uint8','(0,0,0,512,512,256)',,,, +'59e9cfbdba632ac2ab8b23b5','color','color','uint8','(4480,4736,1920,512,512,512)',,,, +'59e9cfbdba632ac2ab8b23b5','segmentation','segmentation','uint16','(4480,4736,1920,512,512,512)',10000,'{}',, diff --git a/test/db/dataSets.csv b/test/db/dataSets.csv index 1033180523a..d0208614f4c 100644 --- a/test/db/dataSets.csv +++ b/test/db/dataSets.csv @@ -1,4 +1,4 @@ -_id,_dataStore,_organization,_publication,inboxSourceHash,sourceDefaultConfiguration,defaultConfiguration,description,displayName,isPublic,isUsable,name,scale,status,sharingToken,logoUrl,sortingKey,details,created,isDeleted +_id,_dataStore,_organization,_publication,inboxSourceHash,defaultViewConfiguration,adminViewConfiguration,description,displayName,isPublic,isUsable,name,scale,status,sharingToken,logoUrl,sortingKey,details,created,isDeleted '570b9f4e4bb848d0885ee711','localhost','5ab0c6a674d0af7b003b23ac',,,,,,,f,f,'2012-06-28_Cortex',,'No longer available on datastore.',,,'2016-04-11T12:57:50.082Z',,'2016-04-11T12:57:50.082Z',f '570b9f4e4bb848d0885ee712','localhost','5ab0c6a674d0af7b003b23ac',,,,,,,f,f,'Experiment_001',,'No longer available on datastore.',,,'2016-04-11T12:57:50.079Z',,'2016-04-11T12:57:50.079Z',f '570b9f4e4bb848d0885ee713','localhost','5ab0c6a674d0af7b003b23ac',,,,,,,f,f,'2012-09-28_ex145_07x2',,'No longer available on datastore.',,,'2016-04-11T12:57:50.080Z',,'2016-04-11T12:57:50.080Z',f From cc3c2d07b69ddd1e4ff2cb1783a32e20827eb685 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 15 Oct 2020 10:53:28 +0200 Subject: [PATCH 20/39] move datasource types --- .../types/schemas/dataset_view_configuration_defaults.js | 8 ++++++-- .../{libs => types/schemas}/datasource.types.js | 0 2 files changed, 6 insertions(+), 2 deletions(-) rename frontend/javascripts/{libs => types/schemas}/datasource.types.js (100%) diff --git a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js index c555868048f..5087bf29222 100644 --- a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js +++ b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js @@ -15,8 +15,12 @@ const eliminateErrors = (instance: Object, errors: Array<*>, defaults: Object) = } else if (error.name === "additionalProperties") { delete instance[error.argument]; } else { - const [invalidFieldName] = error.property.split(".").slice(-1); - instance[invalidFieldName] = defaults[invalidFieldName]; + const invalidFieldName = error.property.split(".")[1]; + if (defaults[invalidFieldName] === null) { + delete instance[invalidFieldName]; + } else { + instance[invalidFieldName] = defaults[invalidFieldName]; + } } }); }; diff --git a/frontend/javascripts/libs/datasource.types.js b/frontend/javascripts/types/schemas/datasource.types.js similarity index 100% rename from frontend/javascripts/libs/datasource.types.js rename to frontend/javascripts/types/schemas/datasource.types.js From bdffbbe962ae5198cb12d40326c618d257098cd2 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 15 Oct 2020 11:02:06 +0200 Subject: [PATCH 21/39] update snapshots --- .../backend-snapshot-tests/tasks.e2e.js.md | 28 ++--- .../backend-snapshot-tests/tasks.e2e.js.snap | Bin 4586 -> 4583 bytes .../timetracking.e2e.js.md | 104 +++++++++--------- .../timetracking.e2e.js.snap | Bin 1972 -> 1971 bytes 4 files changed, 66 insertions(+), 66 deletions(-) diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md index e4daa6968a8..72d3fcdc809 100644 --- a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.md @@ -542,9 +542,9 @@ Generated by [AVA](https://ava.li). { boundingBox: null, boundingBoxVec6: undefined, - created: 1477663769000, + created: 1477666728000, creationInfo: null, - dataSet: 'confocal-multi_knossos', + dataSet: '2012-06-28_Cortex', editPosition: [ 0, 0, @@ -555,8 +555,8 @@ Generated by [AVA](https://ava.li). 0, 0, ], - formattedHash: '81c058', - id: '58135c192faeb34c0081c058', + formattedHash: '8a5352', + id: '581367a82faeb37a008a5352', neededExperience: { domain: 'abc', value: 0, @@ -564,9 +564,9 @@ Generated by [AVA](https://ava.li). projectName: 'Test_Project', script: null, status: { - active: 0, - finished: 1, - open: 9, + active: 1, + finished: -1, + open: 10, }, team: 'team_X1', tracingTime: 0, @@ -593,9 +593,9 @@ Generated by [AVA](https://ava.li). { boundingBox: null, boundingBoxVec6: undefined, - created: 1477666728000, + created: 1477663769000, creationInfo: null, - dataSet: '2012-06-28_Cortex', + dataSet: 'confocal-multi_knossos', editPosition: [ 0, 0, @@ -606,8 +606,8 @@ Generated by [AVA](https://ava.li). 0, 0, ], - formattedHash: '8a5352', - id: '581367a82faeb37a008a5352', + formattedHash: '81c058', + id: '58135c192faeb34c0081c058', neededExperience: { domain: 'abc', value: 0, @@ -615,9 +615,9 @@ Generated by [AVA](https://ava.li). projectName: 'Test_Project', script: null, status: { - active: 1, - finished: -1, - open: 10, + active: 0, + finished: 1, + open: 9, }, team: 'team_X1', tracingTime: 0, diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.snap b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/tasks.e2e.js.snap index 5f9a41d05ea82ca22e6c4679eb4a43d73649a4f5..aa8f9a5dcfb3ee597e7740995519ca155763ba63 100644 GIT binary patch literal 4583 zcmV=lphCSQm-cLuYjX)$8KS5YrjC6S z+*H`+obPS}Q6@4cC=+$y(2oN!I5BcGZXBK=ouA>c&7SZl z8{YpXBQL$SkPzKN`jvkf`|7)8FRq%{vEp&8k(L$>Cgk$rPkW|cx?;okR(<5$G~|=V z7PI&{xwYt2-xIGy7od1eDUVZ`zsjJ$O73_`a2ed6|l$hgydf4uONhxSgL&B#l~v=O2w zX88;l3sW4z-w3!1{0_Ve^f|uI9WE zhDm&Mxm<*C5~heSh0y5wF?zxnix|5{h=zQ8sAZIJjl4|nq!fioipw}}oxI#ktIXao z$3{4x{M}%Bc;nCPGZ{+bkTtvz8Xxit=?`Z1u@QDE7m0r_^9s;<*+4}w8i?@CZ6QGv z1(7fX4D%Y!HnD5Y$44d$A}{S;Ui&|K?WID5jjc^NqSG&ggKPv%AIFNV@ujAQO0T=N zfzs-DU;N{M+J?&NT5n}zm7nE2)iqwXJH81HK1APja7&OAMYe^DHF|Q=8dr$pe4Ous zYudRm&xO1k{oAyM1?jiDgbi|uQ<=tzk><&<6_nm2dc%BsBynYNxcNzkGl{qWQIXon z5aHNh;_^#hnrBwh4T*$VFDzghuQbF!0#~@h^UIqJT zxtW$*ZANrEFaaz8kAoM%VNhuzWR{8Ag(Yashje90;xfu#8HW0NAOndw5rNJUq2bKGgYX-TfjgT?~E#9?@&K`)4570JeeM`poWLzaN6n!6~$BHmE$g zG<`S^Tn;*ExoRArExCBD@zVH#Vt$!OS7GU z-p)vGXF3l4TWVe}9XP!@*?;S^=fCBC)pJiR&B^+rr#4o=OIL#_U_Mwudu_977>_y8 z7fpHRL_bB74oS^|N2@TWjs})oInn-$@OLqI9_*AC;tGSwpcO0vkAcnLFc^Rlz6x9h zg5X9a`hEgz14qE=7`AHlnhv&1)nj5#FWFKuIlilsMr5nJZA1vGyo|aV(tY{(xW0Rn zT@j+*Rwd^BZTe_9wSxo&AMfY8W)6u3^sp1^x;FG_ID~Ku8{)5JlSiC_Nl6QvXUZ*l zJE=vEFdS}}_e!f{?JYEDj~zzI)h2!;Q6MUpz+zh3Pc?^H2g!5bGceFj$S^Py zTnp|4FWRYHl&Xe56WxDhw( z5wYroow8z#FfWwhqR5&H<>^``#q@Sxe9)2dDL%0@1F+DPPm>^Z1P_ zX#y=HhfpxF;Q+~3H#dzGu^sCPa6vxg6Tgd)i5>|3d{|8Rvn`#&(bT-Mq=~>RC`IclGqC88O zj>T6vwK4&Q+ab9V{2FX_W(q+deg}d*;4@$)}3DNQvAyUqkhq!s=u{rPY4)6MChDLR9dyt~hgbVPSP6U3iuq1W7FV0n3&QOED!@w9YiKMoINz)4SuFFZh%RNgHU9Oyoce$b) z@o|Zg&3g@ereyOj0Riv>uoip>?0Q0mgPEWmECXxlj2@l1V2ZXt@;W%6+_xA3Sa1he zr95hI7*rT>R$w7m0iFbJDbEt59KxnLgv&%o8E61=z#{MfcoQ4|#khteKnJ)JYy!K% zA#?0j<@pxs5XGv#O`GLO{FIQCOX)tcT12nSfy6MJ1kj=vy^9ntk!g) zRgMg4=XQ}blMQv!YkO!zY;ARIm4*$~)w2yT z8>(kvL&_#N_=DD*+R$s(Y}yd9^<5hpZOdds?eyAXupv6tauhaHBOiRF?KmcUXiH9G zL@(Kn#faz~cV|Qg)kcKbgp{2g2_}JNd+Kb0zQbS;Ac%sSz%qO0I}Q3@LhvZqLg%~H z$%YRg*$3j1h|Bx9)!SMbb3Se$fDtSNL~Wl!TVHt!%m;} zK?uGCjsikX)ktsXheA*VrqL+~r>YU{kjw>A=Hf}s1c!ASByR#&At9qd7~BRnfP>(4 zI%AKmbfG>AXm*)G^q}G$%Ks4D0G^dSZrkjSUS|m<79i4GoQ+h8lO3 zoAdi>=r(s}hTTp?yM1P_4JXzCmV$L)M+xQ1E%a=_Xy`|X4V(oo1!6y~+cd_NklYV8 zf`0% zsp2A9%tFut7J+*y@i(%X4n7(7n3S^CyYaUUjMPe3>*UPz#6-IK*GjtDB3*q#`YZ2! zHUlYV{UF$`Vp1c7sD;pdRbAbJWuzxdJQz_k+#gBT%m7tFHu$!FsS0 zIE{py4MqV!Sgetw?lU=?pBgjeZ2m~E&1K&2b7M~PeuGWf%==wxI)Qn=T-=0MZpvgx z&(mvjc@yFjQ%()(6mvEWX}mdI?oWD4@eXhwSZ&tG{cVI`Gk6p1HEZPl{sX}m;57P{ znmYG443d#x4!Dk%t8;(%K=N}S-L!F92{{*B27UlGf)DA(e$~0ZQX3)P1ZRU`HjUih z1PCUBW+2!!a(@dUxEVY|KP9Zr{k;Im%U~}s(HT;8?r#tzX8{jb4mN>ZpxA+l3h1D- zrc~~)Z^e9HiR8md$Jfb+d^g0eLN<0=pR_~iy%r}SR&XYm0>bodBX#y+1th-!8^A}v zPT#e0sb>7Kz__*`zXIb#!#sJ`Fu2C0Xwq`93cOsP5wY)v;C+B?$u|o#MeHY4$edEa z;0f?&U`Lqy9WWM%O8EN{_#BiX%&h=ZfSC0P2Dyn2mSiknu=Ye2GJ3C<)eL$V9Ta4z zn1QnQ^U~HYxE@Qt50--mwCWc;1;Kjo3fQ4lzd*{V{7auy1gZP_1>c3F2224R+Vu-= zhvZJM6%ZpK=Ya7_RU+w0!nb=|zu@cHNq)~xEPb@{ka0LptO+!OCE(Gct!MB?{?#*J z;MdGx5Ey=<>KPPWjlb>S4)8R18yu19B*J}81sz}|*ar49v4TKSMtKI94_0cs)jpJ( zTa(fGzI!5_uQRu`0vobgE3hT4^OY(Ht^&`2KWRDNcOclKJX>$js1>O1XNWz3f3CLk zoe9YtFkh((q~Uyj3(2$Kpdof!WEK#?Ltqy$X_OlH+WDSr&bO+sI$sVa)}}b#HQ~q5<@@0G$gSXh@I2TB zir|gT0ppd*LpOnYz$)-Ecpq3XIDY3S?gQheELC+pb@icc?u=fY)xtxv->lR2n>B95 zZ*noeSr_x052UL@l-h$*stu_ho;n^ihBjubF?2w$J*0GBW9ZEEHHI!oTVp7cw#Lxb z?rIEup0uJzHHK8vu=Hhx2KTnCkn{*b04xQo!E4~(pg%o$=_%JuGTqBFj>G1cfOfD9 ztko`u`W7U+z!%`#7z4)A6@%#edK!6Gz4Xf7JaD%;b2`wq27-0qui%Ijaab_72OVH3 zSP%9Amz9u_U^a+?yTN+;)}iVZL%sBx&vtMseS@!9C2fS{e86cI7qZ?3$qMi~I1DQ6 zgiHiMqpsv)b7R}9MA3PIcK`Q`7^RR~Jws_OeQD1Wnx5UOqCmZT z{AflmYXtQ$G+mUP5<$w|S07tdpl`YB{XVzHS6|_;XX`3z>U@B9kaYAuTPY|W%lMS|%csOL^h4GszJAbwq+h?Qevsh|<&Np7ovQFm2yMRf~}NT^{CBG2;Kum?bqzifaFYY0pPS>v%3M3Tfjze01Pf9 zdFv-?8&_f=ekI6!~u&AHNC-;_M5vnXGQCw&I>?Senk69s>6>MHoN z#+4}e^EZ6_0GJR{y1^9solteZG Rvio?-{{d_#gWVcz0007rt4;s_ literal 4586 zcmVg0=BSDlF9D4NoF^bfGZcRNGnt= zeN=2wsE=(${AdfJRHRlwD{8@_j}=P=^?E_H74Jp72v^Jf&t#HK&SWzzOG0*)=lPLj z=07uMX3jb9dEYtn?wf=V3%T<-=kO_i`uNsMYUT+ok9=xoi1e?{Ldc&RW_`&YSpR>| zw0OdwZ290{jJ)*5LPB&8>sSAM!mIC=zqoF4=c>oCMp{}ll#pvjKJA%#&8jW`vF;=1 z3&TEnjFFeV&_sx@>hOpyPyKAklRJyf_C4`RgprpXok&P@k?y)V!)83c_PTjD4~`tT zkCB&NF_n-=jsBv4Y<_k6JI`!kuNbvr4GL zCVIiWt)r z&-I))!Z3-iE|-fiPQnxsrVtukDWfNhv52vIglNdehg!!9H^|HMPD)Xjr1&1^t(TXZ zX_eU<=GX}5lYbjbYqvgNpUqGjhpgd+(4>%GNPjT1kBzX?xk&tXxmSSJ%LXcg(LjW6 zX%7jaD2Rk9V3^l%wuxPHK0Y#45P50$^4fpXYp)R^Y;0}H5uJV^9AqOX`b1W2i!U`b zR(aiZjg(f$`{G{**ELqv)Oo9#s{JhIsj2n4-SJIu@L~GygIj`}D6*|wtk6@F*0@3( z=i_{r-O#~>c`oGT=zG%<7NmQ3H5=p-r!s>RBP~;7D=585^oIG4NaD)ka0`EfyRfA0!bYTP<&dr{NnA$xC&N&`b&21n zx)B?R;FUMx)gxI)rg;oVUFq(k6&pg_jEi#0nuAIWJQ~ zZE$;<{I%X{w!!P>p8ly4Gwc&AsFOBXp!U4>F}-5Fq=5C(wrVQCADjji$u^U z=mupLqro5>?u!2|<=R`^b@eqZWBCptz=p?3g{Cj0)b1v!%It}ixyGZEnZ837X|v?s zL96|uiFOyioeOXgsXs)^dIu-ApRT?}x&;`|fxQ|Enn63b!%S^rRT&)mw&U`6z334W z=Q@KG+bfiX{%9c3RU!4(oKN1`AYb;4=A^!y4FrS*obM{Z$I+Lu#1m#i-nOX%ABu?0 zF`c|lS4b&sK`z|Ng{7Uu1TK14BAUmz0Pk(bb)$EMuH$_a=|f<^FykCcYL{e7>5^0p zSUS8AMh$KiLTn(lF-sRn@C5kID3{u*rF-EI@U3l;q(OtrkIw-3DR>IJ1qv`Y7!Ixg zivY$b)wb}i>{xhenS7|xY0dp&EL{qI0Upt7nEPiS*aCKe_w<>~z5Xx+pM$e# z(`-;#a%uQ*DYzDN(Q;KkW?ccv{orrlFgOnud?XOS8t|$y^VmUcl%;+y#VSh%H^nSV z|E1Z^KyPQHw=SjGoqiONXMjN!J<`~Q+oqTu8ioQ#rRwbwt>C!LR?`m6|{lH;4!cr90!BZ z!&igvfgre5@xGq`yTA$XEp%ITdQAgcu4*wcqnBJ$G8w+>l6qvTylq4XtE`N=8PaX} z^q9W;lT8u4-ZsVO{X=?hIJJQU1t0I{yGIU*1$4I)>b^GgX*h&%D;wf(WRrWGf+vnDT}ITO`sk;^K=F+7V4|w>v2cv)3Mv!Wih& zkUi>B7V65j*Kec)-Zo0b=<%=D*_90N6l@MyrzU?tcI3@$>3yQp2bR7ac*$vn^o zL|5vWl&74@NyK}L=Lpe?Rv}WsR)n|(6|phz>2~i1XoN;}aeEP>(dAu;<|fxb4AI0Y z5C(ngUGkt0-`>SHWh7WA`K&tG-57Rn(E}m8Mw_EjZcd0?yeQ5<4_r$ z59LrbxjU;`xguPKdpa5T!ObP9{e5w+!g7XMe2xI)!4#5O3MNe{(7LWA@h0~yNi?|% zCf?+VZpGJSN;K~^@R<_LyBY+*Pryd-A+YNS83|^C4sZ|HNJsSOzy(vZ1CrOl5#_nX zD8Pcdz&d5B!EsP^ux!7A`1cuSc}ka7r{>JTmyA?2VE%ma(TL*Pwt1Qg>Mjsl(F zZtwzl4;(Yc9#xLEP=_d1wQagu_TyPXQY@us&uTHfHkY%6esAfUqt(l~Kzl4%@w1e< zKvruy*D6O9Y0SYLr==nlRlBRIE8X>#)r~FV(2sF9$me#YHB%kxqSyAa4zYDLb=4Ym zsJ?-1jMbqA7IjG31P5PZ&1oHa&6>?RL~H}M4vn*AszV+0+Eb`Qbg1Pd>QJqG@YS}{ z81SKMa+*r?lI>I~5xwJ{D$!AOCBkSzicXIKQ$UM7bu>XgVK4{~M8Ohpk3I8~2K{d! zcogiQXk1l3>$9fEMG3ef?{d>}RNf$KmA6|pFxPy2@u{1~hN4^iS$`m~>dU^CcD7otp6h+~kP z0B6Hn8%aBN<*L)31<73SbMOi{CT#*YYy!9mtkH1zayPp)-)ECr`^jw5*NX>k0A!%l zL#>b-b0R}UwDpsUXe{}Q%lg5AEx z#wJf=t-IRI`F*u?nR}{+Jq|>BoU_-C6YB)a!6vY$gfit8dNg1(loDbC7l3PkSgQ4y z#<&`iUxTgSpTJR;Ipmv&;ntVsT?{u?FmJvV>}PpaFpM^zMUkEd`@qq%)KRRJ9`Y8J z6H*4;U{ZOixQG_B5VV5D;C@Q{jjV=)`@?>Iz{b=ooNYax+lj!HB4*8(YK{Rya0 z;?+L@OTlKa7dVZCTm;4fKUk^}qaIiR)5ZRzw-oOJ4}$e(jo9B-2)2Va!2z>I?C*ae z_yU|uKT=c2{zgDD2FwMEXt_G}w-S;Ez~6zxO2|ks6D$K;z(IQ2uR8WuY9r(~9Dp7XS}f0bT(6K(PY@ z70^jXO{v)5SIXxb5J9ljj;~`6`Cbpd5<#$C1F|AY@3lAyv4Zo#G!UjA8>yoYt04Im z*aAKRcKWG}OEuz;ImUGb`Q;cV3g*ePg26N{N0C;5b>QU!4Uhdj2tEMVmVC1?lgHk# zT;`Mv22X&$0Xy8>3&8{+D(>$~;B!z0H@6Z@17g-Q805yoT$V9?!NxP0$msn(W;5ue zcTkX>WCqIKFHM`j;D=cHW3U1|q?NzmDF`-$SHK>v`~@FD@Zb8RBuG8yFZeDbwO|_P z)XraUCnR@+oq!k#xfo1RvJy!%3E%E@{(}BIJozdNlPv=@^N?{QPOKTUfMwv(lg(%F zM*ih9U|`qGU2v%#G z)d3XV-H_4vzI!H(uQRu~0vofME3h@K@s%vmu!W7AjeRG>q>bAbA!XHN+l^%mE^J80-TkjZ_0)8{htBe5(hl@#S!0?TX>u z0NyxR?RkpoAoif`B1t%M5!$(CEJjC^3>_bF|;*fj-exZ?JlL~97E@)&oOja+8jfn zv^j=$_LO7j^Q0BM$}yxGhNVv{G_gFavaoiT`huBQ=q)l0AJEdVRcnZtpu4G?Ss{{SZ>kHdno zJ?I3>!Des>xU7VX0dqkVtOT3sM~A9c4E54$KD)u$^aH+Pl~j7NI2v%8$%U->zW&s*0)5-v;P<&bzJ^MF16yBNTkor?WLdVcvaXS> zX{_?GwZ1wxeQrq^gQTMm*i1ojU&g2SUp~c`p(k0N`TRjglJ0(Y{vgA7$`jME$$5kn zb8My~6Cwm%U@7>8)@ycam2^YTf}NCDHPvP>1n&c*_G@GXMYp diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.md b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.md index 16c33863686..e9f2c3b2279 100644 --- a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.md +++ b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.md @@ -35,64 +35,64 @@ Generated by [AVA](https://ava.li). [ { - _id: '5b717cb9790000830123e1f6', + _id: '5b72df70940000eb0000f113', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M1.809S', - timestamp: 1534164150423, + time: 'PT0H0M10.826S', + timestamp: 1534254947940, }, { - _id: '5b71a028790000380423e22e', + _id: '5b72df4b9400009e0000f110', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M1.591S', - timestamp: 1534173221886, + time: 'PT0H0M33.636S', + timestamp: 1534254914304, }, { - _id: '5b71aa19780000f800061b11', + _id: '5b72df3d9400009e0000f10f', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M31.107S', - timestamp: 1534175738759, + time: 'PT0H0M17.551S', + timestamp: 1534254896753, }, { - _id: '5b71aa31780000f900061b12', + _id: '5b72a927960000e60005fb2a', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M28.959S', - timestamp: 1534175769866, + time: 'PT0H0M46.076S', + timestamp: 1534241050172, }, { - _id: '5b71aa38780000d300061b13', + _id: '5b71ac7c7800007e01061b20', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M4.79S', - timestamp: 1534175798825, + time: 'PT0H0M4.622S', + timestamp: 1534176365769, }, { - _id: '5b71aa40780000f900061b14', + _id: '5b71ac507800005901061b1e', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M0.275S', - timestamp: 1534175803615, + time: 'PT0H0M59.521S', + timestamp: 1534176306248, }, { _id: '5b71aab67800001101061b17', @@ -105,64 +105,64 @@ Generated by [AVA](https://ava.li). timestamp: 1534175895929, }, { - _id: '5b71ac507800005901061b1e', + _id: '5b71aa40780000f900061b14', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M59.521S', - timestamp: 1534176306248, + time: 'PT0H0M0.275S', + timestamp: 1534175803615, }, { - _id: '5b71ac7c7800007e01061b20', + _id: '5b71aa38780000d300061b13', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M4.622S', - timestamp: 1534176365769, + time: 'PT0H0M4.79S', + timestamp: 1534175798825, }, { - _id: '5b72a927960000e60005fb2a', + _id: '5b71aa31780000f900061b12', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M46.076S', - timestamp: 1534241050172, + time: 'PT0H0M28.959S', + timestamp: 1534175769866, }, { - _id: '5b72df3d9400009e0000f10f', + _id: '5b71aa19780000f800061b11', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M17.551S', - timestamp: 1534254896753, + time: 'PT0H0M31.107S', + timestamp: 1534175738759, }, { - _id: '5b72df4b9400009e0000f110', + _id: '5b71a028790000380423e22e', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M33.636S', - timestamp: 1534254914304, + time: 'PT0H0M1.591S', + timestamp: 1534173221886, }, { - _id: '5b72df70940000eb0000f113', + _id: '5b717cb9790000830123e1f6', annotation: '570b9ff12a7c0e980056fe8f', project_name: 'Test_Project', task_id: '581367a82faeb37a008a5352', tasktype_id: '570b9f4c2a7c0e4c008da6ee', tasktype_summary: 'ek_0563_BipolarCells', - time: 'PT0H0M10.826S', - timestamp: 1534254947940, + time: 'PT0H0M1.809S', + timestamp: 1534164150423, }, ] @@ -170,24 +170,24 @@ Generated by [AVA](https://ava.li). [ { - _id: '570ba00b2a7c0e4f0056fa9e', + _id: '570ba00b2a7c0e4f0056ff4e', annotation: '78135c192faeb34c0081c05e', project_name: 'Test_Project3(for_annotation_mutations)', task_id: '681367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460379659501, + timestamp: 1460725259501, }, { - _id: '570ba00b2a7c0e4f0056fb1e', + _id: '570ba00b2a7c0e4f0056fd3e', annotation: '78135c192faeb34c0081c05e', project_name: 'Test_Project3(for_annotation_mutations)', task_id: '681367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460466059501, + timestamp: 1460638859501, }, { _id: '570ba00b2a7c0e4f0056fc2e', @@ -200,24 +200,24 @@ Generated by [AVA](https://ava.li). timestamp: 1460552459501, }, { - _id: '570ba00b2a7c0e4f0056fd3e', + _id: '570ba00b2a7c0e4f0056fb1e', annotation: '78135c192faeb34c0081c05e', project_name: 'Test_Project3(for_annotation_mutations)', task_id: '681367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460638859501, + timestamp: 1460466059501, }, { - _id: '570ba00b2a7c0e4f0056ff4e', + _id: '570ba00b2a7c0e4f0056fa9e', annotation: '78135c192faeb34c0081c05e', project_name: 'Test_Project3(for_annotation_mutations)', task_id: '681367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460725259501, + timestamp: 1460379659501, }, ] @@ -225,24 +225,24 @@ Generated by [AVA](https://ava.li). [ { - _id: '570ba00b2a7c0e4f0056fe9e', + _id: '570ba00b2a7c0e4f0056fe4e', annotation: '58135c192faeb34c0081c05d', project_name: 'Test_Project2', task_id: '581367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460379659501, + timestamp: 1460725259501, }, { - _id: '570ba00b2a7c0e4f0056fe1e', + _id: '570ba00b2a7c0e4f0056fe3e', annotation: '58135c192faeb34c0081c05d', project_name: 'Test_Project2', task_id: '581367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460466059501, + timestamp: 1460638859501, }, { _id: '570ba00b2a7c0e4f0056fe2e', @@ -255,23 +255,23 @@ Generated by [AVA](https://ava.li). timestamp: 1460552459501, }, { - _id: '570ba00b2a7c0e4f0056fe3e', + _id: '570ba00b2a7c0e4f0056fe1e', annotation: '58135c192faeb34c0081c05d', project_name: 'Test_Project2', task_id: '581367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460638859501, + timestamp: 1460466059501, }, { - _id: '570ba00b2a7c0e4f0056fe4e', + _id: '570ba00b2a7c0e4f0056fe9e', annotation: '58135c192faeb34c0081c05d', project_name: 'Test_Project2', task_id: '581367a82faeb37a008a5354', tasktype_id: '570b9f4c2a7c0e4c008da6ff', tasktype_summary: 'ek_0674_BipolarCells', time: 'PT0H0M11.795S', - timestamp: 1460725259501, + timestamp: 1460379659501, }, ] diff --git a/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.snap b/frontend/javascripts/test/snapshots/public/test-bundle/test/backend-snapshot-tests/timetracking.e2e.js.snap index 5b043be3dd94bcd7646195806277184fdc9425e3..dbb044ab46b28ef183b28c02c58cb7470cd429f5 100644 GIT binary patch literal 1971 zcmV;k2Tb@uRzV%YYgc#bCXUUhghhgn>am1$9a^=)AUfSn0=XJ9Q?C4izvU zDlvc>i4ir%gqRRTGzxwYG*OWl6c8jaQ)7$?N+b{v{olvN_7=EyGYc`dkn(aZ_OQl&4vxTp54q4<1Z2UdWhxGf_fLeEsmpyF>H#HaBeK+P^-yhcWkF z&*IygE?mEF;r>yt-t*hIabqSQW6ZrvIYPXo*6fq1WBkq7P6czo0+0Y%FoN+9V`^Fo z1+|l5daOt!LYN?7Y6w%$Fb`NkV7+f_8Phyvm>$z?!I?2$(|pV@eMX0YoiM*-OefL| za^N-aBe=**$V{)@xqn{b9xYj7JiTUPW1|@&50?(`2IvMOeT3Wqo&-C=A3iFCw=Be{ z8bZc_8^KEOAvg+nKOuRr-cN<_m4(;_mmT0Zh~a|j;3cphjH)A~rH%?wQx;+oT(aP0 zZ~!y}2$>GLz&7w}fC}O7A0mt>go(h%(_~y}^x$nvr+VzsqQoXtmQC=glw`G-&az@$ z*J2vgCaC6e`I1^n=X0l<%A6o3Ifd6%Eg{5`EGu(KR!mV%L+ypUaX4)`HLLaP47X~< zQrmp%8=h)WTT+XQ`yC{Dic6|&^(5hVWmOc!7}c)cXjkfN*XR>e^jOk9LE|V>YFyW; zwvD}QiyhglTIlTgqDfoa#>S;s+thS>KBE?o&jHgzk!GX@LsS9?5%MuLr=lQhvH8LMX(VZ0Hbj^Zvjhc9m~*H z;qoRp1jeEOoDG(OZQvxBKo@}JW#}BZ+ytHgAAvu?RhTyD;1%#?(6J0X0+-|9>Uu)% z2J69Y5DpPC3lu}=E<+JwJzO?`Z$LwskjdbFuo)Z%V;iUtwPhiuz-2mk5WEe32IC`y z+z!@)k0Mlvy0Q?zz~y&vNh2W(!78u=FvAGB7AzV@g$R^|SOu3gU?=blCqw{?!Rw$K zgho&yf@L9ixX2&_UI%+YV-u!WUup@CzMp7X{ zWg#Ac%M;)|@E4dcijX_O^WZx`MpGfeWg#ZOWfHgxya2uczA=PM0S|%K#!w*|s+^uA zBtE4}tP*4KrzMO}o#TW`(!ZHlbc+LLNqQo%S0}$MkXQEUnZ8m z)~-y|jwUH8ikzcN_kO#jVwqHhmz21f^o&nYM+NN4G&q@J@hB@fkEZ6Qx}^$b;?$&+ zlw?CDNn<%S&Lwy^=?zq-kS$X*&hw5ceZDdft6U|KwNw%nOQp&t`mRc6A55YW6?x~J zH)CU6xFr)x2TLZ&m1%G?@v$f`I_JDj)l-E`F?%%W3O)>(=wCWqnX1frY?POr ze~dYFj8vhLAX_S>1Y0F{)-+I+>U*9{lHw@R#Gk7YiKB)ReF0pNs?B(ER1uwJ3U*W@ z6Q|fR$+k?KE7Ra)61XVGN{%uuJMoWDu}my4OU5{21(}WUg2q?&-p!Ti><=a`Dk{!Z z-l;YFDppBKCKO90SztM+#OZNYrNOEsv(71R?UnPO(s{MCXF1poP6EYCNYZ&}&kDG# z2A_aaV3v=NF7P2Z0WPOs%ap%UnQbrYc^qs9AwMD4fk(gx;1}nmJz?X`4Y(e3fv~Ha4EJcz*}ISRg97eU>Zn*HNaRpbgsfOY%UAx zceoKXSxVoh&CKDDb4jge9kkJVKeyqDbS9&v=HfG5)ROsJ%Jw9b<1js)OBe6fQdC6R zFPv}Ja&)gFC?|ir+ELUjr9F+*mv!xhbiR--b=r;TC2#fi{dz-%bEJk6ZrcofNKNzr zGQC=HEA@DI1}kk%9@`G^qP>{xC~=e#6O9Ij84taC4(tN8SWGX literal 1972 zcmV;l2TS-tRzV~6Q+quo{t1qo;(C`Pi*v9q&dnqZ4jK#{7AM}pbR?#{wO_h5I6H1RAL zjv$~!0YfB)V!}TJV?2mKQ6XwPfIk2^su58`jETfVQ7*r8w6t%L*=9YcX0Aaj@sUnPzVU`#{V7;$x8Qr|7nGW4-!I?2m-CU-bF0Dh)r!c=_bSKgb zGT;qx65QY6lc{?l4Zqk zQk7MzjaSKJvPGqs%4RM%jc|e(=VU&qs4*cFXW0lBXT=27)St^|wZn;Kl(gEnv$kC= z6r*j%FFe(vx~LQu4mwEm6qb~T(UXMZjVPiZhNyNvtzEGzr_v`VCqr@b1hu0~DB)z1 zYFppmw$PbQEBUUz9~#w#Q8p}vqOGZ1wnNEJS35ciRAW~$HQ?0R+SwWGY>o>`viK0? z>Crq3MJ1in_qg1@PEA#{GegyhvvnFNomQ;%4qYeS)@h`4I`wv7=sF3uP9vq$ohKTH zu2aa?X{2=8?6|(28Bh+^%{5EF4)6r%1}}h(U@O=Gc7eTMKR5<{v?>>C^&0TE{&G=Z z`pd<|OnvFkx-YgekPD5E})|=g$>DJACnpv%zHBO@6Wiap6Opk7^(M+#y9@I>q zZZ@JYWc+&R^$5(z!HeKaZ~=%Y02hHB;7FBa8F~ROP85F<$b+rmIB3GGXiW#}xpw1H)2&G;Lbf@wnnybiwbT9%<_;PNN9&4(%$ zYykU#&riq-?3`K}n;IbJU0KQsO_MixMf?vUyIx0kUNr+~+w1UOpO>h!iA0Xsm zupaCVP$6ncLi_-i(_lh9A&-KU;3IGW+|ht*(m;jql!RCXmvvw-xCGei2$>Ih!1usA ziVEQ^3Bkf83>JWG;4r9bBxDv?4&H2}LikET9Dz$OXc&!|8F&i34bFhz7@XV~Dullz z#M5wD2Hpd|gGo(<%mo|4A#kaQ3Q=1UV)9tr6Ces+1z!N?I6|g@C&3%zs1S7(PETSI zC&gp3B(wNL1eW6kl}m=pN&k9cO>#zJl_SQJBSI(EE2e+{!1O=F>`y;6D#-pS{dl{2 z_7}rK+1cN3X8&M>l`WJ1hADFdwVa1ER8m+zVyGlU*br3W%PV?qm9D0eD07x7U0i>( ze3cYVmLi%;N$oq##W>DZX^1MBm6X5*IaaciY5BQJ704uTrcAOa6MYG6nJSctj|64W zI_G&iNvHyuB8E%}!IVj`Wg4PPK0~GurX`jl-TG4{B86CLEYTA)TcipfOl**stmls3 zovlQrSlCdB<5*5tNwQTMX2uHzc`U#7Td4kl4HWwNWhVag=RL6NsE^Jeta zR3MWSH)N7jLnfZJWg1@23kAbGZ>iGft3Bna#49o{$ze?;Rr?l`FcEG?}icr9CUa9&jGWPD0|=OM6zr zWi9vwTm-XRgmi-sz&S9Pel1h_PUT*6SqG;2E&pdTGyoxEul=4|)gdz-PeaC8Px`q+iSY7fXATvX=HpSAKO$H@)I$yJg^L z6Q(4y`RL^yqUlb&=L*;I+F@C{flzAzQ&~ zcL`CltR+OYf+M@Q$T1aL-l_mKYR%@pM&6~^t^_;5QKJ|oH-qUQ4%P#0>Cn{*%UXR| z(4fN&DDh%yu{tw@L(at2f^pDV?}OZ~i={d`)WmFjrwVF3n@O0SgmP?1rZTC*d^JHu zr2T5MIWVrkA|=%=`8JGABuOMclR>^dU9U z14#F3$F0=o;TW#8RatC1!De$Y*&oDGMocv77-k~$?mMv2N}-UNmr1LcVxhImJVp98 zb_^Ick}+%&=CDba^SIJsqn&We<=|J=(*>dO{g+<*BRc)Y)@;{wEpPq%OywVFo~rQ+ GG5`ROhq%Z9 From 5c2f2e9563b6459bddb7d74429c11cf2bf0d3939 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 15 Oct 2020 11:34:12 +0200 Subject: [PATCH 22/39] run eliminate errors always because it checks for errors --- .../types/schemas/dataset_view_configuration_defaults.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js index 5087bf29222..30fe3b89db9 100644 --- a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js +++ b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js @@ -3,7 +3,7 @@ import { getDefaultLayerViewConfiguration, defaultDatasetViewConfiguration, -} from "types/schemas/dataset_view_configuration.schema.js"; +} from "types/schemas/dataset_view_configuration.schema"; import { type APIDataset, type APIDataLayer } from "types/api_flow_types"; import { getDefaultIntensityRangeOfLayer } from "oxalis/model/accessors/dataset_accessor"; import { validateObjectWithType } from "types/validation"; @@ -38,9 +38,7 @@ export const enforceValidatedDatasetViewConfiguration = ( "types::DatasetViewConfiguration", datasetViewConfiguration, ); - if (validationErrors.length) { - eliminateErrors(datasetViewConfiguration, validationErrors, defaultDatasetViewConfiguration); - } + eliminateErrors(datasetViewConfiguration, validationErrors, defaultDatasetViewConfiguration); const { layers } = datasetViewConfiguration; const newLayerConfig = {}; From dee2ecd95d76646ec340952232453d16b8e9d2e3 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 15 Oct 2020 13:28:22 +0200 Subject: [PATCH 23/39] enforce validated configuration in api instead of model_initialization --- frontend/javascripts/admin/admin_rest_api.js | 6 +++++- frontend/javascripts/oxalis/model_initialization.js | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index 638dcefea24..9a29ab51489 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -63,6 +63,7 @@ import * as Utils from "libs/utils"; import messages from "messages"; import window, { location } from "libs/window"; import { saveAs } from "file-saver"; +import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults"; const MAX_SERVER_ITEMS_PER_RESPONSE = 1000; @@ -807,13 +808,16 @@ export function getDatasetViewConfiguration( datasetId: APIDatasetId, displayedVolumeTracings: Array, ): Promise { - return Request.sendJSONReceiveJSON( + const settings = Request.sendJSONReceiveJSON( `/api/dataSetConfigurations/${datasetId.owningOrganization}/${datasetId.name}`, { data: displayedVolumeTracings, method: "POST", }, ); + + enforceValidatedDatasetViewConfiguration(settings, datasetId); + return settings; } export function updateDatasetConfiguration( diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index a0e7bc10009..05a9401e7eb 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -67,7 +67,6 @@ import * as Utils from "libs/utils"; import constants, { ControlModeEnum, type Vector3 } from "oxalis/constants"; import messages from "messages"; import window from "libs/window"; -import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults"; export const HANDLED_ERROR = "error_was_handled"; @@ -127,7 +126,6 @@ export async function initialize( ); initializeDataset(initialFetch, dataset, tracing); - enforceValidatedDatasetViewConfiguration(initialDatasetSettings, dataset); initializeSettings(initialUserSettings, initialDatasetSettings); let initializationInformation = null; From 8aaaabb5da8b67baf8b9a1b4a4bc6fba0860645b Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 15 Oct 2020 13:34:02 +0200 Subject: [PATCH 24/39] use new validation for dataset import as well --- .../dashboard/dataset/dataset_import_view.js | 50 ++++--------------- .../dataset/default_config_component.js | 4 +- .../dataset_view_configuration.schema.js | 22 +++++--- .../dataset_view_configuration_defaults.js | 7 +-- .../types/schemas/datasource.schema.json | 15 ------ .../types/schemas/datasource.types.js | 12 +---- .../types/schemas/user_settings.schema.js | 14 ++---- frontend/javascripts/types/validation.js | 4 +- 8 files changed, 37 insertions(+), 91 deletions(-) diff --git a/frontend/javascripts/dashboard/dataset/dataset_import_view.js b/frontend/javascripts/dashboard/dataset/dataset_import_view.js index 5f4624fb573..fb3e31d1954 100644 --- a/frontend/javascripts/dashboard/dataset/dataset_import_view.js +++ b/frontend/javascripts/dashboard/dataset/dataset_import_view.js @@ -25,7 +25,7 @@ import { trackAction } from "oxalis/model/helpers/analytics"; import Toast from "libs/toast"; import messages from "messages"; import features from "features"; -import { getDefaultIntensityRangeOfLayer } from "oxalis/model/accessors/dataset_accessor"; +import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults"; import { Hideable, hasFormError, jsonEditStyle } from "./helper_components"; import DefaultConfigComponent from "./default_config_component"; @@ -135,49 +135,17 @@ class DatasetImportView extends React.PureComponent { dataSource, }); - const defaultConfigPerLayer = { - brightness: 0, - contrast: 1, - color: [255, 255, 255], - intensityRange: [0, 255], - isDisabled: false, - isInverted: false, - }; - const datasetDefaultConfiguration = (await getDatasetDefaultConfiguration( + const datasetDefaultConfiguration = await getDatasetDefaultConfiguration( this.props.datasetId, - )) || { - layers: _.fromPairs( - dataSource.dataLayers.map(layer => { - // Here we adjust the default intensity range depending on the datatype of the layers. - // Otherwise, the default max value of the intensity range would always be 255 (which is too small for uint16 for example). - // The value can be overwritten by the user configuration (see loadHistorgramData saga) - const currentDefaultLayerConfig = _.clone(defaultConfigPerLayer); - const intensityRange = getDefaultIntensityRangeOfLayer(dataset, layer.name); - currentDefaultLayerConfig.intensityRange = intensityRange; - return [layer.name, currentDefaultLayerConfig]; - }), - ), - }; - const layers = datasetDefaultConfiguration.layers || {}; - // Remove unused brightness and contrast config and replace it with intensityRange if needed. - dataSource.dataLayers.forEach(layer => { - if (layer.category !== "color") { - return; - } - const currentColorLayerConfig = layers[layer.name]; - if (currentColorLayerConfig == null) { - return; - } - - delete currentColorLayerConfig.brightness; - delete currentColorLayerConfig.contrast; - // We want to intentionally manipulate the intensityRange although intensityRange is read-only. - const writeableLayerConfig = (currentColorLayerConfig: any); - writeableLayerConfig.intensityRange = currentColorLayerConfig.intensityRange || [0, 255]; - }); + ); + enforceValidatedDatasetViewConfiguration(datasetDefaultConfiguration, dataset, true); this.props.form.setFieldsValue({ defaultConfiguration: datasetDefaultConfiguration, - defaultConfigurationLayersJson: JSON.stringify(layers, null, " "), + defaultConfigurationLayersJson: JSON.stringify( + datasetDefaultConfiguration.layers, + null, + " ", + ), }); this.setState({ diff --git a/frontend/javascripts/dashboard/dataset/default_config_component.js b/frontend/javascripts/dashboard/dataset/default_config_component.js index 84ff071650f..df6ebce78c8 100644 --- a/frontend/javascripts/dashboard/dataset/default_config_component.js +++ b/frontend/javascripts/dashboard/dataset/default_config_component.js @@ -4,7 +4,7 @@ import { Icon, Input, Checkbox, Alert, Form, InputNumber, Col, Row, Tooltip } fr import * as React from "react"; import { Vector3Input } from "libs/vector_input"; -import { validateLayerConfigurationJSON, syncValidator } from "types/validation"; +import { validateLayerViewConfigurationObjectJSON, syncValidator } from "types/validation"; import { FormItemWithInfo, jsonEditStyle } from "./helper_components"; @@ -66,7 +66,7 @@ export default function DefaultConfigComponent({ form }: { form: Object }) { info="Use the following JSON to define layer-specific properties, such as color, alpha and intensityRange." > {getFieldDecorator("defaultConfigurationLayersJson", { - rules: [{ validator: validateLayerConfigurationJSON }], + rules: [{ validator: validateLayerViewConfigurationObjectJSON }], })()} diff --git a/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js b/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js index 5cadbd6d85a..ebed04f7153 100644 --- a/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js +++ b/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js @@ -23,8 +23,8 @@ export const layerViewConfiguration = { color: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 }, alpha: { type: "number", minimum: 0, maximum: 100 }, intensityRange: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 }, - min: { type: ["number", "null"] }, - max: { type: ["number", "null"] }, + min: { type: "number" }, + max: { type: "number" }, isDisabled: { type: "boolean" }, isInverted: { type: "boolean" }, isInEditMode: { type: "boolean" }, @@ -44,17 +44,21 @@ export const defaultDatasetViewConfiguration = { layers: {}, }; -export const datasetViewConfiguration = { +export const baseDatasetViewConfiguration = { fourBit: { type: "boolean" }, interpolation: { type: "boolean" }, highlightHoveredCellId: { type: "boolean" }, - renderIsosurfaces: { type: "boolean" }, - zoom: { type: ["number", "null"] }, + zoom: { type: "number" }, renderMissingDataBlack: { type: "boolean" }, loadingStrategy: { enum: ["BEST_QUALITY_FIRST", "PROGRESSIVE_QUALITY"] }, segmentationPatternOpacity: { type: "number", minimum: 0, maximum: 100 }, - position: { type: ["array", "null"], items: { type: "number" }, minItems: 3, maxItems: 3 }, - rotation: { type: ["array", "null"], items: { type: "number" }, minItems: 3, maxItems: 3 }, +}; + +export const datasetViewConfiguration = { + ...baseDatasetViewConfiguration, + renderIsosurfaces: { type: "boolean" }, + position: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 }, + rotation: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3 }, layers: { type: "object" }, }; @@ -92,5 +96,9 @@ export default { additionalProperties: false, required: ["color", "alpha", "intensityRange", "isDisabled", "isInverted", "isInEditMode"], }, + "types::LayerViewConfigurationObject": { + type: "object", + additionalProperties: { $ref: "#/definitions/types::OptionalLayerViewConfiguration" }, + }, }, }; diff --git a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js index 30fe3b89db9..1f0bad517b8 100644 --- a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js +++ b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js @@ -33,9 +33,10 @@ export const getSpecificDefaultsForLayers = (dataset: APIDataset, layer: APIData export const enforceValidatedDatasetViewConfiguration = ( datasetViewConfiguration: Object, dataset: APIDataset, + isOptional: boolean = false, ) => { const validationErrors = validateObjectWithType( - "types::DatasetViewConfiguration", + isOptional ? "types::OptionalDatasetViewConfiguration" : "types::DatasetViewConfiguration", datasetViewConfiguration, ); eliminateErrors(datasetViewConfiguration, validationErrors, defaultDatasetViewConfiguration); @@ -50,13 +51,13 @@ export const enforceValidatedDatasetViewConfiguration = ( const existingLayerConfig = layers[layer.name]; if (existingLayerConfig) { const layerErrors = validateObjectWithType( - "types::LayerViewConfiguration", + isOptional ? "types::OptionalLayerViewConfiguration" : "types::LayerViewConfiguration", existingLayerConfig, ); eliminateErrors(existingLayerConfig, layerErrors, layerConfigDefault); newLayerConfig[layer.name] = existingLayerConfig; } else { - newLayerConfig[layer.name] = layerConfigDefault; + newLayerConfig[layer.name] = isOptional ? {} : layerConfigDefault; } }); datasetViewConfiguration.layers = newLayerConfig; diff --git a/frontend/javascripts/types/schemas/datasource.schema.json b/frontend/javascripts/types/schemas/datasource.schema.json index 2dbe00cc171..b2a5a653c29 100644 --- a/frontend/javascripts/types/schemas/datasource.schema.json +++ b/frontend/javascripts/types/schemas/datasource.schema.json @@ -140,21 +140,6 @@ } }, "required": ["id", "dataLayers", "scale"] - }, - "types::LayerUserConfiguration": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "brightness": { "type": "number" }, - "contrast": { "type": "number" }, - "color": { "$ref": "#/definitions/types::Vector3" }, - "intensityRange": { "$ref": "#/definitions/types::Vector2" }, - "isDisabled": { "type": "boolean" }, - "isInverted": { "type": "boolean" }, - "alpha": { "type": "number" } - } - } } } } diff --git a/frontend/javascripts/types/schemas/datasource.types.js b/frontend/javascripts/types/schemas/datasource.types.js index 7ef0d13299c..a942e0394f4 100644 --- a/frontend/javascripts/types/schemas/datasource.types.js +++ b/frontend/javascripts/types/schemas/datasource.types.js @@ -66,14 +66,4 @@ export type DatasourceConfiguration = { dataLayers: Array, // Add minimum=0 and exclusiveMinimum=true to items scale: Array, -}; - -export type LayerUserConfiguration = { - [name: string]: { - brightness: number, - contrast: number, - color: Vector3, - intensityRange: Vector2, - isDisabled: boolean, - }, -}; +}; \ No newline at end of file diff --git a/frontend/javascripts/types/schemas/user_settings.schema.js b/frontend/javascripts/types/schemas/user_settings.schema.js index fde302bfaa3..66114388347 100644 --- a/frontend/javascripts/types/schemas/user_settings.schema.js +++ b/frontend/javascripts/types/schemas/user_settings.schema.js @@ -1,5 +1,7 @@ // @flow +import { baseDatasetViewConfiguration } from "types/schemas/dataset_view_configuration.schema"; + export const userSettings = { clippingDistance: { type: "number", minimum: 1, maximum: 12000 }, clippingDistanceArbitrary: { type: "number", minimum: 1, maximum: 127 }, @@ -25,20 +27,12 @@ export const userSettings = { sphericalCapRadius: { type: "number", minimum: 50, maximum: 500 }, tdViewDisplayPlanes: { type: "boolean" }, hideTreeRemovalWarning: { type: "boolean" }, - fourBit: { type: "boolean" }, - interpolation: { type: "boolean" }, - loadingStrategy: { enum: ["BEST_QUALITY_FIRST", "PROGRESSIVE_QUALITY"] }, - segmentationOpacity: { type: "number", minimum: 0, maximum: 100 }, - highlightHoveredCellId: { type: "boolean" }, - zoom: { type: "number", minimum: 0.005 }, - renderMissingDataBlack: { type: "boolean" }, brushSize: { type: "number", minimum: 1, maximum: 5000 }, - segmentationPatternOpacity: { type: "number", minimum: 0, maximum: 100 }, layoutScaleValue: { type: "number", minimum: 1, maximum: 5 }, autoSaveLayouts: { type: "boolean" }, gpuMemoryFactor: { type: "number" }, - // Deprecated keys kept for compatibility reasons - quality: { type: "number", enum: [0, 1, 2] }, + segmentationOpacity: { type: "number", minimum: 0, maximum: 100 }, + ...baseDatasetViewConfiguration, }; export default { diff --git a/frontend/javascripts/types/validation.js b/frontend/javascripts/types/validation.js index d07c6d5d8ff..806ffa83a46 100644 --- a/frontend/javascripts/types/validation.js +++ b/frontend/javascripts/types/validation.js @@ -2,7 +2,7 @@ import jsonschema from "jsonschema"; -import DatasourceSchema from "types/schemas/datasource.schema"; +import DatasourceSchema from "types/schemas/datasource.schema.json"; import UserSettingsSchema from "types/schemas/user_settings.schema"; import ViewConfigurationSchema from "types/schemas/dataset_view_configuration.schema"; @@ -43,8 +43,8 @@ export const validateObjectWithType = (type: string, json: Object) => { }; export const validateDatasourceJSON = validateWithSchema("types::DatasourceConfiguration"); -export const validateLayerConfigurationJSON = validateWithSchema("types::LayerUserConfiguration"); export const validateUserSettingsJSON = validateWithSchema("types::UserSettings"); +export const validateLayerViewConfigurationObjectJSON = validateWithSchema("types::LayerViewConfigurationObject"); export const isValidJSON = (json: string) => { try { From 8981f57b2965613bef28b70bff8a91888b493596 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 15 Oct 2020 14:41:09 +0200 Subject: [PATCH 25/39] fix frontend tests --- frontend/javascripts/admin/admin_rest_api.js | 12 ++++++------ frontend/javascripts/oxalis/model_initialization.js | 4 ++-- .../javascripts/test/sagas/saga_integration.spec.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/javascripts/admin/admin_rest_api.js b/frontend/javascripts/admin/admin_rest_api.js index 9a29ab51489..9b6dc3e54a8 100644 --- a/frontend/javascripts/admin/admin_rest_api.js +++ b/frontend/javascripts/admin/admin_rest_api.js @@ -804,19 +804,19 @@ export function updateDataset(datasetId: APIDatasetId, dataset: APIDataset): Pro ); } -export function getDatasetViewConfiguration( - datasetId: APIDatasetId, +export async function getDatasetViewConfiguration( + dataset: APIDataset, displayedVolumeTracings: Array, -): Promise { - const settings = Request.sendJSONReceiveJSON( - `/api/dataSetConfigurations/${datasetId.owningOrganization}/${datasetId.name}`, +): Promise { + const settings = await Request.sendJSONReceiveJSON( + `/api/dataSetConfigurations/${dataset.owningOrganization}/${dataset.name}`, { data: displayedVolumeTracings, method: "POST", }, ); - enforceValidatedDatasetViewConfiguration(settings, datasetId); + enforceValidatedDatasetViewConfiguration(settings, dataset); return settings; } diff --git a/frontend/javascripts/oxalis/model_initialization.js b/frontend/javascripts/oxalis/model_initialization.js index 05a9401e7eb..65102936f3f 100644 --- a/frontend/javascripts/oxalis/model_initialization.js +++ b/frontend/javascripts/oxalis/model_initialization.js @@ -120,12 +120,12 @@ export async function initialize( displayedVolumeTracings.push(tracing.volume.id); } + initializeDataset(initialFetch, dataset, tracing); + const initialDatasetSettings = await getDatasetViewConfiguration( dataset, displayedVolumeTracings, ); - - initializeDataset(initialFetch, dataset, tracing); initializeSettings(initialUserSettings, initialDatasetSettings); let initializationInformation = null; diff --git a/frontend/javascripts/test/sagas/saga_integration.spec.js b/frontend/javascripts/test/sagas/saga_integration.spec.js index 1bc124a47d6..69475dd6942 100644 --- a/frontend/javascripts/test/sagas/saga_integration.spec.js +++ b/frontend/javascripts/test/sagas/saga_integration.spec.js @@ -53,7 +53,7 @@ test.serial( Store.getState().tracing.skeleton, [1, 2, 3], [0, 0, 0], - 2, + 1, ), ], ], From ec89820d9de5ed2aabbc0f5a805ec8418ba42263 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 15 Oct 2020 15:33:26 +0200 Subject: [PATCH 26/39] add table for layer view configurations --- .../recommended_configuration_view.js | 1 - .../dataset/default_config_component.js | 67 ++++++++++++++++--- frontend/javascripts/messages.js | 11 +++ .../dataset_view_configuration.schema.js | 6 +- .../types/schemas/datasource.schema.json | 6 -- .../types/schemas/datasource.types.js | 4 +- frontend/javascripts/types/validation.js | 4 +- 7 files changed, 75 insertions(+), 24 deletions(-) diff --git a/frontend/javascripts/admin/tasktype/recommended_configuration_view.js b/frontend/javascripts/admin/tasktype/recommended_configuration_view.js index f33b7576ae0..6c0b02de183 100644 --- a/frontend/javascripts/admin/tasktype/recommended_configuration_view.js +++ b/frontend/javascripts/admin/tasktype/recommended_configuration_view.js @@ -65,7 +65,6 @@ export const DEFAULT_RECOMMENDED_CONFIGURATION: $Shape<{| export const settingComments = { clippingDistance: "orthogonal mode", moveValue: "orthogonal mode", - quality: "0 (high), 1 (medium), 2 (low)", clippingDistanceArbitrary: "flight/oblique mode", moveValue3d: "flight/oblique mode", loadingStrategy: "BEST_QUALITY_FIRST or PROGRESSIVE_QUALITY", diff --git a/frontend/javascripts/dashboard/dataset/default_config_component.js b/frontend/javascripts/dashboard/dataset/default_config_component.js index df6ebce78c8..c6eda3b6546 100644 --- a/frontend/javascripts/dashboard/dataset/default_config_component.js +++ b/frontend/javascripts/dashboard/dataset/default_config_component.js @@ -1,10 +1,13 @@ // @flow -import { Icon, Input, Checkbox, Alert, Form, InputNumber, Col, Row, Tooltip } from "antd"; +import _ from "lodash"; +import { Icon, Input, Checkbox, Alert, Form, InputNumber, Col, Row, Tooltip, Table } from "antd"; import * as React from "react"; import { Vector3Input } from "libs/vector_input"; import { validateLayerViewConfigurationObjectJSON, syncValidator } from "types/validation"; +import { getDefaultLayerViewConfiguration } from "types/schemas/dataset_view_configuration.schema"; +import { layerViewConfigurations } from "messages"; import { FormItemWithInfo, jsonEditStyle } from "./helper_components"; @@ -13,6 +16,30 @@ const FormItem = Form.Item; export default function DefaultConfigComponent({ form }: { form: Object }) { const { getFieldDecorator } = form; + const columns = [ + { + title: "Name", + dataIndex: "name", + }, + { + title: "Key", + dataIndex: "key", + }, + { + title: "Default Value", + dataIndex: "value", + }, + { + title: "Comment", + dataIndex: "comment", + }, + ]; + + const comments = { + alpha: "20 for segementation layer", + loadingStrategy: "BEST_QUALITY_FIRST or PROGRESSIVE_QUALITY", + }; + return (
- - {getFieldDecorator("defaultConfigurationLayersJson", { - rules: [{ validator: validateLayerViewConfigurationObjectJSON }], - })()} - + + + + {getFieldDecorator("defaultConfigurationLayersJson", { + rules: [{ validator: validateLayerViewConfigurationObjectJSON }], + })()} + + + + Valid layer view configurations and their default values: +
+
+ ({ + name: layerViewConfigurations[key], + key, + value: value ? value.toString() : "null", + comment: comments[key] || "", + }))} + size="small" + pagination={false} + className="large-table" + scroll={{ x: "max-content" }} + /> + + ); } diff --git a/frontend/javascripts/messages.js b/frontend/javascripts/messages.js index 83a552a3710..695df605582 100644 --- a/frontend/javascripts/messages.js +++ b/frontend/javascripts/messages.js @@ -41,6 +41,17 @@ export const settings = { autoBrush: "Automatic Brush (Beta)", }; +export const layerViewConfigurations = { + color: "Color", + alpha: "Layer opacity", + intensityRange: "Intensity Range", + min: "Minimum Data Value", + max: "Maximum Data Value", + isDisabled: "Disabled Layer", + isInverted: "Inverted Layer", + isInEditMode: "Configuration Mode", +}; + export default { yes: "Yes", no: "No", diff --git a/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js b/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js index ebed04f7153..fcfabcfbdc1 100644 --- a/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js +++ b/frontend/javascripts/types/schemas/dataset_view_configuration.schema.js @@ -1,10 +1,8 @@ // @flow -export function getDefaultLayerViewConfiguration(dynamicDefault: Object) { +export function getDefaultLayerViewConfiguration(dynamicDefault: Object = {}) { const defaultLayerViewConfiguration = { color: [255, 255, 255], - brightness: null, - contrast: null, alpha: 100, intensityRange: [0, 255], min: null, @@ -48,7 +46,7 @@ export const baseDatasetViewConfiguration = { fourBit: { type: "boolean" }, interpolation: { type: "boolean" }, highlightHoveredCellId: { type: "boolean" }, - zoom: { type: "number" }, + zoom: { type: "number", minimum: 0.005 }, renderMissingDataBlack: { type: "boolean" }, loadingStrategy: { enum: ["BEST_QUALITY_FIRST", "PROGRESSIVE_QUALITY"] }, segmentationPatternOpacity: { type: "number", minimum: 0, maximum: 100 }, diff --git a/frontend/javascripts/types/schemas/datasource.schema.json b/frontend/javascripts/types/schemas/datasource.schema.json index b2a5a653c29..0e5bff66af7 100644 --- a/frontend/javascripts/types/schemas/datasource.schema.json +++ b/frontend/javascripts/types/schemas/datasource.schema.json @@ -7,12 +7,6 @@ "minItems": 3, "maxItems": 3 }, - "types::Vector2": { - "type": "array", - "items": { "type": "number" }, - "minItems": 2, - "maxItems": 2 - }, "types::BoundingBox": { "type": "object", "properties": { diff --git a/frontend/javascripts/types/schemas/datasource.types.js b/frontend/javascripts/types/schemas/datasource.types.js index a942e0394f4..54e41659f1c 100644 --- a/frontend/javascripts/types/schemas/datasource.types.js +++ b/frontend/javascripts/types/schemas/datasource.types.js @@ -6,8 +6,6 @@ type Vector3 = [number, number, number]; -type Vector2 = [number, number]; - type BoundingBox = { topLeft: Vector3, width: number, @@ -66,4 +64,4 @@ export type DatasourceConfiguration = { dataLayers: Array, // Add minimum=0 and exclusiveMinimum=true to items scale: Array, -}; \ No newline at end of file +}; diff --git a/frontend/javascripts/types/validation.js b/frontend/javascripts/types/validation.js index 806ffa83a46..f9996aa9db3 100644 --- a/frontend/javascripts/types/validation.js +++ b/frontend/javascripts/types/validation.js @@ -44,7 +44,9 @@ export const validateObjectWithType = (type: string, json: Object) => { export const validateDatasourceJSON = validateWithSchema("types::DatasourceConfiguration"); export const validateUserSettingsJSON = validateWithSchema("types::UserSettings"); -export const validateLayerViewConfigurationObjectJSON = validateWithSchema("types::LayerViewConfigurationObject"); +export const validateLayerViewConfigurationObjectJSON = validateWithSchema( + "types::LayerViewConfigurationObject", +); export const isValidJSON = (json: string) => { try { From 01941fc83bfb947e190056a25cc46ad268108b04 Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 15 Oct 2020 15:37:21 +0200 Subject: [PATCH 27/39] fix for null values --- .../javascripts/dashboard/dataset/default_config_component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/dashboard/dataset/default_config_component.js b/frontend/javascripts/dashboard/dataset/default_config_component.js index c6eda3b6546..565efa15999 100644 --- a/frontend/javascripts/dashboard/dataset/default_config_component.js +++ b/frontend/javascripts/dashboard/dataset/default_config_component.js @@ -108,7 +108,7 @@ export default function DefaultConfigComponent({ form }: { form: Object }) { dataSource={_.map(getDefaultLayerViewConfiguration(), (value, key: string) => ({ name: layerViewConfigurations[key], key, - value: value ? value.toString() : "null", + value: value == null ? "not set" : value.toString(), comment: comments[key] || "", }))} size="small" From 1c65001b3d415d2209a4679294389096f462344b Mon Sep 17 00:00:00 2001 From: Youri K Date: Thu, 15 Oct 2020 15:58:04 +0200 Subject: [PATCH 28/39] fix frontend test --- frontend/javascripts/test/sagas/saga_integration.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/test/sagas/saga_integration.spec.js b/frontend/javascripts/test/sagas/saga_integration.spec.js index 69475dd6942..1bc124a47d6 100644 --- a/frontend/javascripts/test/sagas/saga_integration.spec.js +++ b/frontend/javascripts/test/sagas/saga_integration.spec.js @@ -53,7 +53,7 @@ test.serial( Store.getState().tracing.skeleton, [1, 2, 3], [0, 0, 0], - 1, + 2, ), ], ], From 1fda7f617bcc9dd7bede7f00fb3971d546ad01b3 Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 16 Oct 2020 10:14:09 +0200 Subject: [PATCH 29/39] add additional frontend tests --- .../dataset_view_configuration.spec.js | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 frontend/javascripts/test/schemas/dataset_view_configuration.spec.js diff --git a/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js b/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js new file mode 100644 index 00000000000..e7a82d8af8b --- /dev/null +++ b/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js @@ -0,0 +1,76 @@ +// @noflow +import _ from "lodash"; +import test from "ava"; +import { validateObjectWithType } from "types/validation"; +import { enforceValidatedDatasetViewConfiguration } from "types/schemas/dataset_view_configuration_defaults"; +import DATASET from "test/fixtures/dataset_server_object"; + +const datasetViewConfigurationType = "types::DatasetViewConfiguration"; + +const CORRECT_DATASET_CONFIGURATION = { + fourBit: false, + interpolation: true, + highlightHoveredCellId: true, + renderIsosurfaces: false, + renderMissingDataBlack: true, + loadingStrategy: "PROGRESSIVE_QUALITY", + segmentationPatternOpacity: 40, + layers: {}, +}; + +test("Validator should report no errors for valid configuration (without optional values)", t => { + t.is( + validateObjectWithType(datasetViewConfigurationType, CORRECT_DATASET_CONFIGURATION).length, + 0, + ); +}); + +test("Validator should report no errors for valid configuration (with optional values)", t => { + const validConfiguration = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + validConfiguration.zoom = 3; + validConfiguration.position = [1, 1, 1]; + validConfiguration.rotation = [1, 1, 1]; + t.is( + validateObjectWithType(datasetViewConfigurationType, CORRECT_DATASET_CONFIGURATION).length, + 0, + ); +}); + +test("Validator should report 1 error for additional property", t => { + const additionalPropertiesObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + additionalPropertiesObject.additionalProperty = 1; + t.is(validateObjectWithType(datasetViewConfigurationType, additionalPropertiesObject).length, 1); +}); + +test("Validator should report 1 error for missing property", t => { + const missingPropertiesObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + delete missingPropertiesObject.layers; + t.is(validateObjectWithType(datasetViewConfigurationType, missingPropertiesObject).length, 1); +}); + +test("Validator should report 1 error for wrong type", t => { + const wrongTypeObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + wrongTypeObject.fourBit = 1; + t.is(validateObjectWithType(datasetViewConfigurationType, wrongTypeObject).length, 1); +}); + +test("validated view configuration should report no errors", t => { + const validatedConfiguration = {}; + enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, false); + t.is(validateObjectWithType(datasetViewConfigurationType, validatedConfiguration).length, 0); +}); + +test("validated view configuration should remove additional properties", t => { + const validatedConfiguration = { additionalProperty: 1 }; + enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, false); + t.is(validateObjectWithType(datasetViewConfigurationType, validatedConfiguration).length, 0); +}); + +test("validated view configuration should not add missing property, when optional", t => { + const validatedConfiguration = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + delete validatedConfiguration.fourBit; + enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, true); + t.is(validatedConfiguration.fourBit === undefined, true); +}); + + From fbfec83946b7b607ca3d52b1c239628d6f4eaced Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 16 Oct 2020 10:44:36 +0200 Subject: [PATCH 30/39] fix pretty --- .../dataset_view_configuration.spec.js | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js b/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js index e7a82d8af8b..2d20f76a74b 100644 --- a/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js +++ b/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js @@ -8,69 +8,67 @@ import DATASET from "test/fixtures/dataset_server_object"; const datasetViewConfigurationType = "types::DatasetViewConfiguration"; const CORRECT_DATASET_CONFIGURATION = { - fourBit: false, - interpolation: true, - highlightHoveredCellId: true, - renderIsosurfaces: false, - renderMissingDataBlack: true, - loadingStrategy: "PROGRESSIVE_QUALITY", - segmentationPatternOpacity: 40, - layers: {}, + fourBit: false, + interpolation: true, + highlightHoveredCellId: true, + renderIsosurfaces: false, + renderMissingDataBlack: true, + loadingStrategy: "PROGRESSIVE_QUALITY", + segmentationPatternOpacity: 40, + layers: {}, }; test("Validator should report no errors for valid configuration (without optional values)", t => { - t.is( - validateObjectWithType(datasetViewConfigurationType, CORRECT_DATASET_CONFIGURATION).length, - 0, - ); + t.is( + validateObjectWithType(datasetViewConfigurationType, CORRECT_DATASET_CONFIGURATION).length, + 0, + ); }); test("Validator should report no errors for valid configuration (with optional values)", t => { - const validConfiguration = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); - validConfiguration.zoom = 3; - validConfiguration.position = [1, 1, 1]; - validConfiguration.rotation = [1, 1, 1]; - t.is( - validateObjectWithType(datasetViewConfigurationType, CORRECT_DATASET_CONFIGURATION).length, - 0, - ); + const validConfiguration = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + validConfiguration.zoom = 3; + validConfiguration.position = [1, 1, 1]; + validConfiguration.rotation = [1, 1, 1]; + t.is( + validateObjectWithType(datasetViewConfigurationType, CORRECT_DATASET_CONFIGURATION).length, + 0, + ); }); test("Validator should report 1 error for additional property", t => { - const additionalPropertiesObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); - additionalPropertiesObject.additionalProperty = 1; - t.is(validateObjectWithType(datasetViewConfigurationType, additionalPropertiesObject).length, 1); + const additionalPropertiesObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + additionalPropertiesObject.additionalProperty = 1; + t.is(validateObjectWithType(datasetViewConfigurationType, additionalPropertiesObject).length, 1); }); test("Validator should report 1 error for missing property", t => { - const missingPropertiesObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); - delete missingPropertiesObject.layers; - t.is(validateObjectWithType(datasetViewConfigurationType, missingPropertiesObject).length, 1); + const missingPropertiesObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + delete missingPropertiesObject.layers; + t.is(validateObjectWithType(datasetViewConfigurationType, missingPropertiesObject).length, 1); }); test("Validator should report 1 error for wrong type", t => { - const wrongTypeObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); - wrongTypeObject.fourBit = 1; - t.is(validateObjectWithType(datasetViewConfigurationType, wrongTypeObject).length, 1); + const wrongTypeObject = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + wrongTypeObject.fourBit = 1; + t.is(validateObjectWithType(datasetViewConfigurationType, wrongTypeObject).length, 1); }); test("validated view configuration should report no errors", t => { - const validatedConfiguration = {}; - enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, false); - t.is(validateObjectWithType(datasetViewConfigurationType, validatedConfiguration).length, 0); + const validatedConfiguration = {}; + enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, false); + t.is(validateObjectWithType(datasetViewConfigurationType, validatedConfiguration).length, 0); }); test("validated view configuration should remove additional properties", t => { - const validatedConfiguration = { additionalProperty: 1 }; - enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, false); - t.is(validateObjectWithType(datasetViewConfigurationType, validatedConfiguration).length, 0); + const validatedConfiguration = { additionalProperty: 1 }; + enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, false); + t.is(validateObjectWithType(datasetViewConfigurationType, validatedConfiguration).length, 0); }); test("validated view configuration should not add missing property, when optional", t => { - const validatedConfiguration = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); - delete validatedConfiguration.fourBit; - enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, true); - t.is(validatedConfiguration.fourBit === undefined, true); + const validatedConfiguration = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + delete validatedConfiguration.fourBit; + enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, true); + t.is(validatedConfiguration.fourBit === undefined, true); }); - - From ff5218342a12e6d72331235658d4c484cabb2f75 Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 16 Oct 2020 11:04:15 +0200 Subject: [PATCH 31/39] reversion of evolution --- .../056-add-layer-specific-view-configs.sql | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql b/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql index 40465afefe2..b8c76b1c3f1 100644 --- a/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql +++ b/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql @@ -10,8 +10,43 @@ SET configuration = jsonb_set( FROM (select _user, _dataset, jsonb_object_agg(layerName, configuration) AS json FROM webknossos.user_dataSetLayerConfigurations dlC GROUP BY _user, _dataset) AS subQ WHERE dsC._user = subQ._user and dsC._dataset = subQ._dataset; +--Rename view configuration back to old naming +ALTER TABLE webknossos.user_dataSetConfigurations RENAME COLUMN viewConfiguration TO configuration; +ALTER TABLE webknossos.user_dataSetConfigurations RENAME CONSTRAINT viewConfigurationIsJsonObject TO configurationIsJsonObject; + +--drop unused table DROP TABLE webknossos.user_dataSetLayerConfigurations; +--rename back to original +DROP VIEW webknossos.dataSets_; + +ALTER TABLE webknossos.dataSets RENAME COLUMN defaultViewConfiguration TO sourceDefaultConfiguration; +ALTER TABLE webknossos.dataSets RENAME COLUMN adminViewConfiguration TO defaultConfiguration; +ALTER TABLE webknossos.dataSets RENAME CONSTRAINT defaultViewConfigurationIsJsonObject TO sourceDefaultConfigurationIsJsonObject; +ALTER TABLE webknossos.dataSets RENAME CONSTRAINT adminViewConfigurationIsJsonObject TO defaultConfigurationIsJsonObject; + +CREATE VIEW webknossos.dataSets_ AS SELECT * FROM webknossos.dataSets WHERE NOT isDeleted; + +UPDATE webknossos.dataSets ds +SET defaultConfiguration = jsonb_set( + defaultConfiguration, + array['layers'], + subQ.json) +FROM (select _dataset, jsonb_object_agg(name, adminViewConfiguration) AS json FROM webknossos.dataSet_layers dl GROUP BY _dataset) AS subQ +WHERE ds._id = subQ._dataset; + +--drop admin view configuration for layers +ALTER TABLE webknossos.dataSet_layers DROP CONSTRAINT adminViewConfigurationIsJsonObject; +ALTER TABLE webknossos.dataSet_layers DROP COLUMN adminViewConfiguration; + +--add configuration container from defaultConfig +UPDATE webknossos.dataSets +SET defaultConfiguration = jsonb_set( + defaultConfiguration, + array['configuration'], + defaultConfiguration) +WHERE defaultConfiguration is not null; + UPDATE webknossos.releaseInformation SET schemaVersion = 55; COMMIT TRANSACTION; From ab429986270f3ea77c1720aadf421a4480c1831b Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 16 Oct 2020 14:27:22 +0200 Subject: [PATCH 32/39] rework update layers --- app/models/binary/DataSet.scala | 27 ++++++++++++++++++-------- app/models/binary/DataSetService.scala | 3 --- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/models/binary/DataSet.scala b/app/models/binary/DataSet.scala index ce607da8bb6..c08b5b35efd 100755 --- a/app/models/binary/DataSet.scala +++ b/app/models/binary/DataSet.scala @@ -420,27 +420,38 @@ class DataSetDataLayerDAO @Inject()(sqlClient: SQLClient, dataSetResolutionsDAO: '#${writeStructTuple(s.boundingBox.toSql.map(_.toString))}', ${s.largestSegmentId}, '#${writeArrayTuple( mappings.map(sanitize(_)).toList)}', #${optionLiteral( s.defaultViewConfiguration.map(d => Json.toJson(d).toString))}, #${optionLiteral( - s.adminViewConfiguration.map(d => Json.toJson(d).toString))})""" + s.adminViewConfiguration.map(d => Json.toJson(d).toString))}) + on conflict (_dataSet, name) do update set category = '#${s.category.toString}', elementClass = '#${s.elementClass.toString}', + boundingBox = '#${writeStructTuple(s.boundingBox.toSql.map(_.toString))}', largestSegmentId = ${s.largestSegmentId}, + mappings = '#${writeArrayTuple(mappings.map(sanitize(_)).toList)}', + defaultViewConfiguration = #${optionLiteral(s.defaultViewConfiguration.map(d => Json.toJson(d).toString))}""" } case d: AbstractDataLayer => { sqlu"""insert into webknossos.dataset_layers(_dataSet, name, category, elementClass, boundingBox, defaultViewConfiguration, adminViewConfiguration) values(${_dataSet.id}, ${d.name}, '#${d.category.toString}', '#${d.elementClass.toString}', '#${writeStructTuple(d.boundingBox.toSql.map(_.toString))}', #${optionLiteral( d.defaultViewConfiguration.map(d => Json.toJson(d).toString))}, #${optionLiteral( - d.adminViewConfiguration.map(d => Json.toJson(d).toString))})""" + d.adminViewConfiguration.map(d => Json.toJson(d).toString))}) + on conflict (_dataSet, name) do update set category = '#${d.category.toString}', elementClass = '#${d.elementClass.toString}', + boundingBox = '#${writeStructTuple(d.boundingBox.toSql.map(_.toString))}', + defaultViewConfiguration = #${optionLiteral(d.defaultViewConfiguration.map(d => Json.toJson(d).toString))}""" } case _ => throw new Exception("DataLayer type mismatch") } def updateLayers(_dataSet: ObjectId, source: InboxDataSource)(implicit ctx: DBAccessContext): Fox[Unit] = { - val clearQuery = - sqlu"delete from webknossos.dataset_layers where _dataSet = ${_dataSet}" - val insertQueries = source.toUsable match { - case Some(usable) => usable.dataLayers.map(insertLayerQuery(_dataSet, _)) - case None => List() + def getSpecificClearQuery(dataLayers: List[DataLayer]) = + sqlu"delete from webknossos.dataset_layers where _dataSet = ${_dataSet} and name not in #${writeStructTupleWithQuotes( + dataLayers.map(d => sanitize(d.name)))}" + val clearQuery = sqlu"delete from webknossos.dataset_layers where _dataSet = ${_dataSet}" + + val queries = source.toUsable match { + case Some(usable) => + getSpecificClearQuery(usable.dataLayers) :: usable.dataLayers.map(insertLayerQuery(_dataSet, _)) + case None => List(clearQuery) } for { - _ <- run(DBIO.sequence(List(clearQuery) ++ insertQueries)) + _ <- run(DBIO.sequence(queries)) _ <- dataSetResolutionsDAO.updateResolutions(_dataSet, source.toUsable.map(_.dataLayers)) } yield () } diff --git a/app/models/binary/DataSetService.scala b/app/models/binary/DataSetService.scala index b78cae67fbe..fb6310b3313 100644 --- a/app/models/binary/DataSetService.scala +++ b/app/models/binary/DataSetService.scala @@ -171,8 +171,6 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, dataSource.hashCode, dataSource, dataSource.isUsable)(GlobalAccessContext) - // TODO should we preserve the adminViewConfiguration here? - _ <- dataSetDataLayerDAO.updateLayers(foundDataSet._id, dataSource) } yield foundDataSet._id private def updateDataSourceDifferentDataStore( @@ -193,7 +191,6 @@ class DataSetService @Inject()(organizationDAO: OrganizationDAO, dataSource.hashCode, dataSource, dataSource.isUsable)(GlobalAccessContext) - _ <- dataSetDataLayerDAO.updateLayers(foundDataSet._id, dataSource) } yield Some(foundDataSet._id) } else { logger.info( From bfc8f3978fc6353feedba2c4acded8afa99d9570 Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 16 Oct 2020 14:39:23 +0200 Subject: [PATCH 33/39] fix naming in reversion of evolution --- .../reversions/056-add-layer-specific-view-configs.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql b/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql index b8c76b1c3f1..1fef908ec3f 100644 --- a/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql +++ b/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql @@ -3,11 +3,11 @@ START TRANSACTION; -- pre-aggregate all layer jsons into one json object, set json object in configuration UPDATE webknossos.user_dataSetConfigurations dsC -SET configuration = jsonb_set( - dsC.configuration, +SET viewConfiguration = jsonb_set( + dsC.viewConfiguration, array['layers'], subQ.json) -FROM (select _user, _dataset, jsonb_object_agg(layerName, configuration) AS json FROM webknossos.user_dataSetLayerConfigurations dlC GROUP BY _user, _dataset) AS subQ +FROM (select _user, _dataset, jsonb_object_agg(layerName, viewConfiguration) AS json FROM webknossos.user_dataSetLayerConfigurations dlC GROUP BY _user, _dataset) AS subQ WHERE dsC._user = subQ._user and dsC._dataset = subQ._dataset; --Rename view configuration back to old naming From 8453954ac5e0f4cd2685814afbba05b462de5905 Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 16 Oct 2020 15:14:13 +0200 Subject: [PATCH 34/39] update documentation --- docs/datasets.md | 16 +++++++++++++--- docs/tracing_ui.md | 8 ++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/datasets.md b/docs/datasets.md index f941e20bc17..7f50fdce55a 100644 --- a/docs/datasets.md +++ b/docs/datasets.md @@ -112,12 +112,22 @@ The `Advanced` view lets you edit the underlying JSON configuration directly. ### View Configuration - `Position`: Default position of the dataset in voxel coordinates. When opening the dataset, users will be located at this position. - `Zoom`: Default zoom. -- `Segmentation Opacity`: Default opacity of the segmentation layer. +- `Segmentation Pattern Opacity`: Default opacity of the patterns rendered inside segmentation cells. - `Interpolation`: Whether interpolation should be enabled by default. -- `Layer Configuration`: This is an advanced feature to control the default settings (e.g. brightness, contrast, color) per layer. It needs to be configured in a JSON format. +- `Layer Configuration`: This is an advanced feature to control the default settings (e.g. alpha, color, intensity range) per layer. It needs to be configured in a JSON format. ![Dataset Editing: View Configuration Tab](images/dataset_view_config.png) +#### View Configuration Hierarchy +There are two ways to set default View Configuration Settings: +- inside the `datasource_properties.json` +- in the `Edit View` for Datasets +The View Configuration from the `Edit View` takes precedence over the `datasource_properties.json`. +You don't have to set complete View Configurations in neither option, as webKnossos will fill missing attributes with sensible defaults. +These View Configurations impact the first appearance of the Dataset for all users. +Each user can further customize their View Configuration in the [Annotation UI Settings](./tracing_ui.md#dataset-settings) + + ## Dataset Sharing Read more in the [Sharing guide](./sharing.md#dataset-sharing) @@ -126,7 +136,7 @@ The system architecture of webKnossos allows for versatile deployment options wh This may be useful when dealing with large datasets that should remain in your data center. [Please contact us](mailto:hello@scalableminds.com) if you require any assistance with your setup. -scalable minds also offers a dataset alignment tools called **Voxelytics Align**. +scalable minds also offers a dataset alignment tool called **Voxelytics Align**. [Learn more.](https://scalableminds.com/voxelytics-align) {% embed url="https://www.youtube.com/watch?v=yYauIHZcI_4 "%} diff --git a/docs/tracing_ui.md b/docs/tracing_ui.md index 93fb921b31e..ec077bafecf 100644 --- a/docs/tracing_ui.md +++ b/docs/tracing_ui.md @@ -81,9 +81,13 @@ Not all settings are available in every annotation mode. ### Dataset Settings For multi-layer datasets, each layer can be adjusted separately. +#### Histogram +The Histogram displays sampled color values of the dataset on a logarithmic scale. +The slider below the Histogram can be used to adjust the dynamic range of the displayed values. +In order to increase the contrast of data, reduce the dynamic range. To decrease the contrast, widen the range. +In order to increase the brightness, move the range to the left. To decrease the brightness, move the range to the right. + #### Colors -- `Brightness`: Increase / Decrease the brightness of the data layer. -- `Contrast`: Increase / Decrease the contrast of the data layer. - `Opacity`: Increase / Decrease the opacity of the data layer. - `Color`: Every data layer can be colored to make them easily identifiable. By default, all layers have a white overlay, showing the true, raw black & white data. - `Visibility`: Use the eye icon on the right side of the name of the data layer to enable/disable this layer. If you hold CTRL while toggling the visibility of a layer, that layer will be made exclusively visible. From 29399002d9da63a50f5a4f45fae1616bf1f95c7d Mon Sep 17 00:00:00 2001 From: Youri K Date: Fri, 16 Oct 2020 15:16:09 +0200 Subject: [PATCH 35/39] fix documentation --- docs/datasets.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/datasets.md b/docs/datasets.md index 7f50fdce55a..ac8d3a72f45 100644 --- a/docs/datasets.md +++ b/docs/datasets.md @@ -122,6 +122,7 @@ The `Advanced` view lets you edit the underlying JSON configuration directly. There are two ways to set default View Configuration Settings: - inside the `datasource_properties.json` - in the `Edit View` for Datasets + The View Configuration from the `Edit View` takes precedence over the `datasource_properties.json`. You don't have to set complete View Configurations in neither option, as webKnossos will fill missing attributes with sensible defaults. These View Configurations impact the first appearance of the Dataset for all users. From a3fa8e859acc2cf7108317e373ef48f04f840ac8 Mon Sep 17 00:00:00 2001 From: Youri K Date: Mon, 19 Oct 2020 13:43:46 +0200 Subject: [PATCH 36/39] rename methods to new naming schema --- app/controllers/ConfigurationController.scala | 25 ++++++++++--------- app/models/user/UserService.scala | 2 +- conf/webknossos.latest.routes | 8 +++--- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/app/controllers/ConfigurationController.scala b/app/controllers/ConfigurationController.scala index a6570a55122..589fee235f0 100755 --- a/app/controllers/ConfigurationController.scala +++ b/app/controllers/ConfigurationController.scala @@ -41,7 +41,7 @@ class ConfigurationController @Inject()( } } - def readDataSet(organizationName: String, dataSetName: String) = + def readDataSetViewConfiguration(organizationName: String, dataSetName: String) = sil.UserAwareAction.async(validateJson[List[String]]) { implicit request => request.identity.toFox .flatMap(user => @@ -54,30 +54,31 @@ class ConfigurationController @Inject()( .map(configuration => Ok(toJson(configuration))) } - def updateDataSet(organizationName: String, dataSetName: String) = + def updateDataSetViewConfiguration(organizationName: String, dataSetName: String) = sil.SecuredAction.async(parse.json(maxLength = 20480)) { implicit request => for { jsConfiguration <- request.body.asOpt[JsObject] ?~> "user.configuration.dataset.invalid" conf = jsConfiguration.fields.toMap dataSetConf = conf - "layers" layerConf = conf.get("layers") - _ <- userService.updateDataSetConfiguration(request.identity, - dataSetName, - organizationName, - dataSetConf, - layerConf) + _ <- userService.updateDataSetViewConfiguration(request.identity, + dataSetName, + organizationName, + dataSetConf, + layerConf) } yield { JsonOk(Messages("user.configuration.dataset.updated")) } } - def readDataSetDefault(organizationName: String, dataSetName: String) = sil.SecuredAction.async { implicit request => - dataSetConfigurationService - .getCompleteAdminViewConfiguration(dataSetName, organizationName) - .map(configuration => Ok(toJson(configuration))) + def readDataSetAdminViewConfiguration(organizationName: String, dataSetName: String) = sil.SecuredAction.async { + implicit request => + dataSetConfigurationService + .getCompleteAdminViewConfiguration(dataSetName, organizationName) + .map(configuration => Ok(toJson(configuration))) } - def updateDataSetDefault(organizationName: String, dataSetName: String) = + def updateDataSetAdminViewConfiguration(organizationName: String, dataSetName: String) = sil.SecuredAction.async(parse.json(maxLength = 20480)) { implicit request => for { dataset <- dataSetDAO.findOneByNameAndOrganizationName(dataSetName, organizationName) ?~> "dataset.notFound" ~> NOT_FOUND diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index dfa8665d449..b3ff61c822a 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -142,7 +142,7 @@ class UserService @Inject()(conf: WkConf, result } - def updateDataSetConfiguration( + def updateDataSetViewConfiguration( user: User, dataSetName: String, organizationName: String, diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index e3247067e2c..3a4961cdbf3 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -28,10 +28,10 @@ POST /auth/createOrganizationWithAdmin # Configurations GET /user/userConfiguration controllers.ConfigurationController.read PUT /user/userConfiguration controllers.ConfigurationController.update -POST /dataSetConfigurations/:organizationName/:dataSetName controllers.ConfigurationController.readDataSet(organizationName: String, dataSetName: String) -PUT /dataSetConfigurations/:organizationName/:dataSetName controllers.ConfigurationController.updateDataSet(organizationName: String, dataSetName: String) -GET /dataSetConfigurations/default/:organizationName/:dataSetName controllers.ConfigurationController.readDataSetDefault(organizationName: String, dataSetName: String) -PUT /dataSetConfigurations/default/:organizationName/:dataSetName controllers.ConfigurationController.updateDataSetDefault(organizationName: String, dataSetName: String) +POST /dataSetConfigurations/:organizationName/:dataSetName controllers.ConfigurationController.readDataSetViewConfiguration(organizationName: String, dataSetName: String) +PUT /dataSetConfigurations/:organizationName/:dataSetName controllers.ConfigurationController.updateDataSetViewConfiguration(organizationName: String, dataSetName: String) +GET /dataSetConfigurations/default/:organizationName/:dataSetName controllers.ConfigurationController.readDataSetAdminViewConfiguration(organizationName: String, dataSetName: String) +PUT /dataSetConfigurations/default/:organizationName/:dataSetName controllers.ConfigurationController.updateDataSetAdminViewConfiguration(organizationName: String, dataSetName: String) # Users POST /user/tasks/request controllers.TaskController.request From 7a74654ff03fde921f0c378c54bd971c88e98e71 Mon Sep 17 00:00:00 2001 From: Youri K Date: Tue, 20 Oct 2020 14:20:35 +0200 Subject: [PATCH 37/39] pr feedback --- .../dashboard/dataset/default_config_component.js | 2 +- .../schemas/dataset_view_configuration.spec.js | 14 ++++++++++++++ .../schemas/dataset_view_configuration_defaults.js | 9 +++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/dashboard/dataset/default_config_component.js b/frontend/javascripts/dashboard/dataset/default_config_component.js index 565efa15999..d68364a93e8 100644 --- a/frontend/javascripts/dashboard/dataset/default_config_component.js +++ b/frontend/javascripts/dashboard/dataset/default_config_component.js @@ -36,7 +36,7 @@ export default function DefaultConfigComponent({ form }: { form: Object }) { ]; const comments = { - alpha: "20 for segementation layer", + alpha: "20 for segmentation layer", loadingStrategy: "BEST_QUALITY_FIRST or PROGRESSIVE_QUALITY", }; diff --git a/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js b/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js index 2d20f76a74b..e4238c2c7d5 100644 --- a/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js +++ b/frontend/javascripts/test/schemas/dataset_view_configuration.spec.js @@ -72,3 +72,17 @@ test("validated view configuration should not add missing property, when optiona enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, true); t.is(validatedConfiguration.fourBit === undefined, true); }); + +test("validated should correctly remove nested additional property for known field", t => { + const validatedConfiguration = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + validatedConfiguration.fourBit = { deeply: "nested" }; + enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, false); + t.is(validateObjectWithType(datasetViewConfigurationType, validatedConfiguration).length, 0); +}); + +test("validated should correctly remove nested additional property for unknown field", t => { + const validatedConfiguration = _.cloneDeep(CORRECT_DATASET_CONFIGURATION); + validatedConfiguration.test = { deeply: "nested" }; + enforceValidatedDatasetViewConfiguration(validatedConfiguration, DATASET, false); + t.is(validateObjectWithType(datasetViewConfigurationType, validatedConfiguration).length, 0); +}); diff --git a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js index 1f0bad517b8..26847e9f07f 100644 --- a/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js +++ b/frontend/javascripts/types/schemas/dataset_view_configuration_defaults.js @@ -1,5 +1,6 @@ // @flow +import _ from "lodash"; import { getDefaultLayerViewConfiguration, defaultDatasetViewConfiguration, @@ -15,7 +16,9 @@ const eliminateErrors = (instance: Object, errors: Array<*>, defaults: Object) = } else if (error.name === "additionalProperties") { delete instance[error.argument]; } else { - const invalidFieldName = error.property.split(".")[1]; + const wrongPropertyPath = error.property.split("."); + // assert(); + const invalidFieldName = wrongPropertyPath[1]; if (defaults[invalidFieldName] === null) { delete instance[invalidFieldName]; } else { @@ -57,7 +60,9 @@ export const enforceValidatedDatasetViewConfiguration = ( eliminateErrors(existingLayerConfig, layerErrors, layerConfigDefault); newLayerConfig[layer.name] = existingLayerConfig; } else { - newLayerConfig[layer.name] = isOptional ? {} : layerConfigDefault; + newLayerConfig[layer.name] = isOptional + ? {} + : _.pickBy(layerConfigDefault, value => value !== null); } }); datasetViewConfiguration.layers = newLayerConfig; From 2e9219a61ae570cc12947f19cb5e9e2e0d570a89 Mon Sep 17 00:00:00 2001 From: Youri K Date: Mon, 26 Oct 2020 13:31:07 +0100 Subject: [PATCH 38/39] bump schema version --- ...view-configs.sql => 057-add-layer-specific-view-configs.sql} | 0 ...view-configs.sql => 057-add-layer-specific-view-configs.sql} | 0 tools/postgres/schema.sql | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename conf/evolutions/{056-add-layer-specific-view-configs.sql => 057-add-layer-specific-view-configs.sql} (100%) rename conf/evolutions/reversions/{056-add-layer-specific-view-configs.sql => 057-add-layer-specific-view-configs.sql} (100%) diff --git a/conf/evolutions/056-add-layer-specific-view-configs.sql b/conf/evolutions/057-add-layer-specific-view-configs.sql similarity index 100% rename from conf/evolutions/056-add-layer-specific-view-configs.sql rename to conf/evolutions/057-add-layer-specific-view-configs.sql diff --git a/conf/evolutions/reversions/056-add-layer-specific-view-configs.sql b/conf/evolutions/reversions/057-add-layer-specific-view-configs.sql similarity index 100% rename from conf/evolutions/reversions/056-add-layer-specific-view-configs.sql rename to conf/evolutions/reversions/057-add-layer-specific-view-configs.sql diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index 24112c1e7fc..65782ce1b10 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -21,7 +21,7 @@ START TRANSACTION; CREATE TABLE webknossos.releaseInformation ( schemaVersion BIGINT NOT NULL ); -INSERT INTO webknossos.releaseInformation(schemaVersion) values(56); +INSERT INTO webknossos.releaseInformation(schemaVersion) values(57); COMMIT TRANSACTION; CREATE TABLE webknossos.analytics( From 526a75af7dc8a63561d1623c5f316f97bb40df2f Mon Sep 17 00:00:00 2001 From: Youri K Date: Mon, 26 Oct 2020 14:34:20 +0100 Subject: [PATCH 39/39] fix version number in evolution --- conf/evolutions/057-add-layer-specific-view-configs.sql | 2 +- .../reversions/057-add-layer-specific-view-configs.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conf/evolutions/057-add-layer-specific-view-configs.sql b/conf/evolutions/057-add-layer-specific-view-configs.sql index 792d00ed133..f1fc934bd1b 100644 --- a/conf/evolutions/057-add-layer-specific-view-configs.sql +++ b/conf/evolutions/057-add-layer-specific-view-configs.sql @@ -68,6 +68,6 @@ WHERE adminViewConfiguration ? 'quality'; ALTER TABLE webknossos.user_dataSetConfigurations RENAME COLUMN configuration TO viewConfiguration; ALTER TABLE webknossos.user_dataSetConfigurations RENAME CONSTRAINT configurationIsJsonObject TO viewConfigurationIsJsonObject; -UPDATE webknossos.releaseInformation SET schemaVersion = 56; +UPDATE webknossos.releaseInformation SET schemaVersion = 57; COMMIT TRANSACTION; diff --git a/conf/evolutions/reversions/057-add-layer-specific-view-configs.sql b/conf/evolutions/reversions/057-add-layer-specific-view-configs.sql index 1fef908ec3f..205d04d0078 100644 --- a/conf/evolutions/reversions/057-add-layer-specific-view-configs.sql +++ b/conf/evolutions/reversions/057-add-layer-specific-view-configs.sql @@ -47,6 +47,6 @@ SET defaultConfiguration = jsonb_set( defaultConfiguration) WHERE defaultConfiguration is not null; -UPDATE webknossos.releaseInformation SET schemaVersion = 55; +UPDATE webknossos.releaseInformation SET schemaVersion = 56; COMMIT TRANSACTION;