Skip to content

Commit 1d572f6

Browse files
committed
Merge remote-tracking branch 'origin/main' into 25.11
2 parents 6c2eb83 + 45847d5 commit 1d572f6

34 files changed

Lines changed: 611 additions & 403 deletions

pnpm-lock.yaml

Lines changed: 121 additions & 113 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@dicebear/core": "^9.2.2",
1717
"@iconify-json/fluent-emoji-flat": "^1.2.3",
1818
"@melloware/react-logviewer": "^5.3.2",
19+
"@microsoft/fetch-event-source": "^2.0.1",
1920
"@react-hook/resize-observer": "^2.0.2",
2021
"@storybook/test": "^8.6.10",
2122
"@tanstack/react-query": "^5.69.0",

react/src/components/AboutBackendAIModal.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,22 @@ const AboutBackendAIModal = ({
2828

2929
return (
3030
<BAIModal
31+
className="about-backendai-modal"
3132
title={
3233
<img
34+
className="about-logo-img"
3335
alt={themeConfig?.logo?.alt || 'Backend.AI Logo'}
3436
src={
3537
isDarkMode && themeConfig?.logo?.srcDark
3638
? themeConfig?.logo?.src || '/manifest/backend.ai-white-text.svg'
3739
: themeConfig?.logo?.srcDark ||
3840
'/manifest/backend.ai-white-text.svg'
3941
}
42+
style={{
43+
width: themeConfig?.logo?.aboutModalSize?.width || 159,
44+
height: themeConfig?.logo?.aboutModalSize?.height || 24,
45+
cursor: 'pointer',
46+
}}
4047
/>
4148
}
4249
onCancel={onRequestClose}

react/src/components/BAIConfirmModalWithInput.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { useTranslation } from 'react-i18next';
88

99
const { Text } = Typography;
1010

11-
interface BAIConfirmModalWithInputProps extends Omit<BAIModalProps, 'icon'> {
11+
interface BAIConfirmModalWithInputProps
12+
extends Omit<BAIModalProps, 'icon' | 'okButtonProps'> {
1213
confirmText: string;
1314
content: React.ReactNode;
1415
title: React.ReactNode;
1516
icon?: React.ReactNode;
17+
okButtonProps?: Omit<BAIModalProps['okButtonProps'], 'disabled' | 'danger'>;
1618
}
1719

1820
const BAIConfirmModalWithInput: React.FC<BAIConfirmModalWithInputProps> = ({
@@ -51,8 +53,12 @@ const BAIConfirmModalWithInput: React.FC<BAIConfirmModalWithInputProps> = ({
5153
form.resetFields();
5254
_.isFunction(onCancel) && onCancel(e);
5355
}}
54-
okButtonProps={{ disabled: confirmText !== typedText, danger: true }}
5556
{...props}
57+
okButtonProps={{
58+
...props.okButtonProps,
59+
disabled: confirmText !== typedText,
60+
danger: true,
61+
}}
5662
>
5763
<Flex direction="column" justify="start" align="start">
5864
{content}

react/src/components/Chat/EndpointTokenSelect.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ function sortEndpointTokenList(
2121
(item) => ({
2222
label: item?.token,
2323
value: item?.token,
24-
disabled: !dayjs(item?.valid_until).tz().isAfter(now),
24+
// FIXME: temporally parse UTC and change to timezone (timezone need to be added in server side)
25+
disabled: !dayjs.utc(item?.valid_until).tz().isAfter(now),
2526
}),
2627
);
2728
}

react/src/components/ImageEnvironmentSelectFormItems.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,14 @@ const ImageEnvironmentSelectFormItems: React.FC<
364364
{t('session.launcher.Version')}
365365
</Typography.Text>
366366
}
367-
rules={[{ required: _.isEmpty(environments?.manual) }]}
367+
rules={[
368+
{
369+
required: _.isEmpty(environments?.manual),
370+
message: t('general.ValueRequired', {
371+
name: t('session.launcher.Environments'),
372+
}),
373+
},
374+
]}
368375
style={{ marginBottom: 10 }}
369376
>
370377
<Select
@@ -564,7 +571,14 @@ const ImageEnvironmentSelectFormItems: React.FC<
564571
<Form.Item
565572
className="image-environment-select-form-item"
566573
name={['environments', 'version']}
567-
rules={[{ required: _.isEmpty(environments?.manual) }]}
574+
rules={[
575+
{
576+
required: _.isEmpty(environments?.manual),
577+
message: t('general.ValueRequired', {
578+
name: t('session.launcher.Version'),
579+
}),
580+
},
581+
]}
568582
>
569583
<Select
570584
ref={versionSelectRef}

react/src/components/InviteFolderSettingModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,8 @@ const InviteFolderSettingModal: React.FC<InviteFolderSettingModalProps> = ({
263263
<Select
264264
style={{ minWidth: 130 }}
265265
options={[
266-
{ label: t('data.folders.View'), value: 'ro' },
267-
{ label: t('data.folders.Edit'), value: 'rw' },
266+
{ label: t('data.ReadOnly'), value: 'ro' },
267+
{ label: t('data.ReadWrite'), value: 'rw' },
268268
]}
269269
defaultValue={perm}
270270
onChange={(nextPerm) => {

react/src/components/ServiceValidationView.tsx

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { baiSignedRequestWithPromise, compareNumberWithUnits } from '../helper';
1+
import {
2+
SSEEventHandlerTypes,
3+
baiSignedRequestWithPromise,
4+
compareNumberWithUnits,
5+
listenToBackgroundTask,
6+
} from '../helper';
27
import { useCurrentDomainValue, useSuspendedBackendaiClient } from '../hooks';
38
import { useTanMutation } from '../hooks/reactQueryAlias';
49
import Flex from './Flex';
@@ -19,6 +24,12 @@ import { useTranslation } from 'react-i18next';
1924
interface ServiceValidationModalProps {
2025
serviceData: any;
2126
}
27+
type BackgroundTaskEvent = {
28+
task_id: string;
29+
message: { event: string; session_id: string };
30+
current_progress: number;
31+
total_progress: number;
32+
};
2233

2334
const ServiceValidationView: React.FC<ServiceValidationModalProps> = ({
2435
serviceData,
@@ -126,58 +137,53 @@ const ServiceValidationView: React.FC<ServiceValidationModalProps> = ({
126137

127138
const validationDateTime = dayjs().format('LLL');
128139

129-
let sse: EventSource | undefined = undefined;
130-
131140
mutationsToValidateService
132141
.mutateAsync(serviceData)
133-
.then((data: any) => {
142+
.then((response: any) => {
134143
setValidationTime(validationDateTime);
135-
// setContainerLogSummary('loading...');
136-
const response = data;
137-
sse = baiClient.maintenance.attach_background_task(response['task_id']);
138144

139-
// TODO:
140145
const timeoutId = setTimeout(() => {
141-
sse?.close();
142-
// something went wrong during validation
143146
setValidationStatus('error');
144147
message.error(t('modelService.CannotValidateNow'));
145148
}, 10000);
146149

147-
sse?.addEventListener('bgtask_updated', async (e) => {
148-
const data = JSON.parse(e['data']);
149-
const msg = JSON.parse(data.message);
150-
if (['session_started', 'session_terminated'].includes(msg.event)) {
151-
const logs = await getLogs(msg.session_id);
152-
setContainerLogSummary(logs);
153-
clearTimeout(timeoutId);
154-
// temporally close sse manually when session is terminated
155-
if (msg.event === 'session_terminated') {
156-
sse?.close();
157-
return;
150+
const SSEEventHandlers: SSEEventHandlerTypes<BackgroundTaskEvent> = {
151+
onUpdated: async (data, controller) => {
152+
const msg = data.message;
153+
if (validationStatus === 'error') {
154+
clearTimeout(timeoutId);
155+
controller?.abort();
156+
} else if (
157+
['session_started', 'session_terminated'].includes(msg.event)
158+
) {
159+
const logs = await getLogs(msg.session_id);
160+
setContainerLogSummary(logs);
161+
clearTimeout(timeoutId);
162+
controller?.abort();
158163
}
159-
}
160-
setValidationStatus('processing');
161-
});
162-
sse?.addEventListener('bgtask_done', async (e) => {
163-
setValidationStatus('finished');
164-
clearTimeout(timeoutId);
165-
sse?.close();
166-
});
167-
sse?.addEventListener('bgtask_failed', async (e) => {
168-
const data = JSON.parse(e['data']);
169-
const msg = JSON.parse(data.message);
170-
const logs = await getLogs(msg.session_id);
171-
setContainerLogSummary(logs);
172-
setValidationStatus('error');
173-
sse?.close();
174-
throw new Error(e['data']);
175-
});
176-
sse?.addEventListener('bgtask_cancelled', async (e) => {
177-
setValidationStatus('error');
178-
sse?.close();
179-
throw new Error(e['data']);
180-
});
164+
setValidationStatus('processing');
165+
},
166+
onDone: (data) => {
167+
setValidationStatus('finished');
168+
clearTimeout(timeoutId);
169+
},
170+
onFailed: async (data) => {
171+
const logs = await getLogs(data.message.session_id);
172+
setContainerLogSummary(logs);
173+
setValidationStatus('error');
174+
throw new Error(data.message.event);
175+
},
176+
onTaskCancelled: (data) => {
177+
setValidationStatus('error');
178+
throw new Error(data.message.event);
179+
},
180+
onTaskFailed: (data) => {
181+
setValidationStatus('error');
182+
throw new Error(data.message.event);
183+
},
184+
};
185+
186+
listenToBackgroundTask(response['task_id'], SSEEventHandlers);
181187
})
182188
.catch((error) => {
183189
message.error(
@@ -190,11 +196,6 @@ const ServiceValidationView: React.FC<ServiceValidationModalProps> = ({
190196
isRunningRef.current = false;
191197
});
192198
isRunningRef.current = true;
193-
194-
return () => {
195-
sse?.close();
196-
};
197-
198199
// eslint-disable-next-line react-hooks/exhaustive-deps
199200
}, []);
200201

react/src/helper/customThemeConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ type LogoConfig = {
1717
width?: number;
1818
height?: number;
1919
};
20+
aboutModalSize?: {
21+
width?: number;
22+
height?: number;
23+
};
2024
};
2125
type SiderConfig = {
2226
theme?: 'light' | 'dark';

react/src/helper/index.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Image } from '../components/ImageEnvironmentSelectFormItems';
33
import { EnvironmentImage } from '../components/ImageList';
44
import { useSuspendedBackendaiClient } from '../hooks';
55
import { AttachmentsProps } from '@ant-design/x';
6+
import { fetchEventSource } from '@microsoft/fetch-event-source';
67
import { SorterResult } from 'antd/es/table/interface';
78
import Big from 'big.js';
89
import dayjs from 'dayjs';
@@ -610,3 +611,88 @@ export function getOS() {
610611
if (_.includes(userAgent, 'linux')) return 'Linux';
611612
return 'Linux';
612613
}
614+
615+
type SSEHandlerKeys =
616+
| 'onUpdated'
617+
| 'onDone'
618+
| 'onFailed'
619+
| 'onTaskFailed'
620+
| 'onTaskCancelled';
621+
622+
export type SSEEventHandlerTypes<
623+
BaseType = unknown,
624+
Overrides extends Partial<Record<SSEHandlerKeys, any>> = {},
625+
> = {
626+
[K in SSEHandlerKeys]: (
627+
data: K extends keyof Overrides ? Overrides[K] : BaseType,
628+
controller?: AbortController,
629+
) => void;
630+
};
631+
/**
632+
* Listens to background task events using Server-Sent Events (SSE).
633+
* @param taskID
634+
* @param handlers
635+
* @returns
636+
*/
637+
export function listenToBackgroundTask<
638+
BaseType = unknown,
639+
Overrides extends Partial<Record<SSEHandlerKeys, any>> = {},
640+
>(
641+
taskID: string,
642+
handlers: Partial<SSEEventHandlerTypes<BaseType, Overrides>>,
643+
): () => void {
644+
const controller = new AbortController();
645+
const signal = controller.signal;
646+
647+
const searchParams = new URLSearchParams({ task_id: taskID });
648+
const reqUrl = `/events/background-task?${searchParams.toString()}`;
649+
650+
// @ts-ignore
651+
const req = globalThis.backendaiclient?.newSignedRequest('GET', reqUrl, null);
652+
653+
if (!req) {
654+
throw new Error('Failed to create request for background task events');
655+
}
656+
657+
fetchEventSource(req.uri, {
658+
signal,
659+
credentials: 'include',
660+
openWhenHidden: true,
661+
headers: {
662+
'x-backendai-sessionid':
663+
localStorage.getItem('backendaiwebui.sessionid') || '',
664+
},
665+
onmessage: (event) => {
666+
const data = JSON.parse(event.data);
667+
668+
switch (event.event) {
669+
case 'bgtask_updated':
670+
handlers.onUpdated?.(data, controller);
671+
break;
672+
case 'bgtask_done':
673+
handlers.onDone?.(data, controller);
674+
controller.abort();
675+
break;
676+
case 'bgtask_failed':
677+
handlers.onTaskFailed?.(data, controller);
678+
controller.abort();
679+
break;
680+
case 'task_failed':
681+
handlers.onFailed?.(data, controller);
682+
controller.abort();
683+
break;
684+
case 'bgtask_cancelled':
685+
handlers.onTaskCancelled?.(data, controller);
686+
controller.abort();
687+
break;
688+
}
689+
},
690+
onerror: (error) => {
691+
console.error('SSE error:', error);
692+
controller.abort();
693+
throw error;
694+
},
695+
});
696+
697+
return controller.abort.bind(controller);
698+
}

0 commit comments

Comments
 (0)