Skip to content

Commit 0904c32

Browse files
committed
Merge branch 'main' into 26.4
2 parents 040d427 + 6507cd9 commit 0904c32

146 files changed

Lines changed: 1095 additions & 570 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.stories.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ const meta: Meta<typeof BAIDeleteConfirmModal> = {
2020
**BAIDeleteConfirmModal** is a unified delete confirmation modal for table row deletion.
2121
2222
## Behavior
23-
- **Single item**: Simple confirm dialog with item name displayed. OK button is immediately enabled.
24-
- **Multiple items (2+)**: Requires typing confirmation text (localized "Delete") before OK is enabled.
25-
- **\`requireConfirmInput\`**: Forces text-input confirmation even for a single item.
23+
- **Single item**: Simple confirm dialog. OK button is immediately enabled.
24+
- **Single item + \`requireConfirmInput\`**: Requires typing the item name. Item list is hidden — the name already appears in the description.
25+
- **Multiple items (2+)**: Shows scrollable item list followed by a confirmation input requiring "Delete" to be typed.
2626
2727
## Key Features
2828
- Accepts \`React.ReactNode\` for item labels (icons, tags, custom rendering)
29-
- Scrollable item list for large selections
29+
- Scrollable item list for multi-item selections
3030
- \`extraContent\` slot for domain-specific additions (checkboxes, warnings)
31-
- Built on \`BAIConfirmModalWithInput\` (multi) and \`BAIModal\` (single)
31+
- Built on \`BAIModal\`
3232
`,
3333
},
3434
},
@@ -74,7 +74,7 @@ export const SingleItemWithInput: Story = {
7474
docs: {
7575
description: {
7676
story:
77-
'Single item with `requireConfirmInput={true}`. User must type the item name to confirm.',
77+
'Single item with `requireConfirmInput={true}`. Item list is hidden (name already appears in description). User must type the item name into the confirmation input to enable the Delete button.',
7878
},
7979
},
8080
},
@@ -106,7 +106,7 @@ export const MultipleItems: Story = {
106106
docs: {
107107
description: {
108108
story:
109-
'Multiple items require typing "Delete" to confirm. Shows scrollable item list.',
109+
'Multiple items require typing "Delete" to confirm. Shows scrollable item list above the confirmation input.',
110110
},
111111
},
112112
},

packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.tsx

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import BAIConfirmModalWithInput from './BAIConfirmModalWithInput';
21
import BAIFlex from './BAIFlex';
32
import BAIModal, { type BAIModalProps } from './BAIModal';
43
import BAIText from './BAIText';
54
import { ExclamationCircleFilled } from '@ant-design/icons';
6-
import { theme, Typography, type InputProps } from 'antd';
5+
import { Form, Input, theme, Typography, type InputProps } from 'antd';
76
import React from 'react';
87
import { Trans, useTranslation } from 'react-i18next';
98

