Skip to content

Commit

Permalink
Enforce Pricing for Team Sharing (#6776)
Browse files Browse the repository at this point in the history
* add new blur component around certain paid features
* updated changelog
* Update frontend/javascripts/components/pricing_enforcers.tsx

Co-authored-by: Philipp Otto <[email protected]>
* Merge branch 'master' into more-plans
* Merge branch 'master' of github.com:scalableminds/webknossos into more-plans
* 'master' of github.com:scalableminds/webknossos:
  correct font in vx dag
  Catch zero-sized buckets in backend, fix http nio timeout handling (#6782)
  Remove debug logging in editable mapping logic (#6783)
  Loki for voxelytics logs (#6770)
* re-styled pricing alert
* Merge branch 'master' of github.com:scalableminds/webknossos into more-plans
* 'master' of github.com:scalableminds/webknossos:
  Update linter to Rome v11.0.0 (#6785)
  Fix publicly shared annotations (#6784)
* add isFeatureAllowedByPricingPlan helper function
* enforce pricing for creating precomputed meshes
* use isFeatureAllowedByPricingPlan for SecuredRoute
* disable proof reading tool
* prevent proof reading tool from being unsed through keyboard shortcut
* Merge branch 'master' into more-plans
  • Loading branch information
hotzenklotz authored Feb 1, 2023
1 parent e90eaf2 commit b4eaeed
Show file tree
Hide file tree
Showing 20 changed files with 223 additions and 131 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Limit paid task/project management features to respective organization plans. [6767](https://github.com/scalableminds/webknossos/pull/6767)
- The dataset list route `GET api/datasets` no longer respects the isEditable filter. [#6759](https://github.com/scalableminds/webknossos/pull/6759)
- Upgrade linter to Rome v11.0.0. [#6785](https://github.com/scalableminds/webknossos/pull/6785)
- Limit paid team sharing features to respective organization plans. [6767](https://github.com/scalableminds/webknossos/pull/6776)

### Fixed
- Fixed node selection and context menu for node ids greater than 130813. [#6724](https://github.com/scalableminds/webknossos/pull/6724) and [#6731](https://github.com/scalableminds/webknossos/pull/6731)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { OxalisState } from "oxalis/store";
import React from "react";
import { useSelector } from "react-redux";
import { APIOrganization } from "types/api_flow_types";
import { PricingPlanEnum } from "./organization_edit_view";
import {
hasPricingPlanExceededStorage,
hasPricingPlanExceededUsers,
hasPricingPlanExpired,
isUserAllowedToRequestUpgrades,
powerPlanFeatures,
PricingPlanEnum,
teamPlanFeatures,
} from "./pricing_plan_utils";
import UpgradePricingPlanModal from "./upgrade_plan_modal";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,6 @@ import { getActiveUserCount } from "./pricing_plan_utils";
import type { OxalisState } from "oxalis/store";

const FormItem = Form.Item;
export enum PricingPlanEnum {
Basic = "Basic",
Team = "Team",
Power = "Power",
TeamTrial = "Team_Trial",
PowerTrial = "Power_Trial",
Custom = "Custom",
}

type Props = {
organization: APIOrganization;
Expand Down
28 changes: 27 additions & 1 deletion frontend/javascripts/admin/organization/pricing_plan_utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { APIOrganization, APIUser } from "types/api_flow_types";
import { PricingPlanEnum } from "./organization_edit_view";

export enum PricingPlanEnum {
Basic = "Basic",
Team = "Team",
Power = "Power",
TeamTrial = "Team_Trial",
PowerTrial = "Power_Trial",
Custom = "Custom",
}

export const teamPlanFeatures = [
"Collaborative Annotation",
Expand Down Expand Up @@ -57,3 +65,21 @@ export function isPricingPlanGreaterEqualThan(
): boolean {
return PLAN_TO_RANK[planA] >= PLAN_TO_RANK[planB];
}

export function isFeatureAllowedByPricingPlan(
organization: APIOrganization | null,
requiredPricingPlan: PricingPlanEnum,
) {
// This function should not be called to check for "Basic" plans since its the default plan for all users anyway.

if (!organization) return false;

if (requiredPricingPlan === PricingPlanEnum.Basic) {
console.debug(
"Restricting a feature to Basic Plan does not make sense. Consider removing the restriction",
);
return true;
}

return isPricingPlanGreaterEqualThan(organization.pricingPlan, requiredPricingPlan);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
sendUpgradePricingPlanUserEmail,
} from "admin/admin_rest_api";
import { powerPlanFeatures, teamPlanFeatures } from "./pricing_plan_utils";
import { PricingPlanEnum } from "./organization_edit_view";
import { PricingPlanEnum } from "./pricing_plan_utils";
import renderIndependently from "libs/render_independently";
import Toast from "libs/toast";
import { TeamAndPowerPlanUpgradeCards } from "./organization_cards";
Expand Down
98 changes: 62 additions & 36 deletions frontend/javascripts/components/pricing_enforcers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import React from "react";
import { useSelector } from "react-redux";
import { Tooltip, Menu, MenuItemProps, Alert, ButtonProps, Button } from "antd";
import { LockOutlined } from "@ant-design/icons";
import { PricingPlanEnum } from "admin/organization/organization_edit_view";
import {
isPricingPlanGreaterEqualThan,
isUserAllowedToRequestUpgrades,
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";

const toolTipMessage = `This feature is not available in your organization's plan. Ask your organization owner to upgrade.`;

const handleMouseClick = (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
Expand All @@ -23,25 +22,18 @@ const handleMenuClick: MenuClickEventHandler = (info) => {
info.domEvent.stopPropagation();
};

export function PricingEnforcedMenuItem({
children,
requiredPricingPlan,
showLockIcon = true,
...menuItemProps
}: {
children: React.ReactNode;
requiredPricingPlan: PricingPlanEnum;
showLockIcon?: boolean;
} & MenuItemProps): JSX.Element {
const currentPricingPlan = useSelector((state: OxalisState) =>
state.activeOrganization ? state.activeOrganization.pricingPlan : PricingPlanEnum.Basic,
);
const isFeatureAllowed = isPricingPlanGreaterEqualThan(currentPricingPlan, requiredPricingPlan);
type RequiredPricingProps = { requiredPricingPlan: PricingPlanEnum };

export const PricingEnforcedMenuItem: React.FunctionComponent<
RequiredPricingProps & MenuItemProps
> = ({ children, requiredPricingPlan, ...menuItemProps }) => {
const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization);
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);

if (isFeatureAllowed) return <Menu.Item {...menuItemProps}>{children}</Menu.Item>;

return (
<Tooltip title={toolTipMessage} placement="right">
<Tooltip title={messages["organization.plan.feature_not_available"]} placement="right">
<Menu.Item
onClick={handleMenuClick}
onAuxClick={handleMouseClick}
Expand All @@ -51,38 +43,72 @@ export function PricingEnforcedMenuItem({
{...menuItemProps}
>
{children}
{showLockIcon ? <LockOutlined style={{ marginLeft: 5 }} /> : null}
<LockOutlined style={{ marginLeft: 5 }} />
</Menu.Item>
</Tooltip>
);
}
};

export function PricingEnforcedButton({
export const PricingEnforcedButton: React.FunctionComponent<RequiredPricingProps & ButtonProps> = ({
children,
requiredPricingPlan,
showLockIcon = true,
...buttonProps
}: {
children: React.ReactNode;
requiredPricingPlan: PricingPlanEnum;
showLockIcon?: boolean;
} & ButtonProps): JSX.Element {
const currentPricingPlan = useSelector((state: OxalisState) =>
state.activeOrganization ? state.activeOrganization.pricingPlan : PricingPlanEnum.Basic,
);
const isFeatureAllowed = isPricingPlanGreaterEqualThan(currentPricingPlan, requiredPricingPlan);
}) => {
const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization);
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);

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

return (
<Tooltip title={toolTipMessage} placement="right">
<Tooltip title={messages["organization.plan.feature_not_available"]} placement="right">
<Button {...buttonProps} disabled>
{children}
{showLockIcon ? <LockOutlined style={{ marginLeft: 5 }} /> : null}
<LockOutlined style={{ marginLeft: 5 }} />
</Button>
</Tooltip>
);
}
};

export const PricingEnforcedBlur: React.FunctionComponent<RequiredPricingProps> = ({
children,
requiredPricingPlan,
}) => {
const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization);
const isFeatureAllowed = isFeatureAllowedByPricingPlan(activeOrganization, requiredPricingPlan);

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

return (
<Tooltip title={messages["organization.plan.feature_not_available"]}>
<div style={{ position: "relative", cursor: "not-allowed" }}>
<div
style={{
filter: "blur(1px)",
pointerEvents: "none",
}}
>
{children}
</div>
<div
style={{
position: "absolute",
left: "calc(50% - 150px)",
top: "calc(50% - 50px)",
width: 300,
maxHeight: 150,
textAlign: "center",
}}
>
<Alert
showIcon
message={messages["organization.plan.feature_not_available"]}
icon={<LockOutlined />}
/>
</div>
</div>
</Tooltip>
);
};

export function PageUnavailableForYourPlanView() {
const activeUser = useSelector((state: OxalisState) => state.activeUser);
Expand Down
15 changes: 7 additions & 8 deletions frontend/javascripts/components/secured_route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import React from "react";
import { Route, withRouter } from "react-router-dom";
import { connect } from "react-redux";
import LoginView from "admin/auth/login_view";
import { PricingPlanEnum } from "admin/organization/organization_edit_view";
import { isPricingPlanGreaterEqualThan } from "admin/organization/pricing_plan_utils";
import {
isFeatureAllowedByPricingPlan,
PricingPlanEnum,
} from "admin/organization/pricing_plan_utils";
import { APIOrganization } from "types/api_flow_types";
import { PageUnavailableForYourPlanView } from "components/pricing_enforcers";
import type { ComponentType } from "react";
Expand Down Expand Up @@ -70,12 +72,9 @@ class SecuredRoute extends React.PureComponent<SecuredRouteProps, State> {

if (
this.props.requiredPricingPlan &&
!(
this.props.activeOrganization &&
isPricingPlanGreaterEqualThan(
this.props.activeOrganization.pricingPlan,
this.props.requiredPricingPlan,
)
!isFeatureAllowedByPricingPlan(
this.props.activeOrganization,
this.props.requiredPricingPlan,
)
) {
return <PageUnavailableForYourPlanView />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import DatasetAccessListView from "dashboard/advanced_dataset/dataset_access_lis
import { OxalisState } from "oxalis/store";
import { isUserAdminOrDatasetManager, isUserAdminOrTeamManager } from "libs/utils";
import { FormItemWithInfo } from "./helper_components";
import { PricingPlanEnum } from "admin/organization/pricing_plan_utils";
import { PricingEnforcedBlur } from "components/pricing_enforcers";

type Props = {
form: FormInstance | null;
Expand All @@ -32,7 +34,9 @@ function DatasetSettingsSharingTab({ form, datasetId, dataset, activeUser }: Pro
info="The dataset can be seen by administrators, dataset managers and by teams that have access to the folder in which the dataset is located. If you want to grant additional teams access, define these teams here."
validateStatus="success"
>
<TeamSelectionComponent mode="multiple" allowNonEditableTeams={isDatasetManagerOrAdmin} />
<PricingEnforcedBlur requiredPricingPlan={PricingPlanEnum.Team}>
<TeamSelectionComponent mode="multiple" allowNonEditableTeams={isDatasetManagerOrAdmin} />
</PricingEnforcedBlur>
</FormItemWithInfo>
);

Expand All @@ -45,9 +49,8 @@ function DatasetSettingsSharingTab({ form, datasetId, dataset, activeUser }: Pro
fetch();
}, []);

function handleSelectCode(event: React.SyntheticEvent): void {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'select' does not exist on type 'EventTar... Remove this comment to see the full error message
event.target.select();
function handleSelectCode(event: React.MouseEvent<HTMLInputElement>): void {
event.currentTarget.select();
}

async function handleCopySharingLink(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import * as React from "react";
import _ from "lodash";
import type { APITeam } from "types/api_flow_types";
import { getEditableTeams, getTeams } from "admin/admin_rest_api";

const { Option } = Select;

type Props = {
value?: APITeam | Array<APITeam>;
onChange?: (value: APITeam | Array<APITeam>) => void;
afterFetchedTeams?: (arg0: Array<APITeam>) => void;
mode?: "default" | "multiple";
mode?: "tags" | "multiple" | undefined;
allowNonEditableTeams?: boolean;
disabled?: boolean;
};
Expand Down Expand Up @@ -83,8 +85,7 @@ class TeamSelectionComponent extends React.PureComponent<Props, State> {
return (
<Select
showSearch
// @ts-expect-error ts-migrate(2322) FIXME: Type '"default" | "multiple"' is not assignable to... Remove this comment to see the full error message
mode={this.props.mode ? this.props.mode : "default"}
mode={this.props.mode}
style={{
width: "100%",
}}
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/dashboard/dataset_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
useFolderQuery,
} from "./dataset/queries";
import { PricingEnforcedButton } from "components/pricing_enforcers";
import { PricingPlanEnum } from "admin/organization/organization_edit_view";
import { PricingPlanEnum } from "admin/organization/pricing_plan_utils";

const { Group: InputGroup } = Input;

Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/dashboard/folders/folder_tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import memoizeOne from "memoize-one";
import classNames from "classnames";
import { FolderItem } from "types/api_flow_types";
import { PricingEnforcedMenuItem } from "components/pricing_enforcers";
import { PricingPlanEnum } from "admin/organization/organization_edit_view";
import { PricingPlanEnum } from "admin/organization/pricing_plan_utils";

const { DirectoryTree } = Tree;

Expand Down
2 changes: 2 additions & 0 deletions frontend/javascripts/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,6 @@ 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":
"This feature is not available in your organization's plan. Ask your organization owner to upgrade.",
};
2 changes: 1 addition & 1 deletion frontend/javascripts/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import window, { document, location } from "libs/window";
import features from "features";
import { setThemeAction } from "oxalis/model/actions/ui_actions";
import { HelpModal } from "oxalis/view/help_modal";
import { PricingPlanEnum } from "admin/organization/organization_edit_view";
import { PricingPlanEnum } from "admin/organization/pricing_plan_utils";
import { PricingEnforcedMenuItem } from "components/pricing_enforcers";

const { SubMenu } = Menu;
Expand Down
Loading

0 comments on commit b4eaeed

Please sign in to comment.