mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-26 06:16:09 +00:00
feat(cloud): complete Workspace settings navigation
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
Server,
|
||||
Puzzle,
|
||||
RefreshCcw,
|
||||
UsersRound,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '@/components/providers/theme-provider';
|
||||
|
||||
@@ -61,6 +62,9 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import WorkspaceSwitcher, {
|
||||
OPEN_WORKSPACE_SETTINGS_EVENT,
|
||||
} from '@/app/home/components/workspace-settings/WorkspaceSwitcher';
|
||||
import NewVersionDialog from '@/app/home/components/new-version-dialog/NewVersionDialog';
|
||||
import SettingsDialog, {
|
||||
SettingsSection,
|
||||
@@ -1676,6 +1680,19 @@ export default function HomeSidebar({
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const openWorkspaceSettings = () => openSettings('workspace');
|
||||
window.addEventListener(
|
||||
OPEN_WORKSPACE_SETTINGS_EVENT,
|
||||
openWorkspaceSettings,
|
||||
);
|
||||
return () =>
|
||||
window.removeEventListener(
|
||||
OPEN_WORKSPACE_SETTINGS_EVENT,
|
||||
openWorkspaceSettings,
|
||||
);
|
||||
});
|
||||
|
||||
function handleSettingsSectionChange(section: SettingsSection) {
|
||||
setSettingsSection(section);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
@@ -1953,6 +1970,11 @@ export default function HomeSidebar({
|
||||
|
||||
{/* Footer */}
|
||||
<SidebarFooter>
|
||||
{currentWorkspace?.workspace.source === 'cloud_projection' && (
|
||||
<div className="px-2 group-data-[collapsible=icon]:px-0">
|
||||
<WorkspaceSwitcher className="w-full group-data-[collapsible=icon]:min-w-0 group-data-[collapsible=icon]:px-2" />
|
||||
</div>
|
||||
)}
|
||||
{/* Models entry */}
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
@@ -2068,6 +2090,15 @@ export default function HomeSidebar({
|
||||
<Settings />
|
||||
{t('account.settings')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
openSettings('workspace');
|
||||
}}
|
||||
>
|
||||
<UsersRound />
|
||||
{t('workspace.settings')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import { Building2 } from 'lucide-react';
|
||||
import {
|
||||
Building2,
|
||||
Check,
|
||||
ExternalLink,
|
||||
Settings,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
switchWorkspaceAndReload,
|
||||
systemInfo,
|
||||
useCurrentWorkspace,
|
||||
useWorkspaceBootstrap,
|
||||
} from '@/app/infra/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const OPEN_WORKSPACE_SETTINGS_EVENT = 'langbot:open-workspace-settings';
|
||||
|
||||
export function requestWorkspaceSettings(): void {
|
||||
window.dispatchEvent(new Event(OPEN_WORKSPACE_SETTINGS_EVENT));
|
||||
}
|
||||
|
||||
export default function WorkspaceSwitcher({
|
||||
className,
|
||||
}: {
|
||||
@@ -23,34 +38,78 @@ export default function WorkspaceSwitcher({
|
||||
const { t } = useTranslation();
|
||||
const currentWorkspace = useCurrentWorkspace();
|
||||
const workspaces = useWorkspaceBootstrap();
|
||||
const isCloud = currentWorkspace?.workspace.source === 'cloud_projection';
|
||||
|
||||
if (!currentWorkspace || workspaces.length <= 1) return null;
|
||||
if (!currentWorkspace || (!isCloud && workspaces.length <= 1)) return null;
|
||||
|
||||
const cloudPortalUrl = `${systemInfo.cloud_service_url.replace(/\/$/, '')}/cloud?workspace=${encodeURIComponent(currentWorkspace.workspace.uuid)}`;
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={currentWorkspace.workspace.uuid}
|
||||
onValueChange={switchWorkspaceAndReload}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className={cn('min-w-44 max-w-64', className)}
|
||||
aria-label={t('workspace.switchWorkspace')}
|
||||
>
|
||||
<Building2 className="size-4 shrink-0" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
{workspaces.map((entry) => (
|
||||
<SelectItem key={entry.workspace.uuid} value={entry.workspace.uuid}>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate">{entry.workspace.name}</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn('min-w-44 max-w-64 justify-start', className)}
|
||||
aria-label={t('workspace.switchWorkspace')}
|
||||
>
|
||||
<Building2 className="size-4 shrink-0" />
|
||||
<span className="truncate">{currentWorkspace.workspace.name}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-64">
|
||||
<DropdownMenuLabel>{t('workspace.switchWorkspace')}</DropdownMenuLabel>
|
||||
{workspaces.map((entry) => {
|
||||
const selected =
|
||||
entry.workspace.uuid === currentWorkspace.workspace.uuid;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={entry.workspace.uuid}
|
||||
onClick={() => {
|
||||
if (!selected)
|
||||
void switchWorkspaceAndReload(entry.workspace.uuid);
|
||||
}}
|
||||
>
|
||||
<Building2 className="size-4" />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{entry.workspace.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t(`workspace.roles.${entry.membership.role}`)}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selected && <Check className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuSeparator />
|
||||
{isCloud && (
|
||||
<>
|
||||
<DropdownMenuLabel className="flex items-center justify-between gap-3 font-normal">
|
||||
<span className="text-muted-foreground">
|
||||
{t('workspace.currentPlan')}
|
||||
</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{currentWorkspace.plan_name || t('workspace.planUnavailable')}
|
||||
</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href={cloudPortalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Sparkles className="size-4" />
|
||||
{t('workspace.upgradePlan')}
|
||||
<ExternalLink className="ml-auto size-3.5" />
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem onClick={requestWorkspaceSettings}>
|
||||
<Settings className="size-4" />
|
||||
{t('workspace.settings')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface CurrentWorkspace {
|
||||
membership: WorkspaceMembership;
|
||||
permissions: string[];
|
||||
placement_generation: number;
|
||||
/** Signed Cloud display metadata; never used for client-side authorization. */
|
||||
plan_name?: string | null;
|
||||
}
|
||||
|
||||
/** Account-scoped Workspace entry returned before a Workspace is selected. */
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
beginAuthenticatedSession,
|
||||
bootstrapWorkspaceSession,
|
||||
@@ -66,6 +66,7 @@ export default function Login() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
const autoSpaceLoginStarted = useRef(false);
|
||||
|
||||
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
||||
resolver: zodResolver(formSchema(t)),
|
||||
@@ -196,7 +197,7 @@ export default function Login() {
|
||||
});
|
||||
}
|
||||
|
||||
const handleSpaceLoginClick = async () => {
|
||||
const handleSpaceLoginClick = useCallback(async () => {
|
||||
setSpaceLoading(true);
|
||||
try {
|
||||
const currentOrigin = window.location.origin;
|
||||
@@ -207,7 +208,20 @@ export default function Login() {
|
||||
toast.error(t('common.spaceLoginFailed'));
|
||||
setSpaceLoading(false);
|
||||
}
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
loading ||
|
||||
!showSpaceLogin ||
|
||||
autoSpaceLoginStarted.current ||
|
||||
new URLSearchParams(window.location.search).get('auto') !== 'space'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
autoSpaceLoginStarted.current = true;
|
||||
void handleSpaceLoginClick();
|
||||
}, [handleSpaceLoginClick, loading, showSpaceLogin]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -1293,6 +1293,10 @@ const enUS = {
|
||||
selectionLoadFailed:
|
||||
'Your Workspaces could not be loaded. Please try again.',
|
||||
switchWorkspace: 'Switch Workspace',
|
||||
settings: 'Workspace Settings',
|
||||
currentPlan: 'Current plan',
|
||||
planUnavailable: 'Unavailable',
|
||||
upgradePlan: 'Change or upgrade plan',
|
||||
ossSingletonDescription:
|
||||
'This self-hosted instance has one Workspace and can include multiple users.',
|
||||
cloudManagedDescription:
|
||||
|
||||
@@ -1227,6 +1227,10 @@ const zhHans = {
|
||||
selectDescription: '选择你要进入的 LangBot 工作区。',
|
||||
selectionLoadFailed: '无法加载你的工作区,请重试。',
|
||||
switchWorkspace: '切换工作区',
|
||||
settings: '工作区设置',
|
||||
currentPlan: '当前计划',
|
||||
planUnavailable: '暂不可用',
|
||||
upgradePlan: '切换或升级计划',
|
||||
ossSingletonDescription:
|
||||
'当前自托管实例只有一个工作区,但可以包含多个用户。',
|
||||
cloudManagedDescription:
|
||||
|
||||
Reference in New Issue
Block a user