feat(desktop): make workspace resource header configurable (#7157)

* feat(desktop): make workspace resource header configurable

* fix(desktop): default workspace resource header to balance
This commit is contained in:
Alex Lee
2026-08-04 17:11:35 +08:00
committed by GitHub
parent 4ac7be8aaf
commit adda396b6d
12 changed files with 430 additions and 169 deletions
+2 -1
View File
@@ -171,7 +171,8 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
- 桌面右上角摘要展示当前 workspace 的可用 CPU 以及账号积分;积分复用 `/api/account/getAmount` 的余额净额口径,即 `balance - deductionBalance` 后保留两位小数展示,点击摘要进入费用中心。
- 桌面右上角的资源入口通过 `src/pages/api/desktop/getResource.ts` 读取当前登录 workspace namespace 的 Kubernetes `ResourceQuota`,默认 quota 名称为 `quota-${namespace}`
- `/api/desktop/getResource` 保留原有 Pod/PVC 运行资源统计,同时返回 `workspaceQuota`,用于展示 CPU、内存、存储、GPU、NodePort 的总量、已用和可用量。
- `/api/desktop/getResource` 保留原有 Pod/PVC 运行资源统计,同时返回 `workspaceQuota`,用于展示 CPU、内存、存储、NodePort 的总量、已用和可用量;GPU 仍在接口里返回,但右上角头部暂不展示
- `desktopConfig.workspaceResourceHeaderEnabled` 控制右上角头部使用资源模式还是旧余额模式,默认关闭,也就是默认显示余额。
- `sealos-desktop-sdk``getWorkspaceQuotaApi` 复用同一个接口,子应用通过 master bridge 获取到的 workspace quota 与桌面右上角展示保持同源。
### 密码登录初始化契约
@@ -104,6 +104,7 @@ desktopConfig:
enterpriseRealNameAuthEnabled: false # 启用企业实名认证
trackingEnabled: false # 启用追踪/统计
realNameAuthEnabled: false # 启用实名认证
workspaceResourceHeaderEnabled: false # 默认显示余额;设为 true 时使用资源头部
licenseCheckEnabled: false # 启用 License 检查
passwordEnabled: true # 启用密码登录
```
@@ -104,6 +104,7 @@ desktopConfig:
enterpriseRealNameAuthEnabled: false # 启用企业实名认证
trackingEnabled: false # 启用追踪/统计
realNameAuthEnabled: false # 启用实名认证
workspaceResourceHeaderEnabled: false # 默认显示余额;设为 true 时使用资源头部
licenseCheckEnabled: false # 启用 License 检查
```
+5 -1
View File
@@ -445,6 +445,10 @@ sealos run desktop-frontend:latest \
sealos run desktop-frontend:latest \
-e HELM_OPTIONS="--set desktopConfig.protocol.enabled=false"
# Enable the top-right workspace resource header
sealos run desktop-frontend:latest \
-e HELM_OPTIONS="--set desktopConfig.workspaceResourceHeaderEnabled=true"
# OAuth providers via HELM_OPTIONS
sealos run desktop-frontend:latest \
-e HELM_OPTIONS="--set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=your-client-id --set desktopConfig.githubClientSecret=your-client-secret"
@@ -462,7 +466,7 @@ sealos run desktop-frontend:latest \
**Common customization options:**
- **UI customization**: `layoutTitle`, `layoutLogo`, `metaTitle`, `metaDescription`, `customerServiceURL`, `protocol.enabled`
- **UI customization**: `layoutTitle`, `layoutLogo`, `metaTitle`, `metaDescription`, `customerServiceURL`, `protocol.enabled`, `workspaceResourceHeaderEnabled`
- **OAuth providers**: `githubEnabled`, `googleEnabled`, `wechatEnabled`, `oauth2Enabled` and their `*ClientId`, `*ClientSecret`
- **Features**: `guideEnabled`, `rechargeEnabled`, `trackingEnabled`, `apiEnabled`, `realNameAuthEnabled`
- **Communication**: `smsEnabled`, `emailEnabled`, `emailHost`, `emailPort`, `emailUser`, `emailPassword`
+5 -1
View File
@@ -445,6 +445,10 @@ sealos run desktop-frontend:latest \
sealos run desktop-frontend:latest \
-e HELM_OPTIONS="--set desktopConfig.protocol.enabled=false"
# 开启右上角 workspace 资源头部
sealos run desktop-frontend:latest \
-e HELM_OPTIONS="--set desktopConfig.workspaceResourceHeaderEnabled=true"
# OAuth 提供商
sealos run desktop-frontend:latest \
-e HELM_OPTIONS="--set desktopConfig.githubEnabled=true --set desktopConfig.githubClientId=your-client-id --set desktopConfig.githubClientSecret=your-client-secret"
@@ -462,7 +466,7 @@ sealos run desktop-frontend:latest \
**常用自定义选项:**
- **UI 自定义**: `layoutTitle`, `layoutLogo`, `metaTitle`, `metaDescription`, `customerServiceURL`, `protocol.enabled`
- **UI 自定义**: `layoutTitle`, `layoutLogo`, `metaTitle`, `metaDescription`, `customerServiceURL`, `protocol.enabled`, `workspaceResourceHeaderEnabled`
- **OAuth 提供商**: `githubEnabled`, `googleEnabled`, `wechatEnabled`, `oauth2Enabled` 及其对应的 `*ClientId`, `*ClientSecret`
- **功能开关**: `guideEnabled`, `rechargeEnabled`, `trackingEnabled`, `apiEnabled`, `realNameAuthEnabled`
- **通讯配置**: `smsEnabled`, `emailEnabled`, `emailHost`, `emailPort`, `emailUser`, `emailPassword`
@@ -76,6 +76,7 @@ desktopConfig:
layoutTitle: "Sealos Cloud"
layoutLogo: "/logo.svg"
layoutBackgroundImage: "/images/bg-light.svg"
workspaceResourceHeaderEnabled: false
customerServiceURL: ""
layoutDocsUseDocs: false
layoutDocsUrl: "https://sealos.run/docs/Intro/"
@@ -49,6 +49,7 @@ data:
title: "{{ .Values.desktopConfig.layoutTitle }}"
logo: "{{ .Values.desktopConfig.layoutLogo }}"
backgroundImage: "{{ .Values.desktopConfig.layoutBackgroundImage }}"
workspaceResourceHeaderEnabled: {{ .Values.desktopConfig.workspaceResourceHeaderEnabled }}
forcedLanguage: {{ if eq .Values.desktopConfig.version "cn" }}"zh"{{ else }}"en"{{ end }}
customerServiceURL: "{{ .Values.desktopConfig.customerServiceURL }}"
discordInviteLink: {{ if eq .Values.desktopConfig.version "en" }}"{{ .Values.desktopConfig.discordInviteLink }}"{{ else }}""{{ end }}
@@ -96,6 +96,7 @@ desktopConfig:
privateProtocol:
zh: https://sealos.io/zh-Hans/docs/msa/privacy-policy
en: https://sealos.io/docs/msa/privacy-policy
workspaceResourceHeaderEnabled: false
# End of auto-configured values
# ============================================================================
@@ -30,6 +30,9 @@ jest.mock('@/stores/session', () => ({
jest.mock('@/api/platform', () => ({ getResource: jest.fn() }));
jest.mock('@/api/auth', () => ({ getAmount: jest.fn() }));
jest.mock('@sealos/ui', () => ({
CurrencySymbol: () => null
}));
jest.mock('next-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
@@ -0,0 +1,144 @@
/** @jest-environment jsdom */
import React from 'react';
import { ChakraProvider, useBreakpointValue } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
import SecondaryLinks from '@/components/SecondaryLinks';
import { useConfigStore } from '@/stores/config';
import { getAmount } from '@/api/auth';
import { getResource } from '@/api/platform';
import { DefaultLayoutConfig } from '@/types/system';
jest.mock('@/stores/config', () => ({
useConfigStore: jest.fn()
}));
jest.mock('@/stores/app', () => ({
__esModule: true,
default: () => ({ openDesktopApp: jest.fn() })
}));
jest.mock('@/stores/guideModal', () => ({
useGuideModalStore: () => ({ openGuideModal: jest.fn(), setInitGuide: jest.fn() })
}));
jest.mock('@/stores/session', () => ({
__esModule: true,
default: () => ({
session: {
user: {
userCrUid: 'user-1',
ns_uid: 'ns-1'
}
}
})
}));
jest.mock('@/api/platform', () => ({
getResource: jest.fn()
}));
jest.mock('@/api/auth', () => ({
getAmount: jest.fn()
}));
jest.mock('@sealos/ui', () => ({
CurrencySymbol: () => null
}));
jest.mock('next-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}));
jest.mock('@chakra-ui/react', () => {
const actual = jest.requireActual('@chakra-ui/react');
return {
...actual,
useBreakpointValue: jest.fn()
};
});
const mockedUseConfigStore = useConfigStore as jest.MockedFunction<typeof useConfigStore>;
const mockedUseBreakpointValue = useBreakpointValue as jest.MockedFunction<
typeof useBreakpointValue
>;
const mockedGetResource = getResource as jest.MockedFunction<typeof getResource>;
const mockedGetAmount = getAmount as jest.MockedFunction<typeof getAmount>;
function renderSecondaryLinks(workspaceResourceHeaderEnabled?: boolean) {
mockedUseConfigStore.mockReturnValue({
layoutConfig: {
common: {},
currencySymbol: 'shellCoin',
...(workspaceResourceHeaderEnabled === undefined ? {} : { workspaceResourceHeaderEnabled })
},
commonConfig: {
guideEnabled: false
}
} as ReturnType<typeof useConfigStore>);
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false }
}
});
return render(
React.createElement(
ChakraProvider,
null,
React.createElement(
QueryClientProvider,
{ client: queryClient },
React.createElement(SecondaryLinks)
)
)
);
}
describe('workspace resource header flag', () => {
it('defaults to the balance header', () => {
expect(DefaultLayoutConfig.workspaceResourceHeaderEnabled).toBe(false);
});
beforeEach(() => {
jest.clearAllMocks();
mockedUseBreakpointValue.mockReturnValue(false);
mockedGetAmount.mockResolvedValue({
data: { balance: 2_000_000, deductionBalance: 500_000 }
} as never);
mockedGetResource.mockResolvedValue({
data: {
workspaceQuota: [
{ type: 'cpu', used: 1, limit: 2, available: 1, usagePercent: 50 },
{ type: 'gpu', used: 1, limit: 1, available: 0, usagePercent: 100 }
]
}
} as never);
});
it('falls back to the balance header when the resource header is disabled', async () => {
renderSecondaryLinks(false);
expect(await screen.findByText('common:balance')).not.toBeNull();
expect(screen.queryByText('common:resources')).toBeNull();
expect(mockedGetResource).not.toHaveBeenCalled();
});
it('falls back to the balance header when the flag is omitted', async () => {
renderSecondaryLinks();
expect(await screen.findByText('common:balance')).not.toBeNull();
expect(screen.queryByText('common:resources')).toBeNull();
expect(mockedGetResource).not.toHaveBeenCalled();
});
it('uses the resource header when enabled', async () => {
renderSecondaryLinks(true);
expect(await screen.findByText('common:resources')).not.toBeNull();
expect(screen.queryByText('common:balance')).toBeNull();
await waitFor(() => expect(mockedGetResource).toHaveBeenCalledTimes(1));
});
});
@@ -23,8 +23,10 @@ import {
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { getResource } from '@/api/platform';
import Decimal from 'decimal.js';
import { getAmount } from '@/api/auth';
import { getResource } from '@/api/platform';
import { CurrencySymbol } from '@sealos/ui';
import useSessionStore from '@/stores/session';
import { formatMoney } from '@/utils/format';
import { Activity, MoreHorizontal } from 'lucide-react';
@@ -49,13 +51,7 @@ type WorkspaceQuotaItem = {
usagePercent: number;
};
const resourceDisplayOrder: WorkspaceQuotaItem['type'][] = [
'cpu',
'memory',
'storage',
'gpu',
'nodeport'
];
const resourceDisplayOrder: WorkspaceQuotaItem['type'][] = ['cpu', 'memory', 'storage', 'nodeport'];
const getAvailableValue = (item: WorkspaceQuotaItem) =>
item.available ?? Math.max(item.limit - item.used, 0);
@@ -74,6 +70,8 @@ export default function SecondaryLinks() {
const { session } = useSessionStore();
const user = session?.user;
const workspaceResourceHeaderEnabled = layoutConfig?.workspaceResourceHeaderEnabled === true;
const currencySymbol = layoutConfig?.currencySymbol || 'shellCoin';
const isCollapsed = useBreakpointValue({
base: true,
@@ -97,17 +95,25 @@ export default function SecondaryLinks() {
const { data: resourceData, isLoading: isResourceLoading } = useQuery({
queryKey: ['getResource', { userId: user?.userCrUid, workspaceId: user?.ns_uid }],
queryFn: getResource,
enabled: !!user,
enabled: !!user && workspaceResourceHeaderEnabled,
staleTime: 60 * 1000
});
const { data: amountData, isLoading: isAmountLoading } = useQuery({
const { data: amountData } = useQuery({
queryKey: ['getAmount', { userId: user?.userCrUid }],
queryFn: getAmount,
enabled: !!user,
staleTime: 60 * 1000
});
const balance = useMemo(() => {
let realBalance = new Decimal(amountData?.data?.balance || 0);
if (amountData?.data?.deductionBalance) {
realBalance = realBalance.minus(new Decimal(amountData.data.deductionBalance));
}
return realBalance.toNumber();
}, [amountData]);
const quotaItems = useMemo(() => {
const items = resourceData?.data?.workspaceQuota || [];
return resourceDisplayOrder.reduce<WorkspaceQuotaItem[]>((acc, type) => {
@@ -133,13 +139,7 @@ export default function SecondaryLinks() {
: isResourceLoading
? t('common:loading')
: '--';
const creditsValue = amountData?.data
? formatMoney((amountData.data.balance || 0) - (amountData.data.deductionBalance || 0))
: null;
const creditsSummary =
creditsValue !== null ? creditsValue.toFixed(2) : isAmountLoading ? t('common:loading') : '--';
const balanceSummary = formatMoney(balance).toFixed(2);
const renderHeaderMetric = (label: string, value: string) => (
<Flex alignItems="baseline" gap="4px" whiteSpace="nowrap" minW={0}>
@@ -156,14 +156,10 @@ export default function SecondaryLinks() {
<Flex alignItems="center" gap="8px" minW={0} flexWrap={allowWrap ? 'wrap' : 'nowrap'}>
{renderHeaderMetric(t('common:resources'), resourceSummary)}
<Divider orientation="vertical" h="16px" borderColor="rgba(37, 99, 235, 0.14)" />
{renderHeaderMetric(t('common:credits'), creditsSummary)}
{renderHeaderMetric(t('common:credits'), balanceSummary)}
</Flex>
);
const handleHeaderClick = () => {
openCostCenterApp();
};
const getQuotaLabel = (type: WorkspaceQuotaItem['type']) => {
if (type === 'cpu') return 'CPU';
if (type === 'memory') return t('common:memory');
@@ -239,102 +235,241 @@ export default function SecondaryLinks() {
}
};
if (!isCollapsed) {
return (
<Flex gap={'4px'} ml={'auto'}>
<Popover trigger="hover" placement="bottom-end" openDelay={120}>
<PopoverTrigger>
<Center
mr={'12px'}
borderRadius={'8px'}
bg={
'linear-gradient(90deg, rgba(129, 203, 252, 0.12) 0%, rgba(81, 159, 245, 0.12) 100%)'
}
h={'36px'}
minW="252px"
maxW="320px"
px={'12px'}
py={'8px'}
color="#2563EB"
fontSize={'14px'}
fontWeight={'500'}
cursor={'pointer'}
onClick={handleHeaderClick}
>
<Activity size={16} />
<Box ml="8px">{renderHeaderSummary()}</Box>
</Center>
</PopoverTrigger>
<Portal>
<PopoverContent
w="320px"
borderRadius="8px"
boxShadow="0 12px 32px rgba(15, 23, 42, 0.16)"
>
<PopoverArrow />
<PopoverBody p="14px">{renderResourceRows()}</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
const handleHeaderClick = () => {
openCostCenterApp();
};
{commonConfig?.guideEnabled ? (
<Center
className="guide-button"
cursor={'pointer'}
{...baseItemStyle}
px={'8px'}
borderRadius={'8px'}
onClick={handleGuideClick}
>
{t('common:guide')}
</Center>
) : null}
const renderDesktopActions = () => (
<>
{commonConfig?.guideEnabled ? (
<Center
className="guide-button"
cursor={'pointer'}
{...baseItemStyle}
px={'8px'}
borderRadius={'8px'}
onClick={handleGuideClick}
>
{t('common:guide')}
</Center>
) : null}
{layoutConfig?.common.docsUrl && (
<Center
{...baseItemStyle}
cursor={'pointer'}
borderRadius={'8px'}
px={'8px'}
onClick={handleDocsClick}
>
{t('common:doc')}
</Center>
)}
{layoutConfig?.common.docsUrl && (
<Center
{...baseItemStyle}
cursor={'pointer'}
borderRadius={'8px'}
px={'8px'}
onClick={handleDocsClick}
>
{t('common:doc')}
</Center>
)}
{layoutConfig?.common.workorderEnabled && (
{layoutConfig?.common.workorderEnabled && (
<Center
cursor={'pointer'}
{...baseItemStyle}
px={'8px'}
borderRadius={'8px'}
border={'1px solid transparent'}
onClick={openWorkOrderApp}
>
{t('v2:ticket')}
</Center>
)}
{layoutConfig?.discordInviteLink && (
<JoinDiscordPrompt>
<Center
cursor={'pointer'}
{...baseItemStyle}
minW="36px"
h="40px"
fontSize="14px"
fontWeight="500"
color="primary"
_hover={{
background: 'secondary'
}}
px={'8px'}
borderRadius={'8px'}
border={'1px solid transparent'}
onClick={openWorkOrderApp}
>
{t('v2:ticket')}
{t('v2:support')}
</Center>
)}
</JoinDiscordPrompt>
)}
</>
);
{layoutConfig?.discordInviteLink && (
<JoinDiscordPrompt>
<Center
cursor={'pointer'}
minW="36px"
h="40px"
fontSize="14px"
fontWeight="500"
color="primary"
_hover={{
background: 'secondary'
}}
px={'8px'}
borderRadius={'8px'}
border={'1px solid transparent'}
const renderMenuActions = () => (
<>
<Divider my="8px" mx="-8px" w="calc(100% + 16px)" bg="rgba(0, 0, 0, 0.1)" />
{layoutConfig?.common.docsUrl && (
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={handleDocsClick}
fontSize="14px"
fontWeight="500"
>
{t('common:doc')}
</MenuItem>
)}
{layoutConfig?.common.workorderEnabled && (
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={openWorkOrderApp}
fontSize="14px"
fontWeight="500"
>
{t('v2:ticket')}
</MenuItem>
)}
{layoutConfig?.discordInviteLink && (
<JoinDiscordPrompt>
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={openWorkOrderApp}
fontSize="14px"
fontWeight="500"
>
{t('v2:support')}
</MenuItem>
</JoinDiscordPrompt>
)}
</>
);
if (workspaceResourceHeaderEnabled) {
if (!isCollapsed) {
return (
<Flex gap={'4px'} ml={'auto'}>
<Popover trigger="hover" placement="bottom-end" openDelay={120}>
<PopoverTrigger>
<Center
mr={'12px'}
borderRadius={'8px'}
bg={
'linear-gradient(90deg, rgba(129, 203, 252, 0.12) 0%, rgba(81, 159, 245, 0.12) 100%)'
}
h={'36px'}
minW="252px"
maxW="320px"
px={'12px'}
py={'8px'}
color="#2563EB"
fontSize={'14px'}
fontWeight={'500'}
cursor={'pointer'}
onClick={handleHeaderClick}
>
<Activity size={16} />
<Box ml="8px">{renderHeaderSummary()}</Box>
</Center>
</PopoverTrigger>
<Portal>
<PopoverContent
w="320px"
borderRadius="8px"
boxShadow="0 12px 32px rgba(15, 23, 42, 0.16)"
>
<PopoverArrow />
<PopoverBody p="14px">{renderResourceRows()}</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
{renderDesktopActions()}
</Flex>
);
}
return (
<Menu>
<MenuButton
as={Center}
{...baseItemStyle}
tabIndex={0}
boxSize={'36px'}
cursor={'pointer'}
borderRadius={'full'}
border={'1px solid rgba(0, 0, 0, 0.05)'}
px={'8px'}
>
<MoreHorizontal size={16} />
</MenuButton>
<MenuList
p="8px"
borderRadius="12px"
boxShadow="0 4px 20px rgba(0, 0, 0, 0.15)"
border="1px solid rgba(0, 0, 0, 0.05)"
minW="200px"
>
<Box
borderRadius="8px"
bg={
'linear-gradient(90deg, rgba(129, 203, 252, 0.12) 0%, rgba(81, 159, 245, 0.12) 100%)'
}
p="12px"
cursor="pointer"
onClick={handleHeaderClick}
>
<Flex
alignItems="center"
justifyContent="space-between"
gap="10px"
color="#2563EB"
flexWrap="wrap"
>
{t('v2:support')}
</Center>
</JoinDiscordPrompt>
)}
<Flex alignItems="center" gap="6px" fontSize="14px" fontWeight={600}>
<Activity size={16} />
</Flex>
{renderHeaderSummary({ allowWrap: true })}
</Flex>
<Box mt="12px">{renderResourceRows({ showTitle: false })}</Box>
</Box>
{renderMenuActions()}
</MenuList>
</Menu>
);
}
if (!isCollapsed) {
return (
<Flex gap={'4px'} ml={'auto'}>
<Center
mr={'12px'}
borderRadius={'8px'}
bg={'linear-gradient(90deg, rgba(129, 203, 252, 0.12) 0%, rgba(81, 159, 245, 0.12) 100%)'}
h={'36px'}
px={'12px'}
py={'8px'}
color="#2563EB"
fontSize={'14px'}
fontWeight={'500'}
cursor={'pointer'}
onClick={handleHeaderClick}
>
<Text>{t('common:balance')}</Text>
<Divider orientation="vertical" mx={'12px'} />
<CurrencySymbol type={currencySymbol} />
<Text ml={'4px'}>{balanceSummary}</Text>
</Center>
{renderDesktopActions()}
</Flex>
);
}
@@ -377,69 +512,32 @@ export default function SecondaryLinks() {
<Flex alignItems="center" gap="6px" fontSize="14px" fontWeight={600}>
<Activity size={16} />
</Flex>
{renderHeaderSummary({ allowWrap: true })}
<Flex alignItems="center" gap="4px" whiteSpace="nowrap" minW={0}>
<Text
fontSize="13px"
fontWeight={500}
lineHeight="20px"
color="rgba(45, 65, 91, 0.68)"
>
{t('common:balance')}
</Text>
<Text
fontSize="14px"
fontWeight={600}
lineHeight="20px"
color="primary"
noOfLines={1}
>
<CurrencySymbol type={currencySymbol} />
<Text as="span" ml="4px">
{balanceSummary}
</Text>
</Text>
</Flex>
</Flex>
<Box mt="12px">{renderResourceRows({ showTitle: false })}</Box>
</Box>
<Divider my="8px" mx="-8px" w="calc(100% + 16px)" bg="rgba(0, 0, 0, 0.1)" />
{/* // [TODO] Guide is currently not compatible with narrow screen. */}
{/* <MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={handleGuideClick}
fontSize="14px"
fontWeight="500"
>
{t('common:guide')}
</MenuItem> */}
{layoutConfig?.common.docsUrl && (
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={handleDocsClick}
fontSize="14px"
fontWeight="500"
>
{t('common:doc')}
</MenuItem>
)}
{layoutConfig?.common.workorderEnabled && (
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={openWorkOrderApp}
fontSize="14px"
fontWeight="500"
>
{t('v2:ticket')}
</MenuItem>
)}
{layoutConfig?.discordInviteLink && (
<JoinDiscordPrompt>
<MenuItem
py="8px"
px="12px"
borderRadius="8px"
_hover={{ bg: 'rgba(0, 0, 0, 0.05)' }}
onClick={openWorkOrderApp}
fontSize="14px"
fontWeight="500"
>
{t('v2:support')}
</MenuItem>
</JoinDiscordPrompt>
)}
{renderMenuActions()}
</MenuList>
</Menu>
);
+2
View File
@@ -70,6 +70,7 @@ export type LayoutConfigType = {
title: string;
logo: string;
backgroundImage: string;
workspaceResourceHeaderEnabled?: boolean;
authTitle?: {
zh: string;
en: string;
@@ -306,6 +307,7 @@ export const DefaultLayoutConfig: LayoutConfigType = {
title: 'Sealos Cloud',
logo: '/logo.svg',
backgroundImage: '/images/bg-light.svg',
workspaceResourceHeaderEnabled: false,
protocol: {
enabled: true,
serviceProtocol: {