Skip to content

Commit 5978a5d

Browse files
committed
feat: add model folder picker actions in deployment add-revision modal
- Add Open/Create/Refresh buttons (Space.Compact) next to the model folder select in both Preset and Custom modes. - Mount a shared FolderCreateModalV2 (initialValues usage_mode='model'), and auto-select the newly created folder after creation. The mutation returns a VFolder global id; re-encode to a VirtualFolderNode global id so BAIVFolderSelect option matching succeeds. - BAIVFolderSelect: hide the trailing '(id)' on the selected label while the user is typing a search query, since the search input visually collides with the parenthetical id.
1 parent 86bd35a commit 5978a5d

2 files changed

Lines changed: 172 additions & 34 deletions

File tree

packages/backend.ai-ui/src/components/fragments/BAIVFolderSelect.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -296,22 +296,24 @@ const BAIVFolderSelect: React.FC<BAIVFolderSelectProps> = ({
296296
) : (
297297
<>
298298
{label}
299-
<BAIText type="secondary">
300-
&nbsp; (
301-
<BAIText
302-
ellipsis
303-
type="secondary"
304-
style={{
305-
width: 80,
306-
}}
307-
monospace
308-
>
309-
{valuePropName === 'id'
310-
? toLocalId(_.toString(value))
311-
: _.toString(value)}
299+
{!optimisticSearchStr && (
300+
<BAIText type="secondary">
301+
&nbsp; (
302+
<BAIText
303+
ellipsis
304+
type="secondary"
305+
style={{
306+
width: 80,
307+
}}
308+
monospace
309+
>
310+
{valuePropName === 'id'
311+
? toLocalId(_.toString(value))
312+
: _.toString(value)}
313+
</BAIText>
314+
)
312315
</BAIText>
313-
)
314-
</BAIText>
316+
)}
315317
</>
316318
);
317319
}}

react/src/components/DeploymentAddRevisionModal.tsx

