From 4cd2ecd40d539e07ff9ce47dd4ce9de4e196bf66 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 6 Jun 2025 14:58:04 +0200 Subject: [PATCH 01/17] refactor orga settings in new style --- .../admin/organization/organization_cards.tsx | 46 +--- .../organization_dangerzone_view.tsx | 55 ++++ .../organization/organization_edit_view.tsx | 246 ------------------ .../organization/organization_profile.tsx | 123 +++++++++ .../organization/organization_settings.tsx | 64 +++++ .../admin/organization/organization_view.tsx | 105 ++++++++ frontend/javascripts/navbar.tsx | 4 +- frontend/javascripts/router.tsx | 14 +- 8 files changed, 372 insertions(+), 285 deletions(-) create mode 100644 frontend/javascripts/admin/organization/organization_dangerzone_view.tsx delete mode 100644 frontend/javascripts/admin/organization/organization_edit_view.tsx create mode 100644 frontend/javascripts/admin/organization/organization_profile.tsx create mode 100644 frontend/javascripts/admin/organization/organization_settings.tsx create mode 100644 frontend/javascripts/admin/organization/organization_view.tsx diff --git a/frontend/javascripts/admin/organization/organization_cards.tsx b/frontend/javascripts/admin/organization/organization_cards.tsx index ec5b9adf43f..1f9320785f5 100644 --- a/frontend/javascripts/admin/organization/organization_cards.tsx +++ b/frontend/javascripts/admin/organization/organization_cards.tsx @@ -76,26 +76,13 @@ export function PlanUpgradeCard({ organization }: { organization: APIOrganizatio organization.pricingPlan === PricingPlanEnum.PowerTrial || organization.pricingPlan === PricingPlanEnum.Custom ) - return null; - - let title = "Upgrade to unlock more features"; - let cardBody = ( - - UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Team) - } - powerUpgradeCallback={() => - UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Power) - } - /> - ); + return "TODO"; if ( organization.pricingPlan === PricingPlanEnum.Team || organization.pricingPlan === PricingPlanEnum.TeamTrial ) { - title = `Upgrade to ${PricingPlanEnum.Power} Plan`; - cardBody = ( + return (

@@ -125,24 +112,14 @@ export function PlanUpgradeCard({ organization }: { organization: APIOrganizatio } return ( - -

- Upgrading your WEBKNOSSOS plan will unlock more advanced features and increase your user and - storage quotas. -

- {cardBody} - + + UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Team) + } + powerUpgradeCallback={() => + UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Power) + } + /> ); } @@ -150,7 +127,7 @@ export function PlanExpirationCard({ organization }: { organization: APIOrganiza if (organization.paidUntil === Constants.MAXIMUM_DATE_TIMESTAMP) return null; return ( - + Your current plan is paid until{" "} @@ -170,6 +147,7 @@ export function PlanExpirationCard({ organization }: { organization: APIOrganiza ); } +// TODO: Consider removing this card export function PlanDashboardCard({ organization, activeUsersCount, diff --git a/frontend/javascripts/admin/organization/organization_dangerzone_view.tsx b/frontend/javascripts/admin/organization/organization_dangerzone_view.tsx new file mode 100644 index 00000000000..8f14190dbc2 --- /dev/null +++ b/frontend/javascripts/admin/organization/organization_dangerzone_view.tsx @@ -0,0 +1,55 @@ +import { AccountSettingsTitle } from "admin/account/account_profile_view"; +import { deleteOrganization } from "admin/rest_api"; +import { Button, Typography } from "antd"; +import { confirmAsync } from "dashboard/dataset/helper_components"; +import { useState } from "react"; +import type { APIOrganization } from "types/api_types"; + +export function OrganizationDangerZoneView({ organization }: { organization: APIOrganization }) { + const [isDeleting, setIsDeleting] = useState(false); + + async function handleDeleteButtonClicked(): Promise { + const isDeleteConfirmed = await confirmAsync({ + title: "Danger Zone", + content: ( +
+ + You will lose access to all the datasets and annotations uploaded/created as part of + this organization! + + + Unless you are part of another WEBKNOSSOS organization, you can NOT login again with + this account and will lose access to WEBKNOSSOS. + +

+ Deleting an organization{" "} + cannot be undone. Are you certain you + want to delete the organization {organization.name}? +

+
+ ), + okText: <>Yes, delete this organization now and log me out., + okType: "danger", + width: 500, + }); + + if (isDeleteConfirmed) { + setIsDeleting(true); + await deleteOrganization(organization.id); + setIsDeleting(false); + window.location.replace(`${window.location.origin}/dashboard`); + } + } + return ( + <> + + + + ); +} diff --git a/frontend/javascripts/admin/organization/organization_edit_view.tsx b/frontend/javascripts/admin/organization/organization_edit_view.tsx deleted file mode 100644 index a96dfcd7ebd..00000000000 --- a/frontend/javascripts/admin/organization/organization_edit_view.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { - CopyOutlined, - IdcardOutlined, - MailOutlined, - SaveOutlined, - TagOutlined, - UserOutlined, -} from "@ant-design/icons"; -import { - deleteOrganization, - getPricingPlanStatus, - getUsers, - updateOrganization, -} from "admin/rest_api"; -import { Button, Card, Col, Form, Input, Row, Skeleton, Space, Typography } from "antd"; -import { confirmAsync } from "dashboard/dataset/helper_components"; -import Toast from "libs/toast"; -import { useEffect, useState } from "react"; -import { connect } from "react-redux"; -import type { APIOrganization, APIPricingPlanStatus } from "types/api_types"; -import { enforceActiveOrganization } from "viewer/model/accessors/organization_accessors"; -import type { WebknossosState } from "viewer/store"; -import { - PlanAboutToExceedAlert, - PlanDashboardCard, - PlanExceededAlert, - PlanExpirationCard, - PlanUpgradeCard, -} from "./organization_cards"; -import { getActiveUserCount } from "./pricing_plan_utils"; - -const FormItem = Form.Item; - -type Props = { - organization: APIOrganization; -}; - -type FormValues = { - displayName: string; - newUserMailingList: string; -}; - -const OrganizationEditView = ({ organization }: Props) => { - const [isFetchingData, setIsFetchingData] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [activeUsersCount, setActiveUsersCount] = useState(1); - const [pricingPlanStatus, setPricingPlanStatus] = useState(null); - - const [form] = Form.useForm(); - - useEffect(() => { - fetchData(); - }, []); - - async function fetchData() { - setIsFetchingData(true); - const [users, pricingPlanStatus] = await Promise.all([getUsers(), getPricingPlanStatus()]); - - setPricingPlanStatus(pricingPlanStatus); - setActiveUsersCount(getActiveUserCount(users)); - setIsFetchingData(false); - } - - async function onFinish(formValues: FormValues) { - await updateOrganization( - organization.id, - formValues.displayName, - formValues.newUserMailingList, - ); - Toast.success("Organization settings were saved successfully."); - } - - async function handleDeleteButtonClicked(): Promise { - const isDeleteConfirmed = await confirmAsync({ - title: "Danger Zone", - content: ( -
- - You will lose access to all the datasets and annotations uploaded/created as part of - this organization! - - - Unless you are part of another WEBKNOSSOS organization, you can NOT login again with - this account and will lose access to WEBKNOSSOS. - -

- Deleting an organization{" "} - cannot be undone. Are you certain you - want to delete the organization {organization.name}? -

-
- ), - okText: <>Yes, delete this organization now and log me out., - okType: "danger", - width: 500, - }); - - if (isDeleteConfirmed) { - setIsDeleting(true); - await deleteOrganization(organization.id); - setIsDeleting(false); - window.location.replace(`${window.location.origin}/dashboard`); - } - } - - async function handleCopyNameButtonClicked(): Promise { - await navigator.clipboard.writeText(organization.id); - Toast.success("Copied organization name to the clipboard."); - } - - const OrgaNameRegexPattern = /^[A-Za-z0-9\\-_\\. ß]+$/; - - if (isFetchingData || !organization || !pricingPlanStatus) - return ( -
- -
- ); - - return ( -
- Your Organization - -

{organization.name}

-
- {pricingPlanStatus.isExceeded ? : null} - {pricingPlanStatus.isAlmostExceeded && !pricingPlanStatus.isExceeded ? ( - - ) : null} - - - - -
- - - } - value={organization.id} - style={{ - width: "calc(100% - 31px)", - }} - readOnly - disabled - /> - - -
- - - - Delete this organization including all annotations, uploaded datasets, and associated - user accounts. Careful, this action can NOT be undone. - - - - - - -
- ); -}; - -const mapStateToProps = (state: WebknossosState): Props => ({ - organization: enforceActiveOrganization(state.activeOrganization), -}); -export default connect(mapStateToProps)(OrganizationEditView); diff --git a/frontend/javascripts/admin/organization/organization_profile.tsx b/frontend/javascripts/admin/organization/organization_profile.tsx new file mode 100644 index 00000000000..f150814898b --- /dev/null +++ b/frontend/javascripts/admin/organization/organization_profile.tsx @@ -0,0 +1,123 @@ +import type { APIOrganization, APIPricingPlanStatus } from "types/api_types"; +import { + PlanAboutToExceedAlert, + PlanExceededAlert, + PlanExpirationCard, + PlanUpgradeCard, +} from "./organization_cards"; +import { useEffect, useState } from "react"; +import { getPricingPlanStatus, getUsers } from "admin/rest_api"; +import { + getActiveUserCount, + hasPricingPlanExceededStorage, + hasPricingPlanExceededUsers, +} from "./pricing_plan_utils"; +import { Card, Row, Col, Spin, Typography } from "antd"; +import { AccountSettingsTitle } from "admin/account/account_profile_view"; +import { formatCountToDataAmountUnit } from "libs/format_utils"; + +export function OrganizationProfileView({ organization }: { organization: APIOrganization }) { + const [isFetchingData, setIsFetchingData] = useState(false); + const [activeUsersCount, setActiveUsersCount] = useState(1); + const [pricingPlanStatus, setPricingPlanStatus] = useState(null); + + useEffect(() => { + fetchData(); + }, []); + + async function fetchData() { + setIsFetchingData(true); + const [users, pricingPlanStatus] = await Promise.all([getUsers(), getPricingPlanStatus()]); + + setPricingPlanStatus(pricingPlanStatus); + setActiveUsersCount(getActiveUserCount(users)); + setIsFetchingData(false); + } + + const hasExceededUserLimit = hasPricingPlanExceededUsers(organization, activeUsersCount); + const hasExceededStorageLimit = hasPricingPlanExceededStorage(organization); + + const maxUsersCountLabel = + organization.includedUsers === Number.POSITIVE_INFINITY ? "∞" : organization.includedUsers; + + const includedStorageLabel = + organization.includedStorageBytes === Number.POSITIVE_INFINITY + ? "∞" + : formatCountToDataAmountUnit(organization.includedStorageBytes, true); + + const usedStorageLabel = formatCountToDataAmountUnit(organization.usedStorageBytes, true); + + const firstRowStats = [ + { + key: "owner", + title: "Owner", + value: "John Doe", + }, + { + key: "plan", + title: "Current Plan", + value: "Basic", + }, + ]; + + const secondRowStats = [ + { + key: "users", + title: "Users", + value: `${activeUsersCount}/${maxUsersCountLabel}`, + }, + { + key: "storage", + title: "Storage", + value: `${usedStorageLabel} / ${includedStorageLabel}`, + }, + + { + key: "credits", + title: "WEBKNOSSOS Credits", + value: "2", + }, + ]; + + return ( + <> + + {pricingPlanStatus?.isExceeded ? : null} + {pricingPlanStatus?.isAlmostExceeded && !pricingPlanStatus.isExceeded ? ( + + ) : null} + + + {firstRowStats.map((stat) => ( + + + + {stat.title} + +
{stat.value}
+
+ + ))} +
+ + {secondRowStats.map((stat) => ( + + + + {stat.title} + +
{stat.value}
+
+ + ))} +
+
+ + + + + ); +} diff --git a/frontend/javascripts/admin/organization/organization_settings.tsx b/frontend/javascripts/admin/organization/organization_settings.tsx new file mode 100644 index 00000000000..13318375964 --- /dev/null +++ b/frontend/javascripts/admin/organization/organization_settings.tsx @@ -0,0 +1,64 @@ +import { MailOutlined, SaveOutlined } from "@ant-design/icons"; +import { AccountSettingsTitle } from "admin/account/account_profile_view"; +import { updateOrganization } from "admin/rest_api"; +import { Button, Form, Input } from "antd"; +import Toast from "libs/toast"; +import type { APIOrganization } from "types/api_types"; + +const FormItem = Form.Item; + +type FormValues = { + displayName: string; + newUserMailingList: string; +}; + +export function OrganizationNotificationsView({ organization }: { organization: APIOrganization }) { + const [form] = Form.useForm(); + + async function onFinish(formValues: FormValues) { + await updateOrganization(organization.id, organization.name, formValues.newUserMailingList); + Toast.success("Notification settings were saved successfully."); + } + + const OrgaNameRegexPattern = /^[A-Za-z0-9\\-_\\. ß]+$/; + + return ( + <> + +
+ + + } + placeholder="mail@example.com" + /> + + +
+ + ); +} diff --git a/frontend/javascripts/admin/organization/organization_view.tsx b/frontend/javascripts/admin/organization/organization_view.tsx new file mode 100644 index 00000000000..676310d180f --- /dev/null +++ b/frontend/javascripts/admin/organization/organization_view.tsx @@ -0,0 +1,105 @@ +import { DeleteOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; +import { Breadcrumb, Layout, Menu } from "antd"; +import { connect } from "react-redux"; +import { Route, useHistory, useLocation, Switch } from "react-router-dom"; +import type { APIOrganization } from "types/api_types"; +import { enforceActiveOrganization } from "viewer/model/accessors/organization_accessors"; +import type { WebknossosState } from "viewer/store"; +import { OrganizationDangerZoneView } from "./organization_dangerzone_view"; +import { OrganizationProfileView } from "./organization_profile"; +import { OrganizationNotificationsView } from "./organization_settings"; +import type { MenuItemGroupType } from "antd/es/menu/interface"; +import { Redirect } from "react-router-dom"; + +const { Sider, Content } = Layout; + +type Props = { + organization: APIOrganization; +}; + +const BREADCRUMB_LABELS = { + profile: "Profile", + settings: "Settings", + delete: "Delete", +}; + +const OrganizationView = ({ organization }: Props) => { + const location = useLocation(); + const history = useHistory(); + const selectedKey = location.pathname.split("/").pop() || "profile"; + + const menuItems: MenuItemGroupType[] = [ + { + label: "Organization", + type: "group", + children: [ + { + key: "profile", + icon: , + label: "Profile", + }, + { + key: "settings", + icon: , + label: "Settings", + }, + { + key: "delete", + icon: , + label: "Delete", + }, + ], + }, + ]; + + const breadcrumbItems = [ + { + title: "Organization", + }, + { + title: BREADCRUMB_LABELS[selectedKey as keyof typeof BREADCRUMB_LABELS], + }, + ]; + + return ( + + + history.push(`/organization/${key}`)} + /> + + + + + } + /> + } + /> + } + /> + } /> + + + + ); +}; + +const mapStateToProps = (state: WebknossosState): Props => ({ + organization: enforceActiveOrganization(state.activeOrganization), +}); +export default connect(mapStateToProps)(OrganizationView); diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 475cf298dd2..88844738868 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -635,9 +635,7 @@ function LoggedInAvatar({ activeOrganization && Utils.isUserAdmin(activeUser) ? { key: "manage-organization", - label: ( - Manage Organization - ), + label: Manage Organization, } : null, isMultiMember diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index 96752be7d96..cca54398e2b 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -6,7 +6,7 @@ import StartResetPasswordView from "admin/auth/start_reset_password_view"; import DatasetAddView from "admin/dataset/dataset_add_view"; import JobListView from "admin/job/job_list_view"; import Onboarding from "admin/onboarding"; -import OrganizationEditView from "admin/organization/organization_edit_view"; +import OrganizationView from "admin/organization/organization_view"; import { PricingPlanEnum } from "admin/organization/pricing_plan_utils"; import ProjectCreateView from "admin/project/project_create_view"; import ProjectListView from "admin/project/project_list_view"; @@ -630,7 +630,17 @@ class ReactRouter extends React.Component { } + render={() => } + /> + } + /> + } /> Date: Mon, 16 Jun 2025 15:00:20 +0200 Subject: [PATCH 02/17] Refactor account settings components and update organization navigation - Updated the navbar link for managing organizations to point to the overview page. - Replaced `AccountSettingsTitle` with `SettingsTitle` in multiple views for consistency. - Introduced `SettingsCard` component for better organization of settings-related UI. - Added new views for organization notifications and overview, enhancing the organization management experience. - Cleaned up unused code and improved the structure of organization-related components. --- .../admin/account/account_auth_token_view.tsx | 4 +- .../admin/account/account_password_view.tsx | 6 +- .../admin/account/account_profile_view.tsx | 22 +- .../admin/account/helpers/settings_card.tsx | 24 ++ .../admin/account/helpers/settings_title.tsx | 21 ++ .../admin/organization/organization_cards.tsx | 350 +++++------------- .../organization_dangerzone_view.tsx | 21 +- ...sx => organization_notifications_view.tsx} | 49 ++- ...ile.tsx => organization_overview_view.tsx} | 42 +-- .../admin/organization/organization_view.tsx | 42 +-- .../admin/organization/pricing_plan_utils.ts | 9 + frontend/javascripts/navbar.tsx | 2 +- frontend/javascripts/router.tsx | 2 +- 13 files changed, 256 insertions(+), 338 deletions(-) create mode 100644 frontend/javascripts/admin/account/helpers/settings_card.tsx create mode 100644 frontend/javascripts/admin/account/helpers/settings_title.tsx rename frontend/javascripts/admin/organization/{organization_settings.tsx => organization_notifications_view.tsx} (51%) rename frontend/javascripts/admin/organization/{organization_profile.tsx => organization_overview_view.tsx} (69%) diff --git a/frontend/javascripts/admin/account/account_auth_token_view.tsx b/frontend/javascripts/admin/account/account_auth_token_view.tsx index d3d82638e18..b9c78e5adaf 100644 --- a/frontend/javascripts/admin/account/account_auth_token_view.tsx +++ b/frontend/javascripts/admin/account/account_auth_token_view.tsx @@ -3,7 +3,7 @@ import { getAuthToken, revokeAuthToken } from "admin/rest_api"; import { Button, Descriptions, Popover, Spin, Typography } from "antd"; import { useWkSelector } from "libs/react_hooks"; import { useEffect, useState } from "react"; -import { AccountSettingsTitle } from "./account_profile_view"; +import { SettingsTitle } from "./helpers/settings_title"; const { Text } = Typography; @@ -80,7 +80,7 @@ function AccountAuthTokenView() { return (
- diff --git a/frontend/javascripts/admin/account/account_password_view.tsx b/frontend/javascripts/admin/account/account_password_view.tsx index 96a75b682e7..e996e7af5ba 100644 --- a/frontend/javascripts/admin/account/account_password_view.tsx +++ b/frontend/javascripts/admin/account/account_password_view.tsx @@ -7,7 +7,7 @@ import { useState } from "react"; import { type RouteComponentProps, withRouter } from "react-router-dom"; import { logoutUserAction } from "viewer/model/actions/user_actions"; import Store from "viewer/store"; -import { AccountSettingsTitle } from "./account_profile_view"; +import { SettingsTitle } from "./helpers/settings_title"; const FormItem = Form.Item; const { Password } = Input; @@ -181,7 +181,7 @@ function AccountPasswordView({ history }: Props) { return (
- + - + -

{title}

- - {description} - - -
- ); -} +import { SettingsTitle } from "./helpers/settings_title"; function AccountProfileView() { const activeUser = useWkSelector((state) => state.activeUser); @@ -100,7 +84,7 @@ function AccountProfileView() { return (
- diff --git a/frontend/javascripts/admin/account/helpers/settings_card.tsx b/frontend/javascripts/admin/account/helpers/settings_card.tsx new file mode 100644 index 00000000000..61d5810776f --- /dev/null +++ b/frontend/javascripts/admin/account/helpers/settings_card.tsx @@ -0,0 +1,24 @@ +import { InfoCircleOutlined } from "@ant-design/icons"; +import { Card, Popover, Typography } from "antd"; + +interface SettingsCardProps { + title: string; + description: React.ReactNode; + explanation?: string; +} + +export function SettingsCard({ title, description, explanation }: SettingsCardProps) { + return ( + + + {title} + {explanation != null ? ( + + + + ) : null} + +
{description}
+
+ ); +} diff --git a/frontend/javascripts/admin/account/helpers/settings_title.tsx b/frontend/javascripts/admin/account/helpers/settings_title.tsx new file mode 100644 index 00000000000..c75f1fb7c19 --- /dev/null +++ b/frontend/javascripts/admin/account/helpers/settings_title.tsx @@ -0,0 +1,21 @@ +import { Divider, Typography } from "antd"; + +const { Text } = Typography; + +export function SettingsTitle({ + title, + description, +}: { + title: string; + description: string; +}) { + return ( +
+

{title}

+ + {description} + + +
+ ); +} diff --git a/frontend/javascripts/admin/organization/organization_cards.tsx b/frontend/javascripts/admin/organization/organization_cards.tsx index 1f9320785f5..ed865d77ede 100644 --- a/frontend/javascripts/admin/organization/organization_cards.tsx +++ b/frontend/javascripts/admin/organization/organization_cards.tsx @@ -1,21 +1,13 @@ -import { - FieldTimeOutlined, - PlusCircleOutlined, - RocketOutlined, - SafetyOutlined, -} from "@ant-design/icons"; -import { Alert, Button, Card, Col, Progress, Row, Tooltip } from "antd"; +import { FieldTimeOutlined, PlusCircleOutlined } from "@ant-design/icons"; +import { Alert, Button, Card, Col, Row } from "antd"; import { formatDateInLocalTimeZone } from "components/formatted_date"; import dayjs from "dayjs"; -import { formatCountToDataAmountUnit, formatCreditsString } from "libs/format_utils"; import { useWkSelector } from "libs/react_hooks"; -import type React from "react"; import type { APIOrganization } from "types/api_types"; import Constants from "viewer/constants"; import { PricingPlanEnum, - hasPricingPlanExceededStorage, - hasPricingPlanExceededUsers, + customPlanFeatures, hasPricingPlanExpired, isUserAllowedToRequestUpgrades, powerPlanFeatures, @@ -23,50 +15,66 @@ import { } from "./pricing_plan_utils"; import UpgradePricingPlanModal from "./upgrade_plan_modal"; -export function TeamAndPowerPlanUpgradeCards({ - teamUpgradeCallback, +function CustomPlanUpgradeCard() { + return ( + +

Contact our support team to upgrade Webknossos to match your organization.

+
    + {customPlanFeatures.map((feature) => ( +
  • {feature}
  • + ))} +
+ +
+ ); +} + +function TeamPlanUpgradeCard({ teamUpgradeCallback }: { teamUpgradeCallback: () => void }) { + return ( + + Buy Upgrade + , + ]} + > +
    + {teamPlanFeatures.map((feature) => ( +
  • {feature}
  • + ))} +
+
+ ); +} + +function PowerPlanUpgradeCard({ powerUpgradeCallback, + description, }: { - teamUpgradeCallback: () => void; powerUpgradeCallback: () => void; + description?: string; }) { return ( - - - - Buy Upgrade - , - ]} - > -
    - {teamPlanFeatures.map((feature) => ( -
  • {feature}
  • - ))} -
-
- - - - Buy Upgrade - , - ]} - > -
    - {powerPlanFeatures.map((feature) => ( -
  • {feature}
  • - ))} -
-
- -
+ + Buy Upgrade + , + ]} + > + {description ?

{description}

: null} +
    + {powerPlanFeatures.map((feature) => ( +
  • {feature}
  • + ))} +
+
); } @@ -76,7 +84,11 @@ export function PlanUpgradeCard({ organization }: { organization: APIOrganizatio organization.pricingPlan === PricingPlanEnum.PowerTrial || organization.pricingPlan === PricingPlanEnum.Custom ) - return "TODO"; + + + + + ; if ( organization.pricingPlan === PricingPlanEnum.Team || @@ -84,42 +96,35 @@ export function PlanUpgradeCard({ organization }: { organization: APIOrganizatio ) { return ( - -

- Upgrading your WEBKNOSSOS plan will unlock more advanced features and increase your user - and storage quotas. -

- -
    - {powerPlanFeatures.map((feature) => ( -
  • {feature}
  • - ))} -
-
- - - + + + UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Power) + } + />
); } return ( - - UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Team) - } - powerUpgradeCallback={() => - UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Power) - } - /> + + + + UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Team) + } + /> + + + + UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Power) + } + /> + + ); } @@ -147,159 +152,6 @@ export function PlanExpirationCard({ organization }: { organization: APIOrganiza ); } -// TODO: Consider removing this card -export function PlanDashboardCard({ - organization, - activeUsersCount, -}: { - organization: APIOrganization; - activeUsersCount: number; -}) { - const usedUsersPercentage = (activeUsersCount / organization.includedUsers) * 100; - const usedStoragePercentage = - (organization.usedStorageBytes / organization.includedStorageBytes) * 100; - - const hasExceededUserLimit = hasPricingPlanExceededUsers(organization, activeUsersCount); - const hasExceededStorageLimit = hasPricingPlanExceededStorage(organization); - - const maxUsersCountLabel = - organization.includedUsers === Number.POSITIVE_INFINITY ? "∞" : organization.includedUsers; - - const includedStorageLabel = - organization.includedStorageBytes === Number.POSITIVE_INFINITY - ? "∞" - : formatCountToDataAmountUnit(organization.includedStorageBytes, true); - - const usedStorageLabel = formatCountToDataAmountUnit(organization.usedStorageBytes, true); - - const storageLabel = ( - - {usedStorageLabel} / - - {includedStorageLabel} - - ); - - const redStrokeColor = "#ff4d4f"; - const greenStrokeColor = "#52c41a"; - - let upgradeUsersAction: React.ReactNode[] = []; - let upgradeStorageAction: React.ReactNode[] = []; - let upgradePlanAction: React.ReactNode[] = []; - - if ( - organization.pricingPlan === PricingPlanEnum.Basic || - organization.pricingPlan === PricingPlanEnum.Team || - organization.pricingPlan === PricingPlanEnum.TeamTrial - ) { - upgradeUsersAction = [ - UpgradePricingPlanModal.upgradePricingPlan(organization) - : UpgradePricingPlanModal.upgradeUserQuota - } - > - Buy Upgrade - , - ]; - upgradeStorageAction = [ - UpgradePricingPlanModal.upgradePricingPlan(organization) - : UpgradePricingPlanModal.upgradeStorageQuota - } - > - Buy Upgrade - , - ]; - upgradePlanAction = [ - [ - - Compare Plans - , - ], - ]; - } - const buyMoreCreditsAction = [ - - - , - ]; - - return ( - <> - - - - - `${activeUsersCount}/${maxUsersCountLabel}`} - strokeColor={hasExceededUserLimit ? redStrokeColor : greenStrokeColor} - status={hasExceededUserLimit ? "exception" : "active"} - /> - - Users - - - - - - storageLabel} - strokeColor={hasExceededStorageLimit ? redStrokeColor : greenStrokeColor} - status={hasExceededStorageLimit ? "exception" : "active"} - /> - - Storage - - - - - - - -

