feat: update launchpad network status (#5444)

* fix read api

* update network

* update

* update

* update style
This commit is contained in:
jingyang
2025-03-07 15:15:39 +08:00
committed by GitHub
parent bf1e746944
commit bd944800e8
9 changed files with 207 additions and 27 deletions
@@ -147,7 +147,7 @@ export default function Desktop(props: any) {
const { data: notification } = await getGlobalNotification();
if (!notification) return;
const newID = notification?.uid;
const title = notification?.i18n[i18n.language].title;
const title = notification?.i18n[i18n?.language]?.title;
if (notification.licenseFrontend) {
message({
@@ -10,11 +10,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { name } = req.body;
const payload = await verifyAccessToken(req.headers);
if (!payload) return jsonRes(res, { code: 401, message: 'failed to get info' });
const namespace = payload.workspaceId;
const _kc = await getUserKubeconfigNotPatch(payload.userCrName);
if (!_kc) return jsonRes(res, { code: 404, message: 'user is not found' });
const realKc = switchKubeconfigNamespace(_kc, namespace);
const kc = K8sApi(realKc);
const kc = K8sApi(switchKubeconfigNamespace(_kc, namespace));
const meta: CRDMeta = {
group: 'notification.sealos.io',
version: 'v1',
@@ -22,40 +23,43 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
plural: 'notifications'
};
// crd patch
const patch = [
{
op: 'add',
path: '/metadata/labels',
value: {
isRead: 'true'
}
value: { isRead: 'true' }
}
];
// const patch = [{ op: 'remove', path: '/metadata/labels/isRead' }]; // dev
const results = [];
let result = [];
for (const n of name) {
try {
let temp = await UpdateCRD(kc, meta, n, patch);
result.push(temp?.body);
const temp = await UpdateCRD(kc, meta, n, patch);
results.push(temp?.body);
} catch (err: any) {
if (err?.body?.code === 403) {
const temp = {
name: n,
reason: err?.body?.reason,
message: err?.body?.message,
code: 403
};
jsonRes(res, { data: temp });
} else {
throw err;
return jsonRes(res, {
data: {
name: n,
reason: err?.body?.reason,
message: err?.body?.message,
code: 403
}
});
}
return jsonRes(res, {
code: 500,
data: {
name: n,
message: err?.message || 'Unknown error'
}
});
}
}
jsonRes(res, { data: result });
return jsonRes(res, { data: results });
} catch (err) {
jsonRes(res, { code: 500, data: err });
return jsonRes(res, { code: 500, data: err });
}
}
@@ -317,5 +317,7 @@
"selected": "Selected",
"please_select": "Please Select",
"refetching_success": "Refresh Successful",
"refresh": "refresh"
"refresh": "refresh",
"Ready": "Preparing",
"Accessible": "Accessible"
}
@@ -317,5 +317,7 @@
"selected": "已选中",
"please_select": "请选择",
"refetching_success": "刷新成功",
"refresh": "刷新"
"refresh": "刷新",
"Ready": "准备中",
"Accessible": "可访问"
}
@@ -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

@@ -60,7 +60,8 @@ export const IconMap = {
container: require('./icons/container.svg').default,
arrowRight: require('./icons/arrowRight.svg').default,
chart: require('./icons/chart.svg').default,
export: require('./icons/export.svg').default
export: require('./icons/export.svg').default,
loading: require('./icons/loading.svg').default
};
export type IconType = keyof typeof IconMap;
@@ -13,6 +13,7 @@ import dayjs from 'dayjs';
import { useTranslation } from 'next-i18next';
import { useMemo } from 'react';
import MonitorModal from './MonitorModal';
import { useNetworkStatus } from '@/hooks/useNetworkStatus';
const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
const { t } = useTranslation();
@@ -33,6 +34,18 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
})),
[app]
);
const networkStatuses = useNetworkStatus(networks);
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]
);
return (
<Box p={'24px'} position={'relative'}>
@@ -110,7 +123,60 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
</Flex>
</th>
<th>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Flex alignItems={'center'} gap={'2px'} justifyContent={'space-between'}>
{network.public && (
<>
{statusMap[network.public]?.isReady ? (
<Center
fontSize={'12px'}
fontWeight={400}
bg={'rgba(3, 152, 85, 0.05)'}
color={'#039855'}
borderRadius={'full'}
p={'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 4px'}
gap={'2px'}
minW={'63px'}
>
<MyIcon
name={'loading'}
w={'12px'}
h={'12px'}
animation={'spin 1s linear infinite'}
sx={{
'@keyframes spin': {
'0%': {
transform: 'rotate(0deg)'
},
'100%': {
transform: 'rotate(360deg)'
}
}
}}
/>
{t('Ready')}
</Center>
)}
</>
)}
<MyTooltip
label={network.public ? t('Open Link') : ''}
placement={'bottom-start'}
@@ -126,9 +192,12 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
}
: {})}
>
{network.public || '-'}
<Flex alignItems={'center'} gap={2}>
{network.public || '-'}
</Flex>
</Box>
</MyTooltip>
{!!network.public && (
<Center
flexShrink={0}
@@ -0,0 +1,46 @@
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);
};
@@ -0,0 +1,46 @@
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
});
}
}