From f48dc847ff9cd6db81d6d756562621f55588cd59 Mon Sep 17 00:00:00 2001 From: dadachann <185672915+dadachann@users.noreply.github.com> Date: Mon, 3 Aug 2026 05:29:55 +0000 Subject: [PATCH] [verified] fix: harden OSS and Cloud workspace UI (#2387) Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com> --- .../home/bots/components/bot-form/BotForm.tsx | 3 +- .../components/home-sidebar/HomeSidebar.tsx | 36 ++++--- .../settings-dialog/SettingsDialog.tsx | 16 ++- web/src/app/home/plugin-pages/page.tsx | 45 +++++--- .../hooks/useAuthenticatedPluginResource.ts | 72 +++++++++---- web/tests/e2e/cloud-storage-privacy.spec.ts | 63 +++++++++++ web/tests/e2e/plugin-page-auth.spec.ts | 88 +++++++++++++++ web/tests/unit/oss-cloud-ui-privacy.test.mjs | 100 ++++++++++++++++++ .../unit/workspace-switcher-layout.test.mjs | 15 ++- 9 files changed, 378 insertions(+), 60 deletions(-) create mode 100644 web/tests/e2e/cloud-storage-privacy.spec.ts create mode 100644 web/tests/e2e/plugin-page-auth.spec.ts create mode 100644 web/tests/unit/oss-cloud-ui-privacy.test.mjs diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index f8db783f1..81ce4138e 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -396,10 +396,9 @@ export default function BotForm({
-
+
{/* Card 1: Basic Information */} diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index 75da46850..3ba151cca 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -8,6 +8,7 @@ import { clearUserInfo, getCloudServiceClientSync, useCurrentWorkspace, + useWorkspaceBootstrap, } from '@/app/infra/http'; import { useTranslation } from 'react-i18next'; import { @@ -32,7 +33,6 @@ import { Zap, FilePlus2, Sparkles, - HardDrive, Server, Puzzle, RefreshCcw, @@ -1637,6 +1637,13 @@ export default function HomeSidebar({ const { theme, setTheme } = useTheme(); const { t } = useTranslation(); const currentWorkspace = useCurrentWorkspace(); + const workspaces = useWorkspaceBootstrap(); + const showWorkspaceSwitcher = + workspaces.length > 1 || + currentWorkspace?.workspace.source === 'cloud_projection'; + const canViewStorageAnalysis = + currentWorkspace?.workspace.source !== 'cloud_projection' && + currentWorkspace?.permissions.includes('audit.view'); const [settingsOpen, setSettingsOpen] = useState(false); const [settingsSection, setSettingsSection] = useState('models'); @@ -1915,9 +1922,11 @@ export default function HomeSidebar({ -
- -
+ {showWorkspaceSwitcher && ( +
+ +
+ )} {/* Navigation items grouped by section */}
@@ -2098,15 +2107,16 @@ export default function HomeSidebar({ {t('workspace.settings')} - { - setUserMenuOpen(false); - openSettings('storageAnalysis'); - }} - > - - {t('storageAnalysis.title')} - + {canViewStorageAnalysis && ( + { + setUserMenuOpen(false); + openSettings('storageAnalysis'); + }} + > + {t('storageAnalysis.title')} + + )} { setUserMenuOpen(false); diff --git a/web/src/app/home/components/settings-dialog/SettingsDialog.tsx b/web/src/app/home/components/settings-dialog/SettingsDialog.tsx index 22cdd90e7..63453a3f5 100644 --- a/web/src/app/home/components/settings-dialog/SettingsDialog.tsx +++ b/web/src/app/home/components/settings-dialog/SettingsDialog.tsx @@ -133,12 +133,14 @@ export default function SettingsDialog({ const permissions = currentWorkspace?.permissions ?? []; const canManageApiKeys = permissions.includes('api_key.manage'); const canViewAudit = permissions.includes('audit.view'); + const canViewStorageAnalysis = + currentWorkspace?.workspace.source !== 'cloud_projection' && canViewAudit; const navItems = allNavItems.filter((item) => { if (item.id === 'apiIntegration') { return canManageApiKeys; } if (item.id === 'storageAnalysis') { - return canViewAudit; + return canViewStorageAnalysis; } return true; }); @@ -146,11 +148,17 @@ export default function SettingsDialog({ useEffect(() => { const forbiddenSection = (section === 'apiIntegration' && !canManageApiKeys) || - (section === 'storageAnalysis' && !canViewAudit); + (section === 'storageAnalysis' && !canViewStorageAnalysis); if (open && forbiddenSection) { onSectionChange('workspace'); } - }, [canManageApiKeys, canViewAudit, open, section, onSectionChange]); + }, [ + canManageApiKeys, + canViewStorageAnalysis, + open, + section, + onSectionChange, + ]); const activeItem = navItems.find((item) => item.id === section); const activeLabel = activeItem?.title ?? t('settingsDialog.title'); @@ -256,7 +264,7 @@ export default function SettingsDialog({ active={open && section === 'apiIntegration'} /> )} - {section === 'storageAnalysis' && ( + {section === 'storageAnalysis' && canViewStorageAnalysis && ( diff --git a/web/src/app/home/plugin-pages/page.tsx b/web/src/app/home/plugin-pages/page.tsx index 73b4812d6..aecdf8845 100644 --- a/web/src/app/home/plugin-pages/page.tsx +++ b/web/src/app/home/plugin-pages/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState, useCallback } from 'react'; import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; import { useTranslation } from 'react-i18next'; import { useTheme } from '@/components/providers/theme-provider'; +import { useAuthenticatedPluginAsset } from '@/hooks/useAuthenticatedPluginResource'; /** * Plugin page that renders a plugin-provided HTML page in an iframe. @@ -80,11 +81,15 @@ function PluginPageIframe({ pageId: string; }) { const iframeRef = useRef(null); - const [loading, setLoading] = useState(true); + const [loadedAssetUrl, setLoadedAssetUrl] = useState(''); const { resolvedTheme } = useTheme(); - const { i18n } = useTranslation(); - - const assetUrl = httpClient.getPluginAssetURL(author, pluginName, pagePath); + const { t, i18n } = useTranslation(); + const { url: assetUrl, error: assetError } = useAuthenticatedPluginAsset( + author, + pluginName, + pagePath, + ); + const loading = !assetUrl || loadedAssetUrl !== assetUrl; // Send context (theme + language) to iframe // Use '*' as targetOrigin because sandboxed iframe has opaque (null) origin @@ -170,23 +175,29 @@ function PluginPageIframe({ return (
- {loading && ( + {assetError ? ( +
+ {t('plugins.loadFailed')} +
+ ) : loading || !assetUrl ? (
Loading...
+ ) : null} + {!assetError && assetUrl && ( +