Skip to content

Commit 754a096

Browse files
authored
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
1 parent 5301ae0 commit 754a096

File tree

12 files changed

+344
-136
lines changed

12 files changed

+344
-136
lines changed

frontend/providers/applaunchpad/src/api/platform.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ export const getPriceBonus = () =>
3838

3939
export const checkPermission = (payload: { appName: string }) =>
4040
GET('/api/platform/checkPermission', payload);
41+
42+
export const checkReady = (appName: string) =>
43+
GET<{ url: string; ready: boolean; error?: string }[]>(`/api/checkReady?appName=${appName}`);

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

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import { getUserNamespace } from '@/utils/user';
1111
import { Box, Button, Center, Flex, Grid, Text, useDisclosure } from '@chakra-ui/react';
1212
import dayjs from 'dayjs';
1313
import { useTranslation } from 'next-i18next';
14-
import { useMemo } from 'react';
14+
import { useMemo, useRef } from 'react';
1515
import MonitorModal from './MonitorModal';
16-
import { useNetworkStatus } from '@/hooks/useNetworkStatus';
16+
import { useQuery } from '@tanstack/react-query';
17+
import { checkReady } from '@/api/platform';
1718

1819
const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
1920
const { t } = useTranslation();
@@ -34,17 +35,42 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
3435
})),
3536
[app]
3637
);
37-
const networkStatuses = useNetworkStatus(networks);
38+
39+
const retryCount = useRef(0);
40+
const { data: networkStatus, refetch } = useQuery({
41+
queryKey: ['networkStatus', app.appName],
42+
queryFn: () => checkReady(app.appName),
43+
retry: 5,
44+
retryDelay: (attemptIndex) => Math.min(1000 * Math.pow(2, attemptIndex), 30000),
45+
onSuccess: (data) => {
46+
const hasUnready = data.some((item) => !item.ready);
47+
if (!hasUnready) {
48+
retryCount.current = 0;
49+
return;
50+
}
51+
if (retryCount.current < 14) {
52+
const delay = Math.min(1000 * Math.pow(2, retryCount.current), 32000);
53+
retryCount.current += 1;
54+
setTimeout(() => {
55+
refetch();
56+
}, delay);
57+
}
58+
},
59+
refetchIntervalInBackground: false,
60+
staleTime: 1000 * 60 * 5
61+
});
3862

3963
const statusMap = useMemo(
4064
() =>
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]
65+
networkStatus
66+
? networkStatus.reduce((acc, item) => {
67+
if (item?.url) {
68+
acc[item.url] = item;
69+
}
70+
return acc;
71+
}, {} as Record<string, { ready: boolean; url: string }>)
72+
: {},
73+
[networkStatus]
4874
);
4975

5076
return (
@@ -123,17 +149,17 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
123149
</Flex>
124150
</th>
125151
<th>
126-
<Flex alignItems={'center'} gap={'2px'} justifyContent={'space-between'}>
152+
<Flex alignItems={'center'} gap={'2px'}>
127153
{network.public && (
128154
<>
129-
{statusMap[network.public]?.isReady ? (
155+
{statusMap[network.public]?.ready ? (
130156
<Center
131157
fontSize={'12px'}
132158
fontWeight={400}
133159
bg={'rgba(3, 152, 85, 0.05)'}
134160
color={'#039855'}
135161
borderRadius={'full'}
136-
p={'2px 4px'}
162+
p={'2px 8px 2px 4px'}
137163
gap={'2px'}
138164
minW={'63px'}
139165
>
@@ -152,7 +178,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
152178
bg={'rgba(17, 24, 36, 0.05)'}
153179
color={'#485264'}
154180
borderRadius={'full'}
155-
p={'2px 4px'}
181+
p={'2px 8px 2px 4px'}
156182
gap={'2px'}
157183
minW={'63px'}
158184
>
@@ -200,6 +226,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => {
200226

201227
{!!network.public && (
202228
<Center
229+
ml={'auto'}
203230
flexShrink={0}
204231
w={'24px'}
205232
h={'24px'}

frontend/providers/applaunchpad/src/hooks/useNetworkStatus.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.

frontend/providers/applaunchpad/src/pages/api/check-ready.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { appDeployKey, ProtocolList } from '@/constants/app';
2+
import { authSession } from '@/services/backend/auth';
3+
import { getK8s } from '@/services/backend/kubernetes';
4+
import { jsonRes } from '@/services/backend/response';
5+
import { ProtocolType } from '@/types/app';
6+
import { NextApiRequest, NextApiResponse } from 'next';
7+
8+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
9+
try {
10+
const { appName } = req.query as { appName: string };
11+
const port = global.AppConfig.cloud.port;
12+
if (!appName) {
13+
throw new Error('appName is empty');
14+
}
15+
16+
const { k8sNetworkingApp, namespace } = await getK8s({
17+
kubeconfig: await authSession(req.headers)
18+
});
19+
20+
const ingress = await k8sNetworkingApp.listNamespacedIngress(
21+
namespace,
22+
undefined,
23+
undefined,
24+
undefined,
25+
undefined,
26+
`${appDeployKey}=${appName}`
27+
);
28+
29+
if (!ingress.body.items || ingress.body.items.length === 0) {
30+
throw new Error('No ingress found');
31+
}
32+
33+
const checkResults = await Promise.all(
34+
ingress.body.items.map(async (item) => {
35+
if (!item.spec?.rules?.[0]) {
36+
return { ready: false, url: '/', error: 'Invalid ingress configuration' };
37+
}
38+
39+
const rule = item.spec.rules[0];
40+
const host = rule.host;
41+
const backendProtocol = item?.metadata?.annotations?.[
42+
'nginx.ingress.kubernetes.io/backend-protocol'
43+
] as ProtocolType;
44+
45+
const fetchUrl = `http://${host}`;
46+
const protocol =
47+
ProtocolList.find((item) => item.value === backendProtocol)?.label || 'https://';
48+
const url = `${protocol}${host}${port ? `${port}` : ''}`;
49+
50+
try {
51+
const response = await fetch(fetchUrl);
52+
53+
if (response.status === 503) {
54+
return { ready: false, url, error: 'Service Unavailable (503)' };
55+
}
56+
57+
const text = await response.text();
58+
if (text.includes('upstream not health')) {
59+
return { ready: false, url, error: 'Upstream not healthy' };
60+
}
61+
62+
return { ready: true, url };
63+
} catch (error) {
64+
return { ready: false, url, error: 'fetch error' };
65+
}
66+
})
67+
);
68+
69+
return jsonRes(res, {
70+
code: 200,
71+
data: checkResults
72+
});
73+
} catch (error: any) {
74+
console.error(error);
75+
return jsonRes(res, {
76+
code: 500,
77+
error: error?.message
78+
});
79+
}
80+
}

frontend/providers/devbox/api/platform.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,8 @@ export const checkUserTask = () =>
2525
POST('/api/guide/checkTask', {
2626
desktopToAppToken: getDesktopSessionFromSessionStorage()?.token
2727
});
28+
29+
export const checkReady = (devboxName: string) =>
30+
GET<{ url: string; ready: boolean; error?: string }[]>(
31+
`/api/checkReady?devboxName=${devboxName}`
32+
);

0 commit comments

Comments
 (0)