mirror of
https://github.com/labring/sealos.git
synced 2026-10-28 02:27:18 +08:00
feat: update launchpad network status (#5444)
* fix read api * update network * update * update * update style
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user