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/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 fe91cd5fc21..c99ce31cad4 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; @@ -186,7 +186,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..62966cd5dad --- /dev/null +++ b/frontend/javascripts/admin/account/helpers/settings_card.tsx @@ -0,0 +1,31 @@ +import { InfoCircleOutlined } from "@ant-design/icons"; +import { Card, Flex, Popover, Typography } from "antd"; + +interface SettingsCardProps { + title: string; + description: React.ReactNode; + explanation?: React.ReactNode; + action?: React.ReactNode; +} + +export function SettingsCard({ title, description, explanation, action }: SettingsCardProps) { + return ( + + + +
+ {title} + + {explanation != null ? ( + + + + ) : null} +
+ {action} +
+
+
{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 ec5b9adf43f..6b857970c2b 100644 --- a/frontend/javascripts/admin/organization/organization_cards.tsx +++ b/frontend/javascripts/admin/organization/organization_cards.tsx @@ -1,21 +1,12 @@ -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, hasPricingPlanExpired, isUserAllowedToRequestUpgrades, powerPlanFeatures, @@ -23,126 +14,89 @@ import { } from "./pricing_plan_utils"; import UpgradePricingPlanModal from "./upgrade_plan_modal"; -export function TeamAndPowerPlanUpgradeCards({ - teamUpgradeCallback, +export function TeamPlanUpgradeCard({ teamUpgradeCallback }: { teamUpgradeCallback: () => void }) { + return ( + + Buy Upgrade + , + ]} + > +
    + {teamPlanFeatures.map((feature) => ( +
  • {feature}
  • + ))} +
+
+ ); +} + +export 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}
  • + ))} +
+
); } export function PlanUpgradeCard({ organization }: { organization: APIOrganization }) { - if ( - organization.pricingPlan === PricingPlanEnum.Power || - 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) - } - /> - ); - if ( organization.pricingPlan === PricingPlanEnum.Team || organization.pricingPlan === PricingPlanEnum.TeamTrial ) { - title = `Upgrade to ${PricingPlanEnum.Power} Plan`; - cardBody = ( + 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 ( - -

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

- {cardBody} -
+ + + + UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Team) + } + /> + + + + UpgradePricingPlanModal.upgradePricingPlan(organization, PricingPlanEnum.Power) + } + /> + + ); } @@ -150,7 +104,7 @@ export function PlanExpirationCard({ organization }: { organization: APIOrganiza if (organization.paidUntil === Constants.MAXIMUM_DATE_TIMESTAMP) return null; return ( - + Your current plan is paid until{" "} @@ -170,158 +124,6 @@ export function PlanExpirationCard({ organization }: { organization: APIOrganiza ); } -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); @@ -379,21 +181,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_danger_zone_view.tsx b/frontend/javascripts/admin/organization/organization_danger_zone_view.tsx new file mode 100644 index 00000000000..86e65b36f7f --- /dev/null +++ b/frontend/javascripts/admin/organization/organization_danger_zone_view.tsx @@ -0,0 +1,66 @@ +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"; +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 ( + <> + + + Delete Organization + + } + /> + + ); +} 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_notifications_view.tsx b/frontend/javascripts/admin/organization/organization_notifications_view.tsx new file mode 100644 index 00000000000..f903c7e29f2 --- /dev/null +++ b/frontend/javascripts/admin/organization/organization_notifications_view.tsx @@ -0,0 +1,118 @@ +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"; + +const FormItem = Form.Item; + +type FormValues = { + displayName: string; + newUserMailingList: string; +}; + +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( + organization.id, + organization.name, + formValues.newUserMailingList, + ); + Store.dispatch(setActiveOrganizationAction(updatedOrganization)); + 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 new file mode 100644 index 00000000000..721d9a2d631 --- /dev/null +++ b/frontend/javascripts/admin/organization/organization_overview_view.tsx @@ -0,0 +1,202 @@ +import { PlusOutlined } from "@ant-design/icons"; +import { SettingsTitle } from "admin/account/helpers/settings_title"; +import { getPricingPlanStatus, getUsers, updateOrganization } from "admin/rest_api"; +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"; +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, + PlanExceededAlert, + PlanExpirationCard, + PlanUpgradeCard, +} from "./organization_cards"; +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(true); + 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); + } + + async function setOrganizationName(newOrgaName: string) { + if (!ORGA_NAME_REGEX_PATTERN.test(newOrgaName)) { + Toast.error( + "Organization name can only contain letters, numbers, spaces, and the following special characters: - _ . ß", + ); + return; + } + + const updatedOrganization = await updateOrganization( + organization.id, + newOrgaName, + organization.newUserMailingList, + ); + Store.dispatch(setActiveOrganizationAction(updatedOrganization)); + } + + 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); + + let upgradeUsersAction: React.ReactNode = null; + let upgradeStorageAction: React.ReactNode = null; + + if ( + organization.pricingPlan === PricingPlanEnum.Basic || + organization.pricingPlan === PricingPlanEnum.Team || + organization.pricingPlan === PricingPlanEnum.TeamTrial + ) { + upgradeUsersAction = ( +
diff --git a/frontend/javascripts/admin/rest_api.ts b/frontend/javascripts/admin/rest_api.ts index 5b4459ef640..97c6c349e8f 100644 --- a/frontend/javascripts/admin/rest_api.ts +++ b/frontend/javascripts/admin/rest_api.ts @@ -1722,13 +1722,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( diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 475cf298dd2..9146e5808cf 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -632,12 +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 @@ -655,10 +660,6 @@ function LoggedInAvatar({ ], } : null, - { - key: "account", - label: Account Settings, - }, { key: "logout", label: ( diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index 071168c64a1..481039d1353 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"; @@ -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"; @@ -630,7 +630,17 @@ class ReactRouter extends React.Component { } + render={() => } + /> + } + /> + } /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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