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 all 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 @@ -22,6 +22,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Changed branding of WEBKNOSSOS including a new logo, new primary colors, and UPPERCASE name. [#6739](https://github.com/scalableminds/webknossos/pull/6739)
- Improves performance for ingesting Voxelytics reporting data. [#6732](https://github.com/scalableminds/webknossos/pull/6732)
- Implements statistics from combined worflow runs in the Voxelytics reporting. [#6732](https://github.com/scalableminds/webknossos/pull/6732)
- Limit paid task/project management features to respective organization plans. [6767](https://github.com/scalableminds/webknossos/pull/6767)

### 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
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
1 change: 1 addition & 0 deletions frontend/javascripts/admin/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ export class InviteUsersModal extends React.Component<
if (this.props.handleVisibleChange != null) this.props.handleVisibleChange(false);
if (this.props.destroy != null) this.props.destroy();
}}
closable
>
{this.getContent(isInvitesDisabled)}
</Modal>
Expand Down
98 changes: 29 additions & 69 deletions frontend/javascripts/admin/organization/organization_edit_view.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RouteComponentProps, withRouter } from "react-router-dom";
import React from "react";
import { connect } from "react-redux";
import { Form, Button, Card, Input, Row, FormInstance, Col, Skeleton } from "antd";
import {
MailOutlined,
Expand All @@ -7,17 +8,14 @@ import {
SaveOutlined,
IdcardOutlined,
} from "@ant-design/icons";
import React from "react";
import { confirmAsync } from "dashboard/dataset/helper_components";
import {
getOrganization,
deleteOrganization,
updateOrganization,
getUsers,
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 @@ -26,7 +24,9 @@ import {
PlanExpirationCard,
PlanUpgradeCard,
} from "./organization_cards";
import { enforceActiveOrganization } from "oxalis/model/accessors/organization_accessors";
import { getActiveUserCount } from "./pricing_plan_utils";
import type { OxalisState } from "oxalis/store";

const FormItem = Form.Item;
export enum PricingPlanEnum {
Expand All @@ -37,80 +37,40 @@ export enum PricingPlanEnum {
PowerTrial = "Power_Trial",
Custom = "Custom",
}

type Props = {
organizationName: string;
organization: APIOrganization;
};

type State = {
displayName: string;
newUserMailingList: string;
pricingPlan: PricingPlanEnum | null | undefined;
isFetchingData: boolean;
isDeleting: boolean;
organization: APIOrganization | null;
activeUsersCount: number;
pricingPlanStatus: APIPricingPlanStatus | null;
};

class OrganizationEditView extends React.PureComponent<Props, State> {
state: State = {
displayName: "",
newUserMailingList: "",
pricingPlan: null,
isFetchingData: false,
isDeleting: false,
organization: null,
activeUsersCount: 1,
pricingPlanStatus: null,
};

formRef = React.createRef<FormInstance>();

componentDidMount() {
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 [organization, users, pricingPlanStatus] = await Promise.all([
getOrganization(this.props.organizationName),
getUsers(),
getPricingPlanStatus(),
]);
const [users, pricingPlanStatus] = await Promise.all([getUsers(), getPricingPlanStatus()]);

const { displayName, newUserMailingList, pricingPlan } = organization;
this.setState({
displayName,
pricingPlan: coalesce(PricingPlanEnum, pricingPlan),
newUserMailingList,
isFetchingData: false,
organization,
pricingPlanStatus,
activeUsersCount: getActiveUserCount(users),
});
Expand All @@ -119,7 +79,7 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'formValues' implicitly has an 'any' typ... Remove this comment to see the full error message
onFinish = async (formValues) => {
await updateOrganization(
this.props.organizationName,
this.props.organization.name,
formValues.displayName,
formValues.newUserMailingList,
);
Expand All @@ -131,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 @@ -142,7 +102,7 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
this.setState({
isDeleting: true,
});
await deleteOrganization(this.props.organizationName);
await deleteOrganization(this.props.organization.name);
this.setState({
isDeleting: false,
});
Expand All @@ -151,17 +111,12 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
};

handleCopyNameButtonClicked = async (): Promise<void> => {
await navigator.clipboard.writeText(this.props.organizationName);
await navigator.clipboard.writeText(this.props.organization.name);
Toast.success("Copied organization name to the clipboard.");
};

render() {
if (
this.state.isFetchingData ||
!this.state.organization ||
!this.state.pricingPlan ||
!this.state.pricingPlanStatus
)
if (this.state.isFetchingData || !this.props.organization || !this.state.pricingPlanStatus)
return (
<div
className="container"
Expand All @@ -188,36 +143,36 @@ 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.state.organization} />
<PlanExceededAlert organization={this.props.organization} />
) : null}
{this.state.pricingPlanStatus.isAlmostExceeded &&
!this.state.pricingPlanStatus.isExceeded ? (
<PlanAboutToExceedAlert organization={this.state.organization} />
<PlanAboutToExceedAlert organization={this.props.organization} />
) : null}
<PlanDashboardCard
organization={this.state.organization}
organization={this.props.organization}
activeUsersCount={this.state.activeUsersCount}
/>
<PlanExpirationCard organization={this.state.organization} />
<PlanUpgradeCard organization={this.state.organization} />
<PlanExpirationCard organization={this.props.organization} />
<PlanUpgradeCard organization={this.props.organization} />
<Card title="Settings" style={{ marginBottom: 20 }}>
<Form
onFinish={this.onFinish}
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">
<Input.Group compact>
<Input
prefix={<IdcardOutlined />}
value={this.props.organizationName}
value={this.props.organization.name}
style={{
width: "calc(100% - 31px)",
}}
Expand Down Expand Up @@ -303,4 +258,9 @@ class OrganizationEditView extends React.PureComponent<Props, State> {
}
}

export default withRouter<RouteComponentProps & Props, any>(OrganizationEditView);
const mapStateToProps = (state: OxalisState): Props => ({
organization: enforceActiveOrganization(state.activeOrganization),
});

const connector = connect(mapStateToProps);
export default connector(OrganizationEditView);
16 changes: 16 additions & 0 deletions frontend/javascripts/admin/organization/pricing_plan_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,19 @@ export function hasPricingPlanExceededStorage(organization: APIOrganization): bo
export function isUserAllowedToRequestUpgrades(user: APIUser): boolean {
return user.isAdmin || user.isOrganizationOwner;
}

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];
}
26 changes: 14 additions & 12 deletions frontend/javascripts/admin/user/user_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ import {
UserOutlined,
} from "@ant-design/icons";
import { connect } from "react-redux";
import * as React from "react";
import React from "react";
import _ from "lodash";
import moment from "moment";
import { location } from "libs/window";
import type { APIUser, APITeamMembership, ExperienceMap } from "types/api_flow_types";
import type {
APIUser,
APITeamMembership,
ExperienceMap,
APIOrganization,
} from "types/api_flow_types";
import { InviteUsersModal } from "admin/onboarding";
import type { OxalisState } from "oxalis/store";
import { enforceActiveUser } from "oxalis/model/accessors/user_accessor";
Expand All @@ -37,13 +42,15 @@ import * as Utils from "libs/utils";
import messages from "messages";
import { logoutUserAction } from "../../oxalis/model/actions/user_actions";
import Store from "../../oxalis/store";
import { enforceActiveOrganization } from "oxalis/model/accessors/organization_accessors";

const { Column } = Table;
const { Search } = Input;
const typeHint: APIUser[] = [];

type StateProps = {
activeUser: APIUser;
activeOrganization: APIOrganization;
};
type Props = RouteComponentProps & StateProps;

Expand All @@ -58,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 @@ -80,7 +86,6 @@ class UserListView extends React.PureComponent<Props, State> {
searchQuery: "",
singleSelectedUser: null,
domainToEdit: null,
maxUserCountPerOrganization: Number.POSITIVE_INFINITY,
};

componentDidMount() {
Expand All @@ -103,15 +108,11 @@ class UserListView extends React.PureComponent<Props, State> {
this.setState({
isLoading: true,
});
const [users, organization] = await Promise.all([
getEditableUsers(),
getOrganization(this.props.activeUser.organization),
]);
const users = await getEditableUsers();

this.setState({
isLoading: false,
users,
maxUserCountPerOrganization: organization.includedUsers,
});
}

Expand Down Expand Up @@ -279,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 @@ -342,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 @@ -394,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 Expand Up @@ -681,6 +682,7 @@ class UserListView extends React.PureComponent<Props, State> {

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

const connector = connect(mapStateToProps);
Expand Down
Loading