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 Task/Project Management Features #6767

Merged
merged 16 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 4 additions & 4 deletions frontend/javascripts/admin/dataset/dataset_upload_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -690,20 +690,20 @@ class DatasetUploadView extends React.Component<PropsWithFormAndRouter, State> {
return;
}

const teamOfOrganisation = fetchedTeams.find(
const teamOfOrganization = fetchedTeams.find(
(team) => team.name === team.organization,
);

if (teamOfOrganisation == null) {
if (teamOfOrganization == null) {
return;
}

if (this.formRef.current == null) return;
this.formRef.current.setFieldsValue({
initialTeams: [teamOfOrganisation],
initialTeams: [teamOfOrganization],
});
this.setState({
selectedTeams: [teamOfOrganisation],
selectedTeams: [teamOfOrganization],
});
}}
/>
Expand Down
57 changes: 7 additions & 50 deletions frontend/javascripts/admin/organization/organization_edit_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
getPricingPlanStatus,
} from "admin/admin_rest_api";
import Toast from "libs/toast";
import { coalesce } from "libs/utils";
import { APIOrganization, APIPricingPlanStatus } from "types/api_flow_types";
import {
PlanAboutToExceedAlert,
Expand All @@ -39,15 +38,11 @@ export enum PricingPlanEnum {
Custom = "Custom",
}

type StateProps = {
type Props = {
organization: APIOrganization;
};
type Props = StateProps;

type State = {
displayName: string;
newUserMailingList: string;
pricingPlan: PricingPlanEnum | null | undefined;
isFetchingData: boolean;
isDeleting: boolean;
activeUsersCount: number;
Expand All @@ -56,9 +51,6 @@ type State = {

class OrganizationEditView extends React.PureComponent<Props, State> {
state: State = {
displayName: this.props.organization.displayName,
newUserMailingList: this.props.organization.newUserMailingList,
pricingPlan: this.props.organization.pricingPlan,
isFetchingData: false,
isDeleting: false,
activeUsersCount: 1,
Expand All @@ -71,43 +63,13 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
this.fetchData();
}

componentDidUpdate(_prevProps: Props, prevState: State) {
if (this.formRef.current != null) {
// initialValues only works on the first render. Afterwards, values need to be updated
// using setFieldsValue
if (
prevState.displayName.length === 0 &&
this.state.displayName.length > 0 &&
this.formRef.current.getFieldValue("displayName") !== this.state.displayName
) {
this.formRef.current.setFieldsValue({
displayName: this.state.displayName,
});
}

if (
prevState.newUserMailingList.length === 0 &&
this.state.newUserMailingList.length > 0 &&
this.formRef.current.getFieldValue("newUserMailingList") !== this.state.newUserMailingList
) {
this.formRef.current.setFieldsValue({
newUserMailingList: this.state.newUserMailingList,
});
}
}
}

async fetchData() {
this.setState({
isFetchingData: true,
});
const [users, pricingPlanStatus] = await Promise.all([getUsers(), getPricingPlanStatus()]);

const { displayName, newUserMailingList, pricingPlan } = this.props.organization;
this.setState({
displayName,
pricingPlan: coalesce(PricingPlanEnum, pricingPlan),
newUserMailingList,
isFetchingData: false,
pricingPlanStatus,
activeUsersCount: getActiveUserCount(users),
Expand All @@ -129,7 +91,7 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
title: (
<p>
Deleting an organization cannot be undone. Are you certain you want to delete the
organization {this.state.displayName}? <br />
organization {this.props.organization.displayName}? <br />
Attention: You will be logged out.
</p>
),
Expand All @@ -154,12 +116,7 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
};

render() {
if (
this.state.isFetchingData ||
!this.props.organization ||
!this.state.pricingPlan ||
!this.state.pricingPlanStatus
)
if (this.state.isFetchingData || !this.props.organization || !this.state.pricingPlanStatus)
return (
<div
className="container"
Expand All @@ -186,7 +143,7 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
>
<Row style={{ color: "#aaa", fontSize: "12" }}>Your Organization</Row>
<Row style={{ marginBottom: 20 }}>
<h2>{this.state.displayName}</h2>
<h2>{this.props.organization.displayName}</h2>
</Row>
{this.state.pricingPlanStatus.isExceeded ? (
<PlanExceededAlert organization={this.props.organization} />
Expand All @@ -207,8 +164,8 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
layout="vertical"
ref={this.formRef}
initialValues={{
displayName: this.state.displayName,
newUserMailingList: this.state.newUserMailingList,
displayName: this.props.organization.displayName,
newUserMailingList: this.props.organization.newUserMailingList,
}}
>
<FormItem label="Organization ID">
Expand Down Expand Up @@ -301,7 +258,7 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
}
}

const mapStateToProps = (state: OxalisState): StateProps => ({
const mapStateToProps = (state: OxalisState): Props => ({
organization: enforceActiveOrganization(state.activeOrganization),
});

Expand Down
36 changes: 14 additions & 22 deletions frontend/javascripts/admin/organization/pricing_plan_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,18 @@ export function isUserAllowedToRequestUpgrades(user: APIUser): boolean {
return user.isAdmin || user.isOrganizationOwner;
}

export function isPricingPlanGreaterEqualThan(planA: PricingPlanEnum, planB: PricingPlanEnum) {
switch (planA) {
case PricingPlanEnum.Power:
case PricingPlanEnum.PowerTrial:
case PricingPlanEnum.Custom:
return true;

case PricingPlanEnum.Team:
case PricingPlanEnum.TeamTrial:
switch (planB) {
case PricingPlanEnum.Power:
case PricingPlanEnum.PowerTrial:
case PricingPlanEnum.Custom:
return false;
default:
return true;
}

case PricingPlanEnum.Basic:
default:
return false;
}
const PLAN_TO_RANK = {
[PricingPlanEnum.Basic]: 0,
[PricingPlanEnum.Team]: 1,
[PricingPlanEnum.TeamTrial]: 1,
[PricingPlanEnum.Power]: 2,
[PricingPlanEnum.PowerTrial]: 2,
[PricingPlanEnum.Custom]: 2,
};

export function isPricingPlanGreaterEqualThan(
planA: PricingPlanEnum,
planB: PricingPlanEnum,
): boolean {
return PLAN_TO_RANK[planA] >= PLAN_TO_RANK[planB];
}
8 changes: 3 additions & 5 deletions frontend/javascripts/admin/user/user_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ type State = {
activationFilter: Array<"true" | "false">;
searchQuery: string;
domainToEdit: string | null | undefined;
maxUserCountPerOrganization: number;
};
const persistence = new Persistence<Pick<State, "searchQuery" | "activationFilter">>(
{
Expand All @@ -87,7 +86,6 @@ class UserListView extends React.PureComponent<Props, State> {
searchQuery: "",
singleSelectedUser: null,
domainToEdit: null,
maxUserCountPerOrganization: this.props.activeOrganization.includedUsers,
};

componentDidMount() {
Expand Down Expand Up @@ -282,7 +280,7 @@ class UserListView extends React.PureComponent<Props, State> {
message="You reached the maximum number of users"
description={`You organization reached the maxmium number of users included in your current plan. Consider upgrading your WEBKNOSSOS plan to accommodate more users or deactivate some user accounts. Email invites are disabled in the meantime. Your organization currently has ${getActiveUserCount(
this.state.users,
)} active users of ${this.state.maxUserCountPerOrganization} allowed by your plan.`}
)} active users of ${this.props.activeOrganization.includedUsers} allowed by your plan.`}
type="error"
showIcon
style={{
Expand Down Expand Up @@ -345,7 +343,7 @@ class UserListView extends React.PureComponent<Props, State> {
};
const noOtherUsers = this.state.users.length < 2;
const isUserInvitesDisabled =
getActiveUserCount(this.state.users) >= this.state.maxUserCountPerOrganization;
getActiveUserCount(this.state.users) >= this.props.activeOrganization.includedUsers;

return (
<div className="container test-UserListView">
Expand Down Expand Up @@ -397,7 +395,7 @@ class UserListView extends React.PureComponent<Props, State> {
</Button>
<InviteUsersModal
currentUserCount={getActiveUserCount(this.state.users)}
maxUserCountPerOrganization={this.state.maxUserCountPerOrganization}
maxUserCountPerOrganization={this.props.activeOrganization.includedUsers}
isOpen={this.state.isInviteModalOpen}
organizationName={this.props.activeUser.organization}
handleVisibleChange={(visible) => {
Expand Down
27 changes: 18 additions & 9 deletions frontend/javascripts/components/pricing_enforcers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@ 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 } from "admin/organization/pricing_plan_utils";
import {
isPricingPlanGreaterEqualThan,
isUserAllowedToRequestUpgrades,
} from "admin/organization/pricing_plan_utils";
import { Link } from "react-router-dom";
import type { MenuClickEventHandler } from "rc-menu/lib/interface";
import type { OxalisState } from "oxalis/store";

const toolTipMessage = `This feature is not available in your organisation's plan. Ask your organisation owner to upgrade.`;
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();
};

const handleMenuClick: MenuClickEventHandler = (info) => {
info.domEvent.preventDefault();
info.domEvent.stopPropagation();
};

export function PricingEnforcedMenuItem({
children,
requiredPricingPlan,
Expand All @@ -32,11 +40,6 @@ export function PricingEnforcedMenuItem({

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

const handleMenuClick: MenuClickEventHandler = (info) => {
info.domEvent.preventDefault();
info.domEvent.stopPropagation();
};

return (
<Tooltip title={toolTipMessage} placement="right">
<Menu.Item
Expand Down Expand Up @@ -82,7 +85,13 @@ export function PricingEnforcedButton({
}

export function PageUnavailableForYourPlanView() {
const organizationName = useSelector((state: OxalisState) => state.activeOrganization?.name);
const activeUser = useSelector((state: OxalisState) => state.activeUser);
const activeOrganization = useSelector((state: OxalisState) => state.activeOrganization);

const LinkToOrganizationSettings =
hotzenklotz marked this conversation as resolved.
Show resolved Hide resolved
activeUser && activeOrganization && isUserAllowedToRequestUpgrades(activeUser) ? (
<Link to={`/organizations/${activeOrganization.name}`}>Go to Organization Settings</Link>
) : null;

return (
<div className="container">
Expand All @@ -99,7 +108,7 @@ export function PageUnavailableForYourPlanView() {
upgrading to a higher WEBKNOSSOS plan to unlock it or ask your organization's owner to
upgrade.
</p>
<Link to={`/organizations/${organizationName}`}>Go to Organization Settings</Link>
{LinkToOrganizationSettings}
hotzenklotz marked this conversation as resolved.
Show resolved Hide resolved
</>
}
type="error"
Expand Down
11 changes: 5 additions & 6 deletions frontend/javascripts/components/secured_route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PageUnavailableForYourPlanView } from "components/pricing_enforcers";
import type { ComponentType } from "react";
import type { RouteComponentProps } from "react-router-dom";
import type { OxalisState } from "oxalis/store";
import { enforceActiveOrganization } from "oxalis/model/accessors/organization_accessors";

type StateProps = {
activeOrganization: APIOrganization | null;
Expand Down Expand Up @@ -64,16 +65,14 @@ class SecuredRoute extends React.PureComponent<SecuredRouteProps, State> {
<Route
{...rest}
render={(props) => {
if (!isCompletelyAuthenticated)
if (!isCompletelyAuthenticated) {
return <LoginView redirect={this.props.location.pathname} />;
}

const organization = enforceActiveOrganization(this.props.activeOrganization);
if (
this.props.requiredPricingPlan &&
this.props.activeOrganization &&
!isPricingPlanGreaterEqualThan(
this.props.activeOrganization.pricingPlan,
this.props.requiredPricingPlan,
)
!isPricingPlanGreaterEqualThan(organization.pricingPlan, this.props.requiredPricingPlan)
) {
return <PageUnavailableForYourPlanView />;
}
Expand Down
1 change: 0 additions & 1 deletion frontend/javascripts/dashboard/dashboard_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,6 @@ class DashboardView extends PureComponent<PropsWithRouter, State> {
) : null;
this.state.pricingPlanStatus?.isAlmostExceeded;

// ToDo enable components below once pricing goes live
const pricingPlanWarnings =
this.props.activeOrganization &&
this.state.pricingPlanStatus?.isAlmostExceeded &&
Expand Down
16 changes: 6 additions & 10 deletions frontend/javascripts/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,12 @@ async function loadHasOrganizations() {
}

async function loadOrganization() {
try {
const { activeUser } = Store.getState();
if (activeUser) {
// organisation can only be loaded for user with a logged in wk account
// anonymous wk session for publicly shared datasets have no orga
const organization = await getOrganization(activeUser?.organization);
Store.dispatch(setActiveOrganizationAction(organization));
}
} catch (e) {
// pass
const { activeUser } = Store.getState();
if (activeUser) {
// organization can only be loaded for user with a logged in wk account
// anonymous wk session for publicly shared datasets have no orga
const organization = await getOrganization(activeUser.organization);
Store.dispatch(setActiveOrganizationAction(organization));
}
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ instead. Only enable this option if you understand its effect. All layers will n
"Your account has been created. An administrator is going to unlock you soon.",
"auth.automatic_user_activation": "User was activated automatically",
"auth.error_no_user": "No active user is logged in.",
"auth.error_no_orga": "No active organziation can be loaded.",
"auth.error_no_organization": "No active organziation can be loaded.",
"auth.invalid_organization_name":
"The link is not valid, since the specified organization does not exist. You are being redirected to the general registration form.",
"request.max_item_count_alert":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export function enforceActiveOrganization(
if (activeOrganization) {
return activeOrganization;
} else {
throw new Error(messages["auth.error_no_orga"]);
throw new Error(messages["auth.error_no_organization"]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ class DatasetInfoTabView extends React.PureComponent<Props, State> {
) : null;
}

getOrganisationLogo(isDatasetViewMode: boolean) {
getOrganizationLogo(isDatasetViewMode: boolean) {
if (!this.props.dataset.logoUrl) {
return null;
}
Expand Down Expand Up @@ -642,7 +642,7 @@ class DatasetInfoTabView extends React.PureComponent<Props, State> {

<div className="info-tab-block">{this.getTracingStatistics()}</div>
{this.getKeyboardShortcuts(isDatasetViewMode)}
{this.getOrganisationLogo(isDatasetViewMode)}
{this.getOrganizationLogo(isDatasetViewMode)}
</div>
);
}
Expand Down