Lines changed: 155 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import {
3232
} from '../hooks/useRuntimeParameterSchema';
3333
import DeploymentPresetDetailModal from './DeploymentPresetDetailModal';
3434
import EnvVarFormList, { type EnvVarFormListValue } from './EnvVarFormList';
35+
import FolderCreateModalV2 from './FolderCreateModalV2';
36+
import { useFolderExplorerOpener } from './FolderExplorerOpener';
3537
import ImageEnvironmentSelectFormItems, {
3638
type ImageEnvironmentFormInput,
3739
} from './ImageEnvironmentSelectFormItems';
@@ -46,7 +48,7 @@ import ResourceAllocationFormItems, {
4648
import VFolderTableFormItem, {
4749
type VFolderTableFormValues,
4850
} from './VFolderTableFormItem';
49-
import { InfoCircleOutlined } from '@ant-design/icons';
51+
import { InfoCircleOutlined, ReloadOutlined } from '@ant-design/icons';
5052
import {
5153
Alert,
5254
App,
@@ -73,15 +75,18 @@ import {
7375
BAIModalProps,
7476
BAIRuntimeVariantSelect,
7577
BAIVFolderSelect,
78+
BAIVFolderSelectRef,
7679
convertToUUID,
7780
safeDecodeUuid,
7881
toGlobalId,
7982
toLocalId,
8083
useBAILogger,
8184
} from 'backend.ai-ui';
8285
import * as _ from 'lodash-es';
86+
import { FolderOpenIcon, PlusIcon } from 'lucide-react';
8387
import React, {
8488
Suspense,
89+
startTransition,
8590
useDeferredValue,
8691
useEffect,
8792
useEffectEvent,
@@ -187,6 +192,15 @@ const DeploymentAddRevisionModal: React.FC<DeploymentAddRevisionModalProps> = ({
187192
// (ServiceLauncherPageContent, ModelCardDeployModal).
188193
const { id: currentProjectId } = useCurrentProjectValue();
189194
const { logger } = useBAILogger();
195+
const { open: openFolderExplorer } = useFolderExplorerOpener();
196+
197+
// Refs to refetch each form's model folder select after creating a new
198+
// model-usage folder, or via the manual refresh button. Two refs because
199+
// the Preset and Custom forms each mount their own BAIVFolderSelect.
200+
const presetVFolderSelectRef = useRef<BAIVFolderSelectRef>(null);
201+
const customVFolderSelectRef = useRef<BAIVFolderSelectRef>(null);
202+
const [isModelFolderCreateModalOpen, setIsModelFolderCreateModalOpen] =
203+
useState(false);
190204

191205
// Defer `open` so the lazy query only fires once the modal has actually
192206
// committed to opening. `loading={deferredOpen !== open}` then lets the
@@ -1272,18 +1286,64 @@ const DeploymentAddRevisionModal: React.FC<DeploymentAddRevisionModalProps> = ({
12721286
</Form.Item>
12731287

12741288
<Form.Item
1275-
name="modelFolderId"
12761289
label={t('deployment.ModelFolder')}
12771290
tooltip={t('deployment.ModelFolderTooltip')}
1278-
rules={[{ required: true }]}
1291+
required
12791292
>
1280-
<BAIVFolderSelect
1281-
currentProjectId={currentProjectId ?? undefined}
1282-
disabled={!currentProjectId}
1283-
excludeDeleted
1284-
filter='usage_mode == "model"'
1285-
style={{ width: '100%' }}
1286-
/>
1293+
<BAIFlex direction="row" gap="xs">
1294+
<Form.Item
1295+
name="modelFolderId"
1296+
noStyle
1297+
rules={[{ required: true }]}
1298+
>
1299+
<BAIVFolderSelect
1300+
ref={presetVFolderSelectRef}
1301+
currentProjectId={currentProjectId ?? undefined}
1302+
disabled={!currentProjectId}
1303+
excludeDeleted
1304+
filter='usage_mode == "model"'
1305+
style={{ flex: 1 }}
1306+
/>
1307+
</Form.Item>
1308+
<Form.Item dependencies={['modelFolderId']} noStyle>
1309+
{({ getFieldValue }: FormInstance<PresetFormValues>) => {
1310+
const modelFolderId = getFieldValue('modelFolderId');
1311+
return (
1312+
<Space.Compact>
1313+
<Tooltip title={t('modelService.OpenFolder')}>
1314+
<Button
1315+
icon={<FolderOpenIcon />}
1316+
disabled={!modelFolderId}
1317+
onClick={() => {
1318+
if (modelFolderId) {
1319+
openFolderExplorer(toLocalId(modelFolderId));
1320+
}
1321+
}}
1322+
/>
1323+
</Tooltip>
1324+
<Tooltip title={t('data.CreateANewStorageFolder')}>
1325+
<Button
1326+
icon={<PlusIcon />}
1327+
onClick={() =>
1328+
setIsModelFolderCreateModalOpen(true)
1329+
}
1330+
/>
1331+
</Tooltip>
1332+
<Tooltip title={t('button.Refresh')}>
1333+
<Button
1334+
icon={<ReloadOutlined />}
1335+
onClick={() => {
1336+
startTransition(() => {
1337+
presetVFolderSelectRef.current?.refetch();
1338+
});
1339+
}}
1340+
/>
1341+
</Tooltip>
1342+
</Space.Compact>
1343+
);
1344+
}}
1345+
</Form.Item>
1346+
</BAIFlex>
12871347
</Form.Item>
12881348
</Form>
12891349
)
@@ -1329,18 +1389,62 @@ const DeploymentAddRevisionModal: React.FC<DeploymentAddRevisionModalProps> = ({
13291389

13301390
<SectionHeader>{t('deployment.step.ModelAndRuntime')}</SectionHeader>
13311391
<Form.Item
1332-
name="modelFolderId"
13331392
label={t('deployment.ModelFolder')}
13341393
tooltip={t('deployment.ModelFolderTooltip')}
1335-
rules={[{ required: true }]}
1394+
required
13361395
>
1337-
<BAIVFolderSelect
1338-
currentProjectId={currentProjectId ?? undefined}
1339-
disabled={!currentProjectId}
1340-
excludeDeleted
1341-
filter='usage_mode == "model"'
1342-
style={{ width: '100%' }}
1343-
/>
1396+
<BAIFlex direction="row" gap="xs">
1397+
<Form.Item
1398+
name="modelFolderId"
1399+
noStyle
1400+
rules={[{ required: true }]}
1401+
>
1402+
<BAIVFolderSelect
1403+
ref={customVFolderSelectRef}
1404+
currentProjectId={currentProjectId ?? undefined}
1405+
disabled={!currentProjectId}
1406+
excludeDeleted
1407+
filter='usage_mode == "model"'
1408+
style={{ flex: 1 }}
1409+
/>
1410+
</Form.Item>
1411+
<Form.Item dependencies={['modelFolderId']} noStyle>
1412+
{({ getFieldValue }: FormInstance<FormValues>) => {
1413+
const modelFolderId = getFieldValue('modelFolderId');
1414+
return (
1415+
<Space.Compact>
1416+
<Tooltip title={t('modelService.OpenFolder')}>
1417+
<Button
1418+
icon={<FolderOpenIcon />}
1419+
disabled={!modelFolderId}
1420+
onClick={() => {
1421+
if (modelFolderId) {
1422+
openFolderExplorer(toLocalId(modelFolderId));
1423+
}
1424+
}}
1425+
/>
1426+
</Tooltip>
1427+
<Tooltip title={t('data.CreateANewStorageFolder')}>
1428+
<Button
1429+
icon={<PlusIcon />}
1430+
onClick={() => setIsModelFolderCreateModalOpen(true)}
1431+
/>
1432+
</Tooltip>
1433+
<Tooltip title={t('button.Refresh')}>
1434+
<Button
1435+
icon={<ReloadOutlined />}
1436+
onClick={() => {
1437+
startTransition(() => {
1438+
customVFolderSelectRef.current?.refetch();
1439+
});
1440+
}}
1441+
/>
1442+
</Tooltip>
1443+
</Space.Compact>
1444+
);
1445+
}}
1446+
</Form.Item>
1447+
</BAIFlex>
13441448
</Form.Item>
13451449
<Form.Item
13461450
name="runtimeVariantId"
@@ -1630,6 +1734,38 @@ const DeploymentAddRevisionModal: React.FC<DeploymentAddRevisionModalProps> = ({
16301734
/>
16311735
</Suspense>
16321736
)}
1737+
<FolderCreateModalV2
1738+
open={isModelFolderCreateModalOpen}
1739+
initialValues={{ usage_mode: 'model' }}
1740+
onRequestClose={(result) => {
1741+
setIsModelFolderCreateModalOpen(false);
1742+
if (result?.id) {
1743+
// `createVfolderV2` returns a `VFolder` (Strawberry) global ID,
1744+
// but BAIVFolderSelect's value query reads from `vfolder_nodes`
1745+
// (`VirtualFolderNode`, Graphene). Both encode the same UUID
1746+
// but with different `__typename:` prefixes, so the select's
1747+
// option matching (`edge.node.id === value`) would fail if we
1748+
// set the raw mutation id directly. Re-encode to the
1749+
// VirtualFolderNode global ID form.
1750+
const newFolderUuid = safeDecodeUuid(result.id);
1751+
if (!newFolderUuid) return;
1752+
const newFolderGlobalId = toGlobalId(
1753+
'VirtualFolderNode',
1754+
newFolderUuid,
1755+
);
1756+
const activeForm =
1757+
effectiveMode === 'preset' ? presetForm : customForm;
1758+
const activeRef =
1759+
effectiveMode === 'preset'
1760+
? presetVFolderSelectRef
1761+
: customVFolderSelectRef;
1762+
activeForm.setFieldValue('modelFolderId', newFolderGlobalId);
1763+
startTransition(() => {
1764+
activeRef.current?.refetch();
1765+
});
1766+
}
1767+
}}
1768+
/>
16331769
</BAIModal>
16341770
);
16351771
};

0 commit comments

Comments
 (0)