{organization.pricingPlan}

-
- Current Plan -
- - - - -

- {organization.creditBalance != null - ? formatCreditsString(organization.creditBalance) - : "No information access"} -

-
- WEBKNOSSOS Credits -
- -
- - ); -} - export function PlanExceededAlert({ organization }: { organization: APIOrganization }) { const hasPlanExpired = hasPricingPlanExpired(organization); const activeUser = useWkSelector((state) => state.activeUser); @@ -357,21 +209,21 @@ export function PlanAboutToExceedAlert({ organization }: { organization: APIOrga ), }); - // else { - // alerts.push({ - // message: - // "Your organization is about to exceed the storage space included in your current plan. Upgrade now to avoid your account from being blocked.", - // actionButton: ( - // - // ), - // }); - // } + else { + alerts.push({ + message: + "Your organization is about to exceed the storage space included in your current plan. Upgrade now to avoid your account from being blocked.", + actionButton: ( + + ), + }); + } return ( <> diff --git a/frontend/javascripts/admin/organization/organization_dangerzone_view.tsx b/frontend/javascripts/admin/organization/organization_dangerzone_view.tsx index 8f14190dbc2..86e65b36f7f 100644 --- a/frontend/javascripts/admin/organization/organization_dangerzone_view.tsx +++ b/frontend/javascripts/admin/organization/organization_dangerzone_view.tsx @@ -1,4 +1,5 @@ -import { AccountSettingsTitle } from "admin/account/account_profile_view"; +import { SettingsCard } from "admin/account/helpers/settings_card"; +import { SettingsTitle } from "admin/account/helpers/settings_title"; import { deleteOrganization } from "admin/rest_api"; import { Button, Typography } from "antd"; import { confirmAsync } from "dashboard/dataset/helper_components"; @@ -42,14 +43,24 @@ export function OrganizationDangerZoneView({ organization }: { organization: API } return ( <> - - + + Delete Organization + + } + /> ); } diff --git a/frontend/javascripts/admin/organization/organization_settings.tsx b/frontend/javascripts/admin/organization/organization_notifications_view.tsx similarity index 51% rename from frontend/javascripts/admin/organization/organization_settings.tsx rename to frontend/javascripts/admin/organization/organization_notifications_view.tsx index 13318375964..5e9a75d7617 100644 --- a/frontend/javascripts/admin/organization/organization_settings.tsx +++ b/frontend/javascripts/admin/organization/organization_notifications_view.tsx @@ -1,7 +1,8 @@ import { MailOutlined, SaveOutlined } from "@ant-design/icons"; -import { AccountSettingsTitle } from "admin/account/account_profile_view"; +import { SettingsCard } from "admin/account/helpers/settings_card"; +import { SettingsTitle } from "admin/account/helpers/settings_title"; import { updateOrganization } from "admin/rest_api"; -import { Button, Form, Input } from "antd"; +import { Button, Col, Form, Input, Row } from "antd"; import Toast from "libs/toast"; import type { APIOrganization } from "types/api_types"; @@ -22,19 +23,18 @@ export function OrganizationNotificationsView({ organization }: { organization: const OrgaNameRegexPattern = /^[A-Za-z0-9\\-_\\. ß]+$/; - return ( - <> - + function getNewUserNotificationsSettings() { + return (
} - placeholder="mail@example.com" + placeholder="email@example.com" + style={{ minWidth: 250 }} />
+ ); + } + + return ( + <> + + + + + + + + + + + + ); } diff --git a/frontend/javascripts/admin/organization/organization_profile.tsx b/frontend/javascripts/admin/organization/organization_overview_view.tsx similarity index 69% rename from frontend/javascripts/admin/organization/organization_profile.tsx rename to frontend/javascripts/admin/organization/organization_overview_view.tsx index f150814898b..1cc03fb8e3a 100644 --- a/frontend/javascripts/admin/organization/organization_profile.tsx +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -1,22 +1,19 @@ +import { SettingsTitle } from "admin/account/helpers/settings_title"; +import { getPricingPlanStatus, getUsers } from "admin/rest_api"; +import { Col, Row, Spin } from "antd"; +import { formatCountToDataAmountUnit } from "libs/format_utils"; +import { useEffect, useState } from "react"; import type { APIOrganization, APIPricingPlanStatus } from "types/api_types"; +import { SettingsCard } from "../account/helpers/settings_card"; import { PlanAboutToExceedAlert, PlanExceededAlert, PlanExpirationCard, PlanUpgradeCard, } from "./organization_cards"; -import { useEffect, useState } from "react"; -import { getPricingPlanStatus, getUsers } from "admin/rest_api"; -import { - getActiveUserCount, - hasPricingPlanExceededStorage, - hasPricingPlanExceededUsers, -} from "./pricing_plan_utils"; -import { Card, Row, Col, Spin, Typography } from "antd"; -import { AccountSettingsTitle } from "admin/account/account_profile_view"; -import { formatCountToDataAmountUnit } from "libs/format_utils"; +import { getActiveUserCount } from "./pricing_plan_utils"; -export function OrganizationProfileView({ organization }: { organization: APIOrganization }) { +export function OrganizationOverviewView({ organization }: { organization: APIOrganization }) { const [isFetchingData, setIsFetchingData] = useState(false); const [activeUsersCount, setActiveUsersCount] = useState(1); const [pricingPlanStatus, setPricingPlanStatus] = useState(null); @@ -34,9 +31,6 @@ export function OrganizationProfileView({ organization }: { organization: APIOrg setIsFetchingData(false); } - const hasExceededUserLimit = hasPricingPlanExceededUsers(organization, activeUsersCount); - const hasExceededStorageLimit = hasPricingPlanExceededStorage(organization); - const maxUsersCountLabel = organization.includedUsers === Number.POSITIVE_INFINITY ? "∞" : organization.includedUsers; @@ -81,7 +75,7 @@ export function OrganizationProfileView({ organization }: { organization: APIOrg return ( <> - + {pricingPlanStatus?.isExceeded ? : null} {pricingPlanStatus?.isAlmostExceeded && !pricingPlanStatus.isExceeded ? ( @@ -90,30 +84,20 @@ export function OrganizationProfileView({ organization }: { organization: APIOrg {firstRowStats.map((stat) => ( - - - {stat.title} - -
{stat.value}
-
+ ))}
- + {secondRowStats.map((stat) => ( - - - {stat.title} - -
{stat.value}
-
+ ))}
- diff --git a/frontend/javascripts/admin/organization/organization_view.tsx b/frontend/javascripts/admin/organization/organization_view.tsx index 676310d180f..744ec407c56 100644 --- a/frontend/javascripts/admin/organization/organization_view.tsx +++ b/frontend/javascripts/admin/organization/organization_view.tsx @@ -1,15 +1,15 @@ -import { DeleteOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; +import { DeleteOutlined, MailOutlined, UserOutlined } from "@ant-design/icons"; import { Breadcrumb, Layout, Menu } from "antd"; +import type { MenuItemGroupType } from "antd/es/menu/interface"; import { connect } from "react-redux"; -import { Route, useHistory, useLocation, Switch } from "react-router-dom"; +import { Route, Switch, useHistory, useLocation } from "react-router-dom"; +import { Redirect } from "react-router-dom"; import type { APIOrganization } from "types/api_types"; import { enforceActiveOrganization } from "viewer/model/accessors/organization_accessors"; import type { WebknossosState } from "viewer/store"; import { OrganizationDangerZoneView } from "./organization_dangerzone_view"; -import { OrganizationProfileView } from "./organization_profile"; -import { OrganizationNotificationsView } from "./organization_settings"; -import type { MenuItemGroupType } from "antd/es/menu/interface"; -import { Redirect } from "react-router-dom"; +import { OrganizationNotificationsView } from "./organization_notifications_view"; +import { OrganizationOverviewView } from "./organization_overview_view"; const { Sider, Content } = Layout; @@ -18,15 +18,15 @@ type Props = { }; const BREADCRUMB_LABELS = { - profile: "Profile", - settings: "Settings", - delete: "Delete", + overview: "Overview", + notifications: "Notification Settings", + delete: "Delete Organization", }; const OrganizationView = ({ organization }: Props) => { const location = useLocation(); const history = useHistory(); - const selectedKey = location.pathname.split("/").pop() || "profile"; + const selectedKey = location.pathname.split("/").pop() || "overview"; const menuItems: MenuItemGroupType[] = [ { @@ -34,14 +34,14 @@ const OrganizationView = ({ organization }: Props) => { type: "group", children: [ { - key: "profile", + key: "overview", icon: , - label: "Profile", + label: "Overview", }, { - key: "settings", - icon: , - label: "Settings", + key: "notifications", + icon: , + label: "Notifications", }, { key: "delete", @@ -77,22 +77,22 @@ const OrganizationView = ({ organization }: Props) => { onClick={({ key }) => history.push(`/organization/${key}`)} /> - - + + } + path="/organization/overview" + render={() => } /> } /> } /> - } /> + } /> diff --git a/frontend/javascripts/admin/organization/pricing_plan_utils.ts b/frontend/javascripts/admin/organization/pricing_plan_utils.ts index 341904a6f2c..ed0b22dfe11 100644 --- a/frontend/javascripts/admin/organization/pricing_plan_utils.ts +++ b/frontend/javascripts/admin/organization/pricing_plan_utils.ts @@ -18,6 +18,7 @@ export const teamPlanFeatures = [ "Priority Email Support", "Everything from Basic plan", ]; + export const powerPlanFeatures = [ "Unlimited Users", "Segmentation Proof-Reading Tool", @@ -26,6 +27,14 @@ export const powerPlanFeatures = [ "Everything from Team and Basic plans", ]; +export const customPlanFeatures = [ + "Single Sign-On with your existing institute user accounts (OpenID Connect)", + "Custom Domain Name (https://webknossos.your-university.org)", + "On-premise or dedicated hosting solutions available", + "Integration with your HPC and storage servers", + "Everything from Power, Team and Basic plans", +]; + export const maxInludedUsersInBasicPlan = 3; export function getActiveUserCount(users: APIUser[]): number { diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 88844738868..160d73844b7 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -635,7 +635,7 @@ function LoggedInAvatar({ activeOrganization && Utils.isUserAdmin(activeUser) ? { key: "manage-organization", - label: Manage Organization, + label: Manage Organization, } : null, isMultiMember diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index cca54398e2b..80d5101b877 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -57,11 +57,11 @@ import type { WebknossosState } from "viewer/store"; import HelpButton from "viewer/view/help_modal"; import TracingLayoutView from "viewer/view/layouting/tracing_layout_view"; +import AccountSettingsView from "admin/account/account_settings_view"; import { getDatasetIdFromNameAndOrganization, getOrganizationForDataset, } from "admin/api/disambiguate_legacy_routes"; -import AccountSettingsView from "admin/account/account_settings_view"; import VerifyEmailView from "admin/auth/verify_email_view"; import { DatasetURLImport } from "admin/dataset/dataset_url_import"; import TimeTrackingOverview from "admin/statistic/time_tracking_overview"; From aa95b90b3dc620ecfd42d75d4090687806d38fa4 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 16 Jun 2025 16:03:52 +0200 Subject: [PATCH 03/17] Enhance organization management UI and functionality - Updated the CustomPlanUpgradeCard to include a MailOutlined icon and improved layout with responsive columns. - Modified OrganizationNotificationsView to dispatch the updated organization state after saving notification settings. - Added organization name editing functionality in OrganizationOverviewView with validation and state management. - Refactored organization overview stats display for better readability and structure. - Improved imports in organization-related components for consistency. --- .../admin/organization/organization_cards.tsx | 41 ++++++++------ .../organization_notifications_view.tsx | 11 ++-- .../organization_overview_view.tsx | 54 +++++++++++++------ .../admin/organization/organization_view.tsx | 1 + 4 files changed, 74 insertions(+), 33 deletions(-) diff --git a/frontend/javascripts/admin/organization/organization_cards.tsx b/frontend/javascripts/admin/organization/organization_cards.tsx index ed865d77ede..7d77ffecd7b 100644 --- a/frontend/javascripts/admin/organization/organization_cards.tsx +++ b/frontend/javascripts/admin/organization/organization_cards.tsx @@ -1,4 +1,4 @@ -import { FieldTimeOutlined, PlusCircleOutlined } from "@ant-design/icons"; +import { FieldTimeOutlined, MailOutlined, PlusCircleOutlined } from "@ant-design/icons"; import { Alert, Button, Card, Col, Row } from "antd"; import { formatDateInLocalTimeZone } from "components/formatted_date"; import dayjs from "dayjs"; @@ -18,15 +18,24 @@ import UpgradePricingPlanModal from "./upgrade_plan_modal"; function CustomPlanUpgradeCard() { return ( -

Contact our support team to upgrade Webknossos to match your organization.

-
    - {customPlanFeatures.map((feature) => ( -
  • {feature}
  • - ))} -
- + + +

+ Contact our support team to upgrade Webknossos to match your organization and customized + your experience. +

+
    + {customPlanFeatures.map((feature) => ( +
  • {feature}
  • + ))} +
+ + + + +
); } @@ -84,11 +93,13 @@ export function PlanUpgradeCard({ organization }: { organization: APIOrganizatio organization.pricingPlan === PricingPlanEnum.PowerTrial || organization.pricingPlan === PricingPlanEnum.Custom ) - - - - - ; + return ( + + + + + + ); if ( organization.pricingPlan === PricingPlanEnum.Team || diff --git a/frontend/javascripts/admin/organization/organization_notifications_view.tsx b/frontend/javascripts/admin/organization/organization_notifications_view.tsx index 5e9a75d7617..9446848ae5d 100644 --- a/frontend/javascripts/admin/organization/organization_notifications_view.tsx +++ b/frontend/javascripts/admin/organization/organization_notifications_view.tsx @@ -5,6 +5,8 @@ import { updateOrganization } from "admin/rest_api"; import { Button, Col, Form, Input, Row } from "antd"; import Toast from "libs/toast"; import type { APIOrganization } from "types/api_types"; +import { setActiveOrganizationAction } from "viewer/model/actions/organization_actions"; +import { Store } from "viewer/singletons"; const FormItem = Form.Item; @@ -17,12 +19,15 @@ export function OrganizationNotificationsView({ organization }: { organization: const [form] = Form.useForm(); async function onFinish(formValues: FormValues) { - await updateOrganization(organization.id, organization.name, formValues.newUserMailingList); + const updatedOrganization = await updateOrganization( + organization.id, + organization.name, + formValues.newUserMailingList, + ); + Store.dispatch(setActiveOrganizationAction(updatedOrganization)); Toast.success("Notification settings were saved successfully."); } - const OrgaNameRegexPattern = /^[A-Za-z0-9\\-_\\. ß]+$/; - function getNewUserNotificationsSettings() { return (
+ {organization.name} + + ), + }, { key: "owner", title: "Owner", @@ -52,9 +86,6 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr title: "Current Plan", value: "Basic", }, - ]; - - const secondRowStats = [ { key: "users", title: "Users", @@ -81,15 +112,8 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr ) : null} - - {firstRowStats.map((stat) => ( - - - - ))} - - - {secondRowStats.map((stat) => ( + + {orgaStats.map((stat) => ( diff --git a/frontend/javascripts/admin/organization/organization_view.tsx b/frontend/javascripts/admin/organization/organization_view.tsx index 744ec407c56..2d2ce68c01f 100644 --- a/frontend/javascripts/admin/organization/organization_view.tsx +++ b/frontend/javascripts/admin/organization/organization_view.tsx @@ -10,6 +10,7 @@ import type { WebknossosState } from "viewer/store"; import { OrganizationDangerZoneView } from "./organization_dangerzone_view"; import { OrganizationNotificationsView } from "./organization_notifications_view"; import { OrganizationOverviewView } from "./organization_overview_view"; +import { Store } from "viewer/singletons"; const { Sider, Content } = Layout; From a2002cbb236f8cf3ca68ffbd0eb56ef62ad41861 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 16 Jun 2025 16:04:08 +0200 Subject: [PATCH 04/17] formatting --- .../admin/organization/organization_overview_view.tsx | 6 +++--- .../javascripts/admin/organization/organization_view.tsx | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/admin/organization/organization_overview_view.tsx b/frontend/javascripts/admin/organization/organization_overview_view.tsx index 17664a8b3ad..3c4244b5abc 100644 --- a/frontend/javascripts/admin/organization/organization_overview_view.tsx +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -2,8 +2,11 @@ import { SettingsTitle } from "admin/account/helpers/settings_title"; import { getPricingPlanStatus, getUsers, updateOrganization } from "admin/rest_api"; import { Col, Row, Spin, Typography } from "antd"; import { formatCountToDataAmountUnit } from "libs/format_utils"; +import Toast from "libs/toast"; import { useEffect, useState } from "react"; import type { APIOrganization, APIPricingPlanStatus } from "types/api_types"; +import { setActiveOrganizationAction } from "viewer/model/actions/organization_actions"; +import { Store } from "viewer/singletons"; import { SettingsCard } from "../account/helpers/settings_card"; import { PlanAboutToExceedAlert, @@ -12,9 +15,6 @@ import { PlanUpgradeCard, } from "./organization_cards"; import { getActiveUserCount } from "./pricing_plan_utils"; -import { setActiveOrganizationAction } from "viewer/model/actions/organization_actions"; -import { Store } from "viewer/singletons"; -import Toast from "libs/toast"; export function OrganizationOverviewView({ organization }: { organization: APIOrganization }) { const [isFetchingData, setIsFetchingData] = useState(false); diff --git a/frontend/javascripts/admin/organization/organization_view.tsx b/frontend/javascripts/admin/organization/organization_view.tsx index 2d2ce68c01f..744ec407c56 100644 --- a/frontend/javascripts/admin/organization/organization_view.tsx +++ b/frontend/javascripts/admin/organization/organization_view.tsx @@ -10,7 +10,6 @@ import type { WebknossosState } from "viewer/store"; import { OrganizationDangerZoneView } from "./organization_dangerzone_view"; import { OrganizationNotificationsView } from "./organization_notifications_view"; import { OrganizationOverviewView } from "./organization_overview_view"; -import { Store } from "viewer/singletons"; const { Sider, Content } = Layout; From b7543ea0cc167b0c4943337479fe422c4a79feca Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 16 Jun 2025 17:31:07 +0200 Subject: [PATCH 05/17] Enhance organization-related components and UI - Updated OrganizationController to include the current user's identity in the organization JSON response. - Modified organization cards to export components for better reusability. - Improved the upgrade plan modal by wrapping components in a ConfigProvider for consistent theming and layout adjustments. - Refactored the modal body to use responsive columns for displaying upgrade options. These changes aim to improve the user experience and maintainability of the organization management interface. --- app/controllers/OrganizationController.scala | 2 +- .../admin/organization/organization_cards.tsx | 6 ++-- .../admin/organization/upgrade_plan_modal.tsx | 34 +++++++++++++------ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/controllers/OrganizationController.scala b/app/controllers/OrganizationController.scala index c16d79ee9fb..240b772da0e 100755 --- a/app/controllers/OrganizationController.scala +++ b/app/controllers/OrganizationController.scala @@ -154,7 +154,7 @@ class OrganizationController @Inject()( _ <- Fox.fromBool(request.identity.isAdminOf(organization._id)) ?~> "notAllowed" ~> FORBIDDEN _ <- organizationDAO.updateFields(organization._id, name, newUserMailingList) updated <- organizationDAO.findOne(organization._id) - organizationJson <- organizationService.publicWrites(updated) + organizationJson <- organizationService.publicWrites(updated, Some(request.identity)) } yield Ok(organizationJson) } } diff --git a/frontend/javascripts/admin/organization/organization_cards.tsx b/frontend/javascripts/admin/organization/organization_cards.tsx index 7d77ffecd7b..6b7c790dae7 100644 --- a/frontend/javascripts/admin/organization/organization_cards.tsx +++ b/frontend/javascripts/admin/organization/organization_cards.tsx @@ -31,7 +31,7 @@ function CustomPlanUpgradeCard() { - @@ -40,7 +40,7 @@ function CustomPlanUpgradeCard() { ); } -function TeamPlanUpgradeCard({ teamUpgradeCallback }: { teamUpgradeCallback: () => void }) { +export function TeamPlanUpgradeCard({ teamUpgradeCallback }: { teamUpgradeCallback: () => void }) { return ( @@ -71,7 +73,10 @@ export function extendPricingPlan(organization: APIOrganization) { } export function upgradeUserQuota() { - renderIndependently((destroyCallback) => ); + + renderIndependently((destroyCallback) => + + ); } function UpgradeUserQuotaModal({ destroy }: { destroy: () => void }) { @@ -116,7 +121,7 @@ function UpgradeUserQuotaModal({ destroy }: { destroy: () => void }) { } export function upgradeStorageQuota() { - renderIndependently((destroyCallback) => ); + renderIndependently((destroyCallback) => ); } function UpgradeStorageQuotaModal({ destroy }: { destroy: () => void }) { const storageInputRef = useRef(null); @@ -222,28 +227,33 @@ function upgradePricingPlan( title = "Upgrade to unlock more features"; okButtonCallback = undefined; modalBody = ( - { + + + { sendUpgradePricingPlanEmail(PricingPlanEnum.Team); Toast.success(messages["organization.plan.upgrage_request_sent"]); destroyCallback(); - }} - powerUpgradeCallback={() => { + }}/> + + + { sendUpgradePricingPlanEmail(PricingPlanEnum.Power); Toast.success(messages["organization.plan.upgrage_request_sent"]); destroyCallback(); - }} - /> + }} /> + ); } return ( + + ); }); } @@ -263,6 +273,7 @@ export function UpgradePricingPlanModal({ "Upgrading your WEBKNOSSOS plan will unlock more advanced features and increase your user and storage quotas."; return ( + ( + + )); } From 749861ad5b02aa03f6a35753ea002e00486ddce8 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 16 Jun 2025 20:21:51 +0200 Subject: [PATCH 06/17] fix setting cards buttons --- .../admin/account/helpers/settings_card.tsx | 23 +++-- .../organization_notifications.tsx | 95 +++++++++++++++++++ .../organization_overview_view.tsx | 75 ++++++++++++++- .../admin/organization/upgrade_plan_modal.tsx | 65 +++++++------ 4 files changed, 219 insertions(+), 39 deletions(-) create mode 100644 frontend/javascripts/admin/organization/organization_notifications.tsx diff --git a/frontend/javascripts/admin/account/helpers/settings_card.tsx b/frontend/javascripts/admin/account/helpers/settings_card.tsx index 61d5810776f..dd79e5317c4 100644 --- a/frontend/javascripts/admin/account/helpers/settings_card.tsx +++ b/frontend/javascripts/admin/account/helpers/settings_card.tsx @@ -1,22 +1,29 @@ import { InfoCircleOutlined } from "@ant-design/icons"; -import { Card, Popover, Typography } from "antd"; +import { Card, Flex, Popover, Typography } from "antd"; interface SettingsCardProps { title: string; description: React.ReactNode; explanation?: string; + action?: React.ReactNode; } -export function SettingsCard({ title, description, explanation }: SettingsCardProps) { +export function SettingsCard({ title, description, explanation, action }: SettingsCardProps) { return ( - {title} - {explanation != null ? ( - - - - ) : null} + +
+ {title} + + {explanation != null ? ( + + + + ) : null} +
+ {action} +
{description}
diff --git a/frontend/javascripts/admin/organization/organization_notifications.tsx b/frontend/javascripts/admin/organization/organization_notifications.tsx new file mode 100644 index 00000000000..bf8f7b96e46 --- /dev/null +++ b/frontend/javascripts/admin/organization/organization_notifications.tsx @@ -0,0 +1,95 @@ +import { MailOutlined, SaveOutlined } from "@ant-design/icons"; +import { SettingsCard } from "admin/account/helpers/settings_card"; +import { SettingsTitle } from "admin/account/helpers/settings_title"; +import { updateOrganization } from "admin/rest_api"; +import { Button, Col, Form, Input, Row } from "antd"; +import Toast from "libs/toast"; +import type { APIOrganization } from "types/api_types"; + +const FormItem = Form.Item; + +type FormValues = { + displayName: string; + newUserMailingList: string; +}; + +export function OrganizationNotificationsView({ organization }: { organization: APIOrganization }) { + const [form] = Form.useForm(); + + async function onFinish(formValues: FormValues) { + await updateOrganization(organization.id, organization.name, formValues.newUserMailingList); + Toast.success("Notification settings were saved successfully."); + } + + function getNewUserNotificationsSettings() { + return ( + + + + } + placeholder="email@example.com" + style={{ minWidth: 250 }} + /> + + + + ); + } + + return ( + <> + + + + + + + + + + + + + + ); +} diff --git a/frontend/javascripts/admin/organization/organization_overview_view.tsx b/frontend/javascripts/admin/organization/organization_overview_view.tsx index 3c4244b5abc..cecd1e4f412 100644 --- a/frontend/javascripts/admin/organization/organization_overview_view.tsx +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -1,6 +1,7 @@ +import { PlusOutlined } from "@ant-design/icons"; import { SettingsTitle } from "admin/account/helpers/settings_title"; import { getPricingPlanStatus, getUsers, updateOrganization } from "admin/rest_api"; -import { Col, Row, Spin, Typography } from "antd"; +import { Button, Col, Row, Spin, Tooltip, Typography } from "antd"; import { formatCountToDataAmountUnit } from "libs/format_utils"; import Toast from "libs/toast"; import { useEffect, useState } from "react"; @@ -14,7 +15,8 @@ import { PlanExpirationCard, PlanUpgradeCard, } from "./organization_cards"; -import { getActiveUserCount } from "./pricing_plan_utils"; +import { PricingPlanEnum, getActiveUserCount } from "./pricing_plan_utils"; +import UpgradePricingPlanModal from "./upgrade_plan_modal"; export function OrganizationOverviewView({ organization }: { organization: APIOrganization }) { const [isFetchingData, setIsFetchingData] = useState(false); @@ -62,6 +64,69 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr const usedStorageLabel = formatCountToDataAmountUnit(organization.usedStorageBytes, true); + let upgradeUsersAction: React.ReactNode = null; + let upgradeStorageAction: React.ReactNode = null; + let upgradePlanAction: React.ReactNode = null; + + if ( + organization.pricingPlan === PricingPlanEnum.Basic || + organization.pricingPlan === PricingPlanEnum.Team || + organization.pricingPlan === PricingPlanEnum.TeamTrial + ) { + upgradeUsersAction = ( + - - ); - } - - return ( - <> - - - - - - - - - - - - - - ); -} diff --git a/frontend/javascripts/admin/organization/organization_overview_view.tsx b/frontend/javascripts/admin/organization/organization_overview_view.tsx index cecd1e4f412..06a4c573ec3 100644 --- a/frontend/javascripts/admin/organization/organization_overview_view.tsx +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -18,6 +18,8 @@ import { import { PricingPlanEnum, getActiveUserCount } from "./pricing_plan_utils"; import UpgradePricingPlanModal from "./upgrade_plan_modal"; +const ORGA_NAME_REGEX_PATTERN = /^[A-Za-z0-9\\-_\\. ß]+$/; + export function OrganizationOverviewView({ organization }: { organization: APIOrganization }) { const [isFetchingData, setIsFetchingData] = useState(false); const [activeUsersCount, setActiveUsersCount] = useState(1); @@ -37,9 +39,7 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr } async function setOrganizationName(newOrgaName: string) { - const OrgaNameRegexPattern = /^[A-Za-z0-9\\-_\\. ß]+$/; - - if (!OrgaNameRegexPattern.test(newOrgaName)) { + if (!ORGA_NAME_REGEX_PATTERN.test(newOrgaName)) { Toast.error( "Organization name can only contain letters, numbers, spaces, and the following special characters: - _ . ß", ); @@ -144,7 +144,7 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr { key: "owner", title: "Owner", - value: "John Doe", + value: organization.ownerName, }, { key: "plan", diff --git a/frontend/javascripts/admin/organization/organization_view.tsx b/frontend/javascripts/admin/organization/organization_view.tsx index 744ec407c56..c5f69e1ef52 100644 --- a/frontend/javascripts/admin/organization/organization_view.tsx +++ b/frontend/javascripts/admin/organization/organization_view.tsx @@ -1,57 +1,55 @@ import { DeleteOutlined, MailOutlined, UserOutlined } from "@ant-design/icons"; import { Breadcrumb, Layout, Menu } from "antd"; import type { MenuItemGroupType } from "antd/es/menu/interface"; -import { connect } from "react-redux"; +import { useWkSelector } from "libs/react_hooks"; import { Route, Switch, useHistory, useLocation } from "react-router-dom"; import { Redirect } from "react-router-dom"; -import type { APIOrganization } from "types/api_types"; +import constants from "viewer/constants"; import { enforceActiveOrganization } from "viewer/model/accessors/organization_accessors"; -import type { WebknossosState } from "viewer/store"; -import { OrganizationDangerZoneView } from "./organization_dangerzone_view"; +import { OrganizationDangerZoneView } from "./organization_danger_zone_view"; import { OrganizationNotificationsView } from "./organization_notifications_view"; import { OrganizationOverviewView } from "./organization_overview_view"; const { Sider, Content } = Layout; -type Props = { - organization: APIOrganization; -}; - const BREADCRUMB_LABELS = { overview: "Overview", notifications: "Notification Settings", delete: "Delete Organization", }; -const OrganizationView = ({ organization }: Props) => { +const MENU_ITEMS: MenuItemGroupType[] = [ + { + label: "Organization", + type: "group", + children: [ + { + key: "overview", + icon: , + label: "Overview", + }, + { + key: "notifications", + icon: , + label: "Notifications", + }, + { + key: "delete", + icon: , + label: "Delete", + }, + ], + }, +]; + +const OrganizationView = () => { + const organization = useWkSelector((state) => + enforceActiveOrganization(state.activeOrganization), + ); const location = useLocation(); const history = useHistory(); const selectedKey = location.pathname.split("/").pop() || "overview"; - const menuItems: MenuItemGroupType[] = [ - { - label: "Organization", - type: "group", - children: [ - { - key: "overview", - icon: , - label: "Overview", - }, - { - key: "notifications", - icon: , - label: "Notifications", - }, - { - key: "delete", - icon: , - label: "Delete", - }, - ], - }, - ]; - const breadcrumbItems = [ { title: "Organization", @@ -64,7 +62,7 @@ const OrganizationView = ({ organization }: Props) => { return ( @@ -73,7 +71,7 @@ const OrganizationView = ({ organization }: Props) => { mode="inline" selectedKeys={[selectedKey]} style={{ height: "100%", padding: 24 }} - items={menuItems} + items={MENU_ITEMS} onClick={({ key }) => history.push(`/organization/${key}`)} /> @@ -99,7 +97,4 @@ const OrganizationView = ({ organization }: Props) => { ); }; -const mapStateToProps = (state: WebknossosState): Props => ({ - organization: enforceActiveOrganization(state.activeOrganization), -}); -export default connect(mapStateToProps)(OrganizationView); +export default OrganizationView; diff --git a/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx b/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx index 4658f4f064a..b89fc032f5a 100644 --- a/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx +++ b/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx @@ -11,7 +11,7 @@ import { sendUpgradePricingPlanStorageEmail, sendUpgradePricingPlanUserEmail, } from "admin/rest_api"; -import { Button, Col, ConfigProvider, Divider, InputNumber, Modal, Row } from "antd"; +import { Button, Col, Divider, InputNumber, Modal, Row } from "antd"; import { formatDateInLocalTimeZone } from "components/formatted_date"; import dayjs from "dayjs"; import features from "features"; @@ -22,7 +22,6 @@ import Toast from "libs/toast"; import messages from "messages"; import type React from "react"; import { useEffect, useRef, useState } from "react"; -import { getAntdTheme, getSystemColorTheme } from "theme"; import type { APIOrganization } from "types/api_types"; import { PowerPlanUpgradeCard, TeamPlanUpgradeCard } from "./organization_cards"; import { powerPlanFeatures, teamPlanFeatures } from "./pricing_plan_utils"; @@ -73,12 +72,7 @@ export function extendPricingPlan(organization: APIOrganization) { } export function upgradeUserQuota() { - renderIndependently((destroyCallback) => ( - - {" "} - … - - )); + renderIndependently((destroyCallback) => ); } function UpgradeUserQuotaModal({ destroy }: { destroy: () => void }) { @@ -123,11 +117,7 @@ function UpgradeUserQuotaModal({ destroy }: { destroy: () => void }) { } export function upgradeStorageQuota() { - renderIndependently((destroyCallback) => ( - - - - )); + renderIndependently((destroyCallback) => ); } function UpgradeStorageQuotaModal({ destroy }: { destroy: () => void }) { const storageInputRef = useRef(null); @@ -236,8 +226,8 @@ function upgradePricingPlan( { - sendUpgradePricingPlanEmail(PricingPlanEnum.Team); + teamUpgradeCallback={async () => { + await sendUpgradePricingPlanEmail(PricingPlanEnum.Team); Toast.success(messages["organization.plan.upgrage_request_sent"]); destroyCallback(); }} @@ -245,8 +235,8 @@ function upgradePricingPlan( { - sendUpgradePricingPlanEmail(PricingPlanEnum.Power); + powerUpgradeCallback={async () => { + await sendUpgradePricingPlanEmail(PricingPlanEnum.Power); Toast.success(messages["organization.plan.upgrage_request_sent"]); destroyCallback(); }} @@ -257,14 +247,12 @@ function upgradePricingPlan( } return ( - - - + ); }); } @@ -322,9 +310,7 @@ export function UpgradePricingPlanModal({ export function orderWebknossosCredits() { renderIndependently((destroyCallback) => ( - - - + )); } From 67e8819f146a6b5ec5dc3de07a64134a55ff9be8 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 18 Jun 2025 15:37:45 +0200 Subject: [PATCH 10/17] Show owner email in notifications --- .../organization_notifications_view.tsx | 18 +++++++++++++++++- frontend/javascripts/navbar.tsx | 13 ++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/frontend/javascripts/admin/organization/organization_notifications_view.tsx b/frontend/javascripts/admin/organization/organization_notifications_view.tsx index 9446848ae5d..f903c7e29f2 100644 --- a/frontend/javascripts/admin/organization/organization_notifications_view.tsx +++ b/frontend/javascripts/admin/organization/organization_notifications_view.tsx @@ -2,8 +2,10 @@ import { MailOutlined, SaveOutlined } from "@ant-design/icons"; import { SettingsCard } from "admin/account/helpers/settings_card"; import { SettingsTitle } from "admin/account/helpers/settings_title"; import { updateOrganization } from "admin/rest_api"; +import { getUsers } from "admin/rest_api"; import { Button, Col, Form, Input, Row } from "antd"; import Toast from "libs/toast"; +import { useEffect, useState } from "react"; import type { APIOrganization } from "types/api_types"; import { setActiveOrganizationAction } from "viewer/model/actions/organization_actions"; import { Store } from "viewer/singletons"; @@ -17,6 +19,20 @@ type FormValues = { export function OrganizationNotificationsView({ organization }: { organization: APIOrganization }) { const [form] = Form.useForm(); + const [ownerEmail, setOwnerEmail] = useState(""); + + useEffect(() => { + async function fetchOwnerEmail() { + const users = await getUsers(); + const owner = users.find( + (user) => user.isOrganizationOwner && user.organization === organization.id, + ); + if (owner) { + setOwnerEmail(owner.email); + } + } + fetchOwnerEmail(); + }, [organization.id]); async function onFinish(formValues: FormValues) { const updatedOrganization = await updateOrganization( @@ -79,7 +95,7 @@ export function OrganizationNotificationsView({ organization }: { organization: diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 160d73844b7..9146e5808cf 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -632,10 +632,17 @@ function LoggedInAvatar({ label: orgName, disabled: true, }, + { + type: "divider", + }, + { + key: "account", + label: Account Settings, + }, activeOrganization && Utils.isUserAdmin(activeUser) ? { key: "manage-organization", - label: Manage Organization, + label: Organization Settings, } : null, isMultiMember @@ -653,10 +660,6 @@ function LoggedInAvatar({ ], } : null, - { - key: "account", - label: Account Settings, - }, { key: "logout", label: ( From 121c91a9e95b2b8e4bbcf4f6f55eddde36c6ba81 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 18 Jun 2025 15:52:16 +0200 Subject: [PATCH 11/17] fix pricing plan label --- .../admin/organization/organization_overview_view.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/admin/organization/organization_overview_view.tsx b/frontend/javascripts/admin/organization/organization_overview_view.tsx index 06a4c573ec3..eb0c6418e1b 100644 --- a/frontend/javascripts/admin/organization/organization_overview_view.tsx +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -149,13 +149,13 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr { key: "plan", title: "Current Plan", - value: "Basic", + value: organization.pricingPlan, action: upgradePlanAction, }, { key: "users", title: "Users", - value: `${activeUsersCount}/${maxUsersCountLabel}`, + value: `${activeUsersCount} / ${maxUsersCountLabel}`, action: upgradeUsersAction, }, { From df8e81d51a6531533fd033d2bbe4e4baa3791da7 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 23 Jun 2025 15:22:47 +0200 Subject: [PATCH 12/17] do not show upgrade options for custom plans yet --- .../admin/organization/organization_cards.tsx | 41 +------------------ .../organization_overview_view.tsx | 16 +++++--- .../admin/organization/pricing_plan_utils.ts | 8 ---- unreleased_changes/8679.md | 2 + 4 files changed, 14 insertions(+), 53 deletions(-) create mode 100644 unreleased_changes/8679.md diff --git a/frontend/javascripts/admin/organization/organization_cards.tsx b/frontend/javascripts/admin/organization/organization_cards.tsx index 6b7c790dae7..6b857970c2b 100644 --- a/frontend/javascripts/admin/organization/organization_cards.tsx +++ b/frontend/javascripts/admin/organization/organization_cards.tsx @@ -1,4 +1,4 @@ -import { FieldTimeOutlined, MailOutlined, PlusCircleOutlined } from "@ant-design/icons"; +import { FieldTimeOutlined, PlusCircleOutlined } from "@ant-design/icons"; import { Alert, Button, Card, Col, Row } from "antd"; import { formatDateInLocalTimeZone } from "components/formatted_date"; import dayjs from "dayjs"; @@ -7,7 +7,6 @@ import type { APIOrganization } from "types/api_types"; import Constants from "viewer/constants"; import { PricingPlanEnum, - customPlanFeatures, hasPricingPlanExpired, isUserAllowedToRequestUpgrades, powerPlanFeatures, @@ -15,31 +14,6 @@ import { } from "./pricing_plan_utils"; import UpgradePricingPlanModal from "./upgrade_plan_modal"; -function CustomPlanUpgradeCard() { - return ( - - - -

- Contact our support team to upgrade Webknossos to match your organization and customized - your experience. -

-
    - {customPlanFeatures.map((feature) => ( -
  • {feature}
  • - ))} -
- - - - -
-
- ); -} - export function TeamPlanUpgradeCard({ teamUpgradeCallback }: { teamUpgradeCallback: () => void }) { return ( - - - -
- ); - if ( organization.pricingPlan === PricingPlanEnum.Team || organization.pricingPlan === PricingPlanEnum.TeamTrial diff --git a/frontend/javascripts/admin/organization/organization_overview_view.tsx b/frontend/javascripts/admin/organization/organization_overview_view.tsx index eb0c6418e1b..ea8d46911d8 100644 --- a/frontend/javascripts/admin/organization/organization_overview_view.tsx +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -190,11 +190,17 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr
- - + {organization.pricingPlan === PricingPlanEnum.Basic || + organization.pricingPlan === PricingPlanEnum.Team || + organization.pricingPlan === PricingPlanEnum.TeamTrial ? ( + <> + + + + ) : null} ); } diff --git a/frontend/javascripts/admin/organization/pricing_plan_utils.ts b/frontend/javascripts/admin/organization/pricing_plan_utils.ts index aee32fab9a7..05a9cd01340 100644 --- a/frontend/javascripts/admin/organization/pricing_plan_utils.ts +++ b/frontend/javascripts/admin/organization/pricing_plan_utils.ts @@ -27,14 +27,6 @@ export const powerPlanFeatures = [ "Everything from Team and Basic plans", ]; -export const customPlanFeatures = [ - "Single Sign-On with your existing institute user accounts (OpenID Connect)", - "Custom Domain Name (https://webknossos.your-institute.org)", - "On-premise or dedicated hosting solutions available", - "Integration with your HPC and storage servers", - "Everything from Power, Team and Basic plans", -]; - export const maxInludedUsersInBasicPlan = 3; export function getActiveUserCount(users: APIUser[]): number { diff --git a/unreleased_changes/8679.md b/unreleased_changes/8679.md new file mode 100644 index 00000000000..c311db9e058 --- /dev/null +++ b/unreleased_changes/8679.md @@ -0,0 +1,2 @@ +### Changed +- Updated the UI for the organizations settings page. [#8679](https://github.com/scalableminds/webknossos/pull/8679) \ No newline at end of file From 66086aa4b21bda34a664d054a2cad95a6174b155 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 24 Jun 2025 13:31:31 +0200 Subject: [PATCH 13/17] fix null / infinity on update --- .../organization_overview_view.tsx | 2 +- frontend/javascripts/admin/rest_api.ts | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/admin/organization/organization_overview_view.tsx b/frontend/javascripts/admin/organization/organization_overview_view.tsx index ea8d46911d8..4b968c60f59 100644 --- a/frontend/javascripts/admin/organization/organization_overview_view.tsx +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -21,7 +21,7 @@ import UpgradePricingPlanModal from "./upgrade_plan_modal"; const ORGA_NAME_REGEX_PATTERN = /^[A-Za-z0-9\\-_\\. ß]+$/; export function OrganizationOverviewView({ organization }: { organization: APIOrganization }) { - const [isFetchingData, setIsFetchingData] = useState(false); + const [isFetchingData, setIsFetchingData] = useState(true); const [activeUsersCount, setActiveUsersCount] = useState(1); const [pricingPlanStatus, setPricingPlanStatus] = useState(null); diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index a9ef19bee06..9210c753986 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -1720,13 +1720,23 @@ export async function updateOrganization( name: string, newUserMailingList: string, ): Promise { - return Request.sendJSONReceiveJSON(`/api/organizations/${organizationId}`, { - method: "PATCH", - data: { - name, - newUserMailingList, + const updatedOrganization = await Request.sendJSONReceiveJSON( + `/api/organizations/${organizationId}`, + { + method: "PATCH", + data: { + name, + newUserMailingList, + }, }, - }); + ); + + return { + ...updatedOrganization, + paidUntil: updatedOrganization.paidUntil ?? Constants.MAXIMUM_DATE_TIMESTAMP, + includedStorageBytes: updatedOrganization.includedStorageBytes ?? Number.POSITIVE_INFINITY, + includedUsers: updatedOrganization.includedUsers ?? Number.POSITIVE_INFINITY, + }; } export async function isDatasetAccessibleBySwitching( From 1903218a5aa24f3d5a46269145fe8e7cc9f9a94c Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 24 Jun 2025 14:15:03 +0200 Subject: [PATCH 14/17] appled PR feedback --- .../admin/organization/pricing_plan_utils.ts | 4 +- .../admin/organization/upgrade_plan_modal.tsx | 7 +- frontend/stylesheets/_drawings.less | 6 +- frontend/stylesheets/dark.less | 9 +- .../images/drawings/add-users-dark-mode.svg | 117 ++++++++++++++ .../images/drawings/add-users-light-mode.svg | 118 ++++++++++++++ .../drawings/upgrade-storage-dark-mode.svg | 153 ++++++++++++++++++ .../drawings/upgrade-storage-light-mode.svg | 153 ++++++++++++++++++ 8 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 public/images/drawings/add-users-dark-mode.svg create mode 100644 public/images/drawings/add-users-light-mode.svg create mode 100644 public/images/drawings/upgrade-storage-dark-mode.svg create mode 100644 public/images/drawings/upgrade-storage-light-mode.svg diff --git a/frontend/javascripts/admin/organization/pricing_plan_utils.ts b/frontend/javascripts/admin/organization/pricing_plan_utils.ts index 05a9cd01340..5973da02ff9 100644 --- a/frontend/javascripts/admin/organization/pricing_plan_utils.ts +++ b/frontend/javascripts/admin/organization/pricing_plan_utils.ts @@ -11,20 +11,20 @@ export enum PricingPlanEnum { } export const teamPlanFeatures = [ + "Everything from Basic plan", "Collaborative Annotation", "Project Management", "Dataset Management and Access Control", "5 Users / 1TB Storage (upgradable)", "Priority Email Support", - "Everything from Basic plan", ]; export const powerPlanFeatures = [ + "Everything from Team and Basic plans", "Unlimited Users", "Segmentation Proof-Reading Tool", "On-premise or dedicated hosting solutions available", "Integration with your HPC and storage servers", - "Everything from Team and Basic plans", ]; export const maxInludedUsersInBasicPlan = 3; diff --git a/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx b/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx index b89fc032f5a..a43ad79cc95 100644 --- a/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx +++ b/frontend/javascripts/admin/organization/upgrade_plan_modal.tsx @@ -378,8 +378,11 @@ function OrderWebknossosCreditsModal({ destroy }: { destroy: () => void }) {

Ordering WEBKNOSSOS credits for your organization will send an email to the WEBKNOSSOS sales team. We typically respond within one business day to discuss payment options and - purchasing requirements. See our FAQ for more - information. + purchasing requirements. See our{" "} + + FAQ + {" "} + for more information. information.

diff --git a/frontend/stylesheets/_drawings.less b/frontend/stylesheets/_drawings.less index 238808f263b..421098c20f4 100644 --- a/frontend/stylesheets/_drawings.less +++ b/frontend/stylesheets/_drawings.less @@ -52,15 +52,15 @@ } .drawing-upgrade-users { - background: right -40px / 35% no-repeat url("/assets/images/pricing/add_users_light_mode.svg"); + background: right -40px / 35% no-repeat url("/assets/images/drawings/add-users-light-mode.svg"); } .drawing-upgrade-storage { - background: url("/assets/images/pricing/upgrade_storage_light_mode.svg") right -10px / 25% no-repeat; + background: url("/assets/images/drawings/upgrade-storage-light-mode.svg") right -10px / 25% no-repeat; } .drawing-license-expired { width: 500px; height: 385px; background: url("/assets/images/drawings/license-expired.svg") center / 100% no-repeat; -} +} \ No newline at end of file diff --git a/frontend/stylesheets/dark.less b/frontend/stylesheets/dark.less index 3db02bd4e02..d989e97447f 100644 --- a/frontend/stylesheets/dark.less +++ b/frontend/stylesheets/dark.less @@ -20,6 +20,7 @@ } .ant-slider:hover { + .ant-slider-track, .ant-slider-handle { background-color: var(--ant-blue-5); @@ -56,6 +57,7 @@ } .voxelytics-view { + // VX reports / DAG visualization .react-flow__minimap-mask { fill: rgba(50, 50, 50, 0.7); @@ -63,6 +65,7 @@ } .floating-buttons-bar { + // Navigation buttons for mobile .ant-btn-default { color: rgba(0, 0, 0, 0.85); @@ -78,11 +81,11 @@ } .drawing-upgrade-users { - background: right -40px / 35% no-repeat url("/assets/images/pricing/add_users_dark_mode.svg"); + background: right -40px / 35% no-repeat url("/assets/images/drawings/add-users-dark-mode.svg"); } .drawing-upgrade-storage { - background: url("/assets/images/pricing/upgrade_storage_dark_mode.svg") right -10px / 25% no-repeat; + background: url("/assets/images/drawings/upgrade-storage-dark-mode.svg") right -10px / 25% no-repeat; } .drawing-empty-list-tasks { @@ -124,4 +127,4 @@ .deemphasized { color: #afb8ba; } -} +} \ No newline at end of file diff --git a/public/images/drawings/add-users-dark-mode.svg b/public/images/drawings/add-users-dark-mode.svg new file mode 100644 index 00000000000..91e5f24e8e5 --- /dev/null +++ b/public/images/drawings/add-users-dark-mode.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/drawings/add-users-light-mode.svg b/public/images/drawings/add-users-light-mode.svg new file mode 100644 index 00000000000..31de49bbddc --- /dev/null +++ b/public/images/drawings/add-users-light-mode.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/drawings/upgrade-storage-dark-mode.svg b/public/images/drawings/upgrade-storage-dark-mode.svg new file mode 100644 index 00000000000..14468f050c7 --- /dev/null +++ b/public/images/drawings/upgrade-storage-dark-mode.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/drawings/upgrade-storage-light-mode.svg b/public/images/drawings/upgrade-storage-light-mode.svg new file mode 100644 index 00000000000..05fe37708c4 --- /dev/null +++ b/public/images/drawings/upgrade-storage-light-mode.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d61a5221523df830dfdc4121e315e918a963a029 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 24 Jun 2025 14:24:35 +0200 Subject: [PATCH 15/17] apply PR feedback --- .../admin/account/helpers/settings_card.tsx | 4 +-- .../organization_overview_view.tsx | 26 ++++++++----------- .../admin/organization/upgrade_plan_modal.tsx | 7 ++++- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/frontend/javascripts/admin/account/helpers/settings_card.tsx b/frontend/javascripts/admin/account/helpers/settings_card.tsx index dd79e5317c4..62966cd5dad 100644 --- a/frontend/javascripts/admin/account/helpers/settings_card.tsx +++ b/frontend/javascripts/admin/account/helpers/settings_card.tsx @@ -4,13 +4,13 @@ import { Card, Flex, Popover, Typography } from "antd"; interface SettingsCardProps { title: string; description: React.ReactNode; - explanation?: string; + explanation?: React.ReactNode; action?: React.ReactNode; } export function SettingsCard({ title, description, explanation, action }: SettingsCardProps) { return ( - +
diff --git a/frontend/javascripts/admin/organization/organization_overview_view.tsx b/frontend/javascripts/admin/organization/organization_overview_view.tsx index 4b968c60f59..721d9a2d631 100644 --- a/frontend/javascripts/admin/organization/organization_overview_view.tsx +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -66,7 +66,6 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr let upgradeUsersAction: React.ReactNode = null; let upgradeStorageAction: React.ReactNode = null; - let upgradePlanAction: React.ReactNode = null; if ( organization.pricingPlan === PricingPlanEnum.Basic || @@ -100,18 +99,6 @@ export function OrganizationOverviewView({ organization }: { organization: APIOr } /> ); - - upgradePlanAction = ( -