Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add owner information to organization #6811

Merged
merged 26 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a22be83
added orga owner to organization REST route
hotzenklotz Feb 3, 2023
bfbde56
add organization owner info to orga page
hotzenklotz Feb 3, 2023
cfca6c7
provide restricted plan message with owner information
hotzenklotz Feb 3, 2023
1574c22
make option to upgrade more prominent
hotzenklotz Feb 3, 2023
b04de23
Merge branch 'master' of github.com:scalableminds/webknossos into org…
hotzenklotz Feb 6, 2023
87474c4
replace tooltips with popovers
hotzenklotz Feb 6, 2023
7de089c
convert owner name information to string
hotzenklotz Feb 6, 2023
46df119
Merge branch 'master' of github.com:scalableminds/webknossos into org…
hotzenklotz Feb 6, 2023
3b33867
updated changelog
hotzenklotz Feb 6, 2023
9690642
remove unused comment
hotzenklotz Feb 6, 2023
fd01e98
Merge branch 'master' of github.com:scalableminds/webknossos into org…
hotzenklotz Feb 7, 2023
b189713
Update app/models/user/User.scala
hotzenklotz Feb 7, 2023
ba8780f
Merge branch 'orga_owner' of github.com:scalableminds/webknossos into…
hotzenklotz Feb 7, 2023
37b549c
Update app/models/organization/OrganizationService.scala
hotzenklotz Feb 7, 2023
f951c47
Merge branch 'orga_owner' of github.com:scalableminds/webknossos into…
hotzenklotz Feb 7, 2023
d11efcc
applied PR feedback
hotzenklotz Feb 7, 2023
3a9e63e
foo
hotzenklotz Feb 7, 2023
5ef0d43
Merge branch 'master' into orga_owner
hotzenklotz Feb 7, 2023
f47dd76
Merge branch 'master' of github.com:scalableminds/webknossos into org…
hotzenklotz Feb 7, 2023
788bee8
Merge branch 'orga_owner' of github.com:scalableminds/webknossos into…
hotzenklotz Feb 7, 2023
cc1c9fb
Merge branch 'master' into orga_owner
hotzenklotz Feb 8, 2023
5dd39a2
Merge branch 'master' of github.com:scalableminds/webknossos into org…
hotzenklotz Feb 8, 2023
ac034c8
applied PR feedback
hotzenklotz Feb 8, 2023
1926512
Merge branch 'orga_owner' of github.com:scalableminds/webknossos into…
hotzenklotz Feb 8, 2023
360332c
fix tests
hotzenklotz Feb 8, 2023
fba3c40
Merge branch 'master' into orga_owner
hotzenklotz Feb 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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 a benign error message which briefly appeared after logging in. [#6810](https://github.com/scalableminds/webknossos/pull/6810)
Expand Down
9 changes: 7 additions & 2 deletions app/models/organization/OrganizationService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -22,13 +22,15 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
dataStoreDAO: DataStoreDAO,
folderDAO: FolderDAO,
folderService: FolderService,
userService: UserService,
rpc: RPC,
initialDataService: InitialDataService,
conf: WkConf,
)(implicit ec: ExecutionContext)
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,
Expand All @@ -38,6 +40,8 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
} else Json.obj()
for {
usedStorageBytes <- organizationDAO.getUsedStorage(organization._id)
owner <- userDAO.findOwnerByOrg(organization._id)(GlobalAccessContext)
ownerName = s"${owner.firstName} ${owner.lastName}"
hotzenklotz marked this conversation as resolved.
Show resolved Hide resolved
} yield
Json.obj(
"id" -> organization._id.toString,
Expand All @@ -49,7 +53,8 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
"paidUntil" -> organization.paidUntil,
"includedUsers" -> organization.includedUsers,
"includedStorageBytes" -> organization.includedStorageBytes,
"usedStorageBytes" -> usedStorageBytes
"usedStorageBytes" -> usedStorageBytes,
"ownerName" -> ownerName
hotzenklotz marked this conversation as resolved.
Show resolved Hide resolved
) ++ adminOnlyInfo
}

Expand Down
13 changes: 13 additions & 0 deletions app/models/user/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
hotzenklotz marked this conversation as resolved.
Show resolved Hide resolved

