diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index ec59aa876e9..be2434d0374 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -16,6 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Changed - Limit paid team sharing features to respective organization plans. [#6767](https://github.com/scalableminds/webknossos/pull/6776) - Rewrite the database tools in `tools/postgres` to JavaScript and adding support for non-default Postgres username-password combinations. [#6803](https://github.com/scalableminds/webknossos/pull/6803) +- Added owner name to organization page. [#6811](https://github.com/scalableminds/webknossos/pull/6811) ### Fixed - Fixed saving allowed teams in dataset settings. [#6817](https://github.com/scalableminds/webknossos/pull/6817) diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala index 614bec70966..959ec566ffb 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) + ownerBox <- userDAO.findOwnerByOrg(organization._id).futureBox + ownerNameOpt = ownerBox.toOption.map(o => s"${o.firstName} ${o.lastName}") } 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, + "ownerName" -> ownerNameOpt ) ++ adminOnlyInfo } diff --git a/app/models/user/User.scala b/app/models/user/User.scala index 9644d244da6..e66f66126a7 100644 --- a/app/models/user/User.scala +++ b/app/models/user/User.scala @@ -159,6 +159,18 @@ class UserDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext) parsed <- Fox.combined(r.toList.map(parse)) } yield parsed + def findOwnerByOrg(organizationId: ObjectId): Fox[User] = + for { + r <- run(q"""select $columns + from $existingCollectionName + where isOrganizationOwner + and not isDeactivated + and _organization = $organizationId + order by _id + limit 1""".as[UsersRow]) + parsed <- parseFirst(r, organizationId) + } yield parsed + def findOneByOrgaAndMultiUser(organizationId: ObjectId, multiUserId: ObjectId)( implicit ctx: DBAccessContext): Fox[User] = for { diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx index 389cc2e808a..839bf78c072 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 /> + + ) : 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}; return ( - + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization, activeUser)} + {getUpgradeNowButton(activeUser, activeOrganization)} + + } placement="right" + trigger="hover" > - + ); }; @@ -57,21 +89,29 @@ export const PricingEnforcedButton: React.FunctionComponent { + const activeUser = useSelector((state: OxalisState) => state.activeUser); const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan); if (isFeatureAllowed) return ; return ( - + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization, activeUser)} + {getUpgradeNowButton(activeUser, activeOrganization)} + + } + placement="bottom" + trigger="hover" > - + ); }; @@ -80,6 +120,7 @@ export const PricingEnforcedBlur: React.FunctionComponent requiredPricingPlan, ...restProps }) => { + const activeUser = useSelector((state: OxalisState) => state.activeUser); const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan); @@ -100,7 +141,16 @@ export const PricingEnforcedBlur: React.FunctionComponent ); return ( - + + {getFeatureNotAvailableInPlanMessage(requiredPricingPlan, activeOrganization, activeUser)} + {getUpgradeNowButton(activeUser, activeOrganization)} + + } + trigger="hover" + >
> } />
-
+ ); }; -export function PageUnavailableForYourPlanView() { +export function PageUnavailableForYourPlanView({ + requiredPricingPlan, +}: { + requiredPricingPlan: PricingPlanEnum; +}) { const activeUser = useSelector((state: OxalisState) => state.activeUser); const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization); const linkToOrganizationSettings = activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? ( - Go to Organization Settings - ) : null; + + + + ) : 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} - + + {getFeatureNotAvailableInPlanMessage( + requiredPricingPlan, + activeOrganization, + activeUser, + )} +

} - type="error" - showIcon + extra={[ + + + , + linkToOrganizationSettings, + ]} />
); 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..a6e3ef9ce9d 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -436,6 +436,11 @@ 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 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/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", diff --git a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts index 8a24d3918f8..234a75cc5e9 100644 --- a/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts +++ b/frontend/javascripts/oxalis/model/accessors/tool_accessor.ts @@ -9,12 +9,12 @@ 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 messages from "messages"; const zoomInToUseToolMessage = "Please zoom in further to use this tool."; @@ -113,6 +113,7 @@ function _getDisabledInfoFromArgs( hasAgglomerateMappings: boolean, genericDisabledExplanation: string, activeOrganization: APIOrganization | null, + activeUser: APIUser | null | undefined, ) { const isProofReadingToolAllowed = isFeatureAllowedByPricingPlan( activeOrganization, @@ -166,7 +167,11 @@ function _getDisabledInfoFromArgs( ? !hasSkeleton ? disabledSkeletonExplanation : disabledAgglomerateMappingsExplanation - : messages["organization.plan.feature_not_available"](PricingPlanEnum.Power), + : getFeatureNotAvailableInPlanMessage( + PricingPlanEnum.Power, + activeOrganization, + activeUser, + ), }, }; } @@ -236,6 +241,7 @@ export function getDisabledInfoForTools(state: OxalisState): Record< hasAgglomerateMappings, genericDisabledExplanation, state.activeOrganization, + 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 bf05a157225..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 @@ -53,10 +53,10 @@ import Store from "oxalis/store"; import Toast from "libs/toast"; import features from "features"; import { + getFeatureNotAvailableInPlanMessage, 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 @@ -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: messages["organization.plan.feature_not_available"](PricingPlanEnum.Team), + title: getFeatureNotAvailableInPlanMessage( + PricingPlanEnum.Team, + activeOrganization, + activeUser, + ), }; } diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts index c70cf8247b9..f17e6354a0a 100644 --- a/frontend/javascripts/types/api_flow_types.ts +++ b/frontend/javascripts/types/api_flow_types.ts @@ -518,6 +518,7 @@ export type APIOrganization = { readonly includedUsers: number; readonly includedStorageBytes: number; readonly usedStorageBytes: number; + readonly ownerName?: string; }; export type APIPricingPlanStatus = { readonly pricingPlan: PricingPlanEnum;