From f4c2bac6459ee6cb728903bc9ce8d6adda3a6fe4 Mon Sep 17 00:00:00 2001
From: Tom Herold
Date: Wed, 2 Nov 2022 15:34:10 +0100
Subject: [PATCH 01/80] first draft for new organization page
---
.../organization/organization_edit_view.tsx | 190 ++++++++++++++----
frontend/javascripts/router.tsx | 2 +-
yarn.lock | 2 +-
3 files changed, 149 insertions(+), 45 deletions(-)
diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx
index 37800bd84c3..39f1f67b038 100644
--- a/frontend/javascripts/admin/organization/organization_edit_view.tsx
+++ b/frontend/javascripts/admin/organization/organization_edit_view.tsx
@@ -1,6 +1,16 @@
import { RouteComponentProps, withRouter } from "react-router-dom";
-import { Form, Button, Card, Input, Row, FormInstance } from "antd";
-import { MailOutlined, TagOutlined, CopyOutlined, KeyOutlined } from "@ant-design/icons";
+import { Form, Button, Card, Input, Row, FormInstance, Progress, Col, Alert } from "antd";
+import {
+ MailOutlined,
+ TagOutlined,
+ CopyOutlined,
+ KeyOutlined,
+ PlusCircleOutlined,
+ SafetyOutlined,
+ FieldTimeOutlined,
+ SaveOutlined,
+ RocketOutlined,
+} from "@ant-design/icons";
import React from "react";
import { confirmAsync } from "dashboard/dataset/helper_components";
import { getOrganization, deleteOrganization, updateOrganization } from "admin/admin_rest_api";
@@ -90,6 +100,7 @@ class OrganizationEditView extends React.PureComponent {
);
window.location.replace(`${window.location.origin}/dashboard/`);
};
+
handleDeleteButtonClicked = async (): Promise => {
const isDeleteConfirmed = await confirmAsync({
title: (
@@ -124,14 +135,113 @@ class OrganizationEditView extends React.PureComponent {
className="container"
style={{
paddingTop: 20,
+ margin: "auto",
+ maxWidth: 800,
}}
>
+ Your Organization
+
+ {this.state.displayName}
+
+
+ Upgrade Now
+
+ }
+ style={{ marginBottom: 20 }}
+ />
+
+
+
+ Upgrade
+ ,
+ ]}
+ >
+
+
+ Users
+
+
+
+
+ Upgrade
+ ,
+ ]}
+ >
+
+
+ Storage
+
+
+
+
+ Compare Plans
+ ,
+ ]}
+ >
+
+ {this.state.pricingPlan}
+
+ Current Plan
+
+
+
+
+
+ Paid Until December 2022
+
+ }>
+ Extend Now
+
+
+
+
+
+
+
+
+ - Advanced segmentation proof-reading tools
+ - Unlimited users
+ - Custom hosting solutions available
+
+
+
+ }>
+ Upgrade Now
+
+
+
+
Edit {this.state.displayName} }
- style={{
- margin: "auto",
- maxWidth: 800,
- }}
+ title="Settings"
+ style={{ marginBottom: 20 }}
+ headStyle={{ backgroundColor: "rgb(245, 245, 245" }}
>
+
+
+
+
+
+ Delete this organization including all annotations, uploaded datasets, and associated
+ user accounts. Careful, this action can NOT be undone.
+
+
-
-
+
+
);
diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx
index 508df5f6248..3ef6f20dfb1 100644
--- a/frontend/javascripts/router.tsx
+++ b/frontend/javascripts/router.tsx
@@ -455,7 +455,7 @@ class ReactRouter extends React.Component {
/>
(
// @ts-expect-error ts-migrate(2339) FIXME: Property 'organizationName' does not exist on type... Remove this comment to see the full error message
diff --git a/yarn.lock b/yarn.lock
index 12027ff6ccf..3ab2f5d5315 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11613,7 +11613,7 @@ rc-table@~7.26.0:
rc-util "^5.22.5"
shallowequal "^1.1.0"
-rc-tabs@^12.2.2, rc-tabs@~12.2.0:
+rc-tabs@~12.2.0:
version "12.2.2"
resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-12.2.2.tgz#2f2f86399778d45c47e6756536e15c13cd064455"
integrity sha512-Q26L64pjTiiARL+T59Pi9d4lZsUV2mhC8NQEpVK1a8dxXS9KQPY58tNtlx++o6qHRnZVo/sx22v9BqtM9ZKh7g==
From 4b7d5098e6b66f11bdff43733978b16ad1709e79 Mon Sep 17 00:00:00 2001
From: Tom Herold
Date: Wed, 2 Nov 2022 16:27:39 +0100
Subject: [PATCH 02/80] enable access to orga page in navbar avatar menu
---
frontend/javascripts/navbar.tsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx
index 13af9ee8edc..9479703bab5 100644
--- a/frontend/javascripts/navbar.tsx
+++ b/frontend/javascripts/navbar.tsx
@@ -527,6 +527,11 @@ function LoggedInAvatar({
{orgDisplayName}
+ {activeOrganization && Utils.isUserAdmin(activeUser) ? (
+
+ Manage Organization
+
+ ) : null}
{isMultiMember ? (
/* The explicit width is a workaround for a layout bug (probably in antd) */
Date: Wed, 2 Nov 2022 16:38:32 +0100
Subject: [PATCH 03/80] Add pricing plan schema
---
conf/evolutions/091-pricing-plans.sql | 42 +++++++++++++++++++
.../reversions/091-pricing-plans.sql | 40 ++++++++++++++++++
tools/postgres/schema.sql | 7 +++-
3 files changed, 87 insertions(+), 2 deletions(-)
create mode 100644 conf/evolutions/091-pricing-plans.sql
create mode 100644 conf/evolutions/reversions/091-pricing-plans.sql
diff --git a/conf/evolutions/091-pricing-plans.sql b/conf/evolutions/091-pricing-plans.sql
new file mode 100644
index 00000000000..c6a90fb189b
--- /dev/null
+++ b/conf/evolutions/091-pricing-plans.sql
@@ -0,0 +1,42 @@
+
+START TRANSACTION;
+
+ALTER TABLE webknossos.organizations
+ADD paidUntil TIMESTAMPTZ DEFAULT NULL,
+ADD includedUsers INTEGER DEFAULT NULL,
+ADD includedStorage BIGINT DEFAULT NULL;
+
+-- Drop dependent views
+DROP VIEW webknossos.userinfos;
+DROP VIEW webknossos.organizations_;
+
+-- Edit pricing plans enum
+ALTER TYPE webknossos.PRICING_PLANS RENAME TO prizing_plans_old;
+CREATE TYPE webknossos.PRICING_PLANS AS ENUM ('Free', 'Team', 'Power', 'Team-Trial', 'Power-Trial', 'Custom');
+ALTER TABLE webknossos.organizations
+ ALTER COLUMN pricingPLan DROP DEFAULT,
+ ALTER COLUMN pricingPlan TYPE webknossos.PRICING_PLANS USING
+ CASE pricingPlan
+ WHEN 'Basic'::webknossos.prizing_plans_old THEN 'Free'::webknossos.PRICING_PLANS
+ WHEN 'Premium'::webknossos.prizing_plans_old THEN 'Team'::webknossos.PRICING_PLANS
+ WHEN 'Pilot'::webknossos.prizing_plans_old THEN 'Team'::webknossos.PRICING_PLANS
+ ELSE 'Custom'::webknossos.PRICING_PLANS
+ END,
+ ALTER COLUMN pricingPlan SET DEFAULT 'Custom'::webknossos.PRICING_PLANS;
+DROP TYPE webknossos.prizing_plans_old;
+
+-- Recreate views
+CREATE VIEW webknossos.organizations_ AS SELECT * FROM webknossos.organizations WHERE NOT isDeleted;
+CREATE VIEW webknossos.userInfos AS
+SELECT
+ u._id AS _user, m.email, u.firstName, u.lastname, o.displayName AS organization_displayName,
+ u.isDeactivated, u.isDatasetManager, u.isAdmin, m.isSuperUser,
+ u._organization, o.name AS organization_name, u.created AS user_created,
+ m.created AS multiuser_created, u._multiUser, m._lastLoggedInIdentity, u.lastActivity
+FROM webknossos.users_ u
+ JOIN webknossos.organizations_ o ON u._organization = o._id
+ JOIN webknossos.multiUsers_ m on u._multiUser = m._id;
+
+UPDATE webknossos.releaseInformation SET schemaVersion = 91;
+
+COMMIT TRANSACTION;
diff --git a/conf/evolutions/reversions/091-pricing-plans.sql b/conf/evolutions/reversions/091-pricing-plans.sql
new file mode 100644
index 00000000000..749324bc2eb
--- /dev/null
+++ b/conf/evolutions/reversions/091-pricing-plans.sql
@@ -0,0 +1,40 @@
+BEGIN transaction;
+
+-- Drop dependent views
+DROP VIEW webknossos.userinfos;
+DROP VIEW webknossos.organizations_;
+
+ALTER TABLE webknossos.organizations
+ DROP COLUMN paidUntil,
+ DROP COLUMN includedUsers,
+ DROP COLUMN includedStorage;
+
+-- Edit pricing plans enum
+ALTER TYPE webknossos.PRICING_PLANS RENAME TO prizing_plans_old;
+CREATE TYPE webknossos.PRICING_PLANS AS ENUM ('Basic', 'Premium', 'Pilot', 'Custom');
+ALTER TABLE webknossos.organizations
+ ALTER COLUMN pricingPLan DROP DEFAULT,
+ ALTER COLUMN pricingPlan TYPE webknossos.PRICING_PLANS USING
+ CASE pricingPlan
+ WHEN 'Free'::webknossos.prizing_plans_old THEN 'Basic'::webknossos.PRICING_PLANS
+ WHEN 'Team'::webknossos.prizing_plans_old THEN 'Premium'::webknossos.PRICING_PLANS
+ ELSE 'Custom'::webknossos.PRICING_PLANS
+ END,
+ ALTER COLUMN pricingPlan SET DEFAULT 'Custom'::webknossos.PRICING_PLANS;
+DROP TYPE webknossos.prizing_plans_old;
+
+-- Recreate views
+CREATE VIEW webknossos.organizations_ AS SELECT * FROM webknossos.organizations WHERE NOT isDeleted;
+CREATE VIEW webknossos.userInfos AS
+SELECT
+ u._id AS _user, m.email, u.firstName, u.lastname, o.displayName AS organization_displayName,
+ u.isDeactivated, u.isDatasetManager, u.isAdmin, m.isSuperUser,
+ u._organization, o.name AS organization_name, u.created AS user_created,
+ m.created AS multiuser_created, u._multiUser, m._lastLoggedInIdentity, u.lastActivity
+FROM webknossos.users_ u
+ JOIN webknossos.organizations_ o ON u._organization = o._id
+ JOIN webknossos.multiUsers_ m on u._multiUser = m._id;
+
+UPDATE webknossos.releaseInformation SET schemaVersion = 90;
+
+COMMIT;
diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql
index 0402ae83084..db372f1a600 100644
--- a/tools/postgres/schema.sql
+++ b/tools/postgres/schema.sql
@@ -19,7 +19,7 @@ START TRANSACTION;
CREATE TABLE webknossos.releaseInformation (
schemaVersion BIGINT NOT NULL
);
-INSERT INTO webknossos.releaseInformation(schemaVersion) values(90);
+INSERT INTO webknossos.releaseInformation(schemaVersion) values(91);
COMMIT TRANSACTION;
@@ -273,7 +273,7 @@ CREATE TABLE webknossos.timespans(
isDeleted BOOLEAN NOT NULL DEFAULT false
);
-CREATE TYPE webknossos.PRICING_PLANS AS ENUM ('Basic', 'Premium', 'Pilot', 'Custom');
+CREATE TYPE webknossos.PRICING_PLANS AS ENUM ('Free', 'Team', 'Power', 'Team-Trial', 'Power-Trial', 'Custom');
CREATE TABLE webknossos.organizations(
_id CHAR(24) PRIMARY KEY,
name VARCHAR(256) NOT NULL UNIQUE,
@@ -284,6 +284,9 @@ CREATE TABLE webknossos.organizations(
overTimeMailingList VARCHAR(512) NOT NULL DEFAULT '',
enableAutoVerify BOOLEAN NOT NULL DEFAULT false,
pricingPlan webknossos.PRICING_PLANS NOT NULL DEFAULT 'Custom',
+ paidUntil TIMESTAMPTZ DEFAULT NULL,
+ includedUsers INTEGER DEFAULT NULL,
+ includedStorage BIGINT DEFAULT NULL,
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
isDeleted BOOLEAN NOT NULL DEFAULT false
);
From 236948320a5dd267744a131caa67ac57195be957 Mon Sep 17 00:00:00 2001
From: Tom Herold
Date: Wed, 2 Nov 2022 16:50:51 +0100
Subject: [PATCH 04/80] fix navbar links to orga page
---
frontend/javascripts/navbar.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx
index 9479703bab5..1e514dbd8c1 100644
--- a/frontend/javascripts/navbar.tsx
+++ b/frontend/javascripts/navbar.tsx
@@ -243,7 +243,7 @@ function AdministrationSubMenu({
{isAdmin && (
- Organization
+ Organization
)}
{features().voxelyticsEnabled && (
@@ -529,7 +529,7 @@ function LoggedInAvatar({
{activeOrganization && Utils.isUserAdmin(activeUser) ? (
- Manage Organization
+ Manage Organization
) : null}
{isMultiMember ? (
From f8c34b2ada1f51a049656fb11ba2d7d03ad8ad30 Mon Sep 17 00:00:00 2001
From: Tom Herold
Date: Wed, 2 Nov 2022 17:56:04 +0100
Subject: [PATCH 05/80] more pricing stuff
---
frontend/javascripts/admin/admin_rest_api.ts | 5 +-
.../organization/organization_edit_view.tsx | 172 +++++++++++++-----
frontend/javascripts/types/api_flow_types.ts | 3 +
3 files changed, 130 insertions(+), 50 deletions(-)
diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts
index 9a59282d8b8..727fda251cb 100644
--- a/frontend/javascripts/admin/admin_rest_api.ts
+++ b/frontend/javascripts/admin/admin_rest_api.ts
@@ -1874,8 +1874,9 @@ export function sendInvitesForOrganization(
});
}
-export function getOrganization(organizationName: string): Promise {
- return Request.receiveJSON(`/api/organizations/${organizationName}`);
+export async function getOrganization(organizationName: string): Promise {
+ const orga = await Request.receiveJSON(`/api/organizations/${organizationName}`);
+ return Promise.resolve({ ...orga, "paidUntil": 1667403046000, "includedUsers": 5,"includedStorage": 1000000 });
}
export async function checkAnyOrganizationExists(): Promise {
diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx
index 39f1f67b038..a41ed2b688e 100644
--- a/frontend/javascripts/admin/organization/organization_edit_view.tsx
+++ b/frontend/javascripts/admin/organization/organization_edit_view.tsx
@@ -1,5 +1,5 @@
import { RouteComponentProps, withRouter } from "react-router-dom";
-import { Form, Button, Card, Input, Row, FormInstance, Progress, Col, Alert } from "antd";
+import { Form, Button, Card, Input, Row, FormInstance, Progress, Col, Alert, Skeleton } from "antd";
import {
MailOutlined,
TagOutlined,
@@ -16,12 +16,18 @@ import { confirmAsync } from "dashboard/dataset/helper_components";
import { getOrganization, deleteOrganization, updateOrganization } from "admin/admin_rest_api";
import Toast from "libs/toast";
import { coalesce } from "libs/utils";
+import { APIOrganization } from "types/api_flow_types";
+import { useIsFetching } from "@tanstack/react-query";
+import { formatDateInLocalTimeZone } from "components/formatted_date";
+import { formatBytes } from "libs/format_utils";
const FormItem = Form.Item;
export enum PricingPlanEnum {
- Basic = "Basic",
- Premium = "Premium",
- Pilot = "Pilot",
+ Free = "Free",
+ Team = "Team",
+ Power = "Power",
+ "Team-Trial" = "Team-Trial",
+ "Power-Trial" = "Power-Trial",
Custom = "Custom",
}
export type PricingPlan = keyof typeof PricingPlanEnum;
@@ -34,6 +40,7 @@ type State = {
pricingPlan: PricingPlan | null | undefined;
isFetchingData: boolean;
isDeleting: boolean;
+ organization: APIOrganization | null;
};
class OrganizationEditView extends React.PureComponent {
@@ -43,6 +50,7 @@ class OrganizationEditView extends React.PureComponent {
pricingPlan: null,
isFetchingData: false,
isDeleting: false,
+ organization: null,
};
formRef = React.createRef();
@@ -80,14 +88,14 @@ class OrganizationEditView extends React.PureComponent {
this.setState({
isFetchingData: true,
});
- const { displayName, newUserMailingList, pricingPlan } = await getOrganization(
- this.props.organizationName,
- );
+ const organization = await getOrganization(this.props.organizationName);
+ const { displayName, newUserMailingList, pricingPlan } = organization;
this.setState({
displayName,
pricingPlan: coalesce(PricingPlanEnum, pricingPlan),
newUserMailingList,
isFetchingData: false,
+ organization: organization,
});
}
@@ -110,7 +118,7 @@ class OrganizationEditView extends React.PureComponent {
Attention: You will be logged out.
),
- okText: "Yes, Delete Organization now",
+ okText: "Yes, delete this organization now.",
});
if (isDeleteConfirmed) {
@@ -129,7 +137,83 @@ class OrganizationEditView extends React.PureComponent {
Toast.success("Organization name copied to clipboard");
};
+ renderUpgradePlanCard = (): React.ReactNode => {
+ if (this.state.pricingPlan === PricingPlanEnum.Free)
+ return (
+
+
+
+
+
+
+ }>
+ Upgrade Now
+
+
+
+
+ );
+
+ if (
+ this.state.pricingPlan === PricingPlanEnum.Team ||
+ this.state.pricingPlan === PricingPlanEnum["Team-Trial"]
+ )
+ return (
+
+
+
+
+ - Advanced segmentation proof-reading tools
+ - Unlimited users
+ - Custom hosting solutions available
+
+
+
+ }>
+ Upgrade Now
+
+
+
+
+ );
+
+ return null;
+ };
+
render() {
+ if (this.state.isFetchingData || !this.state.organization)
+ return (
+
+
+
+ );
+
+ const OrgaNameRegexPattern = new RegExp("^[A-Za-z0-9\\-_\\. ß]+$");
+ const activeUsers = 6;
+ const usedStorageMB = 1000;
+
+ const usedUsersPercentage = (activeUsers / this.state.organization.includedUsers) * 100;
+ const usedStoragePercentage = (usedStorageMB / this.state.organization.includedStorage) * 100;
+
return (
{
{this.state.displayName}
-
- Upgrade Now
-
- }
- style={{ marginBottom: 20 }}
- />
+ {usedStoragePercentage > 100 || usedUsersPercentage > 100 ? (
+
+ Upgrade Now
+
+ }
+ style={{ marginBottom: 20 }}
+ />
+ ) : null}
{
Users
@@ -185,8 +272,13 @@ class OrganizationEditView extends React.PureComponent {
@@ -209,35 +301,19 @@ class OrganizationEditView extends React.PureComponent {
-
- Paid Until December 2022
-
- }>
- Extend Now
-
-
-
-
-
-
- - Advanced segmentation proof-reading tools
- - Unlimited users
- - Custom hosting solutions available
-
+ Paid Until {formatDateInLocalTimeZone(this.state.organization.paidUntil)}
- }>
- Upgrade Now
+ }>
+ Extend Now
+ {renderUpgradePlanCard()}
+
{
rules={[
{
required: true,
- // @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'RegExp | ... Remove this comment to see the full error message
- pattern: "^[A-Za-z0-9\\-_\\. ß]+$",
+ pattern: OrgaNameRegexPattern,
message:
- "The organization name must not contain any special characters and can not be empty.",
+ "Organization names must not contain any special characters and can not be empty.",
},
]}
>
@@ -295,6 +370,7 @@ class OrganizationEditView extends React.PureComponent {
{
required: false,
type: "email",
+ message: "Please provide a valid email address.",
},
]}
>
diff --git a/frontend/javascripts/types/api_flow_types.ts b/frontend/javascripts/types/api_flow_types.ts
index b6a88ac4b34..104fe450daa 100644
--- a/frontend/javascripts/types/api_flow_types.ts
+++ b/frontend/javascripts/types/api_flow_types.ts
@@ -511,6 +511,9 @@ export type APIOrganization = {
readonly pricingPlan: PricingPlan;
readonly enableAutoVerify: boolean;
readonly newUserMailingList: string;
+ readonly paidUntil: string,
+ readonly includedUsers: number,
+ readonly includedStorage: number // megabytes
};
export type APIBuildInfo = {
webknossos: {
From bdf7474c058e3de622f17af989ec05087cd52b03 Mon Sep 17 00:00:00 2001
From: Tom Herold
Date: Wed, 2 Nov 2022 18:02:43 +0100
Subject: [PATCH 06/80] orga page refactoring
---
.../organization/organization_edit_view.tsx | 70 +++++++++++--------
1 file changed, 41 insertions(+), 29 deletions(-)
diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx
index a41ed2b688e..bdd9a76d170 100644
--- a/frontend/javascripts/admin/organization/organization_edit_view.tsx
+++ b/frontend/javascripts/admin/organization/organization_edit_view.tsx
@@ -192,22 +192,7 @@ class OrganizationEditView extends React.PureComponent {
return null;
};
- render() {
- if (this.state.isFetchingData || !this.state.organization)
- return (
-
-
-
- );
-
- const OrgaNameRegexPattern = new RegExp("^[A-Za-z0-9\\-_\\. ß]+$");
+ renderCurrentPlanCards = (): React.ReactNode => {
const activeUsers = 6;
const usedStorageMB = 1000;
@@ -215,18 +200,7 @@ class OrganizationEditView extends React.PureComponent {
const usedStoragePercentage = (usedStorageMB / this.state.organization.includedStorage) * 100;
return (
-
-
Your Organization
-
- {this.state.displayName}
-
+ <>
{usedStoragePercentage > 100 || usedUsersPercentage > 100 ? (
{
+ >
+ );
+ };
+
+ render() {
+ if (this.state.isFetchingData || !this.state.organization)
+ return (
+
+
+
+ );
+
+ const OrgaNameRegexPattern = new RegExp("^[A-Za-z0-9\\-_\\. ß]+$");
+
+ return (
+
+
Your Organization
+
+ {this.state.displayName}
+
+
+ {this.renderCurrentPlanCards()}
+
@@ -312,7 +323,8 @@ class OrganizationEditView extends React.PureComponent {
- {renderUpgradePlanCard()}
+
+ {this.renderUpgradePlanCard()}
Date: Thu, 3 Nov 2022 09:56:42 +0100
Subject: [PATCH 07/80] Use default user/storage values for different plans
---
conf/evolutions/091-pricing-plans.sql | 3 +++
1 file changed, 3 insertions(+)
diff --git a/conf/evolutions/091-pricing-plans.sql b/conf/evolutions/091-pricing-plans.sql
index c6a90fb189b..b06d307a743 100644
--- a/conf/evolutions/091-pricing-plans.sql
+++ b/conf/evolutions/091-pricing-plans.sql
@@ -25,6 +25,9 @@ ALTER TABLE webknossos.organizations
ALTER COLUMN pricingPlan SET DEFAULT 'Custom'::webknossos.PRICING_PLANS;
DROP TYPE webknossos.prizing_plans_old;
+UPDATE webknossos.organizations SET includedUsers = 3, includedStorage = 5e10 WHERE pricingplan = 'Free'::webknossos.PRICING_PLANS;
+UPDATE webknossos.organizations SET includedUsers = 5, includedStorage = 1e12 WHERE pricingplan = 'Team'::webknossos.PRICING_PLANS;
+
-- Recreate views
CREATE VIEW webknossos.organizations_ AS SELECT * FROM webknossos.organizations WHERE NOT isDeleted;
CREATE VIEW webknossos.userInfos AS
From f2a6b76124047a2983c5222d78c8849b365b1fc5 Mon Sep 17 00:00:00 2001
From: frcroth
Date: Thu, 3 Nov 2022 10:27:38 +0100
Subject: [PATCH 08/80] Add new model fields
---
app/controllers/InitialDataController.scala | 5 +++-
app/models/organization/Organization.scala | 8 ++++++
.../organization/OrganizationService.scala | 25 +++++++++++++------
3 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/app/controllers/InitialDataController.scala b/app/controllers/InitialDataController.scala
index d7307e8239f..c24b7aa8f5e 100644
--- a/app/controllers/InitialDataController.scala
+++ b/app/controllers/InitialDataController.scala
@@ -68,7 +68,10 @@ Samplecountry
additionalInformation,
"/assets/images/oxalis.svg",
"Sample Organization",
- PricingPlan.Custom)
+ PricingPlan.Custom,
+ None,
+ None,
+ None)
private val organizationTeam =
Team(organizationTeamId, defaultOrganization._id, defaultOrganization.name, isOrganizationTeam = true)
private val userId = ObjectId.generate
diff --git a/app/models/organization/Organization.scala b/app/models/organization/Organization.scala
index 58993d92309..3cc87a23dfd 100755
--- a/app/models/organization/Organization.scala
+++ b/app/models/organization/Organization.scala
@@ -3,6 +3,7 @@ package models.organization
import com.scalableminds.util.accesscontext.DBAccessContext
import com.scalableminds.util.tools.Fox
import com.scalableminds.webknossos.schema.Tables._
+
import javax.inject.Inject
import models.team.PricingPlan
import models.team.PricingPlan.PricingPlan
@@ -10,6 +11,7 @@ import slick.jdbc.PostgresProfile.api._
import slick.lifted.Rep
import utils.{ObjectId, SQLClient, SQLDAO}
+import java.sql.Timestamp
import scala.concurrent.ExecutionContext
case class Organization(
@@ -19,6 +21,9 @@ case class Organization(
logoUrl: String,
displayName: String,
pricingPlan: PricingPlan,
+ paidUntil: Option[Timestamp],
+ includedUsers: Option[Int],
+ includedStorage: Option[Long],
newUserMailingList: String = "",
overTimeMailingList: String = "",
enableAutoVerify: Boolean = false,
@@ -45,6 +50,9 @@ class OrganizationDAO @Inject()(sqlClient: SQLClient)(implicit ec: ExecutionCont
r.logourl,
r.displayname,
pricingPlan,
+ r.paiduntil,
+ r.includedusers,
+ r.includedstorage,
r.newusermailinglist,
r.overtimemailinglist,
r.enableautoverify,
diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala
index aa842fdf85d..7980d676ff9 100644
--- a/app/models/organization/OrganizationService.scala
+++ b/app/models/organization/OrganizationService.scala
@@ -27,7 +27,10 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
val adminOnlyInfo = if (requestingUser.exists(_.isAdminOf(organization._id))) {
Json.obj(
"newUserMailingList" -> organization.newUserMailingList,
- "pricingPlan" -> organization.pricingPlan
+ "pricingPlan" -> organization.pricingPlan,
+ "paidUntil" -> organization.paidUntil,
+ "includedUsers" -> organization.includedUsers,
+ "includedStorage" -> organization.includedStorage
)
} else Json.obj()
Fox.successful(
@@ -76,13 +79,19 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
.replaceAll(" ", "_")
existingOrganization <- organizationDAO.findOneByName(organizationName)(GlobalAccessContext).futureBox
_ <- bool2Fox(existingOrganization.isEmpty) ?~> "organization.name.alreadyInUse"
- initialPricingPlan = if (conf.Features.isDemoInstance) PricingPlan.Basic else PricingPlan.Custom
- organization = Organization(ObjectId.generate,
- organizationName,
- "",
- "",
- organizationDisplayName,
- initialPricingPlan)
+ initialPricingParameters = if (conf.Features.isDemoInstance) (PricingPlan.Basic, Some(3), Some(50000000000L))
+ else (PricingPlan.Custom, None, None)
+ organization = Organization(
+ ObjectId.generate,
+ organizationName,
+ "",
+ "",
+ organizationDisplayName,
+ initialPricingParameters._1,
+ None,
+ initialPricingParameters._2,
+ initialPricingParameters._3
+ )
organizationTeam = Team(ObjectId.generate, organization._id, "Default", isOrganizationTeam = true)
_ <- organizationDAO.insertOne(organization)
_ <- teamDAO.insertOne(organizationTeam)
From ef4a854fd57e83750de607bd9a065c5adb75fe02 Mon Sep 17 00:00:00 2001
From: frcroth
Date: Thu, 3 Nov 2022 11:14:35 +0100
Subject: [PATCH 09/80] Assert user count does not exceed includedUsers when
joining org
---
app/controllers/AuthenticationController.scala | 6 ++++++
app/models/organization/OrganizationService.scala | 13 ++++++++++++-
conf/messages | 1 +
3 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala
index 93ee19c12be..80a8f8174ea 100755
--- a/app/controllers/AuthenticationController.scala
+++ b/app/controllers/AuthenticationController.scala
@@ -97,6 +97,8 @@ class AuthenticationController @Inject()(
organization <- organizationService.findOneByInviteByNameOrDefault(
inviteBox.toOption,
organizationName)(GlobalAccessContext) ?~> Messages("organization.notFound", signUpData.organization)
+ _ <- organizationService
+ .assertUsersCanBeAdded(organization)(GlobalAccessContext, ec) ?~> "organization.users.userLimitReached"
autoActivate = inviteBox.toOption.map(_.autoActivate).getOrElse(organization.enableAutoVerify)
user <- userService.insert(organization._id,
email,
@@ -310,6 +312,10 @@ class AuthenticationController @Inject()(
invite <- inviteDAO.findOneByTokenValue(inviteToken) ?~> "invite.invalidToken"
organization <- organizationDAO.findOne(invite._organization)(GlobalAccessContext) ?~> "invite.invalidToken"
_ <- userService.assertNotInOrgaYet(request.identity._multiUser, organization._id)
+ requestingMultiUser <- multiUserDAO.findOne(request.identity._multiUser)
+ _ <- Fox.runIf(!requestingMultiUser.isSuperUser)(
+ organizationService
+ .assertUsersCanBeAdded(organization)(GlobalAccessContext, ec)) ?~> "organization.users.userLimitReached"
_ <- userService.joinOrganization(request.identity, organization._id, autoActivate = invite.autoActivate)
_ = analyticsService.track(JoinOrganizationEvent(request.identity, organization))
userEmail <- userService.emailFor(request.identity)
diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala
index 7980d676ff9..27792ccd07f 100644
--- a/app/models/organization/OrganizationService.scala
+++ b/app/models/organization/OrganizationService.scala
@@ -4,10 +4,11 @@ import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContex
import com.scalableminds.util.tools.{Fox, FoxImplicits, TextUtils}
import com.scalableminds.webknossos.datastore.rpc.RPC
import controllers.InitialDataService
+
import javax.inject.Inject
import models.binary.{DataStore, DataStoreDAO}
import models.team.{PricingPlan, Team, TeamDAO}
-import models.user.{Invite, MultiUserDAO, User}
+import models.user.{Invite, MultiUserDAO, User, UserDAO}
import play.api.libs.json.{JsObject, Json}
import utils.{ObjectId, WkConf}
@@ -15,6 +16,7 @@ import scala.concurrent.{ExecutionContext, Future}
class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
multiUserDAO: MultiUserDAO,
+ userDAO: UserDAO,
teamDAO: TeamDAO,
dataStoreDAO: DataStoreDAO,
rpc: RPC,
@@ -111,4 +113,13 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
} yield ()
}
+ def assertUsersCanBeAdded(organization: Organization, usersToAddCount: Int = 1)(implicit ctx: DBAccessContext,
+ ec: ExecutionContext): Fox[Unit] =
+ for {
+ _ <- organizationDAO.findOne(organization._id)
+ userCount <- userDAO.countAllForOrganization(organization._id)
+ _ <- Fox.runOptional(organization.includedUsers)(includedUsers =>
+ bool2Fox(userCount + usersToAddCount <= includedUsers))
+ } yield ()
+
}
diff --git a/conf/messages b/conf/messages
index 3f96e782cfb..85aff95646b 100644
--- a/conf/messages
+++ b/conf/messages
@@ -54,6 +54,7 @@ organization.list.failed=Failed to retrieve list of organizations.
organization.name.invalid=The chosen organization name is invalid.
organization.name.alreadyInUse=The chosen organization name is already in use.
organization.alreadyJoined=Your account is already associated with the selected organization.
+organization.users.userLimitReached=Can not add user because it would exceed the limit of included users in this plan.
user.activated={0} has been activated
user.notFound=User not found
From 9ddcaf569b5d0090159a6dcc4450488633e4bbeb Mon Sep 17 00:00:00 2001
From: frcroth
Date: Thu, 3 Nov 2022 11:22:54 +0100
Subject: [PATCH 10/80] Pass incudedStorage in MB
---
app/models/organization/OrganizationService.scala | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala
index 27792ccd07f..4dae0aa36a0 100644
--- a/app/models/organization/OrganizationService.scala
+++ b/app/models/organization/OrganizationService.scala
@@ -32,7 +32,7 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
"pricingPlan" -> organization.pricingPlan,
"paidUntil" -> organization.paidUntil,
"includedUsers" -> organization.includedUsers,
- "includedStorage" -> organization.includedStorage
+ "includedStorage" -> organization.includedStorage.map(bytes => bytes / 1000000)
)
} else Json.obj()
Fox.successful(
From 2a8fa902e4d1ce4f36959a137bf1837567daf1e4 Mon Sep 17 00:00:00 2001
From: Tom Herold
Date: Thu, 3 Nov 2022 11:58:21 +0100
Subject: [PATCH 11/80] fix deprecation warning
---
frontend/javascripts/oxalis/view/help_modal.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/javascripts/oxalis/view/help_modal.tsx b/frontend/javascripts/oxalis/view/help_modal.tsx
index 8db15dabd79..e60617e5d7a 100644
--- a/frontend/javascripts/oxalis/view/help_modal.tsx
+++ b/frontend/javascripts/oxalis/view/help_modal.tsx
@@ -80,7 +80,7 @@ export function HelpModal(props: HelpModalProps) {
Date: Thu, 3 Nov 2022 12:00:36 +0100
Subject: [PATCH 12/80] refactor orga view into sub-components
---
frontend/javascripts/admin/admin_rest_api.ts | 7 +-
.../admin/organization/organization_cards.tsx | 168 +++++++++++++++
.../organization/organization_edit_view.tsx | 196 ++----------------
frontend/javascripts/types/api_flow_types.ts | 6 +-
4 files changed, 191 insertions(+), 186 deletions(-)
create mode 100644 frontend/javascripts/admin/organization/organization_cards.tsx
diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts
index 727fda251cb..81eb233d9ca 100644
--- a/frontend/javascripts/admin/admin_rest_api.ts
+++ b/frontend/javascripts/admin/admin_rest_api.ts
@@ -1876,7 +1876,12 @@ export function sendInvitesForOrganization(
export async function getOrganization(organizationName: string): Promise {
const orga = await Request.receiveJSON(`/api/organizations/${organizationName}`);
- return Promise.resolve({ ...orga, "paidUntil": 1667403046000, "includedUsers": 5,"includedStorage": 1000000 });
+ return Promise.resolve({
+ ...orga,
+ paidUntil: 1667403046000,
+ includedUsers: 5,
+ includedStorage: 1000000,
+ });
}
export async function checkAnyOrganizationExists(): Promise {
diff --git a/frontend/javascripts/admin/organization/organization_cards.tsx b/frontend/javascripts/admin/organization/organization_cards.tsx
new file mode 100644
index 00000000000..39f7ae24c72
--- /dev/null
+++ b/frontend/javascripts/admin/organization/organization_cards.tsx
@@ -0,0 +1,168 @@
+import {
+ FieldTimeOutlined,
+ PlusCircleOutlined,
+ RocketOutlined,
+ SafetyOutlined,
+} from "@ant-design/icons";
+import { Alert, Button, Card, Col, Progress, Row } from "antd";
+import { formatDateInLocalTimeZone } from "components/formatted_date";
+import { formatBytes } from "libs/format_utils";
+import React from "react";
+import { APIOrganization } from "types/api_flow_types";
+import { PricingPlanEnum } from "./organization_edit_view";
+
+export function PlanUpgradeCard({ organization }: { organization: APIOrganization }) {
+ if (organization.pricingPlan === PricingPlanEnum.Free)
+ return (
+
+
+
+
+
+
+ }>
+ Upgrade Now
+
+
+
+
+ );
+
+ if (
+ organization.pricingPlan === PricingPlanEnum.Team ||
+ organization.pricingPlan === PricingPlanEnum["Team-Trial"]
+ )
+ return (
+
+
+
+
+ - Advanced segmentation proof-reading tools
+ - Unlimited users
+ - Custom hosting solutions available
+
+
+
+ }>
+ Upgrade Now
+
+
+
+
+ );
+
+ return null;
+}
+
+export function PlanExpirationCard({ organization }: { organization: APIOrganization }) {
+ return (
+
+
+ Paid Until {formatDateInLocalTimeZone(organization.paidUntil)}
+
+ }>
+ Extend Now
+
+
+
+
+ );
+}
+
+export function PlanDashboardCard({ organization }: { organization: APIOrganization }) {
+ const activeUsers = 6;
+ const usedStorageMB = 1000;
+
+ const usedUsersPercentage = (activeUsers / organization.includedUsers) * 100;
+ const usedStoragePercentage = (usedStorageMB / organization.includedStorage) * 100;
+
+ return (
+ <>
+ {usedStoragePercentage > 100 || usedUsersPercentage > 100 ? (
+
+ Upgrade Now
+
+ }
+ style={{ marginBottom: 20 }}
+ />
+ ) : null}
+
+
+
+ Upgrade
+ ,
+ ]}
+ >
+
+
+ Users
+
+
+
+
+ Upgrade
+ ,
+ ]}
+ >
+
+
+ Storage
+
+
+
+
+ Compare Plans
+ ,
+ ]}
+ >
+
+ {organization.pricingPlan}
+
+ Current Plan
+
+
+
+ >
+ );
+}
diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx
index bdd9a76d170..d9a86b2773d 100644
--- a/frontend/javascripts/admin/organization/organization_edit_view.tsx
+++ b/frontend/javascripts/admin/organization/organization_edit_view.tsx
@@ -1,15 +1,11 @@
import { RouteComponentProps, withRouter } from "react-router-dom";
-import { Form, Button, Card, Input, Row, FormInstance, Progress, Col, Alert, Skeleton } from "antd";
+import { Form, Button, Card, Input, Row, FormInstance, Col, Skeleton } from "antd";
import {
MailOutlined,
TagOutlined,
CopyOutlined,
KeyOutlined,
- PlusCircleOutlined,
- SafetyOutlined,
- FieldTimeOutlined,
SaveOutlined,
- RocketOutlined,
} from "@ant-design/icons";
import React from "react";
import { confirmAsync } from "dashboard/dataset/helper_components";
@@ -17,9 +13,7 @@ import { getOrganization, deleteOrganization, updateOrganization } from "admin/a
import Toast from "libs/toast";
import { coalesce } from "libs/utils";
import { APIOrganization } from "types/api_flow_types";
-import { useIsFetching } from "@tanstack/react-query";
-import { formatDateInLocalTimeZone } from "components/formatted_date";
-import { formatBytes } from "libs/format_utils";
+import { PlanDashboardCard, PlanExpirationCard, PlanUpgradeCard } from "./organization_cards";
const FormItem = Form.Item;
export enum PricingPlanEnum {
@@ -132,154 +126,14 @@ class OrganizationEditView extends React.PureComponent {
window.location.replace(`${window.location.origin}/dashboard`);
}
};
+
handleCopyNameButtonClicked = async (): Promise => {
await navigator.clipboard.writeText(this.props.organizationName);
Toast.success("Organization name copied to clipboard");
};
- renderUpgradePlanCard = (): React.ReactNode => {
- if (this.state.pricingPlan === PricingPlanEnum.Free)
- return (
-
-
-
-
-
-
- }>
- Upgrade Now
-
-
-
-
- );
-
- if (
- this.state.pricingPlan === PricingPlanEnum.Team ||
- this.state.pricingPlan === PricingPlanEnum["Team-Trial"]
- )
- return (
-
-
-
-
- - Advanced segmentation proof-reading tools
- - Unlimited users
- - Custom hosting solutions available
-
-
-
- }>
- Upgrade Now
-
-
-
-
- );
-
- return null;
- };
-
- renderCurrentPlanCards = (): React.ReactNode => {
- const activeUsers = 6;
- const usedStorageMB = 1000;
-
- const usedUsersPercentage = (activeUsers / this.state.organization.includedUsers) * 100;
- const usedStoragePercentage = (usedStorageMB / this.state.organization.includedStorage) * 100;
-
- return (
- <>
- {usedStoragePercentage > 100 || usedUsersPercentage > 100 ? (
-
- Upgrade Now
-
- }
- style={{ marginBottom: 20 }}
- />
- ) : null}
-
-
-
- Upgrade
- ,
- ]}
- >
-
-
- Users
-
-
-
-
- Upgrade
- ,
- ]}
- >
-
-
- Storage
-
-
-
-
- Compare Plans
- ,
- ]}
- >
-
- {this.state.pricingPlan}
-
- Current Plan
-
-
-
- >
- );
- };
-
render() {
- if (this.state.isFetchingData || !this.state.organization)
+ if (this.state.isFetchingData || !this.state.organization || !this.state.pricingPlan)
return (
{
{this.state.displayName}
-
- {this.renderCurrentPlanCards()}
-
-
-
-
- Paid Until {formatDateInLocalTimeZone(this.state.organization.paidUntil)}
-
-
- }>
- Extend Now
-
-
-
-
-
- {this.renderUpgradePlanCard()}
-
+
+
+
{
placeholder="mail@example.com"
/>
- }
>
- }
- >
- Save
-
-
+ Save
+
-
Date: Thu, 3 Nov 2022 13:39:04 +0100
Subject: [PATCH 13/80] adapt enum in scala
---
app/models/organization/OrganizationService.scala | 2 +-
app/models/team/PricingPlan.scala | 2 +-
tools/postgres/schema.sql | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala
index 7980d676ff9..196886cbcbf 100644
--- a/app/models/organization/OrganizationService.scala
+++ b/app/models/organization/OrganizationService.scala
@@ -79,7 +79,7 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO,
.replaceAll(" ", "_")
existingOrganization <- organizationDAO.findOneByName(organizationName)(GlobalAccessContext).futureBox
_ <- bool2Fox(existingOrganization.isEmpty) ?~> "organization.name.alreadyInUse"
- initialPricingParameters = if (conf.Features.isDemoInstance) (PricingPlan.Basic, Some(3), Some(50000000000L))
+ initialPricingParameters = if (conf.Features.isDemoInstance) (PricingPlan.Free, Some(3), Some(50000000000L))
else (PricingPlan.Custom, None, None)
organization = Organization(
ObjectId.generate,
diff --git a/app/models/team/PricingPlan.scala b/app/models/team/PricingPlan.scala
index 734bd28288b..1d4be4ece5a 100644
--- a/app/models/team/PricingPlan.scala
+++ b/app/models/team/PricingPlan.scala
@@ -4,5 +4,5 @@ import com.scalableminds.util.enumeration.ExtendedEnumeration
object PricingPlan extends ExtendedEnumeration {
type PricingPlan = Value
- val Basic, Premium, Pilot, Custom = Value
+ val Free, Team, Power, Team_Trial, Power_Trial, Custom = Value
}
diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql
index db372f1a600..df47dddcc18 100644
--- a/tools/postgres/schema.sql
+++ b/tools/postgres/schema.sql
@@ -273,7 +273,7 @@ CREATE TABLE webknossos.timespans(
isDeleted BOOLEAN NOT NULL DEFAULT false
);
-CREATE TYPE webknossos.PRICING_PLANS AS ENUM ('Free', 'Team', 'Power', 'Team-Trial', 'Power-Trial', 'Custom');
+CREATE TYPE webknossos.PRICING_PLANS AS ENUM ('Free', 'Team', 'Power', 'Team_Trial', 'Power_Trial', 'Custom');
CREATE TABLE webknossos.organizations(
_id CHAR(24) PRIMARY KEY,
name VARCHAR(256) NOT NULL UNIQUE,
From 74b595146cbbca5a9379cbf0d0b0ad0164b5ac41 Mon Sep 17 00:00:00 2001
From: Tom Herold
Date: Thu, 3 Nov 2022 15:49:35 +0100
Subject: [PATCH 14/80] many tweaks to orga view
---
frontend/javascripts/admin/admin_rest_api.ts | 10 +-
.../admin/organization/organization_cards.tsx | 117 +++++++++---------
.../organization/organization_edit_view.tsx | 7 +-
.../admin/organization/upgrade_plan_modal.tsx | 27 ++++
frontend/javascripts/types/api_flow_types.ts | 4 +-
5 files changed, 90 insertions(+), 75 deletions(-)
create mode 100644 frontend/javascripts/admin/organization/upgrade_plan_modal.tsx
diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts
index 81eb233d9ca..9a59282d8b8 100644
--- a/frontend/javascripts/admin/admin_rest_api.ts
+++ b/frontend/javascripts/admin/admin_rest_api.ts
@@ -1874,14 +1874,8 @@ export function sendInvitesForOrganization(
});
}
-export async function getOrganization(organizationName: string): Promise {
- const orga = await Request.receiveJSON(`/api/organizations/${organizationName}`);
- return Promise.resolve({
- ...orga,
- paidUntil: 1667403046000,
- includedUsers: 5,
- includedStorage: 1000000,
- });
+export function getOrganization(organizationName: string): Promise {
+ return Request.receiveJSON(`/api/organizations/${organizationName}`);
}
export async function checkAnyOrganizationExists(): Promise {
diff --git a/frontend/javascripts/admin/organization/organization_cards.tsx b/frontend/javascripts/admin/organization/organization_cards.tsx
index 39f7ae24c72..f19de8c1a5a 100644
--- a/frontend/javascripts/admin/organization/organization_cards.tsx
+++ b/frontend/javascripts/admin/organization/organization_cards.tsx
@@ -6,71 +6,62 @@ import {
} from "@ant-design/icons";
import { Alert, Button, Card, Col, Progress, Row } from "antd";
import { formatDateInLocalTimeZone } from "components/formatted_date";
-import { formatBytes } from "libs/format_utils";
import React from "react";
import { APIOrganization } from "types/api_flow_types";
import { PricingPlanEnum } from "./organization_edit_view";
+import UpgradePricingPlanModal from "./upgrade_plan_modal";
export function PlanUpgradeCard({ organization }: { organization: APIOrganization }) {
- if (organization.pricingPlan === PricingPlanEnum.Free)
- return (
-
-
-
-
-
-
- }>
- Upgrade Now
-
-
-
-
- );
+ if (
+ organization.pricingPlan === PricingPlanEnum.Power ||
+ organization.pricingPlan === PricingPlanEnum.PowerTrial ||
+ organization.pricingPlan === PricingPlanEnum.Custom
+ )
+ return null;
+
+ let title = `Upgrade to ${PricingPlanEnum.Team} Plan`;
+ let featureDescriptions = ["TODO", "TODO", "TODO"];
+ let onOkCallback = UpgradePricingPlanModal.open;
if (
organization.pricingPlan === PricingPlanEnum.Team ||
- organization.pricingPlan === PricingPlanEnum["Team-Trial"]
- )
- return (
-
-
-
-
- - Advanced segmentation proof-reading tools
- - Unlimited users
- - Custom hosting solutions available
-
-
-
- }>
- Upgrade Now
-
-
-
-
- );
+ organization.pricingPlan === PricingPlanEnum.TeamTrial
+ ) {
+ let title = `Upgrade to ${PricingPlanEnum.Power} Plan`;
+ let featureDescriptions = ["TODO", "TODO", "TODO"];
+ }
- return null;
+ return (
+
+
+
+
+ {featureDescriptions.map((feature) => (
+ - {feature}
+ ))}
+
+
+
+ } onClick={onOkCallback}>
+ Upgrade Now
+
+
+
+
+ );
}
export function PlanExpirationCard({ organization }: { organization: APIOrganization }) {
return (
- Paid Until {formatDateInLocalTimeZone(organization.paidUntil)}
+
+ Paid Until {formatDateInLocalTimeZone(organization.paidUntil, "YYYY-MM-DD")}
+
}>
Extend Now
@@ -82,12 +73,20 @@ export function PlanExpirationCard({ organization }: { organization: APIOrganiza
}
export function PlanDashboardCard({ organization }: { organization: APIOrganization }) {
- const activeUsers = 6;
- const usedStorageMB = 1000;
+ const activeUsers = 3;
+ const usedStorageMB = 900;
const usedUsersPercentage = (activeUsers / organization.includedUsers) * 100;
const usedStoragePercentage = (usedStorageMB / organization.includedStorage) * 100;
+ const usedStorageLabel =
+ organization.pricingPlan == PricingPlanEnum.Free
+ ? `${usedStorageMB / 1000}/${organization.includedStorage / 1000}GB`
+ : `${usedStorageMB / 1000 ** 2}/${organization.includedStorage / 1000 ** 2}TB`;
+
+ const redStrokeColor = "#ff4d4f";
+ const greenStrokeColor = "#52c41a";
+
return (
<>
{usedStoragePercentage > 100 || usedUsersPercentage > 100 ? (
@@ -117,8 +116,8 @@ export function PlanDashboardCard({ organization }: { organization: APIOrganizat
type="dashboard"
percent={usedUsersPercentage}
format={() => `${activeUsers}/${organization.includedUsers}`}
- success={{ strokeColor: "#ff4d4f !important" }}
- style={{ color: usedUsersPercentage > 100 ? "#ff4d4f !important" : "inherit" }}
+ strokeColor={usedUsersPercentage > 100 ? redStrokeColor : greenStrokeColor}
+ status={usedUsersPercentage > 100 ? "exception" : "active"}
/>
Users
@@ -136,13 +135,9 @@ export function PlanDashboardCard({ organization }: { organization: APIOrganizat