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

Enforce Pricing for Team Sharing #6776

Merged
merged 13 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
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 @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be moved below the "is basic" check.


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