From a22be83e71c1b5d32f3e3d8a4e755a19d60e51e7 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 3 Feb 2023 14:15:35 +0100 Subject: [PATCH 01/14] added orga owner to organization REST route --- app/models/organization/OrganizationService.scala | 9 +++++++-- app/models/user/User.scala | 13 +++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala index 614bec70966..754df7bfae0 100644 --- a/app/models/organization/OrganizationService.scala +++ b/app/models/organization/OrganizationService.scala @@ -9,7 +9,7 @@ import javax.inject.Inject import models.binary.{DataStore, DataStoreDAO} import models.folder.{Folder, FolderDAO, FolderService} import models.team.{PricingPlan, Team, TeamDAO} -import models.user.{Invite, MultiUserDAO, User, UserDAO} +import models.user.{Invite, MultiUserDAO, User, UserDAO, UserService} import play.api.libs.json.{JsObject, Json} import utils.{ObjectId, WkConf} @@ -22,6 +22,7 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, dataStoreDAO: DataStoreDAO, folderDAO: FolderDAO, folderService: FolderService, + userService: UserService, rpc: RPC, initialDataService: InitialDataService, conf: WkConf, @@ -29,6 +30,7 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, extends FoxImplicits { def publicWrites(organization: Organization, requestingUser: Option[User] = None): Fox[JsObject] = { + val adminOnlyInfo = if (requestingUser.exists(_.isAdminOf(organization._id))) { Json.obj( "newUserMailingList" -> organization.newUserMailingList, @@ -38,6 +40,8 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, } else Json.obj() for { usedStorageBytes <- organizationDAO.getUsedStorage(organization._id) + owner <- userDAO.findOwnerByOrg(organization._id)(GlobalAccessContext) + ownerJson <- userService.compactWrites(owner) } yield Json.obj( "id" -> organization._id.toString, @@ -49,7 +53,8 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, "paidUntil" -> organization.paidUntil, "includedUsers" -> organization.includedUsers, "includedStorageBytes" -> organization.includedStorageBytes, - "usedStorageBytes" -> usedStorageBytes + "usedStorageBytes" -> usedStorageBytes, + "owner" -> ownerJson ) ++ adminOnlyInfo } diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 57bb39a1b48..131bdb8cb25 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -159,6 +159,19 @@ class UserDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext) parsed <- Fox.combined(r.toList.map(parse)) } yield parsed + def findOwnerByOrg(organizationId: ObjectId)(implicit ctx: DBAccessContext): Fox[User] = + for { + accessQuery <- accessQueryFromAccessQ(listAccessQ) + r <- run(q"""select $columns + from $existingCollectionName + where $accessQuery + and isOrganizationOwner + and not isDeactivated + and _organization = $organizationId + order by _id""".as[UsersRow]) + parsed <- parseFirst(r, organizationId) + } yield parsed + def findOneByOrgaAndMultiUser(organizationId: ObjectId, multiUserId: ObjectId)( implicit ctx: DBAccessContext): Fox[User] = for { From bfbde5646c7345ffea75e9a925955c708eacdc02 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 3 Feb 2023 14:32:50 +0100 Subject: [PATCH 02/14] add organization owner info to orga page --- .../admin/organization/organization_edit_view.tsx | 10 ++++++++++ frontend/javascripts/types/api_flow_types.ts | 1 + 2 files changed, 11 insertions(+) diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx index 389cc2e808a..609de268056 100644 --- a/frontend/javascripts/admin/organization/organization_edit_view.tsx +++ b/frontend/javascripts/admin/organization/organization_edit_view.tsx @@ -7,6 +7,7 @@ import { CopyOutlined, SaveOutlined, IdcardOutlined, + UserOutlined, } from "@ant-design/icons"; import { confirmAsync } from "dashboard/dataset/helper_components"; import { @@ -169,6 +170,7 @@ class OrganizationEditView extends React.PureComponent { width: "calc(100% - 31px)", }} readOnly + disabled /> + + ) : undefined; return (
- -

- The requested feature is not available in your WEBKNOSSOS organization. Consider - upgrading to a higher WEBKNOSSOS plan to unlock it or ask your organization's owner to - upgrade. -

- {linkToOrganizationSettings} - + + {getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} +

} - type="error" - showIcon + extra={[ + + + , + linkToOrganizationSettings, + ]} />
); } + +// style={{ +// maxWidth: "500px", +// margin: "0 auto", +// }} diff --git a/frontend/javascripts/components/secured_route.tsx b/frontend/javascripts/components/secured_route.tsx index 5dd58270925..028d86b78de 100644 --- a/frontend/javascripts/components/secured_route.tsx +++ b/frontend/javascripts/components/secured_route.tsx @@ -77,7 +77,11 @@ class SecuredRoute extends React.PureComponent { this.props.requiredPricingPlan, ) ) { - return ; + return ( + + ); } if (Component != null) { diff --git a/frontend/javascripts/messages.tsx b/frontend/javascripts/messages.tsx index 1cc029b19ed..b3d72a8904d 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -436,6 +436,9 @@ instead. Only enable this option if you understand its effect. All layers will n "ui.no_form_active": "Could not set the initial form values as the form could not be loaded.", "organization.plan.upgrage_request_sent": "An email with your upgrade request has been sent to the WEBKNOSSOS sales team.", - "organization.plan.feature_not_available": (requiredPlan: string) => - `This feature is not available in your organization's plan. Ask your organization owner to upgrade at least a ${requiredPlan} plan.`, + "organization.plan.feature_not_available": ( + requiredPlan: string, + organizationOwnerName: string, + ) => + `This feature is not available in your organization's plan. Ask your organization owner ${organizationOwnerName} to upgrade to at least a ${requiredPlan} plan.`, }; diff --git a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts index 8a24d3918f8..05426ae79c0 100644 --- a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts @@ -11,10 +11,10 @@ import { getVisibleSegmentationLayer } from "oxalis/model/accessors/dataset_acce import { isMagRestrictionViolated } from "oxalis/model/accessors/flycam_accessor"; import { APIOrganization } from "types/api_flow_types"; import { + getFeatureNotAvailabeInPlanMessage, isFeatureAllowedByPricingPlan, PricingPlanEnum, } from "admin/organization/pricing_plan_utils"; -import messages from "messages"; const zoomInToUseToolMessage = "Please zoom in further to use this tool."; @@ -166,7 +166,7 @@ function _getDisabledInfoFromArgs( ? !hasSkeleton ? disabledSkeletonExplanation : disabledAgglomerateMappingsExplanation - : messages["organization.plan.feature_not_available"](PricingPlanEnum.Power), + : getFeatureNotAvailabeInPlanMessage(PricingPlanEnum.Power, activeOrganization), }, }; } diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index bf05a157225..ce838436da2 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -53,10 +53,10 @@ import Store from "oxalis/store"; import Toast from "libs/toast"; import features from "features"; import { + getFeatureNotAvailabeInPlanMessage, isFeatureAllowedByPricingPlan, PricingPlanEnum, } from "admin/organization/pricing_plan_utils"; -import messages from "messages"; const { Option } = Select; // Interval in ms to check for running mesh file computation jobs for this dataset @@ -323,7 +323,7 @@ class SegmentsView extends React.Component { if (!isFeatureAllowedByPricingPlan(activeOrganization, PricingPlanEnum.Team)) { return { disabled: true, - title: messages["organization.plan.feature_not_available"](PricingPlanEnum.Team), + title: getFeatureNotAvailabeInPlanMessage(PricingPlanEnum.Team, activeOrganization), }; } From 1574c22b681b99687b68ba92969ba923a8ac7f21 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 3 Feb 2023 16:48:22 +0100 Subject: [PATCH 04/14] make option to upgrade more prominent --- .../components/pricing_enforcers.tsx | 30 ++++++++++++++++--- frontend/javascripts/oxalis/constants.ts | 2 +- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/components/pricing_enforcers.tsx b/frontend/javascripts/components/pricing_enforcers.tsx index 2f823fb9300..567fbfb9dfc 100644 --- a/frontend/javascripts/components/pricing_enforcers.tsx +++ b/frontend/javascripts/components/pricing_enforcers.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useSelector } from "react-redux"; -import { Tooltip, Menu, MenuItemProps, Alert, ButtonProps, Button, Result } from "antd"; +import { Tooltip, Menu, MenuItemProps, Alert, ButtonProps, Button, Result, Popover } from "antd"; import { LockOutlined } from "@ant-design/icons"; import { getFeatureNotAvailabeInPlanMessage, @@ -11,6 +11,9 @@ import { isUserAllowedToRequestUpgrades } from "admin/organization/pricing_plan_ import { Link } from "react-router-dom"; import type { MenuClickEventHandler } from "rc-menu/lib/interface"; import type { OxalisState } from "oxalis/store"; +import { rgbToHex } from "libs/utils"; +import { PRIMARY_COLOR } from "oxalis/constants"; +import UpgradePricingPlanModal from "admin/organization/upgrade_plan_modal"; const handleMouseClick = (event: React.MouseEvent) => { event.preventDefault(); @@ -27,15 +30,34 @@ type RequiredPricingProps = { requiredPricingPlan: PricingPlanEnum }; export const PricingEnforcedMenuItem: React.FunctionComponent< RequiredPricingProps & MenuItemProps > = ({ children, requiredPricingPlan, ...menuItemProps }) => { + const activeUser = useSelector((state: OxalisState) => state.activeUser); const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan); if (isFeatureAllowed) return {children}; + const upgradeNowButton = + activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? ( + + ) : null; + return ( - + {getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} + {upgradeNowButton} + + } placement="right" + trigger="hover" > - + ); }; diff --git a/frontend/javascripts/oxalis/constants.ts b/frontend/javascripts/oxalis/constants.ts index ccc099c271e..7086833911b 100644 --- a/frontend/javascripts/oxalis/constants.ts +++ b/frontend/javascripts/oxalis/constants.ts @@ -335,7 +335,7 @@ export type TypedArray = export type TypedArrayWithoutBigInt = Exclude; -export const PRIMARY_COLOR = [86, 96, 255]; +export const PRIMARY_COLOR: Vector3 = [86, 96, 255]; export enum LOG_LEVELS { NOTSET = "NOTSET", From 87474c4b8170fe961e033aa4c88a00a1a5d70dab Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 6 Feb 2023 17:00:41 +0100 Subject: [PATCH 05/14] replace tooltips with popovers --- .../admin/organization/upgrade_plan_modal.tsx | 1 + .../components/pricing_enforcers.tsx | 67 +++++++++++++------ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx b/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx index 73d84b121c7..fc1c25a77ca 100644 --- a/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx +++ b/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx @@ -299,6 +299,7 @@ function UpgradePricingPlanModal({ ) : null} } + zIndex={10000} // overlay everything >
{ event.preventDefault(); event.stopPropagation(); @@ -27,33 +31,36 @@ const handleMenuClick: MenuClickEventHandler = (info) => { type RequiredPricingProps = { requiredPricingPlan: PricingPlanEnum }; +function getUpgradeNowButton() { + const activeUser = useSelector((state: OxalisState) => state.activeUser); + const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); + + return activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? ( + + ) : null; +} + export const PricingEnforcedMenuItem: React.FunctionComponent< RequiredPricingProps & MenuItemProps > = ({ children, requiredPricingPlan, ...menuItemProps }) => { - const activeUser = useSelector((state: OxalisState) => state.activeUser); const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan); if (isFeatureAllowed) return {children}; - const upgradeNowButton = - activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? ( - - ) : null; - return ( +
{getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} - {upgradeNowButton} + {getUpgradeNowButton()}
} placement="right" @@ -85,15 +92,22 @@ export const PricingEnforcedButton: React.FunctionComponent{children}; return ( - + {getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} + {getUpgradeNowButton()} +
+ } + placement="bottom" + trigger="hover" > - + ); }; @@ -122,7 +136,16 @@ export const PricingEnforcedBlur: React.FunctionComponent ); return ( - + + {getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} + {getUpgradeNowButton()} + + } + trigger="hover" + >
/>
-
+ ); }; From 7de089ce5ae1ed09d569a22c62af6914bfab2f86 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 6 Feb 2023 17:28:36 +0100 Subject: [PATCH 06/14] convert owner name information to string --- .../organization/OrganizationService.scala | 4 ++-- .../organization/organization_edit_view.tsx | 2 +- .../admin/organization/pricing_plan_utils.ts | 11 ++++++++--- .../components/pricing_enforcers.tsx | 18 +++++++++++------- frontend/javascripts/types/api_flow_types.ts | 2 +- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala index 754df7bfae0..6ba4849ed11 100644 --- a/app/models/organization/OrganizationService.scala +++ b/app/models/organization/OrganizationService.scala @@ -41,7 +41,7 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, for { usedStorageBytes <- organizationDAO.getUsedStorage(organization._id) owner <- userDAO.findOwnerByOrg(organization._id)(GlobalAccessContext) - ownerJson <- userService.compactWrites(owner) + ownerName = s"${owner.firstName} ${owner.lastName}" } yield Json.obj( "id" -> organization._id.toString, @@ -54,7 +54,7 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, "includedUsers" -> organization.includedUsers, "includedStorageBytes" -> organization.includedStorageBytes, "usedStorageBytes" -> usedStorageBytes, - "owner" -> ownerJson + "ownerName" -> ownerName ) ++ adminOnlyInfo } diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx index 609de268056..839bf78c072 100644 --- a/frontend/javascripts/admin/organization/organization_edit_view.tsx +++ b/frontend/javascripts/admin/organization/organization_edit_view.tsx @@ -181,7 +181,7 @@ class OrganizationEditView extends React.PureComponent { } - value={`${this.props.organization.owner.firstName} ${this.props.organization.owner.lastName}`} + value={this.props.organization.ownerName} readOnly disabled /> diff --git a/frontend/javascripts/admin/organization/pricing_plan_utils.ts b/frontend/javascripts/admin/organization/pricing_plan_utils.ts index b6c74eeb118..e44a273df5b 100644 --- a/frontend/javascripts/admin/organization/pricing_plan_utils.ts +++ b/frontend/javascripts/admin/organization/pricing_plan_utils.ts @@ -89,10 +89,15 @@ export function getFeatureNotAvailabeInPlanMessage( requiredPricingPlan: PricingPlanEnum, organization: APIOrganization | null, ) { + let organizationOwnerName = ""; + // expected naming schema for owner: "(M. Mustermann)" | "" - const organizationOwnerName = organization - ? `(${organization.owner.firstName[0]}. ${organization.owner.lastName})` - : ""; + if (organization) { + { + const [firstName, ...rest] = organization.ownerName.split(" "); + organizationOwnerName = `(${firstName[0]}. ${rest.join(" ")})`; + } + } return messages["organization.plan.feature_not_available"]( requiredPricingPlan, diff --git a/frontend/javascripts/components/pricing_enforcers.tsx b/frontend/javascripts/components/pricing_enforcers.tsx index 91ec66da190..2772c241c16 100644 --- a/frontend/javascripts/components/pricing_enforcers.tsx +++ b/frontend/javascripts/components/pricing_enforcers.tsx @@ -14,6 +14,7 @@ import type { OxalisState } from "oxalis/store"; import { rgbToHex } from "libs/utils"; import { PRIMARY_COLOR } from "oxalis/constants"; import UpgradePricingPlanModal from "admin/organization/upgrade_plan_modal"; +import { APIOrganization, APIUser } from "types/api_flow_types"; const PRIMARY_COLOR_HEX = rgbToHex(PRIMARY_COLOR); @@ -31,10 +32,10 @@ const handleMenuClick: MenuClickEventHandler = (info) => { type RequiredPricingProps = { requiredPricingPlan: PricingPlanEnum }; -function getUpgradeNowButton() { - const activeUser = useSelector((state: OxalisState) => state.activeUser); - const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); - +function getUpgradeNowButton( + activeUser: APIUser | null | undefined, + activeOrganization: APIOrganization | undefined, +) { return activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? ( +
+ +
) : null; } @@ -61,7 +62,7 @@ export const PricingEnforcedMenuItem: React.FunctionComponent< color={PRIMARY_COLOR_HEX} content={
- {getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization)} {getUpgradeNowButton(activeUser, activeOrganization)}
} @@ -99,7 +100,7 @@ export const PricingEnforcedButton: React.FunctionComponent - {getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization)} {getUpgradeNowButton(activeUser, activeOrganization)} } @@ -144,7 +145,7 @@ export const PricingEnforcedBlur: React.FunctionComponent color={PRIMARY_COLOR_HEX} content={
- {getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization)} {getUpgradeNowButton(activeUser, activeOrganization)}
} @@ -171,7 +172,7 @@ export const PricingEnforcedBlur: React.FunctionComponent > } /> @@ -202,7 +203,7 @@ export function PageUnavailableForYourPlanView({ title="Feature not available" subTitle={

- {getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)} + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization)}

} extra={[ @@ -214,4 +215,4 @@ export function PageUnavailableForYourPlanView({ /> ); -} \ No newline at end of file +} diff --git a/frontend/javascripts/messages.tsx b/frontend/javascripts/messages.tsx index b3d72a8904d..e180c6be5ac 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -440,5 +440,5 @@ instead. Only enable this option if you understand its effect. All layers will n requiredPlan: string, organizationOwnerName: string, ) => - `This feature is not available in your organization's plan. Ask your organization owner ${organizationOwnerName} to upgrade to at least a ${requiredPlan} plan.`, + `This feature is not available in your organization's plan. Ask the owner of your organization ${organizationOwnerName} to upgrade to at least a ${requiredPlan} plan.`, }; diff --git a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts index 05426ae79c0..de406a44cad 100644 --- a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts @@ -11,7 +11,7 @@ import { getVisibleSegmentationLayer } from "oxalis/model/accessors/dataset_acce import { isMagRestrictionViolated } from "oxalis/model/accessors/flycam_accessor"; import { APIOrganization } from "types/api_flow_types"; import { - getFeatureNotAvailabeInPlanMessage, + getFeatureNotAvailableInPlanMessage, isFeatureAllowedByPricingPlan, PricingPlanEnum, } from "admin/organization/pricing_plan_utils"; @@ -166,7 +166,7 @@ function _getDisabledInfoFromArgs( ? !hasSkeleton ? disabledSkeletonExplanation : disabledAgglomerateMappingsExplanation - : getFeatureNotAvailabeInPlanMessage(PricingPlanEnum.Power, activeOrganization), + : getFeatureNotAvailableInPlanMessage(PricingPlanEnum.Power, activeOrganization), }, }; } diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index ce838436da2..6e1d090a7ec 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -53,7 +53,7 @@ import Store from "oxalis/store"; import Toast from "libs/toast"; import features from "features"; import { - getFeatureNotAvailabeInPlanMessage, + getFeatureNotAvailableInPlanMessage, isFeatureAllowedByPricingPlan, PricingPlanEnum, } from "admin/organization/pricing_plan_utils"; @@ -323,7 +323,7 @@ class SegmentsView extends React.Component { if (!isFeatureAllowedByPricingPlan(activeOrganization, PricingPlanEnum.Team)) { return { disabled: true, - title: getFeatureNotAvailabeInPlanMessage(PricingPlanEnum.Team, activeOrganization), + title: getFeatureNotAvailableInPlanMessage(PricingPlanEnum.Team, activeOrganization), }; } diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts index d09df624e65..f17e6354a0a 100644 --- a/frontend/javascripts/types/api_flow_types.ts +++ b/frontend/javascripts/types/api_flow_types.ts @@ -518,7 +518,7 @@ export type APIOrganization = { readonly includedUsers: number; readonly includedStorageBytes: number; readonly usedStorageBytes: number; - readonly ownerName: string; + readonly ownerName?: string; }; export type APIPricingPlanStatus = { readonly pricingPlan: PricingPlanEnum; From 3a9e63e4a66ef3bf5e10ec626fb145db4eddeb76 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 7 Feb 2023 14:14:49 +0100 Subject: [PATCH 12/14] foo --- frontend/javascripts/components/pricing_enforcers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/components/pricing_enforcers.tsx b/frontend/javascripts/components/pricing_enforcers.tsx index 38d0ce83b0d..aa6de635501 100644 --- a/frontend/javascripts/components/pricing_enforcers.tsx +++ b/frontend/javascripts/components/pricing_enforcers.tsx @@ -34,7 +34,7 @@ type RequiredPricingProps = { requiredPricingPlan: PricingPlanEnum }; function getUpgradeNowButton( activeUser: APIUser | null | undefined, - activeOrganization: APIOrganization | null + activeOrganization: APIOrganization | null, ) { return activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? (
From ac034c84cdee73c63289b6096401571f8c16d6be Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 8 Feb 2023 15:12:54 +0100 Subject: [PATCH 13/14] applied PR feedback --- .../admin/organization/pricing_plan_utils.ts | 6 +++++- .../components/pricing_enforcers.tsx | 18 +++++++++++++----- frontend/javascripts/messages.tsx | 4 +++- .../oxalis/model/accessors/tool_accessor.ts | 11 +++++++++-- .../segments_tab/segments_view.tsx | 7 ++++++- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/frontend/javascripts/admin/organization/pricing_plan_utils.ts b/frontend/javascripts/admin/organization/pricing_plan_utils.ts index 629035d3d2c..700eb9347a7 100644 --- a/frontend/javascripts/admin/organization/pricing_plan_utils.ts +++ b/frontend/javascripts/admin/organization/pricing_plan_utils.ts @@ -88,9 +88,13 @@ export function isFeatureAllowedByPricingPlan( export function getFeatureNotAvailableInPlanMessage( requiredPricingPlan: PricingPlanEnum, organization: APIOrganization | null, + activeUser: APIUser | null | undefined, ) { - let organizationOwnerName = ""; + if (activeUser?.isOrganizationOwner) { + return messages["organization.plan.feature_not_available.owner"](requiredPricingPlan); + } + let organizationOwnerName = ""; // expected naming schema for owner: "(M. Mustermann)" | "" if (organization?.ownerName) { { diff --git a/frontend/javascripts/components/pricing_enforcers.tsx b/frontend/javascripts/components/pricing_enforcers.tsx index aa6de635501..4df09e44d43 100644 --- a/frontend/javascripts/components/pricing_enforcers.tsx +++ b/frontend/javascripts/components/pricing_enforcers.tsx @@ -62,7 +62,7 @@ export const PricingEnforcedMenuItem: React.FunctionComponent< color={PRIMARY_COLOR_HEX} content={
- {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization)} + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization, activeUser)} {getUpgradeNowButton(activeUser, activeOrganization)}
} @@ -100,7 +100,7 @@ export const PricingEnforcedButton: React.FunctionComponent - {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization)} + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization, activeUser)} {getUpgradeNowButton(activeUser, activeOrganization)}
} @@ -145,7 +145,7 @@ export const PricingEnforcedBlur: React.FunctionComponent color={PRIMARY_COLOR_HEX} content={
- {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization)} + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization, activeUser)} {getUpgradeNowButton(activeUser, activeOrganization)}
} @@ -172,7 +172,11 @@ export const PricingEnforcedBlur: React.FunctionComponent > } /> @@ -203,7 +207,11 @@ export function PageUnavailableForYourPlanView({ title="Feature not available" subTitle={

- {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization)} + {getFeatureNotAvailableInPlanMessage( + requiredPricingPlan, + activeOrganization, + activeUser, + )}

} extra={[ diff --git a/frontend/javascripts/messages.tsx b/frontend/javascripts/messages.tsx index e180c6be5ac..a6e3ef9ce9d 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -440,5 +440,7 @@ instead. Only enable this option if you understand its effect. All layers will n requiredPlan: string, organizationOwnerName: string, ) => - `This feature is not available in your organization's plan. Ask the owner of your organization ${organizationOwnerName} to upgrade to at least a ${requiredPlan} plan.`, + `This feature is not available in your organization's plan. Ask the owner of your organization ${organizationOwnerName} to upgrade to a ${requiredPlan} plan or higher.`, + "organization.plan.feature_not_available.owner": (requiredPlan: string) => + `This feature is not available in your organization's plan. Consider upgrading to a ${requiredPlan} plan or higher.`, }; diff --git a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts index de406a44cad..3dd21c4447f 100644 --- a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts @@ -9,12 +9,13 @@ import { } from "oxalis/model/accessors/volumetracing_accessor"; import { getVisibleSegmentationLayer } from "oxalis/model/accessors/dataset_accessor"; import { isMagRestrictionViolated } from "oxalis/model/accessors/flycam_accessor"; -import { APIOrganization } from "types/api_flow_types"; +import { APIOrganization, APIUser } from "types/api_flow_types"; import { getFeatureNotAvailableInPlanMessage, isFeatureAllowedByPricingPlan, PricingPlanEnum, } from "admin/organization/pricing_plan_utils"; +import { enforceActiveUser } from "./user_accessor"; const zoomInToUseToolMessage = "Please zoom in further to use this tool."; @@ -113,6 +114,7 @@ function _getDisabledInfoFromArgs( hasAgglomerateMappings: boolean, genericDisabledExplanation: string, activeOrganization: APIOrganization | null, + activeUser: APIUser, ) { const isProofReadingToolAllowed = isFeatureAllowedByPricingPlan( activeOrganization, @@ -166,7 +168,11 @@ function _getDisabledInfoFromArgs( ? !hasSkeleton ? disabledSkeletonExplanation : disabledAgglomerateMappingsExplanation - : getFeatureNotAvailableInPlanMessage(PricingPlanEnum.Power, activeOrganization), + : getFeatureNotAvailableInPlanMessage( + PricingPlanEnum.Power, + activeOrganization, + activeUser, + ), }, }; } @@ -236,6 +242,7 @@ export function getDisabledInfoForTools(state: OxalisState): Record< hasAgglomerateMappings, genericDisabledExplanation, state.activeOrganization, + enforceActiveUser(state.activeUser), ); } diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index 6e1d090a7ec..4688a27b5cd 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -320,10 +320,15 @@ class SegmentsView extends React.Component { let disabled = true; const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); + const activeUser = useSelector((state: OxalisState) => state.activeUser); if (!isFeatureAllowedByPricingPlan(activeOrganization, PricingPlanEnum.Team)) { return { disabled: true, - title: getFeatureNotAvailableInPlanMessage(PricingPlanEnum.Team, activeOrganization), + title: getFeatureNotAvailableInPlanMessage( + PricingPlanEnum.Team, + activeOrganization, + activeUser, + ), }; } From 360332cfbf6568d06beecbcd453aed58fc6544e6 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 8 Feb 2023 16:23:04 +0100 Subject: [PATCH 14/14] fix tests --- frontend/javascripts/oxalis/model/accessors/tool_accessor.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts index 3dd21c4447f..234a75cc5e9 100644 --- a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts @@ -15,7 +15,6 @@ import { isFeatureAllowedByPricingPlan, PricingPlanEnum, } from "admin/organization/pricing_plan_utils"; -import { enforceActiveUser } from "./user_accessor"; const zoomInToUseToolMessage = "Please zoom in further to use this tool."; @@ -114,7 +113,7 @@ function _getDisabledInfoFromArgs( hasAgglomerateMappings: boolean, genericDisabledExplanation: string, activeOrganization: APIOrganization | null, - activeUser: APIUser, + activeUser: APIUser | null | undefined, ) { const isProofReadingToolAllowed = isFeatureAllowedByPricingPlan( activeOrganization, @@ -242,7 +241,7 @@ export function getDisabledInfoForTools(state: OxalisState): Record< hasAgglomerateMappings, genericDisabledExplanation, state.activeOrganization, - enforceActiveUser(state.activeUser), + state.activeUser, ); }