From 8f2428cc60037d9bd14d2c4cba247cfbf2c82ade Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 4 Jun 2025 17:11:38 +0200 Subject: [PATCH 01/44] Update navbar links and add account settings route - Changed the "Change Password" link to point to "/account/password". - Added new links for "Auth Token" and "Account Settings" in the navbar. - Introduced secured routes for account settings in the router. --- .../admin/auth/account_settings_view.tsx | 148 ++++++++++++++++++ frontend/javascripts/navbar.tsx | 10 +- frontend/javascripts/router.tsx | 11 ++ 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 frontend/javascripts/admin/auth/account_settings_view.tsx diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/auth/account_settings_view.tsx new file mode 100644 index 00000000000..a89f0e2d3fc --- /dev/null +++ b/frontend/javascripts/admin/auth/account_settings_view.tsx @@ -0,0 +1,148 @@ +import { Layout, Menu } from "antd"; +import { Route, Switch, useLocation, useHistory } from "react-router-dom"; +import { + CheckOutlined, + LockOutlined, + MailOutlined, + SafetyOutlined, + SettingOutlined, +} from "@ant-design/icons"; +import ChangePasswordView from "./change_password_view"; +import AuthTokenView from "./auth_token_view"; +import { useWkSelector } from "libs/react_hooks"; +import type { APIUserTheme } from "types/api_types"; +import { getSystemColorTheme } from "theme"; +import { updateSelectedThemeOfUser } from "admin/rest_api"; +import Store from "viewer/store"; +import { setActiveUserAction } from "viewer/model/actions/user_actions"; +import { setThemeAction } from "viewer/model/actions/ui_actions"; + +const { Sider, Content } = Layout; + +function EmailSettings() { + return ( +
+

Email Settings

+

Email settings page content will be added here.

+
+ ); +} + +function PasskeysSettings() { + return ( +
+

Passkeys

+

Passkeys settings page content will be added here.

+
+ ); +} + +function AppearanceSettings() { + const activeUser = useWkSelector((state) => state.activeUser); + const { selectedTheme } = activeUser || { selectedTheme: "auto" }; + + const setSelectedTheme = async (newTheme: APIUserTheme) => { + if (!activeUser) return; + + if (newTheme === "auto") newTheme = getSystemColorTheme(); + + if (selectedTheme !== newTheme) { + const newUser = await updateSelectedThemeOfUser(activeUser.id, newTheme); + Store.dispatch(setThemeAction(newTheme)); + Store.dispatch(setActiveUserAction(newUser)); + } + }; + + return ( +
+

Appearance

+
+

Theme

+ : null, + onClick: () => setSelectedTheme("auto"), + }, + { + key: "light", + label: "Light", + icon: selectedTheme === "light" ? : null, + onClick: () => setSelectedTheme("light"), + }, + { + key: "dark", + label: "Dark", + icon: selectedTheme === "dark" ? : null, + onClick: () => setSelectedTheme("dark"), + }, + ]} + /> +
+
+ ); +} + +function AccountSettingsView() { + const location = useLocation(); + const history = useHistory(); + const selectedKey = location.pathname.split("/").pop() || "email"; + + const menuItems = [ + { + key: "email", + icon: , + label: "Email", + }, + { + key: "password", + icon: , + label: "Password", + }, + { + key: "passkeys", + icon: , + label: "Passkeys", + }, + { + key: "token", + icon: , + label: "Auth Token", + }, + { + key: "appearance", + icon: , + label: "Appearance", + }, + ]; + + return ( + + + history.push(`/account/${key}`)} + /> + + + + + + + + + + + + + ); +} + +export default AccountSettingsView; diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index bef570567a4..a88b54cff1c 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -488,7 +488,7 @@ function NotificationIcon({ sendAnalyticsEvent("open_whats_new_view"); if (window.Olvy) { - // Setting the target lazily, to finally let olvy load the “what’s new” modal, as it should be shown now. + // Setting the target lazily, to finally let olvy load the "what's new" modal, as it should be shown now. window.Olvy.config.target = "#unused-olvy-target"; window.Olvy.show(); } @@ -675,9 +675,13 @@ function LoggedInAvatar({ : null, { key: "resetpassword", - label: Change Password, + label: Change Password, + }, + { key: "token", label: Auth Token }, + { + key: "account", + label: Account Settings, }, - { key: "token", label: Auth Token }, { key: "theme", label: "Theme", diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index 6176b7aa097..f979c002030 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -75,6 +75,7 @@ import type { EmptyObject } from "types/globals"; import { getDatasetIdOrNameFromReadableURLPart } from "viewer/model/accessors/dataset_accessor"; import { Store } from "viewer/singletons"; import { CommandPalette } from "viewer/view/components/command_palette"; +import AccountSettingsView from "admin/auth/account_settings_view"; const { Content } = Layout; @@ -814,6 +815,16 @@ class ReactRouter extends React.Component { {!features().isWkorgInstance && ( )} + } + /> + } + /> From 58c2d49fb8d565b9415ebfbd887207762796f35e Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 4 Jun 2025 18:14:55 +0200 Subject: [PATCH 02/44] Refactor account settings view and add profile management --- .../admin/auth/account_settings_view.tsx | 130 +++-------- .../admin/auth/auth_token_view.tsx | 25 +-- .../admin/auth/change_password_view.tsx | 212 +++++++++--------- .../javascripts/admin/auth/profile_view.tsx | 109 +++++++++ 4 files changed, 250 insertions(+), 226 deletions(-) create mode 100644 frontend/javascripts/admin/auth/profile_view.tsx diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/auth/account_settings_view.tsx index a89f0e2d3fc..81a200ece0b 100644 --- a/frontend/javascripts/admin/auth/account_settings_view.tsx +++ b/frontend/javascripts/admin/auth/account_settings_view.tsx @@ -1,33 +1,12 @@ import { Layout, Menu } from "antd"; import { Route, Switch, useLocation, useHistory } from "react-router-dom"; -import { - CheckOutlined, - LockOutlined, - MailOutlined, - SafetyOutlined, - SettingOutlined, -} from "@ant-design/icons"; +import { LockOutlined, SafetyOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; import ChangePasswordView from "./change_password_view"; import AuthTokenView from "./auth_token_view"; -import { useWkSelector } from "libs/react_hooks"; -import type { APIUserTheme } from "types/api_types"; -import { getSystemColorTheme } from "theme"; -import { updateSelectedThemeOfUser } from "admin/rest_api"; -import Store from "viewer/store"; -import { setActiveUserAction } from "viewer/model/actions/user_actions"; -import { setThemeAction } from "viewer/model/actions/ui_actions"; +import ProfileView from "./profile_view"; const { Sider, Content } = Layout; -function EmailSettings() { - return ( -
-

Email Settings

-

Email settings page content will be added here.

-
- ); -} - function PasskeysSettings() { return (
@@ -37,66 +16,16 @@ function PasskeysSettings() { ); } -function AppearanceSettings() { - const activeUser = useWkSelector((state) => state.activeUser); - const { selectedTheme } = activeUser || { selectedTheme: "auto" }; - - const setSelectedTheme = async (newTheme: APIUserTheme) => { - if (!activeUser) return; - - if (newTheme === "auto") newTheme = getSystemColorTheme(); - - if (selectedTheme !== newTheme) { - const newUser = await updateSelectedThemeOfUser(activeUser.id, newTheme); - Store.dispatch(setThemeAction(newTheme)); - Store.dispatch(setActiveUserAction(newUser)); - } - }; - - return ( -
-

Appearance

-
-

Theme

- : null, - onClick: () => setSelectedTheme("auto"), - }, - { - key: "light", - label: "Light", - icon: selectedTheme === "light" ? : null, - onClick: () => setSelectedTheme("light"), - }, - { - key: "dark", - label: "Dark", - icon: selectedTheme === "dark" ? : null, - onClick: () => setSelectedTheme("dark"), - }, - ]} - /> -
-
- ); -} - function AccountSettingsView() { const location = useLocation(); const history = useHistory(); - const selectedKey = location.pathname.split("/").pop() || "email"; + const selectedKey = location.pathname.split("/").pop() || "profile"; const menuItems = [ { - key: "email", - icon: , - label: "Email", + key: "profile", + icon: , + label: "Profile", }, { key: "password", @@ -113,34 +42,31 @@ function AccountSettingsView() { icon: , label: "Auth Token", }, - { - key: "appearance", - icon: , - label: "Appearance", - }, ]; return ( - - - history.push(`/account/${key}`)} - /> - - - - - - - - - - - + +

Account Settings

+ + + history.push(`/account/${key}`)} + /> + + + + + + + + + + + ); } diff --git a/frontend/javascripts/admin/auth/auth_token_view.tsx b/frontend/javascripts/admin/auth/auth_token_view.tsx index c095d1ac067..09b11442184 100644 --- a/frontend/javascripts/admin/auth/auth_token_view.tsx +++ b/frontend/javascripts/admin/auth/auth_token_view.tsx @@ -46,16 +46,11 @@ function AuthTokenView() { return (
- +

API Authentication

+ -

Auth Token

+

Auth Token

@@ -95,17 +90,15 @@ function AuthTokenView() { )} - - - - +

- An Auth Token is a series of symbols that serves to authenticate you. It is used in - communication with the Python API and sent with every request to verify your identity. + Your Auth Token is a unique string of characters that authenticates you when using our + Python API. It is + request to verify your identity.

- You should revoke it if somebody else has acquired your token or you have the suspicion - this has happened.{" "} + Revoke your token if it has been compromised or if you suspect someone else has gained + access to it. Read more

diff --git a/frontend/javascripts/admin/auth/change_password_view.tsx b/frontend/javascripts/admin/auth/change_password_view.tsx index 0c10dc44cd7..c605a6138e9 100644 --- a/frontend/javascripts/admin/auth/change_password_view.tsx +++ b/frontend/javascripts/admin/auth/change_password_view.tsx @@ -45,116 +45,112 @@ function ChangePasswordView({ history }: Props) { } return ( - - -

Change Password

- - - - - } - placeholder="Old Password" - /> - - - checkPasswordsAreMatching(value, ["password", "password2"]), - }, - ]} - > - - } - placeholder="New Password" - /> - - - checkPasswordsAreMatching(value, ["password", "password1"]), - }, - ]} - > - - } - placeholder="Confirm New Password" - /> - - - - - - -
+ /> + + + + + +
+
); } diff --git a/frontend/javascripts/admin/auth/profile_view.tsx b/frontend/javascripts/admin/auth/profile_view.tsx new file mode 100644 index 00000000000..516c416e0ec --- /dev/null +++ b/frontend/javascripts/admin/auth/profile_view.tsx @@ -0,0 +1,109 @@ +import { Descriptions, Typography, Dropdown, Divider, Card } from "antd"; +import { useWkSelector } from "libs/react_hooks"; +import { formatUserName } from "viewer/model/accessors/user_accessor"; +import * as Utils from "libs/utils"; +import { CheckOutlined, DownOutlined } from "@ant-design/icons"; +import type { APIUserTheme } from "types/api_types"; +import { getSystemColorTheme } from "theme"; +import { updateSelectedThemeOfUser } from "admin/rest_api"; +import Store from "viewer/store"; +import { setActiveUserAction } from "viewer/model/actions/user_actions"; +import { setThemeAction } from "viewer/model/actions/ui_actions"; + +const { Title, Text } = Typography; + +function ProfileView() { + const activeUser = useWkSelector((state) => state.activeUser); + const activeOrganization = useWkSelector((state) => state.activeOrganization); + const { selectedTheme } = activeUser || { selectedTheme: "auto" }; + + if (!activeUser) return null; + + const role = Utils.isUserAdmin(activeUser) + ? "Administrator" + : Utils.isUserTeamManager(activeUser) + ? "Team Manager" + : "User"; + + const setSelectedTheme = async (newTheme: APIUserTheme) => { + if (newTheme === "auto") newTheme = getSystemColorTheme(); + + if (selectedTheme !== newTheme) { + const newUser = await updateSelectedThemeOfUser(activeUser.id, newTheme); + Store.dispatch(setThemeAction(newTheme)); + Store.dispatch(setActiveUserAction(newUser)); + } + }; + + const themeItems = [ + { + key: "auto", + label: "System Default", + icon: selectedTheme === "auto" ? : null, + onClick: () => setSelectedTheme("auto"), + }, + { + key: "light", + label: "Light", + icon: selectedTheme === "light" ? : null, + onClick: () => setSelectedTheme("light"), + }, + { + key: "dark", + label: "Dark", + icon: selectedTheme === "dark" ? : null, + onClick: () => setSelectedTheme("dark"), + }, + ]; + + const profileItems = [ + { + label: "Name", + children: formatUserName(activeUser, activeUser), + }, + { + label: "Email", + children: activeUser.email, + }, + { + label: "Organization", + children: activeOrganization?.name || activeUser.organization, + }, + { + label: "Role", + children: role, + }, + { + label: "Theme", + children: ( + + + {themeItems.find((item) => item.key === selectedTheme)?.label} + + + ), + }, + ]; + + return ( +
+ Profile + + Manage your personal information and preferences + + + + + + +
+ ); +} + +export default ProfileView; From 9367076ee4703897779e9f8dc3f240b773901be7 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 4 Jun 2025 22:40:14 +0200 Subject: [PATCH 03/44] WIP account setting sub page styling --- .../admin/auth/account_settings_view.tsx | 49 ++++---- .../admin/auth/auth_token_view.tsx | 117 +++++++++--------- .../admin/auth/change_password_view.tsx | 40 +++++- .../javascripts/admin/auth/profile_view.tsx | 49 +++++--- 4 files changed, 145 insertions(+), 110 deletions(-) diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/auth/account_settings_view.tsx index 81a200ece0b..e5d2dd5d127 100644 --- a/frontend/javascripts/admin/auth/account_settings_view.tsx +++ b/frontend/javascripts/admin/auth/account_settings_view.tsx @@ -7,15 +7,6 @@ import ProfileView from "./profile_view"; const { Sider, Content } = Layout; -function PasskeysSettings() { - return ( -
-

Passkeys

-

Passkeys settings page content will be added here.

-
- ); -} - function AccountSettingsView() { const location = useLocation(); const history = useHistory(); @@ -23,24 +14,31 @@ function AccountSettingsView() { const menuItems = [ { - key: "profile", - icon: , - label: "Profile", - }, - { - key: "password", - icon: , - label: "Password", - }, - { - key: "passkeys", - icon: , - label: "Passkeys", + label: "Account", + type: "group", + children: [ + { + key: "profile", + icon: , + label: "Profile", + }, + { + key: "password", + icon: , + label: "Password", + }, + ], }, { - key: "token", - icon: , - label: "Auth Token", + label: "Developer", + type: "group", + children: [ + { + key: "token", + icon: , + label: "Auth Token", + }, + ], }, ]; @@ -61,7 +59,6 @@ function AccountSettingsView() { - diff --git a/frontend/javascripts/admin/auth/auth_token_view.tsx b/frontend/javascripts/admin/auth/auth_token_view.tsx index 09b11442184..e5f5b5dc013 100644 --- a/frontend/javascripts/admin/auth/auth_token_view.tsx +++ b/frontend/javascripts/admin/auth/auth_token_view.tsx @@ -1,16 +1,18 @@ -import { CopyOutlined, SwapOutlined } from "@ant-design/icons"; +import Icon, { CopyOutlined, InfoCircleOutlined, SwapOutlined } from "@ant-design/icons"; import { getAuthToken, revokeAuthToken } from "admin/rest_api"; -import { Button, Col, Form, Input, Row, Space, Spin } from "antd"; +import { Button, Typography, Descriptions, Form, Input, Row, Space, Spin, Popover } from "antd"; import { useWkSelector } from "libs/react_hooks"; import Toast from "libs/toast"; import { useEffect, useState } from "react"; -const FormItem = Form.Item; +import { AccountSettingTitle } from "./profile_view"; + +const { Text } = Typography; function AuthTokenView() { const activeUser = useWkSelector((state) => state.activeUser); const [isLoading, setIsLoading] = useState(true); const [currentToken, setCurrentToken] = useState(""); - const [form] = Form.useForm(); + useEffect(() => { fetchData(); }, []); @@ -44,65 +46,58 @@ function AuthTokenView() { } }; + const APIitems = [ + { + label: "Auth Token", + children: ( + + {currentToken} + + ), + }, + { + label: ( + <> + Token Revocation + + + + + ), + children: ( + + ), + }, + activeUser + ? { + label: "Organization ID", + children: ( + + {activeUser.organization} + + ), + } + : null, + { + label: "API Documentation", + children: Read the docs, + }, + ]; + return (
-

API Authentication

- - - -

Auth Token

-
- - - - - -
- {activeUser != null && ( - <> -

Organization ID

-
- - - -
); } diff --git a/frontend/javascripts/admin/auth/change_password_view.tsx b/frontend/javascripts/admin/auth/change_password_view.tsx index c605a6138e9..ff9bc911ea0 100644 --- a/frontend/javascripts/admin/auth/change_password_view.tsx +++ b/frontend/javascripts/admin/auth/change_password_view.tsx @@ -1,11 +1,12 @@ import { LockOutlined } from "@ant-design/icons"; -import { Alert, Button, Col, Form, Input, Row } from "antd"; +import { Alert, Button, Col, Descriptions, Form, Input, Row, Space } from "antd"; import Request from "libs/request"; import Toast from "libs/toast"; import messages from "messages"; import { type RouteComponentProps, withRouter } from "react-router-dom"; import { logoutUserAction } from "viewer/model/actions/user_actions"; import Store from "viewer/store"; +import { AccountSettingTitle } from "./profile_view"; const FormItem = Form.Item; const { Password } = Input; @@ -44,9 +45,41 @@ function ChangePasswordView({ history }: Props) { return Promise.resolve(); } + const items = [ + { + label: "Password", + children: ( + <> + + + } + value="***************" + /> + + + + ), + }, + ]; + return (
-

Password

+ +
@@ -144,12 +177,13 @@ function ChangePasswordView({ history }: Props) { width: "100%", }} > - Change Password + Update Password
+
); } diff --git a/frontend/javascripts/admin/auth/profile_view.tsx b/frontend/javascripts/admin/auth/profile_view.tsx index 516c416e0ec..a748d8c3fc4 100644 --- a/frontend/javascripts/admin/auth/profile_view.tsx +++ b/frontend/javascripts/admin/auth/profile_view.tsx @@ -12,6 +12,21 @@ import { setThemeAction } from "viewer/model/actions/ui_actions"; const { Title, Text } = Typography; +export function AccountSettingTitle({ + title, + description, +}: { title: string; description: string }) { + return ( +
+

{title}

+ + {description} + + +
+ ); +} + function ProfileView() { const activeUser = useWkSelector((state) => state.activeUser); const activeOrganization = useWkSelector((state) => state.activeOrganization); @@ -76,32 +91,26 @@ function ProfileView() { { label: "Theme", children: ( - - - {themeItems.find((item) => item.key === selectedTheme)?.label} - - + }> + {themeItems.find((item) => item.key === selectedTheme)?.label} + ), }, ]; return (
- Profile - - Manage your personal information and preferences - - - - - - + +
); } From 33b0b1aac82e1e8cbdb4ba52d866dfccf7bdf590 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Thu, 5 Jun 2025 13:24:57 +0200 Subject: [PATCH 04/44] formatting --- biome.json | 6 +- .../admin/auth/account_settings_view.tsx | 6 +- .../admin/auth/auth_token_view.tsx | 17 +- .../admin/auth/change_password_view.tsx | 279 ++++++++++-------- .../javascripts/admin/auth/profile_view.tsx | 16 +- frontend/javascripts/router.tsx | 2 +- tools/postgres/dbtool.js | 4 +- tools/proxy/proxy.js | 10 +- vitest.config.ts | 2 +- 9 files changed, 172 insertions(+), 170 deletions(-) diff --git a/biome.json b/biome.json index 56d96c037c1..221b190e61d 100644 --- a/biome.json +++ b/biome.json @@ -100,12 +100,10 @@ }, "overrides": [ { - "include": [ - "**/package.json" - ], + "include": ["**/package.json"], "formatter": { "lineWidth": 1 } } ] -} \ No newline at end of file +} diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/auth/account_settings_view.tsx index e5d2dd5d127..606111171a9 100644 --- a/frontend/javascripts/admin/auth/account_settings_view.tsx +++ b/frontend/javascripts/admin/auth/account_settings_view.tsx @@ -1,8 +1,8 @@ +import { SafetyOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; import { Layout, Menu } from "antd"; -import { Route, Switch, useLocation, useHistory } from "react-router-dom"; -import { LockOutlined, SafetyOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; -import ChangePasswordView from "./change_password_view"; +import { Route, Switch, useHistory, useLocation } from "react-router-dom"; import AuthTokenView from "./auth_token_view"; +import ChangePasswordView from "./change_password_view"; import ProfileView from "./profile_view"; const { Sider, Content } = Layout; diff --git a/frontend/javascripts/admin/auth/auth_token_view.tsx b/frontend/javascripts/admin/auth/auth_token_view.tsx index e5f5b5dc013..621c1e5e649 100644 --- a/frontend/javascripts/admin/auth/auth_token_view.tsx +++ b/frontend/javascripts/admin/auth/auth_token_view.tsx @@ -1,8 +1,7 @@ -import Icon, { CopyOutlined, InfoCircleOutlined, SwapOutlined } from "@ant-design/icons"; +import { InfoCircleOutlined, SwapOutlined } from "@ant-design/icons"; import { getAuthToken, revokeAuthToken } from "admin/rest_api"; -import { Button, Typography, Descriptions, Form, Input, Row, Space, Spin, Popover } from "antd"; +import { Button, Descriptions, Popover, Spin, Typography } from "antd"; import { useWkSelector } from "libs/react_hooks"; -import Toast from "libs/toast"; import { useEffect, useState } from "react"; import { AccountSettingTitle } from "./profile_view"; @@ -34,18 +33,6 @@ function AuthTokenView() { } }; - const copyTokenToClipboard = async () => { - await navigator.clipboard.writeText(currentToken); - Toast.success("Token copied to clipboard"); - }; - - const copyOrganizationIdToClipboard = async () => { - if (activeUser != null) { - await navigator.clipboard.writeText(activeUser.organization); - Toast.success("Organization ID copied to clipboard"); - } - }; - const APIitems = [ { label: "Auth Token", diff --git a/frontend/javascripts/admin/auth/change_password_view.tsx b/frontend/javascripts/admin/auth/change_password_view.tsx index ff9bc911ea0..5d835ac6b21 100644 --- a/frontend/javascripts/admin/auth/change_password_view.tsx +++ b/frontend/javascripts/admin/auth/change_password_view.tsx @@ -1,8 +1,9 @@ import { LockOutlined } from "@ant-design/icons"; -import { Alert, Button, Col, Descriptions, Form, Input, Row, Space } from "antd"; +import { Alert, Button, Descriptions, Form, Input, List, Space } from "antd"; import Request from "libs/request"; import Toast from "libs/toast"; import messages from "messages"; +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"; @@ -18,6 +19,7 @@ const MIN_PASSWORD_LENGTH = 8; function ChangePasswordView({ history }: Props) { const [form] = Form.useForm(); + const [isResetPasswordVisible, setResetPasswordVisible] = useState(false); function onFinish(formValues: Record) { Request.sendJSONReceiveJSON("/api/auth/changePassword", { @@ -45,145 +47,160 @@ function ChangePasswordView({ history }: Props) { return Promise.resolve(); } - const items = [ + function getPasswordComponent() { + return isResetPasswordVisible ? ( +
+ + + } + placeholder="Old Password" + /> + + + checkPasswordsAreMatching(value, ["password", "password2"]), + }, + ]} + > + + } + placeholder="New Password" + /> + + + checkPasswordsAreMatching(value, ["password", "password1"]), + }, + ]} + > + + } + placeholder="Confirm New Password" + /> + + + + + + + + + + ) : ( + <> + + + + + + ); + } + + function handleResetPassword() { + setResetPasswordVisible(true); + } + + const passwordItems = [ { label: "Password", - children: ( - <> - - - } - value="***************" - /> - - - - ), + children: getPasswordComponent(), + }, + ]; + + const passKeyList = [ + { + name: "passkey1", + details: "2024-05-01", + }, + { + name: "passkey2", + details: "2025-05-01", }, ]; return (
- - - -
- - - } - placeholder="Old Password" - /> - - - checkPasswordsAreMatching(value, ["password", "password2"]), - }, - ]} - > - - } - placeholder="New Password" - /> - - - checkPasswordsAreMatching(value, ["password", "password1"]), - }, - ]} - > - - } - placeholder="Confirm New Password" - /> - - - - - - - -
+ + + ( + Delete]}> + + + )} + />
); } diff --git a/frontend/javascripts/admin/auth/profile_view.tsx b/frontend/javascripts/admin/auth/profile_view.tsx index a748d8c3fc4..357e4ab5d1a 100644 --- a/frontend/javascripts/admin/auth/profile_view.tsx +++ b/frontend/javascripts/admin/auth/profile_view.tsx @@ -1,16 +1,16 @@ -import { Descriptions, Typography, Dropdown, Divider, Card } from "antd"; +import { CheckOutlined, DownOutlined } from "@ant-design/icons"; +import { updateSelectedThemeOfUser } from "admin/rest_api"; +import { Descriptions, Divider, Dropdown, Typography } from "antd"; import { useWkSelector } from "libs/react_hooks"; -import { formatUserName } from "viewer/model/accessors/user_accessor"; import * as Utils from "libs/utils"; -import { CheckOutlined, DownOutlined } from "@ant-design/icons"; -import type { APIUserTheme } from "types/api_types"; import { getSystemColorTheme } from "theme"; -import { updateSelectedThemeOfUser } from "admin/rest_api"; -import Store from "viewer/store"; -import { setActiveUserAction } from "viewer/model/actions/user_actions"; +import type { APIUserTheme } from "types/api_types"; +import { formatUserName } from "viewer/model/accessors/user_accessor"; import { setThemeAction } from "viewer/model/actions/ui_actions"; +import { setActiveUserAction } from "viewer/model/actions/user_actions"; +import Store from "viewer/store"; -const { Title, Text } = Typography; +const { Text } = Typography; export function AccountSettingTitle({ title, diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index f979c002030..ed81b290f31 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -63,6 +63,7 @@ import { getDatasetIdFromNameAndOrganization, getOrganizationForDataset, } from "admin/api/disambiguate_legacy_routes"; +import AccountSettingsView from "admin/auth/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"; @@ -75,7 +76,6 @@ import type { EmptyObject } from "types/globals"; import { getDatasetIdOrNameFromReadableURLPart } from "viewer/model/accessors/dataset_accessor"; import { Store } from "viewer/singletons"; import { CommandPalette } from "viewer/view/components/command_palette"; -import AccountSettingsView from "admin/auth/account_settings_view"; const { Content } = Layout; diff --git a/tools/postgres/dbtool.js b/tools/postgres/dbtool.js index 3f7dc1960f0..5ff3b9691b8 100755 --- a/tools/postgres/dbtool.js +++ b/tools/postgres/dbtool.js @@ -15,10 +15,10 @@ const PG_CONFIG = (() => { const url = new URL(rawUrl); url.username = url.username ? url.username - : process.env.POSTGRES_USER ?? process.env.PGUSER ?? "postgres"; + : (process.env.POSTGRES_USER ?? process.env.PGUSER ?? "postgres"); url.password = url.password ? url.password - : process.env.POSTGRES_PASSWORD ?? process.env.PGPASSWORD ?? "postgres"; + : (process.env.POSTGRES_PASSWORD ?? process.env.PGPASSWORD ?? "postgres"); url.port = url.port ? url.port : 5432; const urlWithDefaultDatabase = new URL(url); diff --git a/tools/proxy/proxy.js b/tools/proxy/proxy.js index dbeb55dc91f..dc9a7d69c36 100644 --- a/tools/proxy/proxy.js +++ b/tools/proxy/proxy.js @@ -1,7 +1,7 @@ const express = require("express"); const httpProxy = require("http-proxy"); -const { spawn, exec } = require("child_process"); -const path = require("path"); +const { spawn, exec } = require("node:child_process"); +const path = require("node:path"); const prefixLines = require("prefix-stream-lines"); const proxy = httpProxy.createProxyServer({ @@ -11,7 +11,7 @@ const proxy = httpProxy.createProxyServer({ const app = express(); const ROOT = path.resolve(path.join(__dirname, "..", "..")); -const PORT = parseInt(process.env.PORT || 9000, 10); +const PORT = Number.parseInt(process.env.PORT || 9000, 10); const HOST = `http://127.0.0.1:${PORT}`; const loggingPrefix = "Proxy:"; @@ -90,7 +90,7 @@ process.on("SIGINT", shutdown); proxy.on("error", (err, req, res) => { console.error(loggingPrefix, "Sending Bad gateway due to the following error: ", err); - res.writeHead(503, { 'Content-Type': 'text/html' }); + res.writeHead(503, { "Content-Type": "text/html" }); res.end(` @@ -129,7 +129,7 @@ function toWebpackDev(req, res) { } function toSam(req, res) { - proxy.web(req, res, { target: `http://127.0.0.1:8080` }); + proxy.web(req, res, { target: "http://127.0.0.1:8080" }); } proxy.on("proxyReq", (proxyReq, req) => { diff --git a/vitest.config.ts b/vitest.config.ts index 08f21a10e31..38d0b4ab1f6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,5 @@ -import { defineConfig } from "vitest/config"; import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; // This config object is intentionally left under-specified (see config.test.include). // Other vitest_*.config.ts import this config. Vitest should always be called with From 4a4c4605b8ffd2633c8f8b608537bd1c8ecbf020 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Thu, 5 Jun 2025 13:36:59 +0200 Subject: [PATCH 05/44] fix typeing --- .../admin/auth/account_settings_view.tsx | 3 ++- .../admin/auth/auth_token_view.tsx | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/auth/account_settings_view.tsx index 606111171a9..f081b1b35e7 100644 --- a/frontend/javascripts/admin/auth/account_settings_view.tsx +++ b/frontend/javascripts/admin/auth/account_settings_view.tsx @@ -1,5 +1,6 @@ import { SafetyOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; import { Layout, Menu } from "antd"; +import type { MenuItemGroupType } from "antd/es/menu/interface"; import { Route, Switch, useHistory, useLocation } from "react-router-dom"; import AuthTokenView from "./auth_token_view"; import ChangePasswordView from "./change_password_view"; @@ -12,7 +13,7 @@ function AccountSettingsView() { const history = useHistory(); const selectedKey = location.pathname.split("/").pop() || "profile"; - const menuItems = [ + const menuItems: MenuItemGroupType[] = [ { label: "Account", type: "group", diff --git a/frontend/javascripts/admin/auth/auth_token_view.tsx b/frontend/javascripts/admin/auth/auth_token_view.tsx index 621c1e5e649..0a9cd3a571f 100644 --- a/frontend/javascripts/admin/auth/auth_token_view.tsx +++ b/frontend/javascripts/admin/auth/auth_token_view.tsx @@ -60,16 +60,18 @@ function AuthTokenView() { ), }, - activeUser - ? { - label: "Organization ID", - children: ( - - {activeUser.organization} - - ), - } - : null, + ...(activeUser + ? [ + { + label: "Organization ID", + children: ( + + {activeUser.organization} + + ), + }, + ] + : []), { label: "API Documentation", children: Read the docs, From 8e18e2a045af0d4b88796b3dc4cfbf8eb9bdfae5 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Thu, 5 Jun 2025 14:46:02 +0200 Subject: [PATCH 06/44] added breadcrumbs --- .../javascripts/admin/auth/account_settings_view.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/auth/account_settings_view.tsx index f081b1b35e7..fbec2dda944 100644 --- a/frontend/javascripts/admin/auth/account_settings_view.tsx +++ b/frontend/javascripts/admin/auth/account_settings_view.tsx @@ -1,5 +1,5 @@ import { SafetyOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; -import { Layout, Menu } from "antd"; +import { Breadcrumb, Layout, Menu } from "antd"; import type { MenuItemGroupType } from "antd/es/menu/interface"; import { Route, Switch, useHistory, useLocation } from "react-router-dom"; import AuthTokenView from "./auth_token_view"; @@ -43,9 +43,10 @@ function AccountSettingsView() { }, ]; + const subPageBreadcrumb = selectedKey.charAt(0).toUpperCase() + selectedKey.slice(1); + return ( -

Account Settings

history.push(`/account/${key}`)} /> - + + + Account Settings + {subPageBreadcrumb} + From 77e602ab78536dbbde6831b4eceabfcf801b38f5 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Thu, 5 Jun 2025 15:20:10 +0200 Subject: [PATCH 07/44] added redirects to old routes --- .../admin/auth/account_settings_view.tsx | 44 ++++++++--------- frontend/javascripts/navbar.tsx | 48 ++----------------- frontend/javascripts/router.tsx | 6 +-- 3 files changed, 26 insertions(+), 72 deletions(-) diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/auth/account_settings_view.tsx index fbec2dda944..0fdf5e754b6 100644 --- a/frontend/javascripts/admin/auth/account_settings_view.tsx +++ b/frontend/javascripts/admin/auth/account_settings_view.tsx @@ -47,29 +47,27 @@ function AccountSettingsView() { return ( - - - history.push(`/account/${key}`)} - /> - - - - Account Settings - {subPageBreadcrumb} - - - - - - - - - + + history.push(`/account/${key}`)} + /> + + + + Account Settings + {subPageBreadcrumb} + + + + + + + + ); } diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index a88b54cff1c..475cf298dd2 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -1,7 +1,6 @@ import { BarChartOutlined, BellOutlined, - CheckOutlined, HomeOutlined, QuestionCircleOutlined, SwapOutlined, @@ -36,7 +35,6 @@ import { sendAnalyticsEvent, switchToOrganization, updateNovelUserExperienceInfos, - updateSelectedThemeOfUser, } from "admin/rest_api"; import type { ItemType, MenuItemType, SubMenuType } from "antd/es/menu/interface"; import { MaintenanceBanner, UpgradeVersionBanner } from "banners"; @@ -50,20 +48,14 @@ import * as Utils from "libs/utils"; import window, { location } from "libs/window"; import messages from "messages"; import type { MenuClickEventHandler } from "rc-menu/lib/interface"; -import { getAntdTheme, getSystemColorTheme } from "theme"; -import type { - APIOrganizationCompact, - APIUser, - APIUserCompact, - APIUserTheme, -} from "types/api_types"; +import { getAntdTheme } from "theme"; +import type { APIOrganizationCompact, APIUser, APIUserCompact } from "types/api_types"; import constants from "viewer/constants"; import { isAnnotationFromDifferentOrganization, isAnnotationOwner as isAnnotationOwnerAccessor, } from "viewer/model/accessors/annotation_accessor"; import { formatUserName } from "viewer/model/accessors/user_accessor"; -import { setThemeAction } from "viewer/model/actions/ui_actions"; import { logoutUserAction, setActiveUserAction } from "viewer/model/actions/user_actions"; import type { WebknossosState } from "viewer/store"; import Store from "viewer/store"; @@ -573,7 +565,7 @@ function LoggedInAvatar({ handleLogout: (event: React.SyntheticEvent) => void; navbarHeight: number; } & SubMenuProps) { - const { firstName, lastName, organization: organizationId, selectedTheme } = activeUser; + const { firstName, lastName, organization: organizationId } = activeUser; const usersOrganizations = useFetch(getUsersOrganizations, [], []); const activeOrganization = usersOrganizations.find((org) => org.id === organizationId); const switchableOrganizations = usersOrganizations.filter((org) => org.id !== organizationId); @@ -593,16 +585,6 @@ function LoggedInAvatar({ } }; - const setSelectedTheme = async (newTheme: APIUserTheme) => { - if (newTheme === "auto") newTheme = getSystemColorTheme(); - - if (selectedTheme !== newTheme) { - const newUser = await updateSelectedThemeOfUser(activeUser.id, newTheme); - Store.dispatch(setThemeAction(newTheme)); - Store.dispatch(setActiveUserAction(newUser)); - } - }; - const maybeOrganizationFilterInput = switchableOrganizations.length > ORGANIZATION_COUNT_THRESHOLD_FOR_SEARCH_INPUT ? [ @@ -673,34 +655,10 @@ function LoggedInAvatar({ ], } : null, - { - key: "resetpassword", - label: Change Password, - }, - { key: "token", label: Auth Token }, { key: "account", label: Account Settings, }, - { - key: "theme", - label: "Theme", - children: [ - ["auto", "System-default"], - ["light", "Light"], - ["dark", "Dark"], - ].map(([key, label]) => { - return { - key, - label: label, - icon: selectedTheme === key ? : null, - onClick: () => { - // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message - setSelectedTheme(key); - }, - }; - }), - }, { key: "logout", label: ( diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index ed81b290f31..1a4c1051839 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -1,6 +1,4 @@ import AcceptInviteView from "admin/auth/accept_invite_view"; -import AuthTokenView from "admin/auth/auth_token_view"; -import ChangePasswordView from "admin/auth/change_password_view"; import FinishResetPasswordView from "admin/auth/finish_reset_password_view"; import LoginView from "admin/auth/login_view"; import RegistrationView from "admin/auth/registration_view"; @@ -643,12 +641,12 @@ class ReactRouter extends React.Component { } /> } /> } /> From 09d50eaf5ee082f853c21f64d8868d4e259e817f Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Thu, 5 Jun 2025 20:20:31 +0200 Subject: [PATCH 08/44] apply feedback --- .../javascripts/admin/auth/account_settings_view.tsx | 12 +++++++++--- .../javascripts/admin/auth/change_password_view.tsx | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/auth/account_settings_view.tsx index 0fdf5e754b6..f37ae239144 100644 --- a/frontend/javascripts/admin/auth/account_settings_view.tsx +++ b/frontend/javascripts/admin/auth/account_settings_view.tsx @@ -8,6 +8,12 @@ import ProfileView from "./profile_view"; const { Sider, Content } = Layout; +const BREADCRUMB_LABELS = { + token: "Auth Token", + password: "Password", + profile: "Profile", +}; + function AccountSettingsView() { const location = useLocation(); const history = useHistory(); @@ -43,8 +49,6 @@ function AccountSettingsView() { }, ]; - const subPageBreadcrumb = selectedKey.charAt(0).toUpperCase() + selectedKey.slice(1); - return ( @@ -59,7 +63,9 @@ function AccountSettingsView() { Account Settings - {subPageBreadcrumb} + + {BREADCRUMB_LABELS[selectedKey as keyof typeof BREADCRUMB_LABELS]} + diff --git a/frontend/javascripts/admin/auth/change_password_view.tsx b/frontend/javascripts/admin/auth/change_password_view.tsx index 5d835ac6b21..d4a2203eaab 100644 --- a/frontend/javascripts/admin/auth/change_password_view.tsx +++ b/frontend/javascripts/admin/auth/change_password_view.tsx @@ -148,7 +148,7 @@ function ChangePasswordView({ history }: Props) { ) : ( <> - + From 14e624b74949a7c03cd38af14eeb4e52618c7fc7 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Thu, 5 Jun 2025 20:23:46 +0200 Subject: [PATCH 09/44] fix typo --- frontend/javascripts/admin/auth/auth_token_view.tsx | 4 ++-- frontend/javascripts/admin/auth/change_password_view.tsx | 6 +++--- frontend/javascripts/admin/auth/profile_view.tsx | 4 ++-- frontend/javascripts/messages.tsx | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/javascripts/admin/auth/auth_token_view.tsx b/frontend/javascripts/admin/auth/auth_token_view.tsx index 0a9cd3a571f..c16b9102f90 100644 --- a/frontend/javascripts/admin/auth/auth_token_view.tsx +++ b/frontend/javascripts/admin/auth/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 { AccountSettingTitle } from "./profile_view"; +import { AccountSettingsTitle } from "./profile_view"; const { Text } = Typography; @@ -80,7 +80,7 @@ function AuthTokenView() { return (
- diff --git a/frontend/javascripts/admin/auth/change_password_view.tsx b/frontend/javascripts/admin/auth/change_password_view.tsx index d4a2203eaab..1be4acdc673 100644 --- a/frontend/javascripts/admin/auth/change_password_view.tsx +++ b/frontend/javascripts/admin/auth/change_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 { AccountSettingTitle } from "./profile_view"; +import { AccountSettingsTitle } from "./profile_view"; const FormItem = Form.Item; const { Password } = Input; @@ -181,7 +181,7 @@ function ChangePasswordView({ history }: Props) { return (
- + - + - diff --git a/frontend/javascripts/messages.tsx b/frontend/javascripts/messages.tsx index e9e7dc996c0..468b2297bef 100644 --- a/frontend/javascripts/messages.tsx +++ b/frontend/javascripts/messages.tsx @@ -415,7 +415,7 @@ instead. Only enable this option if you understand its effect. All layers will n "Unfortunately, we cannot provide the service without your consent to the processing of your data.", "auth.tos_check_required": "Unfortunately, we cannot provide the service without your consent to our terms of service.", - "auth.reset_logout": "You will be logged out, after successfully changing your password.", + "auth.reset_logout": "You will be logged out after successfully changing your password.", "auth.reset_old_password": "Please input your old password!", "auth.reset_new_password": "Please input your new password!", "auth.reset_new_password2": "Please repeat your new password!", From cb704990f5a6f6d52945b01da3b1f139786fd0bd Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 6 Jun 2025 10:14:36 +0200 Subject: [PATCH 10/44] move account related views into separate directory --- .../account_auth_token_view.tsx} | 6 +++--- .../account_password_view.tsx} | 6 +++--- .../account_profile_view.tsx} | 4 ++-- .../{auth => account}/account_settings_view.tsx | 14 +++++++------- frontend/javascripts/router.tsx | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) rename frontend/javascripts/admin/{auth/auth_token_view.tsx => account/account_auth_token_view.tsx} (94%) rename frontend/javascripts/admin/{auth/change_password_view.tsx => account/account_password_view.tsx} (96%) rename frontend/javascripts/admin/{auth/profile_view.tsx => account/account_profile_view.tsx} (97%) rename frontend/javascripts/admin/{auth => account}/account_settings_view.tsx (80%) diff --git a/frontend/javascripts/admin/auth/auth_token_view.tsx b/frontend/javascripts/admin/account/account_auth_token_view.tsx similarity index 94% rename from frontend/javascripts/admin/auth/auth_token_view.tsx rename to frontend/javascripts/admin/account/account_auth_token_view.tsx index c16b9102f90..d3d82638e18 100644 --- a/frontend/javascripts/admin/auth/auth_token_view.tsx +++ b/frontend/javascripts/admin/account/account_auth_token_view.tsx @@ -3,11 +3,11 @@ 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 "./profile_view"; +import { AccountSettingsTitle } from "./account_profile_view"; const { Text } = Typography; -function AuthTokenView() { +function AccountAuthTokenView() { const activeUser = useWkSelector((state) => state.activeUser); const [isLoading, setIsLoading] = useState(true); const [currentToken, setCurrentToken] = useState(""); @@ -91,4 +91,4 @@ function AuthTokenView() { ); } -export default AuthTokenView; +export default AccountAuthTokenView; diff --git a/frontend/javascripts/admin/auth/change_password_view.tsx b/frontend/javascripts/admin/account/account_password_view.tsx similarity index 96% rename from frontend/javascripts/admin/auth/change_password_view.tsx rename to frontend/javascripts/admin/account/account_password_view.tsx index 1be4acdc673..96a75b682e7 100644 --- a/frontend/javascripts/admin/auth/change_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 "./profile_view"; +import { AccountSettingsTitle } from "./account_profile_view"; const FormItem = Form.Item; const { Password } = Input; @@ -17,7 +17,7 @@ type Props = { const MIN_PASSWORD_LENGTH = 8; -function ChangePasswordView({ history }: Props) { +function AccountPasswordView({ history }: Props) { const [form] = Form.useForm(); const [isResetPasswordVisible, setResetPasswordVisible] = useState(false); @@ -205,4 +205,4 @@ function ChangePasswordView({ history }: Props) { ); } -export default withRouter(ChangePasswordView); +export default withRouter(AccountPasswordView); diff --git a/frontend/javascripts/admin/auth/profile_view.tsx b/frontend/javascripts/admin/account/account_profile_view.tsx similarity index 97% rename from frontend/javascripts/admin/auth/profile_view.tsx rename to frontend/javascripts/admin/account/account_profile_view.tsx index 1aa168d88fe..4a446d554c0 100644 --- a/frontend/javascripts/admin/auth/profile_view.tsx +++ b/frontend/javascripts/admin/account/account_profile_view.tsx @@ -27,7 +27,7 @@ export function AccountSettingsTitle({ ); } -function ProfileView() { +function AccountProfileView() { const activeUser = useWkSelector((state) => state.activeUser); const activeOrganization = useWkSelector((state) => state.activeOrganization); const { selectedTheme } = activeUser || { selectedTheme: "auto" }; @@ -115,4 +115,4 @@ function ProfileView() { ); } -export default ProfileView; +export default AccountProfileView; diff --git a/frontend/javascripts/admin/auth/account_settings_view.tsx b/frontend/javascripts/admin/account/account_settings_view.tsx similarity index 80% rename from frontend/javascripts/admin/auth/account_settings_view.tsx rename to frontend/javascripts/admin/account/account_settings_view.tsx index f37ae239144..8135ad863cd 100644 --- a/frontend/javascripts/admin/auth/account_settings_view.tsx +++ b/frontend/javascripts/admin/account/account_settings_view.tsx @@ -2,9 +2,9 @@ import { SafetyOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons import { Breadcrumb, Layout, Menu } from "antd"; import type { MenuItemGroupType } from "antd/es/menu/interface"; import { Route, Switch, useHistory, useLocation } from "react-router-dom"; -import AuthTokenView from "./auth_token_view"; -import ChangePasswordView from "./change_password_view"; -import ProfileView from "./profile_view"; +import AccountAuthTokenView from "./account_auth_token_view"; +import AccountPasswordView from "./account_password_view"; +import AccountProfileView from "./account_profile_view"; const { Sider, Content } = Layout; @@ -68,10 +68,10 @@ function AccountSettingsView() { - - - - + + + + diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index 1a4c1051839..96752be7d96 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -61,7 +61,7 @@ import { getDatasetIdFromNameAndOrganization, getOrganizationForDataset, } from "admin/api/disambiguate_legacy_routes"; -import AccountSettingsView from "admin/auth/account_settings_view"; +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 dd6c8e628c0a93f0fa793826bd9bd2449cc24989 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 6 Jun 2025 14:57:42 +0200 Subject: [PATCH 11/44] style background --- .../admin/account/account_settings_view.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/javascripts/admin/account/account_settings_view.tsx b/frontend/javascripts/admin/account/account_settings_view.tsx index 8135ad863cd..fbec4437e8b 100644 --- a/frontend/javascripts/admin/account/account_settings_view.tsx +++ b/frontend/javascripts/admin/account/account_settings_view.tsx @@ -1,7 +1,7 @@ import { SafetyOutlined, SettingOutlined, UserOutlined } from "@ant-design/icons"; import { Breadcrumb, Layout, Menu } from "antd"; import type { MenuItemGroupType } from "antd/es/menu/interface"; -import { Route, Switch, useHistory, useLocation } from "react-router-dom"; +import { Redirect, Route, Switch, useHistory, useLocation } from "react-router-dom"; import AccountAuthTokenView from "./account_auth_token_view"; import AccountPasswordView from "./account_password_view"; import AccountProfileView from "./account_profile_view"; @@ -50,17 +50,19 @@ function AccountSettingsView() { ]; return ( - + history.push(`/account/${key}`)} /> - + Account Settings @@ -71,7 +73,7 @@ function AccountSettingsView() { - + } /> From 4cd2ecd40d539e07ff9ce47dd4ce9de4e196bf66 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Fri, 6 Jun 2025 14:58:04 +0200 Subject: [PATCH 12/44] 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 13/44] 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 14/44] 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 15/44] 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 16/44] 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 17/44] 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 21/44] 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 22/44] 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 23/44] 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 b09480679d14d86dd37cbdb51b7731d42ab4f789 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Mon, 23 Jun 2025 16:22:32 +0200 Subject: [PATCH 24/44] update the account settings page with card style --- .../admin/account/account_auth_token_view.tsx | 47 ++++++------- .../admin/account/account_password_view.tsx | 22 ++----- .../admin/account/account_profile_view.tsx | 37 ++++++----- .../admin/account/account_settings_view.tsx | 66 +++++++++---------- 4 files changed, 84 insertions(+), 88 deletions(-) diff --git a/frontend/javascripts/admin/account/account_auth_token_view.tsx b/frontend/javascripts/admin/account/account_auth_token_view.tsx index b9c78e5adaf..244ceba68d4 100644 --- a/frontend/javascripts/admin/account/account_auth_token_view.tsx +++ b/frontend/javascripts/admin/account/account_auth_token_view.tsx @@ -1,9 +1,10 @@ -import { InfoCircleOutlined, SwapOutlined } from "@ant-design/icons"; +import { SwapOutlined } from "@ant-design/icons"; import { getAuthToken, revokeAuthToken } from "admin/rest_api"; -import { Button, Descriptions, Popover, Spin, Typography } from "antd"; +import { Button, Col, Row, Spin, Typography } from "antd"; import { useWkSelector } from "libs/react_hooks"; import { useEffect, useState } from "react"; import { SettingsTitle } from "./helpers/settings_title"; +import { SettingsCard } from "./helpers/settings_card"; const { Text } = Typography; @@ -35,27 +36,19 @@ function AccountAuthTokenView() { const APIitems = [ { - label: "Auth Token", - children: ( + title: "Auth Token", + value: ( {currentToken} ), }, { - label: ( - <> - Token Revocation - - - - - ), - children: ( - ), @@ -63,8 +56,8 @@ function AccountAuthTokenView() { ...(activeUser ? [ { - label: "Organization ID", - children: ( + title: "Organization ID", + value: ( {activeUser.organization} @@ -73,8 +66,8 @@ function AccountAuthTokenView() { ] : []), { - label: "API Documentation", - children: Read the docs, + title: "API Documentation", + value: Read the docs, }, ]; @@ -85,7 +78,17 @@ function AccountAuthTokenView() { description="Access the WEBKNOSSO Python API with your API token" /> - + + {APIitems.map((item) => ( + + + + ))} +
); diff --git a/frontend/javascripts/admin/account/account_password_view.tsx b/frontend/javascripts/admin/account/account_password_view.tsx index e996e7af5ba..5fd041c2a29 100644 --- a/frontend/javascripts/admin/account/account_password_view.tsx +++ b/frontend/javascripts/admin/account/account_password_view.tsx @@ -1,5 +1,5 @@ import { LockOutlined } from "@ant-design/icons"; -import { Alert, Button, Descriptions, Form, Input, List, Space } from "antd"; +import { Alert, Button, Col, Form, Input, List, Row, Space } from "antd"; import Request from "libs/request"; import Toast from "libs/toast"; import messages from "messages"; @@ -8,6 +8,7 @@ import { type RouteComponentProps, withRouter } from "react-router-dom"; import { logoutUserAction } from "viewer/model/actions/user_actions"; import Store from "viewer/store"; import { SettingsTitle } from "./helpers/settings_title"; +import { SettingsCard } from "./helpers/settings_card"; const FormItem = Form.Item; const { Password } = Input; @@ -161,13 +162,6 @@ function AccountPasswordView({ history }: Props) { setResetPasswordVisible(true); } - const passwordItems = [ - { - label: "Password", - children: getPasswordComponent(), - }, - ]; - const passKeyList = [ { name: "passkey1", @@ -182,13 +176,11 @@ function AccountPasswordView({ history }: Props) { return (
- + + + + + state.activeUser); @@ -57,24 +58,24 @@ function AccountProfileView() { const profileItems = [ { - label: "Name", - children: formatUserName(activeUser, activeUser), + title: "Name", + value: formatUserName(activeUser, activeUser), }, { - label: "Email", - children: activeUser.email, + title: "Email", + value: activeUser.email, }, { - label: "Organization", - children: activeOrganization?.name || activeUser.organization, + title: "Organization", + value: activeOrganization?.name || activeUser.organization, }, { - label: "Role", - children: role, + title: "Role", + value: role, }, { - label: "Theme", - children: ( + title: "Theme", + value: ( }> {themeItems.find((item) => item.key === selectedTheme)?.label} @@ -88,13 +89,13 @@ function AccountProfileView() { title="Profile" description="Manage your personal information and preferences" /> - + + {profileItems.map((item) => ( + + + + ))} +
); } diff --git a/frontend/javascripts/admin/account/account_settings_view.tsx b/frontend/javascripts/admin/account/account_settings_view.tsx index fbec4437e8b..22525ffe4bf 100644 --- a/frontend/javascripts/admin/account/account_settings_view.tsx +++ b/frontend/javascripts/admin/account/account_settings_view.tsx @@ -14,41 +14,41 @@ const BREADCRUMB_LABELS = { profile: "Profile", }; +const MENU_ITEMS: MenuItemGroupType[] = [ + { + label: "Account", + type: "group", + children: [ + { + key: "profile", + icon: , + label: "Profile", + }, + { + key: "password", + icon: , + label: "Password", + }, + ], + }, + { + label: "Developer", + type: "group", + children: [ + { + key: "token", + icon: , + label: "Auth Token", + }, + ], + }, +]; + function AccountSettingsView() { const location = useLocation(); const history = useHistory(); const selectedKey = location.pathname.split("/").pop() || "profile"; - const menuItems: MenuItemGroupType[] = [ - { - label: "Account", - type: "group", - children: [ - { - key: "profile", - icon: , - label: "Profile", - }, - { - key: "password", - icon: , - label: "Password", - }, - ], - }, - { - label: "Developer", - type: "group", - children: [ - { - key: "token", - icon: , - label: "Auth Token", - }, - ], - }, - ]; - return ( history.push(`/account/${key}`)} /> - - + + Account Settings {BREADCRUMB_LABELS[selectedKey as keyof typeof BREADCRUMB_LABELS]} From a91f88b9fe87d2c7cd17fb5a8c9d159450b404ea Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 24 Jun 2025 10:36:59 +0200 Subject: [PATCH 25/44] Update frontend/javascripts/router.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- frontend/javascripts/router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index 96752be7d96..071168c64a1 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -641,7 +641,7 @@ class ReactRouter extends React.Component { } + render={() => } /> Date: Tue, 24 Jun 2025 10:39:20 +0200 Subject: [PATCH 26/44] Update frontend/javascripts/admin/account/account_password_view.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../admin/account/account_password_view.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/javascripts/admin/account/account_password_view.tsx b/frontend/javascripts/admin/account/account_password_view.tsx index 96a75b682e7..fe91cd5fc21 100644 --- a/frontend/javascripts/admin/account/account_password_view.tsx +++ b/frontend/javascripts/admin/account/account_password_view.tsx @@ -24,12 +24,17 @@ function AccountPasswordView({ history }: Props) { function onFinish(formValues: Record) { Request.sendJSONReceiveJSON("/api/auth/changePassword", { data: formValues, - }).then(async () => { - Toast.success(messages["auth.reset_pw_confirmation"]); - await Request.receiveJSON("/api/auth/logout"); - history.push("/auth/login"); - Store.dispatch(logoutUserAction()); - }); + }) + .then(async () => { + Toast.success(messages["auth.reset_pw_confirmation"]); + await Request.receiveJSON("/api/auth/logout"); + history.push("/auth/login"); + Store.dispatch(logoutUserAction()); + }) + .catch((error) => { + console.error("Password change failed:", error); + Toast.error("Failed to change password. Please try again."); + }); } function checkPasswordsAreMatching(value: string, otherPasswordFieldKey: string[]) { From d0cd5b366a4c3bd3e6c63ab3ca6cf231b7dffa11 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 24 Jun 2025 13:25:30 +0200 Subject: [PATCH 27/44] more stuff --- .../admin/account/account_auth_token_view.tsx | 8 ++- .../admin/account/account_password_view.tsx | 54 ++++++++++--------- .../admin/account/account_profile_view.tsx | 9 +++- .../admin/account/helpers/settings_card.tsx | 2 +- 4 files changed, 44 insertions(+), 29 deletions(-) diff --git a/frontend/javascripts/admin/account/account_auth_token_view.tsx b/frontend/javascripts/admin/account/account_auth_token_view.tsx index 244ceba68d4..d6ed30fdd96 100644 --- a/frontend/javascripts/admin/account/account_auth_token_view.tsx +++ b/frontend/javascripts/admin/account/account_auth_token_view.tsx @@ -1,4 +1,4 @@ -import { SwapOutlined } from "@ant-design/icons"; +import { ExportOutlined, SwapOutlined } from "@ant-design/icons"; import { getAuthToken, revokeAuthToken } from "admin/rest_api"; import { Button, Col, Row, Spin, Typography } from "antd"; import { useWkSelector } from "libs/react_hooks"; @@ -67,7 +67,11 @@ function AccountAuthTokenView() { : []), { title: "API Documentation", - value: Read the docs, + value: ( + + Read the docs + + ), }, ]; diff --git a/frontend/javascripts/admin/account/account_password_view.tsx b/frontend/javascripts/admin/account/account_password_view.tsx index 5fd041c2a29..9a155e52a64 100644 --- a/frontend/javascripts/admin/account/account_password_view.tsx +++ b/frontend/javascripts/admin/account/account_password_view.tsx @@ -1,5 +1,5 @@ -import { LockOutlined } from "@ant-design/icons"; -import { Alert, Button, Col, Form, Input, List, Row, Space } from "antd"; +import { DeleteOutlined, LockOutlined, ReloadOutlined } from "@ant-design/icons"; +import { Alert, Button, Col, Form, Input, Row, Space, Typography } from "antd"; import Request from "libs/request"; import Toast from "libs/toast"; import messages from "messages"; @@ -147,14 +147,7 @@ function AccountPasswordView({ history }: Props) { ) : ( - <> - - - - - + "***********" ); } @@ -164,12 +157,14 @@ function AccountPasswordView({ history }: Props) { const passKeyList = [ { - name: "passkey1", - details: "2024-05-01", + title: "passkey1", + value: "2024-05-01", + action:
); } diff --git a/frontend/javascripts/admin/account/account_profile_view.tsx b/frontend/javascripts/admin/account/account_profile_view.tsx index f340eca2488..0b6a67a6541 100644 --- a/frontend/javascripts/admin/account/account_profile_view.tsx +++ b/frontend/javascripts/admin/account/account_profile_view.tsx @@ -72,6 +72,9 @@ function AccountProfileView() { { title: "Role", value: role, + explanation: ( + Learn More + ), }, { title: "Theme", @@ -92,7 +95,11 @@ function AccountProfileView() { {profileItems.map((item) => ( - + ))} diff --git a/frontend/javascripts/admin/account/helpers/settings_card.tsx b/frontend/javascripts/admin/account/helpers/settings_card.tsx index dd79e5317c4..75cc9cb278c 100644 --- a/frontend/javascripts/admin/account/helpers/settings_card.tsx +++ b/frontend/javascripts/admin/account/helpers/settings_card.tsx @@ -4,7 +4,7 @@ import { Card, Flex, Popover, Typography } from "antd"; interface SettingsCardProps { title: string; description: React.ReactNode; - explanation?: string; + explanation?: React.ReactNode; action?: React.ReactNode; } From 66086aa4b21bda34a664d054a2cad95a6174b155 Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Tue, 24 Jun 2025 13:31:31 +0200 Subject: [PATCH 28/44] 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 29/44] 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 30/44] 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 = ( - , + ]} + > +
    + {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 From 0418bbf8e0a912f248939e671bee4c10d0ffdb8f Mon Sep 17 00:00:00 2001 From: Tom Herold Date: Wed, 25 Jun 2025 10:37:40 +0200 Subject: [PATCH 34/44] refinement --- .../admin/account/account_password_view.tsx | 9 ++------- .../admin/account/account_settings_view.tsx | 16 ++++++++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/frontend/javascripts/admin/account/account_password_view.tsx b/frontend/javascripts/admin/account/account_password_view.tsx index c151d7ee095..44182d482fd 100644 --- a/frontend/javascripts/admin/account/account_password_view.tsx +++ b/frontend/javascripts/admin/account/account_password_view.tsx @@ -162,13 +162,8 @@ function AccountPasswordView({ history }: Props) { const passKeyList = [ { - title: "passkey1", - value: "2024-05-01", - action: