mirror of
https://github.com/labring/sealos.git
synced 2026-10-31 02:11:58 +08:00
feat: update devbox network status (#5452)
* feat: update launchpad network * launchpad check * update devbox network status * delete log * log * fix default * update time * update message
This commit is contained in:
@@ -38,3 +38,6 @@ export const getPriceBonus = () =>
|
||||
|
||||
export const checkPermission = (payload: { appName: string }) =>
|
||||
GET('/api/platform/checkPermission', payload);
|
||||
|
||||
export const checkReady = (appName: string) =>
|
||||
GET<{ url: string; ready: boolean; error?: string }[]>(`/api/checkReady?appName=${appName}`);
|
||||
|
||||
@@ -11,9 +11,10 @@ import { getUserNamespace } from '@/utils/user';
|
||||
import { Box, Button, Center, Flex, Grid, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import MonitorModal from './MonitorModal';
|
||||
import { useNetworkStatus } from '@/hooks/useNetworkStatus';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { checkReady } from '@/api/platform';
|
||||
|
||||
const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -34,17 +35,42 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
|
||||
})),
|
||||
[app]
|
||||
);
|
||||
const networkStatuses = useNetworkStatus(networks);
|
||||
|
||||
const retryCount = useRef(0);
|
||||
const { data: networkStatus, refetch } = useQuery({
|
||||
queryKey: ['networkStatus', app.appName],
|
||||
queryFn: () => checkReady(app.appName),
|
||||
retry: 5,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * Math.pow(2, attemptIndex), 30000),
|
||||
onSuccess: (data) => {
|
||||
const hasUnready = data.some((item) => !item.ready);
|
||||
if (!hasUnready) {
|
||||
retryCount.current = 0;
|
||||
return;
|
||||
}
|
||||
if (retryCount.current < 14) {
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCount.current), 32000);
|
||||
retryCount.current += 1;
|
||||
setTimeout(() => {
|
||||
refetch();
|
||||
}, delay);
|
||||
}
|
||||
},
|
||||
refetchIntervalInBackground: false,
|
||||
staleTime: 1000 * 60 * 5
|
||||
});
|
||||
|
||||
const statusMap = useMemo(
|
||||
() =>
|
||||
networkStatuses.reduce((acc, status) => {
|
||||
if (status.data?.url) {
|
||||
acc[status.data.url] = status.data;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, { isReady?: boolean; error?: string }>),
|
||||
[networkStatuses]
|
||||
networkStatus
|
||||
? networkStatus.reduce((acc, item) => {
|
||||
if (item?.url) {
|
||||
acc[item.url] = item;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, { ready: boolean; url: string }>)
|
||||
: {},
|
||||
[networkStatus]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -123,17 +149,17 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
|
||||
</Flex>
|
||||
</th>
|
||||
<th>
|
||||
<Flex alignItems={'center'} gap={'2px'} justifyContent={'space-between'}>
|
||||
<Flex alignItems={'center'} gap={'2px'}>
|
||||
{network.public && (
|
||||
<>
|
||||
{statusMap[network.public]?.isReady ? (
|
||||
{statusMap[network.public]?.ready ? (
|
||||
<Center
|
||||
fontSize={'12px'}
|
||||
fontWeight={400}
|
||||
bg={'rgba(3, 152, 85, 0.05)'}
|
||||
color={'#039855'}
|
||||
borderRadius={'full'}
|
||||
p={'2px 4px'}
|
||||
p={'2px 8px 2px 4px'}
|
||||
gap={'2px'}
|
||||
minW={'63px'}
|
||||
>
|
||||
@@ -152,7 +178,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
|
||||
bg={'rgba(17, 24, 36, 0.05)'}
|
||||
color={'#485264'}
|
||||
borderRadius={'full'}
|
||||
p={'2px 4px'}
|
||||
p={'2px 8px 2px 4px'}
|
||||
gap={'2px'}
|
||||
minW={'63px'}
|
||||
>
|
||||
@@ -200,6 +226,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
|
||||
|
||||
{!!network.public && (
|
||||
<Center
|
||||
ml={'auto'}
|
||||
flexShrink={0}
|
||||
w={'24px'}
|
||||
h={'24px'}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useQueries } from '@tanstack/react-query';
|
||||
|
||||
interface NetworkStatus {
|
||||
url: string;
|
||||
isReady: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
code: number;
|
||||
data?: { ready: boolean };
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const useNetworkStatus = (networks: { inline: string; public: string }[]) => {
|
||||
const urlPairs = networks
|
||||
.map((network) => network.public)
|
||||
.filter(Boolean)
|
||||
.map((originalUrl) => ({
|
||||
originalUrl,
|
||||
fetchUrl: originalUrl.replace(/^(wss|grpcs):\/\//, 'https://')
|
||||
}));
|
||||
|
||||
return useQueries({
|
||||
queries: urlPairs.map(({ originalUrl, fetchUrl }) => ({
|
||||
queryKey: ['networkStatus', originalUrl],
|
||||
queryFn: async (): Promise<NetworkStatus> => {
|
||||
const response = await fetch(`/api/check-ready?url=${encodeURIComponent(fetchUrl)}`);
|
||||
const data: ApiResponse = await response.json();
|
||||
if (data.code !== 200) {
|
||||
throw new Error(data.message || data.error || 'Service not ready');
|
||||
}
|
||||
|
||||
return {
|
||||
url: originalUrl,
|
||||
isReady: data.data?.ready ?? false
|
||||
};
|
||||
},
|
||||
retry: 5,
|
||||
retryDelay: (attemptIndex: number) => Math.min(1000 * Math.pow(2, attemptIndex), 30000),
|
||||
refetchIntervalInBackground: false,
|
||||
staleTime: 1000 * 60 * 5
|
||||
}))
|
||||
} as const);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { authSession } from '@/services/backend/auth';
|
||||
import { getK8s } from '@/services/backend/kubernetes';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(request: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { url } = request.query as { url: string };
|
||||
if (!url) {
|
||||
return jsonRes(res, {
|
||||
code: 400,
|
||||
error: '缺少 URL 参数'
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.status === 503) {
|
||||
return jsonRes(res, {
|
||||
code: 503,
|
||||
message: 'Service Unavailable (503)'
|
||||
});
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (text.includes('upstream not health')) {
|
||||
return jsonRes(res, {
|
||||
code: 503,
|
||||
message: 'Upstream not healthy'
|
||||
});
|
||||
}
|
||||
|
||||
return jsonRes(res, {
|
||||
code: 200,
|
||||
data: {
|
||||
ready: true
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return jsonRes(res, {
|
||||
code: 500,
|
||||
error: error?.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { appDeployKey, ProtocolList } from '@/constants/app';
|
||||
import { authSession } from '@/services/backend/auth';
|
||||
import { getK8s } from '@/services/backend/kubernetes';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { ProtocolType } from '@/types/app';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { appName } = req.query as { appName: string };
|
||||
const port = global.AppConfig.cloud.port;
|
||||
if (!appName) {
|
||||
throw new Error('appName is empty');
|
||||
}
|
||||
|
||||
const { k8sNetworkingApp, namespace } = await getK8s({
|
||||
kubeconfig: await authSession(req.headers)
|
||||
});
|
||||
|
||||
const ingress = await k8sNetworkingApp.listNamespacedIngress(
|
||||
namespace,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`${appDeployKey}=${appName}`
|
||||
);
|
||||
|
||||
if (!ingress.body.items || ingress.body.items.length === 0) {
|
||||
throw new Error('No ingress found');
|
||||
}
|
||||
|
||||
const checkResults = await Promise.all(
|
||||
ingress.body.items.map(async (item) => {
|
||||
if (!item.spec?.rules?.[0]) {
|
||||
return { ready: false, url: '/', error: 'Invalid ingress configuration' };
|
||||
}
|
||||
|
||||
const rule = item.spec.rules[0];
|
||||
const host = rule.host;
|
||||
const backendProtocol = item?.metadata?.annotations?.[
|
||||
'nginx.ingress.kubernetes.io/backend-protocol'
|
||||
] as ProtocolType;
|
||||
|
||||
const fetchUrl = `http://${host}`;
|
||||
const protocol =
|
||||
ProtocolList.find((item) => item.value === backendProtocol)?.label || 'https://';
|
||||
const url = `${protocol}${host}${port ? `${port}` : ''}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(fetchUrl);
|
||||
|
||||
if (response.status === 503) {
|
||||
return { ready: false, url, error: 'Service Unavailable (503)' };
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (text.includes('upstream not health')) {
|
||||
return { ready: false, url, error: 'Upstream not healthy' };
|
||||
}
|
||||
|
||||
return { ready: true, url };
|
||||
} catch (error) {
|
||||
return { ready: false, url, error: 'fetch error' };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return jsonRes(res, {
|
||||
code: 200,
|
||||
data: checkResults
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return jsonRes(res, {
|
||||
code: 500,
|
||||
error: error?.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -25,3 +25,8 @@ export const checkUserTask = () =>
|
||||
POST('/api/guide/checkTask', {
|
||||
desktopToAppToken: getDesktopSessionFromSessionStorage()?.token
|
||||
});
|
||||
|
||||
export const checkReady = (devboxName: string) =>
|
||||
GET<{ url: string; ready: boolean; error?: string }[]>(
|
||||
`/api/checkReady?devboxName=${devboxName}`
|
||||
);
|
||||
|
||||
+112
-20
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, Flex, Text, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, Button, Center, Flex, Text, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -12,6 +12,9 @@ import { useCopyData } from '@/utils/tools';
|
||||
|
||||
import { useDevboxStore } from '@/stores/devbox';
|
||||
import { useEnvStore } from '@/stores/env';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { checkReady } from '@/api/platform';
|
||||
|
||||
const MonitorModal = dynamic(() => import('@/components/modals/MonitorModal'));
|
||||
|
||||
@@ -22,6 +25,43 @@ const MainBody = () => {
|
||||
const { env } = useEnvStore();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const retryCount = useRef(0);
|
||||
const { data: networkStatus, refetch } = useQuery({
|
||||
queryKey: ['networkStatus', devboxDetail?.name],
|
||||
queryFn: () => (devboxDetail?.name ? checkReady(devboxDetail?.name) : []),
|
||||
retry: 5,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * Math.pow(2, attemptIndex), 30000),
|
||||
onSuccess: (data) => {
|
||||
const hasUnready = data.some((item) => !item.ready);
|
||||
if (!hasUnready) {
|
||||
retryCount.current = 0;
|
||||
return;
|
||||
}
|
||||
if (retryCount.current < 14) {
|
||||
const delay = Math.min(1000 * Math.pow(2, retryCount.current), 32000);
|
||||
retryCount.current += 1;
|
||||
setTimeout(() => {
|
||||
refetch();
|
||||
}, delay);
|
||||
}
|
||||
},
|
||||
refetchIntervalInBackground: false,
|
||||
staleTime: 1000 * 60 * 5
|
||||
});
|
||||
|
||||
const statusMap = useMemo(
|
||||
() =>
|
||||
networkStatus
|
||||
? networkStatus.reduce((acc, item) => {
|
||||
if (item?.url) {
|
||||
acc[item.url] = item;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, { ready: boolean; url: string }>)
|
||||
: {},
|
||||
[networkStatus]
|
||||
);
|
||||
|
||||
const networkColumn: {
|
||||
title: string;
|
||||
dataIndex?: keyof NetworkType;
|
||||
@@ -78,33 +118,85 @@ const MainBody = () => {
|
||||
render: (item: NetworkType) => {
|
||||
if (item.openPublicDomain) {
|
||||
const address = item.customDomain || item.publicDomain;
|
||||
const displayAddress = `https://${address}`;
|
||||
return (
|
||||
<Tooltip
|
||||
label={t('open_link')}
|
||||
hasArrow
|
||||
bg={'#FFFFFF'}
|
||||
color={'grayModern.900'}
|
||||
fontSize={'12px'}
|
||||
fontWeight={400}
|
||||
py={2}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<Text
|
||||
className="guide-network-address"
|
||||
cursor="pointer"
|
||||
color={'grayModern.600'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => window.open(`https://${address}`, '_blank')}
|
||||
<Flex gap={'2px'} alignItems={'center'}>
|
||||
{displayAddress && (
|
||||
<>
|
||||
{statusMap[displayAddress]?.ready ? (
|
||||
<Center
|
||||
fontSize={'12px'}
|
||||
fontWeight={400}
|
||||
bg={'rgba(3, 152, 85, 0.05)'}
|
||||
color={'#039855'}
|
||||
borderRadius={'full'}
|
||||
p={'2px 8px 2px 4px'}
|
||||
gap={'2px'}
|
||||
minW={'63px'}
|
||||
>
|
||||
<Center w={'6px'} h={'6px'} borderRadius={'full'} bg={'#039855'}></Center>
|
||||
{t('Accessible')}
|
||||
</Center>
|
||||
) : (
|
||||
<Center
|
||||
fontSize={'12px'}
|
||||
fontWeight={400}
|
||||
bg={'rgba(17, 24, 36, 0.05)'}
|
||||
color={'#485264'}
|
||||
borderRadius={'full'}
|
||||
p={'2px 8px 2px 4px'}
|
||||
gap={'2px'}
|
||||
minW={'63px'}
|
||||
>
|
||||
<MyIcon
|
||||
name={'loadingCircle'}
|
||||
w={'12px'}
|
||||
h={'12px'}
|
||||
animation={'spin 1s linear infinite'}
|
||||
sx={{
|
||||
'@keyframes spin': {
|
||||
'0%': {
|
||||
transform: 'rotate(0deg)'
|
||||
},
|
||||
'100%': {
|
||||
transform: 'rotate(360deg)'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{t('prepare')}
|
||||
</Center>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Tooltip
|
||||
label={t('open_link')}
|
||||
hasArrow
|
||||
bg={'#FFFFFF'}
|
||||
color={'grayModern.900'}
|
||||
fontSize={'12px'}
|
||||
fontWeight={400}
|
||||
py={2}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
https://{address}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
<Text
|
||||
className="guide-network-address"
|
||||
cursor="pointer"
|
||||
color={'grayModern.600'}
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
onClick={() => window.open(`https://${address}`, '_blank')}
|
||||
>
|
||||
https://{address}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return <Text>-</Text>;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Box bg={'white'} borderRadius="lg" pl={6} pt={2} pr={6} pb={6} h={'full'} borderWidth={1}>
|
||||
{/* monitor */}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { devboxKey } from '@/constants/devbox';
|
||||
import { authSession } from '@/services/backend/auth';
|
||||
import { getK8s } from '@/services/backend/kubernetes';
|
||||
import { jsonRes } from '@/services/backend/response';
|
||||
import { ProtocolType } from '@/types/devbox';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const devboxName = request.nextUrl.searchParams.get('devboxName');
|
||||
|
||||
if (!devboxName) {
|
||||
throw new Error('devboxName is empty');
|
||||
}
|
||||
|
||||
const { k8sNetworkingApp, namespace } = await getK8s({
|
||||
kubeconfig: await authSession(request.headers)
|
||||
});
|
||||
const label = `${devboxKey}=${devboxName}`;
|
||||
const ingress = await k8sNetworkingApp.listNamespacedIngress(
|
||||
namespace,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
label
|
||||
);
|
||||
|
||||
if (!ingress.body.items || ingress.body.items.length === 0) {
|
||||
throw new Error('No ingress found');
|
||||
}
|
||||
|
||||
const checkResults = await Promise.all(
|
||||
ingress.body.items.map(async (item) => {
|
||||
if (!item.spec?.rules?.[0]) {
|
||||
return { ready: false, url: '/', error: 'Invalid ingress configuration' };
|
||||
}
|
||||
|
||||
const rule = item.spec.rules[0];
|
||||
const host = rule.host;
|
||||
const fetchUrl = `http://${host}`;
|
||||
const url = `https://${host}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(fetchUrl, {
|
||||
cache: 'no-store'
|
||||
});
|
||||
|
||||
if (response.status === 503) {
|
||||
return { ready: false, url, error: 'Service Unavailable (503)' };
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (text.includes('upstream not health')) {
|
||||
return { ready: false, url, error: 'Upstream not healthy' };
|
||||
}
|
||||
|
||||
return { ready: true, url };
|
||||
} catch (error) {
|
||||
return { ready: false, url, error: 'fetch error' };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return jsonRes({
|
||||
code: 200,
|
||||
data: checkResults
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return jsonRes({
|
||||
code: 500,
|
||||
error: error?.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none">
|
||||
<path opacity="0.8" d="M6 1.23828C6.33333 1.23828 6.5 1.40495 6.5 1.73828V3.23828C6.5 3.57161 6.33333 3.73828 6 3.73828C5.66667 3.73828 5.5 3.57161 5.5 3.23828V1.73828C5.5 1.40495 5.66667 1.23828 6 1.23828Z" fill="black"/>
|
||||
<path opacity="0.4" d="M6 8.73828C6.33333 8.73828 6.5 8.90495 6.5 9.23828V10.7383C6.5 11.0716 6.33333 11.2383 6 11.2383C5.66667 11.2383 5.5 11.0716 5.5 10.7383V9.23828C5.5 8.90495 5.66667 8.73828 6 8.73828Z" fill="black"/>
|
||||
<path opacity="0.2" d="M11 6.23828C11 6.57161 10.8333 6.73828 10.5 6.73828H9C8.66667 6.73828 8.5 6.57161 8.5 6.23828C8.5 5.90495 8.66667 5.73828 9 5.73828H10.5C10.8333 5.73828 11 5.90495 11 6.23828Z" fill="black"/>
|
||||
<path opacity="0.6" d="M3.5 6.23828C3.5 6.57161 3.33333 6.73828 3 6.73828H1.5C1.16667 6.73828 1 6.57161 1 6.23828C1 5.90495 1.16667 5.73828 1.5 5.73828H3C3.33333 5.73828 3.5 5.90495 3.5 6.23828Z" fill="black"/>
|
||||
<path opacity="0.1" d="M9.53561 2.70284C9.77131 2.93854 9.77131 3.17424 9.53561 3.40995L8.47495 4.47061C8.23925 4.70631 8.00354 4.70631 7.76784 4.47061C7.53214 4.2349 7.53214 3.9992 7.76784 3.7635L8.8285 2.70284C9.0642 2.46714 9.29991 2.46714 9.53561 2.70284Z" fill="black"/>
|
||||
<path opacity="0.5" d="M4.23214 8.00585C4.46784 8.24155 4.46784 8.47725 4.23214 8.71295L3.17148 9.77361C2.93578 10.0093 2.70008 10.0093 2.46437 9.77361C2.22867 9.53791 2.22867 9.30221 2.46437 9.06651L3.52503 8.00585C3.76074 7.77015 3.99644 7.77015 4.23214 8.00585Z" fill="black"/>
|
||||
<path opacity="0.3" d="M9.53536 9.77383C9.29966 10.0095 9.06396 10.0095 8.82826 9.77383L7.7676 8.71317C7.53189 8.47747 7.53189 8.24176 7.7676 8.00606C8.0033 7.77036 8.239 7.77036 8.4747 8.00606L9.53536 9.06672C9.77107 9.30242 9.77107 9.53813 9.53536 9.77383Z" fill="black"/>
|
||||
<path opacity="0.7" d="M4.23239 4.47033C3.99668 4.70603 3.76098 4.70603 3.52528 4.47033L2.46462 3.40967C2.22892 3.17397 2.22892 2.93827 2.46462 2.70256C2.70032 2.46686 2.93602 2.46686 3.17173 2.70256L4.23239 3.76322C4.46809 3.99893 4.46809 4.23463 4.23239 4.47033Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -85,7 +85,8 @@ const map = {
|
||||
webstorm: require('./icons/jetbrains/webstorm.svg').default,
|
||||
gift: require('./icons/gift.svg').default,
|
||||
fileConfig: require('./icons/fileConfig.svg').default,
|
||||
trae: require('./icons/trae.svg').default
|
||||
trae: require('./icons/trae.svg').default,
|
||||
loadingCircle: require('./icons/loadingCircle.svg').default
|
||||
};
|
||||
|
||||
const MyIcon = ({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"Accessible": "Accessible",
|
||||
"Add Port": "Add Port",
|
||||
"Amount": "Amount",
|
||||
"CNAME Tips": "To set up a custom domain, please add a `CNAME` record for your domain pointing to {domain} at your domain registrar. You can bind your custom domain once the DNS resolution takes effect.",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Delete": "Deleting",
|
||||
"Error": "Error",
|
||||
"Failed": "Failed",
|
||||
"GPU": "GPU",
|
||||
"Gpu under inventory Tip": "{gputype} is out of stock. Please select a different type or reduce the quantity",
|
||||
"Input your custom domain": "Enter your custom domain",
|
||||
"Inventory": "Stock",
|
||||
@@ -101,6 +103,8 @@
|
||||
"export_yaml": "Export YAML",
|
||||
"external_address": "Public Address",
|
||||
"framework": "Framework",
|
||||
"gpu": "GPU",
|
||||
"gpu_exceeds_quota": "Requested GPU exceeds your quota. Please contact admin.",
|
||||
"guide": {
|
||||
"click_shadow_skip": "Click on any shadow to skip",
|
||||
"deploy_cost": "Current resource usage and estimated daily consumption",
|
||||
@@ -193,6 +197,7 @@
|
||||
"pause_success": "Shutdown succeeded",
|
||||
"please_enter_devbox_name_confirm": "Please enter <strong>{name}</strong> to confirm: ",
|
||||
"port": "Port",
|
||||
"prepare": "Waiting",
|
||||
"privacy_and_security_agreement": "Privacy and Security Agreement",
|
||||
"privacy_and_security_agreement_tips": "Please read and agree to the Privacy and Security Agreement.",
|
||||
"private": "Private",
|
||||
@@ -284,8 +289,5 @@
|
||||
"vm": "vm",
|
||||
"vscode": "VS Code",
|
||||
"vscode_tooltip": "Click to develop in VSCode",
|
||||
"yaml_file": "YAML",
|
||||
"gpu_exceeds_quota": "Requested GPU exceeds your quota. Please contact admin.",
|
||||
"GPU": "GPU",
|
||||
"gpu": "GPU"
|
||||
"yaml_file": "YAML"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"Accessible": "可访问",
|
||||
"Add Port": "添加端口",
|
||||
"Amount": "数量",
|
||||
"CNAME Tips": "请到您的域名服务商处,添加该域名的 `CNAME` 解析到 {domain},解析生效后即可绑定自定义域名。",
|
||||
@@ -8,6 +9,7 @@
|
||||
"Delete": "删除中",
|
||||
"Error": "错误",
|
||||
"Failed": "失败",
|
||||
"GPU": "GPU",
|
||||
"Gpu under inventory Tip": "{gputype} 库存不足,请更换型号或减少数量",
|
||||
"Input your custom domain": "输入您的自定义域名",
|
||||
"Inventory": "库存",
|
||||
@@ -102,6 +104,8 @@
|
||||
"export_yaml": "导出 YAML",
|
||||
"external_address": "公网地址",
|
||||
"framework": "框架",
|
||||
"gpu": "GPU",
|
||||
"gpu_exceeds_quota": "申请的 GPU 超出配额限制,请联系管理员",
|
||||
"guide": {
|
||||
"click_shadow_skip": "点击任意阴影跳过",
|
||||
"deploy_cost": "当前资源,预估单日消耗",
|
||||
@@ -141,8 +145,8 @@
|
||||
"jetbrains_guide_move_to_path": "将下载好的私钥移动到目录:<blue>~/.ssh/sealos/</blue> 。",
|
||||
"jetbrains_guide_one_click_setup": "一键配置",
|
||||
"jetbrains_guide_one_click_setup_desc": "点击上方按钮 <blue>下载 bat/sh 脚本</blue>,下载到本地后<blue>直接双击运行</blue>即可完成 SSH 连接信息的配置。",
|
||||
"jetbrains_guide_one_click_setup_desc_windows": "点击上方按钮 <blue>下载 bat/sh 脚本</blue>,下载到本地后<blue>右键点击文件 > 使用 PowerShell 运行</blue> 即可完成 SSH 连接信息的配置。",
|
||||
"jetbrains_guide_one_click_setup_desc_2": "<lightColor>或者,您也可以复制下方脚本到本地命令行,然后直接执行。</lightColor>",
|
||||
"jetbrains_guide_one_click_setup_desc_windows": "点击上方按钮 <blue>下载 bat/sh 脚本</blue>,下载到本地后<blue>右键点击文件 > 使用 PowerShell 运行</blue> 即可完成 SSH 连接信息的配置。",
|
||||
"jetbrains_guide_post_connection": "后续使用可点击下方按钮进行查看。",
|
||||
"jetbrains_guide_post_use": "后续使用",
|
||||
"jetbrains_guide_prepare": "前置准备",
|
||||
@@ -195,6 +199,7 @@
|
||||
"pause_success": "关机成功",
|
||||
"please_enter_devbox_name_confirm": "请输入 <strong>{name}</strong> 确认:",
|
||||
"port": "端口",
|
||||
"prepare": "待启动",
|
||||
"privacy_and_security_agreement": "安全隐私说明",
|
||||
"privacy_and_security_agreement_tips": "请阅读并同意安全隐私协议。",
|
||||
"private": "私有",
|
||||
@@ -286,8 +291,5 @@
|
||||
"vscode": "VS Code",
|
||||
"vscodeInsider": "VSCode Insider",
|
||||
"vscode_tooltip": "点击在 VSCode 中开发",
|
||||
"yaml_file": "YAML 文件",
|
||||
"gpu_exceeds_quota": "申请的 GPU 超出配额限制,请联系管理员",
|
||||
"GPU": "GPU",
|
||||
"gpu": "GPU"
|
||||
"yaml_file": "YAML 文件"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user