@@ -39,7 +38,7 @@ export interface BAIDeleteConfirmModalProps extends Omit<
3938
inputLabel?: React.ReactNode;
4039
/** Additional props for the confirmation Input. */
4140
inputProps?: InputProps;
42-
/** Content rendered between the item list and the input field (e.g. checkboxes). */
41+
/** Content rendered between the input field and "cannot be undone" text (e.g. checkboxes). */
4342
extraContent?: React.ReactNode;
4443
/** Max height (px) of the scrollable item list. Default: 200. Set 0 for no limit. */
4544
itemListMaxHeight?: number;
@@ -71,6 +70,8 @@ const BAIDeleteConfirmModal: React.FC<BAIDeleteConfirmModalProps> = ({
7170

7271
const { t } = useTranslation();
7372
const { token } = theme.useToken();
73+
const [form] = Form.useForm();
74+
const typedText = Form.useWatch('confirmText', form) ?? '';
7475

7576
const needsInput = items.length > 1 || requireConfirmInput;
7677

@@ -101,6 +102,17 @@ const BAIDeleteConfirmModal: React.FC<BAIDeleteConfirmModalProps> = ({
101102
/>
102103
);
103104

105+
const modalTitle = (
106+
<BAIFlex direction="column" justify="start" align="start">
107+
<Text strong>
108+
<ExclamationCircleFilled
109+
style={{ color: token.colorWarning, marginRight: token.sizeXXS }}
110+
/>
111+
{resolvedTitle}
112+
</Text>
113+
</BAIFlex>
114+
);
115+
104116
const itemListContent =
105117
items.length > 0 ? (
106118
<div
@@ -125,49 +137,58 @@ const BAIDeleteConfirmModal: React.FC<BAIDeleteConfirmModalProps> = ({
125137
</div>
126138
) : null;
127139

128-
const bodyContent = (
129-
<BAIFlex direction="column" align="stretch" gap="xs">
130-
<Text>{resolvedDescription}</Text>
131-
{itemListContent}
132-
<Text type="danger">
133-
{t('comp:BAIDeleteConfirmModal.CannotBeUndone')}
134-
</Text>
135-
{extraContent}
136-
</BAIFlex>
137-
);
138-
139140
if (needsInput) {
140141
return (
141-
<BAIConfirmModalWithInput
142-
{...restModalProps}
142+
<BAIModal
143143
destroyOnHidden
144-
title={resolvedTitle}
145-
confirmText={resolvedConfirmText}
146-
content={bodyContent}
147-
inputLabel={resolvedInputLabel}
148-
inputProps={inputProps}
144+
{...restModalProps}
145+
title={modalTitle}
149146
okText={resolvedOkText}
150-
okButtonProps={okButtonProps}
151-
onOk={onOk}
152-
onCancel={onCancel}
153-
/>
147+
okButtonProps={{
148+
danger: true,
149+
disabled: typedText !== resolvedConfirmText,
150+
...okButtonProps,
151+
}}
152+
onOk={(e) => {
153+
form.resetFields();
154+
onOk?.(e);
155+
}}
156+
onCancel={(e) => {
157+
form.resetFields();
158+
onCancel?.(e);
159+
}}
160+
>
161+
<BAIFlex direction="column" align="stretch" gap="xs">
162+
<Text>{resolvedDescription}</Text>
163+
{items.length > 1 && itemListContent}
164+
<Form
165+
form={form}
166+
layout="vertical"
167+
requiredMark={false}
168+
preserve={false}
169+
>
170+
<Form.Item
171+
name="confirmText"
172+
label={resolvedInputLabel}
173+
style={{ marginBottom: 0 }}
174+
>
175+
<Input autoFocus autoComplete="off" allowClear {...inputProps} />
176+
</Form.Item>
177+
</Form>
178+
<Text type="danger">
179+
{t('comp:BAIDeleteConfirmModal.CannotBeUndone')}
180+
</Text>
181+
{extraContent}
182+
</BAIFlex>
183+
</BAIModal>
154184
);
155185
}
156186

157187
return (
158188
<BAIModal
159189
{...restModalProps}
160190
destroyOnHidden
161-
title={
162-
<BAIFlex direction="column" justify="start" align="start">
163-
<Text strong>
164-
<ExclamationCircleFilled
165-
style={{ color: token.colorWarning, marginRight: 5 }}
166-
/>
167-
{resolvedTitle}
168-
</Text>
169-
</BAIFlex>
170-
}
191+
title={modalTitle}
171192
okText={resolvedOkText}
172193
okButtonProps={{
173194
danger: true,
@@ -177,7 +198,14 @@ const BAIDeleteConfirmModal: React.FC<BAIDeleteConfirmModalProps> = ({
177198
onOk={onOk}
178199
onCancel={onCancel}
179200
>
180-
{bodyContent}
201+
<BAIFlex direction="column" align="stretch" gap="xs">
202+
<Text>{resolvedDescription}</Text>
203+
{itemListContent}
204+
<Text type="danger">
205+
{t('comp:BAIDeleteConfirmModal.CannotBeUndone')}
206+
</Text>
207+
{extraContent}
208+
</BAIFlex>
181209
</BAIModal>
182210
);
183211
};

packages/backend.ai-ui/src/components/BAIGraphQLPropertyFilter.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -475,9 +475,12 @@ const BAIGraphQLPropertyFilter: React.FC<BAIGraphQLPropertyFilterProps> = ({
475475
onChange: propOnChange,
476476
});
477477

478-
const [conditions, setConditions] = useState<FilterCondition[]>(() =>
479-
convertGraphQLFilterToConditions(value, filterProperties),
480-
);
478+
// Reassign sequential ids: the converter generates random ids per call,
479+
// which would change every render and remount every Tag.
480+
const conditions = convertGraphQLFilterToConditions(
481+
value,
482+
filterProperties,
483+
).map((c, i) => ({ ...c, id: `cond-${i}` }));
481484

482485
const [search, setSearch] = useState<string>('');
483486
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs | null>(null);
@@ -505,15 +508,11 @@ const BAIGraphQLPropertyFilter: React.FC<BAIGraphQLPropertyFilterProps> = ({
505508
const [isValid, setIsValid] = useState(true);
506509
const [isFocused, setIsFocused] = useState(false);
507510

508-
const propertyOptions = useMemo(
509-
() =>
510-
filterProperties.map((property) => ({
511-
label: property.propertyLabel,
512-
value: property.key,
513-
filter: property,
514-
})),
515-
[filterProperties],
516-
);
511+
const propertyOptions = filterProperties.map((property) => ({
512+
label: property.propertyLabel,
513+
value: property.key,
514+
filter: property,
515+
}));
517516

518517
const availableOperators = useMemo(() => {
519518
const mode = getEffectiveValueMode(selectedProperty);
@@ -536,7 +535,6 @@ const BAIGraphQLPropertyFilter: React.FC<BAIGraphQLPropertyFilterProps> = ({
536535
}, [availableOperators, t]);
537536

538537
const updateConditions = (newConditions: FilterCondition[]) => {
539-
setConditions(newConditions);
540538
const filter = convertConditionsToGraphQLFilter(
541539
newConditions,
542540
filterProperties,

packages/backend.ai-ui/src/locale/ko.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
"CannotBeUndone": "이 작업은 되돌릴 수 없습니다.",
9797
"DeleteItem": "삭제",
9898
"DeleteNItems": "{{count}}개 항목 삭제",
99-
"TypeToConfirm": "<code>{{confirmText}}</code>을(를) 입력하여 확인하세요."
99+
"TypeToConfirm": "삭제를 위해 <code>{{confirmText}}</code>을(를) 입력하세요."
100100
},
101101
"comp:BAIDeploymentSelect": {
102102
"SelectDeployment": "배포를 선택해주세요"

packages/eslint-config-bai/react.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ export const react = [
3636
},
3737
},
3838

39+
{
40+
files: ["**/*.ts", "**/*.tsx"],
41+
rules: {
42+
"no-restricted-syntax": [
43+
"warn",
44+
{
45+
selector: "ImportDeclaration[source.value=/^src\\u002F.+/]",
46+
message:
47+
"Use a relative import path instead of 'src/...'. Mixing absolute and relative paths in the same file is inconsistent; prefer relative paths.",
48+
},
49+
],
50+
},
51+
},
52+
3953
{
4054
files: ["**/*.tsx", "**/*.jsx"],
4155
rules: {

react/src/components/AdminModelCardSettingModal.tsx

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import {
1111
} from '../hooks/useCurrentProject';
1212
import FolderCreateModalV2 from './FolderCreateModalV2';
1313
import FolderLink from './FolderLink';
14-
import { shapes } from '@dicebear/collection';
15-
import { createAvatar } from '@dicebear/core';
14+
import VFolderNodeIdenticonV2 from './VFolderNodeIdenticonV2';
1615
import {
1716
App,
1817
Button,
@@ -23,7 +22,6 @@ import {
2322
Popconfirm,
2423
Select,
2524
Typography,
26-
theme,
2725
} from 'antd';
2826
import {
2927
BAIButton,
@@ -33,7 +31,6 @@ import {
3331
BAIVFolderSelect,
3432
BAIVFolderSelectRef,
3533
convertToUUID,
36-
mergeFilterValues,
3734
toGlobalId,
3835
toLocalId,
3936
useBAILogger,
@@ -83,7 +80,6 @@ const AdminModelCardSettingModal: React.FC<AdminModelCardSettingModalProps> = ({
8380
const { t } = useTranslation();
8481
const { message } = App.useApp();
8582
const { logger } = useBAILogger();
86-
const { token } = theme.useToken();
8783
const formRef = useRef<FormInstance<FormInputType>>(null);
8884
const vfolderSelectRef = useRef<BAIVFolderSelectRef>(null);
8985
const [isOpenCreateFolderModal, setIsOpenCreateFolderModal] = useState(false);
@@ -101,6 +97,7 @@ const AdminModelCardSettingModal: React.FC<AdminModelCardSettingModalProps> = ({
10197
metadata {
10298
name
10399
}
100+
...VFolderNodeIdenticonV2Fragment
104101
}
105102
domainName
106103
projectId
@@ -307,21 +304,11 @@ const AdminModelCardSettingModal: React.FC<AdminModelCardSettingModalProps> = ({
307304
{isEditMode ? (
308305
<Form.Item label={t('adminModelCard.ModelStorageFolder')}>
309306
<BAIFlex gap="xs" align="center">
310-
<img
311-
src={createAvatar(shapes, {
312-
seed: modelCard.vfolderId,
313-
shape3: [],
314-
})?.toDataUri()}
315-
alt="VFolder Identicon"
316-
style={{
317-
borderRadius: '0.25em',
318-
width: '1em',
319-
height: '1em',
320-
borderWidth: 0.5,
321-
borderStyle: 'solid',
322-
borderColor: token.colorBorder,
323-
}}
324-
/>
307+
{modelCard.vfolder && (
308+
<VFolderNodeIdenticonV2
309+
vfolderNodeIdenticonFrgmt={modelCard.vfolder}
310+
/>
311+
)}
325312
<FolderLink
326313
folderId={modelCard.vfolderId}
327314
folderName={
@@ -347,8 +334,8 @@ const AdminModelCardSettingModal: React.FC<AdminModelCardSettingModalProps> = ({
347334
<BAIVFolderSelect
348335
ref={vfolderSelectRef}
349336
excludeDeleted
350-
currentProjectId={currentProject.id ?? undefined}
351-
filter={mergeFilterValues(['ownership_type == "group"'])}
337+
filter='ownership_type == "group"'
338+
currentProjectId={modelStoreProject?.id ?? undefined}
352339
style={{ flex: 1 }}
353340
/>
354341
</Form.Item>

react/src/components/AgentDetailDrawer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
@license
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
5+
import { AgentDetailDrawerFragment$key } from '../__generated__/AgentDetailDrawerFragment.graphql';
56
import AgentDetailDrawerContent from './AgentDetailDrawerContent';
67
import { Drawer, type DrawerProps, Skeleton } from 'antd';
78
import { BAIFetchKeyButton, toLocalId, useBAILogger } from 'backend.ai-ui';
89
import { Suspense, useEffect, useEffectEvent, useTransition } from 'react';
910
import { useTranslation } from 'react-i18next';
1011
import { graphql, useMutation, useRefetchableFragment } from 'react-relay';
11-
import { AgentDetailDrawerFragment$key } from 'src/__generated__/AgentDetailDrawerFragment.graphql';
1212

1313
interface AgentDetailDrawerProps extends DrawerProps {
1414
onRequestClose?: () => void;

react/src/components/AgentDetailDrawerContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
@license
33
Copyright (c) 2015-2026 Lablup Inc. All rights reserved.
44
*/
5+
import { AgentDetailDrawerContentFragment$key } from '../__generated__/AgentDetailDrawerContentFragment.graphql';
6+
import { useSuspendedBackendaiClient } from '../hooks';
57
import AgentActionButtons from './AgentNodeItems/AgentActionButtons';
68
import AgentComputePlugins from './AgentNodeItems/AgentComputePlugins';
79
import AgentResources from './AgentNodeItems/AgentResources';
@@ -19,8 +21,6 @@ import * as _ from 'lodash-es';
1921
import { useState } from 'react';
2022
import { useTranslation } from 'react-i18next';
2123
import { graphql, useFragment } from 'react-relay';
22-
import { AgentDetailDrawerContentFragment$key } from 'src/__generated__/AgentDetailDrawerContentFragment.graphql';
23-
import { useSuspendedBackendaiClient } from 'src/hooks';
2424

2525
interface AgentDetailDrawerContentProps {
2626
agentNodeFrgmt?: AgentDetailDrawerContentFragment$key | null;

react/src/components/AgentList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
AgentListQuery$variables,
99
} from '../__generated__/AgentListQuery.graphql';
1010
import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions';
11+
import { useBAISettingUserState } from '../hooks/useBAISetting';
1112
import { useThemeMode } from '../hooks/useThemeMode';
1213
import AgentDetailDrawer from './AgentDetailDrawer';
1314
import BAIRadioGroup from './BAIRadioGroup';
@@ -32,7 +33,6 @@ import { parseAsString, useQueryStates } from 'nuqs';
3233
import React, { useState, useDeferredValue } from 'react';
3334
import { useTranslation } from 'react-i18next';
3435
import { graphql, useLazyLoadQuery } from 'react-relay';
35-
import { useBAISettingUserState } from 'src/hooks/useBAISetting';
3636

3737
type Agent = NonNullable<
3838
NonNullable<AgentListQuery$data['agent_nodes']>['edges'][number]

0 commit comments

Comments
 (0)