def findOneByOrgaAndMultiUser(organizationId: ObjectId, multiUserId: ObjectId)(
implicit ctx: DBAccessContext): Fox[User] =
for {
Expand Down
10 changes: 10 additions & 0 deletions frontend/javascripts/admin/organization/organization_edit_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CopyOutlined,
SaveOutlined,
IdcardOutlined,
UserOutlined,
} from "@ant-design/icons";
import { confirmAsync } from "dashboard/dataset/helper_components";
import {
Expand Down Expand Up @@ -169,13 +170,22 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
width: "calc(100% - 31px)",
}}
readOnly
disabled
/>
<Button
onClick={this.handleCopyNameButtonClicked}
icon={<CopyOutlined className="without-icon-margin" />}
/>
</Input.Group>
</FormItem>
<FormItem label="Organization Owner">
<Input
prefix={<UserOutlined />}
value={this.props.organization.ownerName}
readOnly
disabled
/>
</FormItem>
<FormItem
label="Organization Name"
name="displayName"
Expand Down
21 changes: 21 additions & 0 deletions frontend/javascripts/admin/organization/pricing_plan_utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import messages from "messages";
import { APIOrganization, APIUser } from "types/api_flow_types";

export enum PricingPlanEnum {
Expand Down Expand Up @@ -83,3 +84,23 @@ export function isFeatureAllowedByPricingPlan(

return isPricingPlanGreaterEqualThan(organization.pricingPlan, requiredPricingPlan);
}

export function getFeatureNotAvailabeInPlanMessage(
requiredPricingPlan: PricingPlanEnum,
organization: APIOrganization | null,
) {
let organizationOwnerName = "";

// expected naming schema for owner: "(M. Mustermann)" | ""
philippotto marked this conversation as resolved.
Show resolved Hide resolved
if (organization) {
{
const [firstName, ...rest] = organization.ownerName.split(" ");
organizationOwnerName = `(${firstName[0]}. ${rest.join(" ")})`;
}
}

return messages["organization.plan.feature_not_available"](
requiredPricingPlan,
organizationOwnerName,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ function UpgradePricingPlanModal({
) : null}
</>
}
zIndex={10000} // overlay everything
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philippotto I tried to click somewhere in the background (document.body.click()) to make the menu disappear but that did not work. With the high zIndex you can at least be sure that it overlays anything.

>
<div
style={{
Expand Down
117 changes: 84 additions & 33 deletions frontend/javascripts/components/pricing_enforcers.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import React from "react";
import { useSelector } from "react-redux";
import { Tooltip, Menu, MenuItemProps, Alert, ButtonProps, Button } from "antd";
import { Menu, MenuItemProps, Alert, ButtonProps, Button, Result, Popover } from "antd";
import { LockOutlined } from "@ant-design/icons";
import {
getFeatureNotAvailabeInPlanMessage,
isFeatureAllowedByPricingPlan,
PricingPlanEnum,
} from "admin/organization/pricing_plan_utils";
import { isUserAllowedToRequestUpgrades } from "admin/organization/pricing_plan_utils";
import { Link } from "react-router-dom";
import messages from "messages";
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";
import { APIOrganization, APIUser } from "types/api_flow_types";

const PRIMARY_COLOR_HEX = rgbToHex(PRIMARY_COLOR);

const popOverStyle = { color: "white", maxWidth: 250 };

const handleMouseClick = (event: React.MouseEvent) => {
event.preventDefault();
Expand All @@ -24,18 +32,41 @@ const handleMenuClick: MenuClickEventHandler = (info) => {

type RequiredPricingProps = { requiredPricingPlan: PricingPlanEnum };

function getUpgradeNowButton(
activeUser: APIUser | null | undefined,
activeOrganization: APIOrganization | undefined,
) {
return activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? (
<Button
size="small"
onClick={() => UpgradePricingPlanModal.upgradePricingPlan(activeOrganization)}
style={{ marginTop: 10 }}
>
Upgrade Now
</Button>
) : 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 <Menu.Item {...menuItemProps}>{children}</Menu.Item>;

return (
<Tooltip
title={messages["organization.plan.feature_not_available"](requiredPricingPlan)}
<Popover
color={PRIMARY_COLOR_HEX}
content={
<div style={popOverStyle}>
{getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)}
hotzenklotz marked this conversation as resolved.
Show resolved Hide resolved
{getUpgradeNowButton(activeUser, activeOrganization)}
</div>
}
placement="right"
trigger="hover"
>
<Menu.Item
onClick={handleMenuClick}
Expand All @@ -48,7 +79,7 @@ export const PricingEnforcedMenuItem: React.FunctionComponent<
{children}
<LockOutlined style={{ marginLeft: 5 }} />
</Menu.Item>
</Tooltip>
</Popover>
);
};

Expand All @@ -57,21 +88,29 @@ export const PricingEnforcedButton: React.FunctionComponent<RequiredPricingProps
requiredPricingPlan,
...buttonProps
}) => {
const activeUser = useSelector((state: OxalisState) => state.activeUser);
const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization);
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);

if (isFeatureAllowed) return <Button {...buttonProps}>{children}</Button>;

return (
<Tooltip
title={messages["organization.plan.feature_not_available"](requiredPricingPlan)}
placement="right"
<Popover
color={PRIMARY_COLOR_HEX}
content={
<div style={popOverStyle}>
{getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)}
{getUpgradeNowButton(activeUser, activeOrganization)}
</div>
}
placement="bottom"
trigger="hover"
>
<Button {...buttonProps} disabled>
{children}
<LockOutlined style={{ marginLeft: 5 }} />
</Button>
</Tooltip>
</Popover>
);
};

