Skip to content

Commit

Permalink
Merge branch 'master' of github.com:scalableminds/webknossos into dro…
Browse files Browse the repository at this point in the history
…pdown-menu

* 'master' of github.com:scalableminds/webknossos:
  Avoid SQL error when fetching view config for zero-layer dataset (#6912)
  Fix date formatting for VX reports (#6908)
  Fix rare crash when viewing shared annotation (#6892)
  Slim down view mode dropdown by using icons (#6900)
  Logging on password reset/change (#6901)
  When merging volume tracings, also merge segment lists (#6882)
  avoid spinner when switching tabs in dashboard (#6894)
  • Loading branch information
hotzenklotz committed Mar 13, 2023
2 parents d95cc01 + 0f6233f commit 17bcd86
Show file tree
Hide file tree
Showing 28 changed files with 255 additions and 99 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released

### Changed
- Upgraded antd UI library to v4.24.8 [#6865](https://github.com/scalableminds/webknossos/pull/6865)
- The view mode dropdown was slimmed down by using icons to make the toolbar more space efficient. [#6900](https://github.com/scalableminds/webknossos/pull/6900)

### Fixed
- Fixed a bug where N5 datasets reading with end-chunks that have a chunk size differing from the metadata-supplied chunk size would fail for some areas. [#6890](https://github.com/scalableminds/webknossos/pull/6890)
- Fixed potential crash when trying to edit certain annotation properties of a shared annotation. [#6892](https://github.com/scalableminds/webknossos/pull/6892)
- Fixed a bug where merging multiple volume annotations would result in inconsistent segment lists. [#6882](https://github.com/scalableminds/webknossos/pull/6882)
- Fixed a bug where uploading multiple annotations with volume layers at once would fail. [#6882](https://github.com/scalableminds/webknossos/pull/6882)
- Fixed a bug where dates were formatted incorrectly in Voxelytics reports. [#6908](https://github.com/scalableminds/webknossos/pull/6908)
- Fix antd deprecation warning for Dropdown menus. [#6898](https://github.com/scalableminds/webknossos/pull/6898)

### Removed
Expand Down
6 changes: 4 additions & 2 deletions app/controllers/AnnotationIOController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.scalableminds.webknossos.datastore.models.datasource.{
SegmentationLayer
}
import com.scalableminds.webknossos.tracingstore.tracings.TracingType
import com.scalableminds.webknossos.tracingstore.tracings.volume.VolumeTracingDefaults
import com.scalableminds.webknossos.tracingstore.tracings.volume.{VolumeTracingDefaults, VolumeTracingDownsampling}
import com.typesafe.scalalogging.LazyLogging
import io.swagger.annotations._

Expand Down Expand Up @@ -280,7 +280,9 @@ Expects:
fallbackLayer = fallbackLayerOpt.map(_.name),
largestSegmentId =
annotationService.combineLargestSegmentIdsByPrecedence(volumeTracing.largestSegmentId,
fallbackLayerOpt.map(_.largestSegmentId))
fallbackLayerOpt.map(_.largestSegmentId)),
resolutions =
VolumeTracingDownsampling.resolutionsForVolumeTracing(dataSource, fallbackLayerOpt).map(vec3IntToProto)
)
}

Expand Down
2 changes: 2 additions & 0 deletions app/controllers/AuthenticationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ class AuthenticationController @Inject()(
bearerTokenAuthenticatorService.userForToken(passwords.token.trim).futureBox.flatMap {
case Full(user) =>
for {
- <- Fox.successful(logger.info(s"Multiuser ${user._multiUser} reset their password."))
_ <- multiUserDAO.updatePasswordInfo(user._multiUser, passwordHasher.hash(passwords.password1))(
GlobalAccessContext)
_ <- bearerTokenAuthenticatorService.remove(passwords.token.trim)
Expand All @@ -420,6 +421,7 @@ class AuthenticationController @Inject()(
Future.successful(NotFound(Messages("error.noUser")))
case Some(user) =>
for {
- <- Fox.successful(logger.info(s"Multiuser ${user._multiUser} changed their password."))
_ <- multiUserDAO.updatePasswordInfo(user._multiUser, passwordHasher.hash(passwords.password1))
_ <- combinedAuthenticatorService.discard(request.authenticator, Ok)
userEmail <- userService.emailFor(user)
Expand Down
7 changes: 4 additions & 3 deletions app/controllers/JobsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,10 @@ class JobsController @Inject()(jobDAO: JobDAO,
sil.SecuredAction.async { implicit request =>
log(Some(slackNotificationService.noticeFailedJobRequest)) {
for {
organization <- organizationDAO.findOneByName(organizationName) ?~> Messages("organization.notFound",
organizationName)
_ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.applyMergerMode.notAllowed.organization" ~> FORBIDDEN
organization <- organizationDAO.findOneByName(organizationName)(GlobalAccessContext) ?~> Messages(
"organization.notFound",
organizationName)
_ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.materializeVolumeAnnotation.notAllowed.organization" ~> FORBIDDEN
dataSet <- dataSetDAO.findOneByNameAndOrganization(dataSetName, organization._id) ?~> Messages(
"dataSet.notFound",
dataSetName) ~> NOT_FOUND
Expand Down
12 changes: 8 additions & 4 deletions app/models/user/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -417,14 +417,18 @@ class UserDataSetLayerConfigurationDAO @Inject()(sqlClient: SqlClient, userDAO:
def findAllByLayerNameForUserAndDataset(layerNames: List[String],
userId: ObjectId,
dataSetId: ObjectId): Fox[Map[String, LayerViewConfiguration]] =
for {
rows <- run(q"""select layerName, viewConfiguration
if (layerNames.isEmpty) {
Fox.successful(Map.empty[String, LayerViewConfiguration])
} else {
for {
rows <- run(q"""select layerName, viewConfiguration
from webknossos.user_dataSetLayerConfigurations
where _dataset = $dataSetId
and _user = $userId
and layerName in ${SqlToken.tupleFromList(layerNames)}""".as[(String, String)])
parsed = rows.flatMap(t => Json.parse(t._2).asOpt[LayerViewConfiguration].map((t._1, _)))
} yield parsed.toMap
parsed = rows.flatMap(t => Json.parse(t._2).asOpt[LayerViewConfiguration].map((t._1, _)))
} yield parsed.toMap
}

def updateDatasetConfigurationForUserAndDatasetAndLayer(
userId: ObjectId,
Expand Down
2 changes: 1 addition & 1 deletion conf/messages
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ job.inferNuclei.notAllowed.organization = Currently nuclei inferral is only allo
job.inferNeurons.notAllowed.organization = Currently neuron inferral is only allowed for datasets of your own organization.
job.meshFile.notAllowed.organization = Calculating mesh files is only allowed for datasets of your own organization.
job.globalizeFloodfill.notAllowed.organization = Globalizing floodfills is only allowed for datasets of your own organization.
job.applyMergerMode.notAllowed.organization = Applying merger mode tracings is only allowed for datasets of your own organization.
job.materializeVolumeAnnotation.notAllowed.organization = Materializing volume annotations is only allowed for datasets of your own organization.
job.noWorkerForDatastore = Processing jobs are not available for the datastore this dataset belongs to.

voxelytics.disabled = Voxelytics workflow reporting and logging are not enabled for this WEBKNOSSOS instance.
Expand Down
2 changes: 2 additions & 0 deletions frontend/javascripts/admin/admin_rest_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,8 @@ export async function getPricingPlanStatus(): Promise<APIPricingPlanStatus> {
return Request.receiveJSON("/api/pricing/status");
}

export const cachedGetPricingPlanStatus = _.memoize(getPricingPlanStatus);

// ### BuildInfo webknossos
export function getBuildInfo(): Promise<APIBuildInfo> {
return Request.receiveJSON("/api/buildinfo", {
Expand Down
7 changes: 5 additions & 2 deletions frontend/javascripts/dashboard/dashboard_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { APIOrganization, APIPricingPlanStatus, APIUser } from "types/api_f
import type { OxalisState } from "oxalis/store";
import { enforceActiveUser } from "oxalis/model/accessors/user_accessor";
import {
getPricingPlanStatus,
cachedGetPricingPlanStatus,
getUser,
updateNovelUserExperienceInfos,
} from "admin/admin_rest_api";
Expand Down Expand Up @@ -120,7 +120,10 @@ class DashboardView extends PureComponent<PropsWithRouter, State> {
const user =
this.props.userId != null ? await getUser(this.props.userId) : this.props.activeUser;

const pricingPlanStatus = await getPricingPlanStatus();
// Use a cached version of this route to avoid that a tab switch in the dashboard
// causes a whole-page spinner. Since the different tabs are controlled by the
// router, the DashboardView re-mounts.
const pricingPlanStatus = await cachedGetPricingPlanStatus();

this.setState({
user,
Expand Down
2 changes: 2 additions & 0 deletions frontend/javascripts/libs/format_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import updateLocale from "dayjs/plugin/updateLocale";
import relativeTime from "dayjs/plugin/relativeTime";
import localizedFormat from "dayjs/plugin/localizedFormat";
import calendar from "dayjs/plugin/calendar";
import utc from "dayjs/plugin/utc";
import weekday from "dayjs/plugin/weekday";
Expand All @@ -22,6 +23,7 @@ dayjs.extend(utc);
dayjs.extend(calendar);
dayjs.extend(weekday);
dayjs.extend(localeData);
dayjs.extend(localizedFormat);
dayjs.updateLocale("en", {
weekStart: 1,
calendar: {
Expand Down
13 changes: 13 additions & 0 deletions frontend/javascripts/oxalis/model/accessors/annotation_accessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { OxalisState } from "oxalis/store";

export function mayEditAnnotationProperties(state: OxalisState) {
const { owner, restrictions } = state.tracing;
const activeUser = state.activeUser;

return !!(
restrictions.allowUpdate &&
restrictions.allowSave &&
activeUser &&
owner?.id === activeUser.id
);
}
43 changes: 31 additions & 12 deletions frontend/javascripts/oxalis/model/sagas/annotation_saga.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
import type { Saga } from "oxalis/model/sagas/effect-generators";
import {
takeLatest,
select,
take,
retry,
delay,
Expand All @@ -31,6 +30,7 @@ import {
cancel,
cancelled,
} from "typed-redux-saga";
import { select } from "oxalis/model/sagas/effect-generators";
import { getMappingInfo } from "oxalis/model/accessors/dataset_accessor";
import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor";
import { Model } from "oxalis/singletons";
Expand All @@ -40,14 +40,17 @@ import constants, { MappingStatusEnum } from "oxalis/constants";
import messages from "messages";
import { APIUserCompact } from "types/api_flow_types";
import { Button } from "antd";
import ErrorHandling from "libs/error_handling";
import { mayEditAnnotationProperties } from "../accessors/annotation_accessor";

/* Note that this must stay in sync with the back-end constant
compare https://github.com/scalableminds/webknossos/issues/5223 */
const MAX_MAG_FOR_AGGLOMERATE_MAPPING = 16;
export function* pushAnnotationUpdateAsync() {
const tracing = yield* select((state) => state.tracing);

if (!tracing.restrictions.allowUpdate) {
export function* pushAnnotationUpdateAsync(action: Action) {
const tracing = yield* select((state) => state.tracing);
const mayEdit = yield* select((state) => mayEditAnnotationProperties(state));
if (!mayEdit) {
return;
}

Expand All @@ -66,14 +69,30 @@ export function* pushAnnotationUpdateAsync() {
description: tracing.description,
viewConfiguration,
};
yield* retry(
SETTINGS_MAX_RETRY_COUNT,
SETTINGS_RETRY_DELAY,
editAnnotation,
tracing.annotationId,
tracing.annotationType,
editObject,
);
try {
yield* retry(
SETTINGS_MAX_RETRY_COUNT,
SETTINGS_RETRY_DELAY,
editAnnotation,
tracing.annotationId,
tracing.annotationType,
editObject,
);
} catch (error) {
// If the annotation cannot be saved repeatedly (retries will continue for 5 minutes),
// we will only notify the user if the name, visibility or description could not be changed.
// Otherwise, we won't notify the user and won't let the sagas crash as the actual skeleton/volume
// tracings are handled separately.
console.error(error);
ErrorHandling.notify(error as Error);
if (
["SET_ANNOTATION_NAME", "SET_ANNOTATION_VISIBILITY", "SET_ANNOTATION_DESCRIPTION"].includes(
action.type,
)
) {
Toast.error("Could not update annotation property. Please try again.");
}
}
}

function* pushAnnotationLayerUpdateAsync(action: EditAnnotationLayerAction): Saga<void> {
Expand Down
9 changes: 6 additions & 3 deletions frontend/javascripts/oxalis/model/sagas/effect-generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { select as _select, take as _take } from "typed-redux-saga";
import type { Channel } from "redux-saga";
import { ActionPattern } from "redux-saga/effects";

export function* select<T>(fn: (state: OxalisState) => T) {
const res: T = yield _select(fn);
return res;
// Ensures that the type of state is known. Otherwise,
// a statement such as
// const tracing = yield* select((state) => state.tracing);
// would result in tracing being any.
export function select<T>(fn: (state: OxalisState) => T) {
return _select(fn);
}

export function* take(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Props = {
};
const positionIconStyle: React.CSSProperties = {
transform: "rotate(-45deg)",
marginRight: 0,
};
const warningColors: React.CSSProperties = {
color: "rgb(255, 155, 85)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { makeComponentLazy } from "libs/react_helpers";
import { AsyncButton } from "components/async_clickables";
import { PricingEnforcedBlur } from "components/pricing_enforcers";
import { PricingPlanEnum } from "admin/organization/pricing_plan_utils";
import { mayEditAnnotationProperties } from "oxalis/model/accessors/annotation_accessor";

const RadioGroup = Radio.Group;
const sharingActiveNode = true;
Expand Down Expand Up @@ -181,11 +182,10 @@ function _ShareModalView(props: Props) {
const [sharedTeams, setSharedTeams] = useState<APITeam[]>([]);
const sharingToken = useDatasetSharingToken(dataset);

const { owner, othersMayEdit, restrictions } = tracing;
const { othersMayEdit } = tracing;
const [newOthersMayEdit, setNewOthersMayEdit] = useState(othersMayEdit);

const hasUpdatePermissions =
restrictions.allowUpdate && restrictions.allowSave && activeUser && owner?.id === activeUser.id;
const hasUpdatePermissions = useSelector(mayEditAnnotationProperties);
useEffect(() => setVisibility(annotationVisibility), [annotationVisibility]);

const fetchAndSetSharedTeams = async () => {
Expand Down
67 changes: 47 additions & 20 deletions frontend/javascripts/oxalis/view/action-bar/view_modes_view.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Select } from "antd";
import { Button, Dropdown, MenuProps, Space } from "antd";
import { connect } from "react-redux";
import type { Dispatch } from "redux";
import React, { PureComponent } from "react";
Expand All @@ -9,9 +9,9 @@ import {
import type { OxalisState, AllowedMode } from "oxalis/store";
import Store from "oxalis/store";
import * as Utils from "libs/utils";
import type { ViewMode } from "oxalis/constants";
import { ViewMode, ViewModeValues } from "oxalis/constants";
import constants from "oxalis/constants";
const { Option } = Select;

type StateProps = {
viewMode: ViewMode;
allowedModes: Array<AllowedMode>;
Expand All @@ -21,6 +21,17 @@ type DispatchProps = {
};
type Props = StateProps & DispatchProps;

const VIEW_MODE_TO_ICON = {
[constants.MODE_PLANE_TRACING]: <i className="fas fa-th-large without-icon-margin" />,
[constants.MODE_ARBITRARY]: <i className="fas fa-globe without-icon-margin" />,
[constants.MODE_ARBITRARY_PLANE]: (
<i
className="fas fa-square-full without-icon-margin"
style={{ transform: "scale(0.8, 1) rotate(-45deg)" }}
/>
),
};

class ViewModesView extends PureComponent<Props, {}> {
handleChange = (mode: ViewMode) => {
// If we switch back from any arbitrary mode we stop recording.
Expand Down Expand Up @@ -51,24 +62,40 @@ class ViewModesView extends PureComponent<Props, {}> {
}

render() {
const handleMenuClick: MenuProps["onClick"] = (args) => {
if (ViewModeValues.includes(args.key as ViewMode)) {
this.handleChange(args.key as ViewMode);
}
};

const MENU_ITEMS: MenuProps["items"] = [
{
key: "1",
type: "group",
label: "Select View Mode",
children: ViewModeValues.map((mode) => ({
label: Utils.capitalize(mode),
key: mode,
disabled: this.isDisabled(mode),
icon: <span style={{ marginRight: 8 }}>{VIEW_MODE_TO_ICON[mode]}</span>,
})),
},
];

const menuProps = {
items: MENU_ITEMS,
onClick: handleMenuClick,
};

return (
<Select
value={this.props.viewMode}
style={{
width: 120,
}}
onChange={this.handleChange}
>
{[
constants.MODE_PLANE_TRACING,
constants.MODE_ARBITRARY,
constants.MODE_ARBITRARY_PLANE,
].map((mode) => (
<Option key={mode} disabled={this.isDisabled(mode)} value={mode}>
{Utils.capitalize(mode)}
</Option>
))}
</Select>
// The outer div is necessary for proper spacing.
<div>
<Dropdown menu={menuProps}>
<Button>
<Space>{VIEW_MODE_TO_ICON[this.props.viewMode]}</Space>
</Button>
</Dropdown>
</div>
);
}
}
Expand Down
Loading

0 comments on commit 17bcd86

Please sign in to comment.