Skip to content

Commit bd94480

Browse files
authored
feat: update launchpad network status (#5444)
* fix read api * update network * update * update * update style
1 parent bf1e746 commit bd94480

File tree

9 files changed

+207
-27
lines changed

9 files changed

+207
-27
lines changed

frontend/desktop/src/components/desktop_content/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export default function Desktop(props: any) {
147147
const { data: notification } = await getGlobalNotification();
148148
if (!notification) return;
149149
const newID = notification?.uid;
150-
const title = notification?.i18n[i18n.language].title;
150+
const title = notification?.i18n[i18n?.language]?.title;
151151

152152
if (notification.licenseFrontend) {
153153
message({

frontend/desktop/src/pages/api/notification/read.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,52 +10,56 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
1010
const { name } = req.body;
1111
const payload = await verifyAccessToken(req.headers);
1212
if (!payload) return jsonRes(res, { code: 401, message: 'failed to get info' });
13+
1314
const namespace = payload.workspaceId;
1415
const _kc = await getUserKubeconfigNotPatch(payload.userCrName);
1516
if (!_kc) return jsonRes(res, { code: 404, message: 'user is not found' });
16-
const realKc = switchKubeconfigNamespace(_kc, namespace);
17-
const kc = K8sApi(realKc);
17+
18+
const kc = K8sApi(switchKubeconfigNamespace(_kc, namespace));
1819
const meta: CRDMeta = {
1920
group: 'notification.sealos.io',
2021
version: 'v1',
2122
namespace,
2223
plural: 'notifications'
2324
};
2425

25-
// crd patch
2626
const patch = [
2727
{
2828
op: 'add',
2929
path: '/metadata/labels',
30-
value: {
31-
isRead: 'true'
32-
}
30+
value: { isRead: 'true' }
3331
}
3432
];
3533
// const patch = [{ op: 'remove', path: '/metadata/labels/isRead' }]; // dev
34+
const results = [];
3635

37-
let result = [];
3836
for (const n of name) {
3937
try {
40-
let temp = await UpdateCRD(kc, meta, n, patch);
41-
result.push(temp?.body);
38+
const temp = await UpdateCRD(kc, meta, n, patch);
39+
results.push(temp?.body);
4240
} catch (err: any) {
4341
if (err?.body?.code === 403) {
44-
const temp = {
45-
name: n,
46-
reason: err?.body?.reason,
47-
message: err?.body?.message,
48-
code: 403
49-
};
50-
51-
jsonRes(res, { data: temp });
52-
} else {
53-
throw err;
42+
return jsonRes(res, {
43+
data: {
44+
name: n,
45+
reason: err?.body?.reason,
46+
message: err?.body?.message,
47+
code: 403
48+
}
49+
});
5450
}
51+
return jsonRes(res, {
52+
code: 500,
53+
data: {
54+
name: n,
55+
message: err?.message || 'Unknown error'
56+
}
57+
});
5558
}
5659
}
57-
jsonRes(res, { data: result });
60+
61+
return jsonRes(res, { data: results });
5862
} catch (err) {
59-
jsonRes(res, { code: 500, data: err });
63+
return jsonRes(res, { code: 500, data: err });
6064
}
6165
}

frontend/providers/applaunchpad/public/locales/en/common.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,5 +317,7 @@
317317
"selected": "Selected",
318318
"please_select": "Please Select",
319319
"refetching_success": "Refresh Successful",
320-
"refresh": "refresh"
320+
"refresh": "refresh",
321+
"Ready": "Preparing",
322+
"Accessible": "Accessible"
321323
}

frontend/providers/applaunchpad/public/locales/zh/common.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,5 +317,7 @@
317317
"selected": "已选中",
318318
"please_select": "请选择",
319319
"refetching_success": "刷新成功",
320-
"refresh": "刷新"
320+
"refresh": "刷新",
321+
"Ready": "准备中",
322+
"Accessible": "可访问"
321323
}
Lines changed: 10 additions & 0 deletions
Loading

frontend/providers/applaunchpad/src/components/Icon/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export const IconMap = {
6060
container: require('./icons/container.svg').default,
6161
arrowRight: require('./icons/arrowRight.svg').default,
6262
chart: require('./icons/chart.svg').default,
63-
export: require('./icons/export.svg').default
63+
export: require('./icons/export.svg').default,
64+
loading: require('./icons/loading.svg').default
6465
};
6566

6667
export type IconType = keyof typeof IconMap;

frontend/providers/applaunchpad/src/components/app/detail/index/AppMainInfo.tsx

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import dayjs from 'dayjs';
1313
import { useTranslation } from 'next-i18next';
1414
import { useMemo } from 'react';
1515
import MonitorModal from './MonitorModal';
16+
import { useNetworkStatus } from '@/hooks/useNetworkStatus';
1617

1718
const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
1819
const { t } = useTranslation();
@@ -33,6 +34,18 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
3334
})),
3435
[app]
3536
);
37+
const networkStatuses = useNetworkStatus(networks);
38+
39+
const statusMap = useMemo(
40+
() =>
41+
networkStatuses.reduce((acc, status) => {
42+
if (status.data?.url) {
43+
acc[status.data.url] = status.data;
44+
}
45+
return acc;
46+
}, {} as Record<string, { isReady?: boolean; error?: string }>),
47+
[networkStatuses]
48+
);
3649