Expand All @@ -80,6 +119,7 @@ export const PricingEnforcedBlur: React.FunctionComponent<RequiredPricingProps>
requiredPricingPlan,
...restProps
}) => {
const activeUser = useSelector((state: OxalisState) => state.activeUser);
const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization);
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);

Expand All @@ -100,7 +140,16 @@ export const PricingEnforcedBlur: React.FunctionComponent<RequiredPricingProps>
);

return (
<Tooltip title={messages["organization.plan.feature_not_available"](requiredPricingPlan)}>
<Popover
color={PRIMARY_COLOR_HEX}
content={
<div style={popOverStyle}>
{getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)}
{getUpgradeNowButton(activeUser, activeOrganization)}
</div>
}
trigger="hover"
>
<div style={{ position: "relative", cursor: "not-allowed" }}>
<div
style={{
Expand All @@ -122,45 +171,47 @@ export const PricingEnforcedBlur: React.FunctionComponent<RequiredPricingProps>
>
<Alert
showIcon
message={messages["organization.plan.feature_not_available"](requiredPricingPlan)}
message={getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)}
icon={<LockOutlined />}
/>
</div>
</div>
</Tooltip>
</Popover>
);
};

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) ? (
<Link to={`/organizations/${activeOrganization.name}`}>Go to Organization Settings</Link>
) : null;
<Link to={`/organizations/${activeOrganization.name}`}>
<Button>Go to Organization Settings</Button>
</Link>
) : undefined;

return (
<div className="container">
<Alert
style={{
maxWidth: "500px",
margin: "0 auto",
}}
message="Feature not available"
description={
<>
<p>
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.
</p>
{linkToOrganizationSettings}
</>
<Result
status="warning"
title="Feature not available"
subTitle={
<p style={{ maxWidth: "500px", margin: "0 auto" }}>
{getFeatureNotAvailabeInPlanMessage(requiredPricingPlan, activeOrganization)}
</p>
}
type="error"
showIcon
extra={[
<Link to="/">
<Button type="primary">Return to Dashboard</Button>
</Link>,
linkToOrganizationSettings,
]}
/>
</div>
);
}
}
6 changes: 5 additions & 1 deletion frontend/javascripts/components/secured_route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ class SecuredRoute extends React.PureComponent<SecuredRouteProps, State> {
this.props.requiredPricingPlan,
)
) {
return <PageUnavailableForYourPlanView />;
return (
<PageUnavailableForYourPlanView
requiredPricingPlan={this.props.requiredPricingPlan}
/>
);
}

if (Component != null) {
Expand Down
7 changes: 5 additions & 2 deletions frontend/javascripts/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Ask the owner of your organization (John Doe)" would be more idiomatic english than "organization owner", I think.
Also, I'd change the type to organizationOwnerName: string | null and then do a conditional on that so that the double space is avoided if the owner is not known.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I'd change the type to organizationOwnerName: string | null and then do a conditional on that so that the double space is avoided if the owner is not known.

Mhm, I was hoping that getFeatureNotAvailabeInPlanMessage() already checks against null to avoid doing this logic in the messages module. it, does not, however, prevent a double space.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i see. i don't even know whether the double space is rendered at all? I think this could get collapsed into a single space by the browser? if it's rendered, fixing the doublespace without moving the null-check could work something like this: ... owner ${organizationOwnerName}to (no space before "to") and then organizationOwnerName should have a trailing space if it's not empty...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, I would not optimize for extremely unlikely case that an orga has no owner. This can only ever be the case if a DB migration was not executed correctly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point 👍

};
2 changes: 1 addition & 1 deletion frontend/javascripts/oxalis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ export type TypedArray =

export type TypedArrayWithoutBigInt = Exclude<TypedArray, BigUint64Array>;

export const PRIMARY_COLOR = [86, 96, 255];
export const PRIMARY_COLOR: Vector3 = [86, 96, 255];

export enum LOG_LEVELS {
NOTSET = "NOTSET",
Expand Down
Loading