3750
return (
3851
<Box p={'24px'} position={'relative'}>
@@ -110,7 +123,60 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
110123
</Flex>
111124
</th>
112125
<th>
113-
<Flex alignItems={'center'} justifyContent={'space-between'}>
126+
<Flex alignItems={'center'} gap={'2px'} justifyContent={'space-between'}>
127+
{network.public && (
128+
<>
129+
{statusMap[network.public]?.isReady ? (
130+
<Center
131+
fontSize={'12px'}
132+
fontWeight={400}
133+
bg={'rgba(3, 152, 85, 0.05)'}
134+
color={'#039855'}
135+
borderRadius={'full'}
136+
p={'2px 4px'}
137+
gap={'2px'}
138+
minW={'63px'}
139+
>
140+
<Center
141+
w={'6px'}
142+
h={'6px'}
143+
borderRadius={'full'}
144+
bg={'#039855'}
145+
></Center>
146+
{t('Accessible')}
147+
</Center>
148+
) : (
149+
<Center
150+
fontSize={'12px'}
151+
fontWeight={400}
152+
bg={'rgba(17, 24, 36, 0.05)'}
153+
color={'#485264'}
154+
borderRadius={'full'}
155+
p={'2px 4px'}
156+
gap={'2px'}
157+
minW={'63px'}
158+
>
159+
<MyIcon
160+
name={'loading'}
161+
w={'12px'}
162+
h={'12px'}
163+
animation={'spin 1s linear infinite'}
164+
sx={{
165+
'@keyframes spin': {
166+
'0%': {
167+
transform: 'rotate(0deg)'
168+
},
169+
'100%': {
170+
transform: 'rotate(360deg)'
171+
}
172+
}
173+
}}
174+
/>
175+
{t('Ready')}
176+
</Center>
177+
)}
178+
</>
179+
)}
114180
<MyTooltip
115181
label={network.public ? t('Open Link') : ''}
116182
placement={'bottom-start'}
@@ -126,9 +192,12 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
126192
}
127193
: {})}
128194
>
129-
{network.public || '-'}
195+
<Flex alignItems={'center'} gap={2}>
196+
{network.public || '-'}
197+
</Flex>
130198
</Box>
131199
</MyTooltip>
200+
132201
{!!network.public && (
133202
<Center
134203
flexShrink={0}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useQueries } from '@tanstack/react-query';
2+
3+
interface NetworkStatus {
4+
url: string;
5+
isReady: boolean;
6+
error?: string;
7+
}
8+
9+
interface ApiResponse {
10+
code: number;
11+
data?: { ready: boolean };
12+
message?: string;
13+
error?: string;
14+
}
15+
16+
export const useNetworkStatus = (networks: { inline: string; public: string }[]) => {
17+
const urlPairs = networks
18+
.map((network) => network.public)
19+
.filter(Boolean)
20+
.map((originalUrl) => ({
21+
originalUrl,
22+
fetchUrl: originalUrl.replace(/^(wss|grpcs):\/\//, 'https://')
23+
}));
24+
25+
return useQueries({
26+
queries: urlPairs.map(({ originalUrl, fetchUrl }) => ({
27+
queryKey: ['networkStatus', originalUrl],
28+
queryFn: async (): Promise<NetworkStatus> => {
29+
const response = await fetch(`/api/check-ready?url=${encodeURIComponent(fetchUrl)}`);
30+
const data: ApiResponse = await response.json();
31+
if (data.code !== 200) {
32+
throw new Error(data.message || data.error || 'Service not ready');
33+
}
34+
35+
return {
36+
url: originalUrl,
37+
isReady: data.data?.ready ?? false
38+
};
39+
},
40+
retry: 5,
41+
retryDelay: (attemptIndex: number) => Math.min(1000 * Math.pow(2, attemptIndex), 30000),
42+
refetchIntervalInBackground: false,
43+
staleTime: 1000 * 60 * 5
44+
}))
45+
} as const);
46+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { authSession } from '@/services/backend/auth';
2+
import { getK8s } from '@/services/backend/kubernetes';
3+
import { jsonRes } from '@/services/backend/response';
4+
import { NextApiRequest, NextApiResponse } from 'next';
5+
6+
export default async function handler(request: NextApiRequest, res: NextApiResponse) {
7+
try {
8+
const { url } = request.query as { url: string };
9+
if (!url) {
10+
return jsonRes(res, {
11+
code: 400,
12+
error: '缺少 URL 参数'
13+
});
14+
}
15+
16+
const response = await fetch(url);
17+
18+
if (response.status === 503) {
19+
return jsonRes(res, {
20+
code: 503,
21+
message: 'Service Unavailable (503)'
22+
});
23+
}
24+
25+
const text = await response.text();
26+
if (text.includes('upstream not health')) {
27+
return jsonRes(res, {
28+
code: 503,
29+
message: 'Upstream not healthy'
30+
});
31+
}
32+
33+
return jsonRes(res, {
34+
code: 200,
35+
data: {
36+
ready: true
37+
}
38+
});
39+
} catch (error: any) {
40+
console.error(error);
41+
return jsonRes(res, {
42+
code: 500,
43+
error: error?.message
44+
});
45+
}
46+
}

0 commit comments

Comments
 (0)