Skip to content

Commit 91322cc

Browse files
committed
feat(FR-2888): add BAIRuntimeVariantSelect and remove runtimeVariants block from DeploymentAddRevisionModalQuery (#7400)
Resolves #7399 ([FR-2888](https://lablup.atlassian.net/browse/FR-2888)) ## Summary - Add `BAIRuntimeVariantSelect` to `backend.ai-ui` — self-paginated runtime variant select backed by Strawberry V2 `runtimeVariants(...)` + `runtimeVariant(id:)` point lookup, mirroring the `BAIAvailablePresetSelect` / `BAIProjectVfolderSelect` shape from FR-2886. Exposes an `onResolvedNamesChange(map)` callback so parents can maintain a tiny id→name map for branching logic without re-querying. - Migrate `DeploymentAddRevisionCustomContent` to consume `BAIRuntimeVariantSelect` in place of the inline antd `<Select>`. The "Load current revision" prefill reads the name directly from the fragment via a new `runtimeVariant { name }` selection on `modelRuntimeConfig` — no name-map round-trip needed at prefill time. - Drop the `runtimeVariants { edges { node { id name } } }` block from `DeploymentAddRevisionModalQuery` and the corresponding `runtimeVariantsData` prop pass from `DeploymentAddRevisionModal` to the Custom body — no remaining consumer needs the full preload. - Add `comp:BAIRuntimeVariantSelect.SelectRuntimeVariant` to all 21 BAI UI locale files (English + 20 translations). ## Test plan - [ ] Open the Add Revision modal → Custom mode: runtime variant picker paginates and searches server-side; selected value name resolves via the point lookup. - [ ] Click "Load current revision": form prefills with the saved revision's variant; `variantName === 'custom'` branching (command/file segmented, runtime parameter section visibility) behaves identically to pre-migration. - [ ] Switch Preset → Custom: the preset-transfer prefill still applies the variant id and the picker shows the resolved label. - [ ] `bash scripts/verify.sh` — Relay / Lint / Format PASS. TypeScript baseline (SessionLauncherPage / StatisticsPage / StorageHostSettingPage / UserCredentialsPage / UserSettingsPage / VFolderNodeListPage) unchanged. [FR-2888]: https://lablup.atlassian.net/browse/FR-2888?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent f6d3792 commit 91322cc

25 files changed

Lines changed: 385 additions & 34 deletions
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { BAIRuntimeVariantSelectPaginatedQuery } from '../../__generated__/BAIRuntimeVariantSelectPaginatedQuery.graphql';
2+
import { BAIRuntimeVariantSelectValueQuery } from '../../__generated__/BAIRuntimeVariantSelectValueQuery.graphql';
3+
import { convertToUUID, toLocalId } from '../../helper';
4+
import useDebouncedDeferredValue from '../../helper/useDebouncedDeferredValue';
5+
import { useFetchKey } from '../../hooks';
6+
import { useLazyPaginatedQuery } from '../../hooks/usePaginatedQuery';
7+
import BAISelect, { BAISelectProps } from '../BAISelect';
8+
import TotalFooter from '../TotalFooter';
9+
import { useControllableValue } from 'ahooks';
10+
import { GetRef, Skeleton } from 'antd';
11+
import * as _ from 'lodash-es';
12+
import {
13+
useDeferredValue,
14+
useEffect,
15+
useEffectEvent,
16+
useImperativeHandle,
17+
useOptimistic,
18+
useRef,
19+
useState,
20+
useTransition,
21+
} from 'react';
22+
import { useTranslation } from 'react-i18next';
23+
import { graphql, useLazyLoadQuery } from 'react-relay';
24+
25+
export type RuntimeVariantNode = NonNullable<
26+
NonNullable<
27+
BAIRuntimeVariantSelectPaginatedQuery['response']['runtimeVariants']
28+
>['edges'][number]
29+
>['node'];
30+
31+
export interface BAIRuntimeVariantSelectRef {
32+
refetch: () => void;
33+
}
34+
35+
export interface BAIRuntimeVariantSelectProps extends Omit<
36+
BAISelectProps,
37+
'options' | 'labelInValue' | 'ref'
38+
> {
39+
/**
40+
* Notifies the parent of resolved id→name pairs as the paginated list and
41+
* selected-value point lookup fan in. The parent typically merges these
42+
* into a local map so it can resolve the *currently selected* variant id
43+
* back to its name elsewhere in the form (e.g., for `variantName === 'custom'`
44+
* branching) without re-querying.
45+
*/
46+
onResolvedNamesChange?: (nameMap: Record<string, string>) => void;
47+
ref?: React.Ref<BAIRuntimeVariantSelectRef>;
48+
}
49+
50+
const BAIRuntimeVariantSelect: React.FC<BAIRuntimeVariantSelectProps> = ({
51+
loading,
52+
onResolvedNamesChange,
53+
ref,
54+
...selectProps
55+
}) => {
56+
'use memo';
57+
const { t } = useTranslation();
58+
const selectRef = useRef<GetRef<typeof BAISelect>>(null);
59+
const [controllableValue, setControllableValue] = useControllableValue<
60+
string | undefined
61+
>(selectProps);
62+
const [controllableOpen, setControllableOpen] = useControllableValue<boolean>(
63+
selectProps,
64+
{
65+
valuePropName: 'open',
66+
trigger: 'onOpenChange',
67+
defaultValuePropName: 'defaultOpen',
68+
},
69+
);
70+
const deferredOpen = useDeferredValue(controllableOpen);
71+
const [searchStr, setSearchStr] = useState<string>();
72+
const deferredSearchStr = useDebouncedDeferredValue(searchStr);
73+
const [optimisticSearchStr, setOptimisticSearchStr] =
74+
useOptimistic(searchStr);
75+
const [isPendingRefetch, startRefetchTransition] = useTransition();
76+
const [fetchKey, updateFetchKey] = useFetchKey();
77+
const deferredFetchKey = useDeferredValue(fetchKey);
78+
79+
const deferredControllableValue = useDeferredValue(controllableValue);
80+
81+
// Selected-value name lookup. `RuntimeVariantFilter` only exposes `name` —
82+
// no id filter — so we resolve the single selected variant via the
83+
// `runtimeVariant(id:)` point lookup. `@skip` collapses the request when
84+
// nothing is selected.
85+
const selectedUuid = deferredControllableValue
86+
? convertToUUID(_.toString(deferredControllableValue))
87+
: '';
88+
const { runtimeVariant: selectedVariant } =
89+
useLazyLoadQuery<BAIRuntimeVariantSelectValueQuery>(
90+
graphql`
91+
query BAIRuntimeVariantSelectValueQuery($id: UUID!, $skip: Boolean!) {
92+
runtimeVariant(id: $id) @skip(if: $skip) {
93+
id
94+
name
95+
}
96+
}
97+
`,
98+
{
99+
id: selectedUuid,
100+
skip: !selectedUuid,
101+
},
102+
{
103+
fetchPolicy: selectedUuid ? 'store-or-network' : 'store-only',
104+
fetchKey: deferredFetchKey,
105+
},
106+
);
107+
108+
const mergedFilter: NonNullable<
109+
BAIRuntimeVariantSelectPaginatedQuery['variables']['filter']
110+
> | null = deferredSearchStr
111+
? { name: { iContains: deferredSearchStr } }
112+
: null;
113+
114+
const { paginationData, result, loadNext, isLoadingNext } =
115+
useLazyPaginatedQuery<
116+
BAIRuntimeVariantSelectPaginatedQuery,
117+
RuntimeVariantNode
118+
>(
119+
graphql`
120+
query BAIRuntimeVariantSelectPaginatedQuery(
121+
$offset: Int!
122+
$limit: Int!
123+
$filter: RuntimeVariantFilter
124+
) {
125+
runtimeVariants(
126+
offset: $offset
127+
limit: $limit
128+
filter: $filter
129+
orderBy: [{ field: NAME, direction: "ASC" }]
130+
) {
131+
count
132+
edges {
133+
node {
134+
id
135+
name
136+
}
137+
}
138+
}
139+
}
140+
`,
141+
{ limit: 20 },
142+
{
143+
filter: mergedFilter,
144+
},
145+
{
146+
fetchPolicy: deferredOpen ? 'network-only' : 'store-only',
147+
fetchKey: deferredFetchKey,
148+
},
149+
{
150+
getTotal: (r) => r.runtimeVariants?.count ?? undefined,
151+
getItem: (r) => r.runtimeVariants?.edges?.map((edge) => edge?.node),
152+
getId: (item) => item?.id,
153+
},
154+
);
155+
156+
useImperativeHandle(
157+
ref,
158+
() => ({
159+
refetch: () => {
160+
startRefetchTransition(() => {
161+
updateFetchKey();
162+
});
163+
},
164+
}),
165+
[updateFetchKey, startRefetchTransition],
166+
);
167+
168+
// Notify parent of resolved id→name pairs. We feed *both* the currently
169+
// selected variant (from the point lookup) and the visible page (from the
170+
// paginated list), so callers get name resolution as soon as either lands.
171+
const notifyResolvedNames = useEffectEvent(() => {
172+
if (!onResolvedNamesChange) return;
173+
const nameMap: Record<string, string> = {};
174+
if (selectedVariant?.id && selectedVariant.name) {
175+
const uuid = toLocalId(selectedVariant.id);
176+
if (uuid) nameMap[uuid] = selectedVariant.name;
177+
}
178+
for (const node of paginationData ?? []) {
179+
if (node?.id && node.name) {
180+
const uuid = toLocalId(node.id);
181+
if (uuid) nameMap[uuid] = node.name;
182+
}
183+
}
184+
if (!_.isEmpty(nameMap)) onResolvedNamesChange(nameMap);
185+
});
186+
187+
useEffect(() => {
188+
notifyResolvedNames();
189+
}, [selectedVariant, paginationData]);
190+
191+
const availableOptions = _.map(paginationData, (item) => ({
192+
label: item?.name,
193+
value: item?.id ? toLocalId(item.id) : undefined,
194+
}));
195+
196+
const selectedLabel = selectedVariant?.name;
197+
const controllableValueWithLabel = deferredControllableValue
198+
? {
199+
label: selectedLabel ?? _.toString(deferredControllableValue),
200+
value: _.toString(deferredControllableValue),
201+
}
202+
: undefined;
203+
204+
const [optimisticValueWithLabel, setOptimisticValueWithLabel] = useState(
205+
controllableValueWithLabel,
206+
);
207+
208+
return (
209+
<BAISelect
210+
ref={selectRef}
211+
placeholder={t('comp:BAIRuntimeVariantSelect.SelectRuntimeVariant')}
212+
loading={
213+
loading ||
214+
controllableValue !== deferredControllableValue ||
215+
searchStr !== deferredSearchStr ||
216+
isPendingRefetch
217+
}
218+
{...selectProps}
219+
searchAction={async (value) => {
220+
setOptimisticSearchStr(value);
221+
setSearchStr(value);
222+
await selectProps.searchAction?.(value);
223+
}}
224+
showSearch={
225+
selectProps.showSearch === false
226+
? false
227+
: {
228+
searchValue: optimisticSearchStr,
229+
autoClearSearchValue: true,
230+
...(_.isObject(selectProps.showSearch)
231+
? _.omit(selectProps.showSearch, ['searchValue'])
232+
: {}),
233+
filterOption: false,
234+
}
235+
}
236+
value={
237+
controllableValue !== deferredControllableValue
238+
? optimisticValueWithLabel
239+
: controllableValueWithLabel
240+
}
241+
labelInValue
242+
onChange={(value, option) => {
243+
if (_.isUndefined(value) || _.isNull(value)) {
244+
setOptimisticValueWithLabel(undefined);
245+
setControllableValue(undefined, option);
246+
return;
247+
}
248+
const v = _.castArray(value)[0];
249+
const label = _.isString(v.label)
250+
? v.label
251+
: (availableOptions.find((opt) => opt.value === v.value)?.label ??
252+
_.toString(v.value));
253+
const next = { label, value: _.toString(v.value) };
254+
setOptimisticValueWithLabel(next);
255+
setControllableValue(next.value, option);
256+
}}
257+
options={availableOptions}
258+
endReached={() => {
259+
loadNext();
260+
}}
261+
open={controllableOpen}
262+
onOpenChange={setControllableOpen}
263+
notFoundContent={
264+
_.isUndefined(paginationData) ? (
265+
<Skeleton.Input active size="small" block />
266+
) : undefined
267+
}
268+
footer={
269+
_.isNumber(result.runtimeVariants?.count) &&
270+
result.runtimeVariants.count > 0 ? (
271+
<TotalFooter
272+
loading={isLoadingNext}
273+
total={result.runtimeVariants.count}
274+
/>
275+
) : undefined
276+
}
277+
/>
278+
);
279+
};
280+
281+
export default BAIRuntimeVariantSelect;

packages/backend.ai-ui/src/components/fragments/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ export type {
101101
BAIAvailablePresetSelectRef,
102102
DeploymentRevisionPresetNode,
103103
} from './BAIAvailablePresetSelect';
104+
export { default as BAIRuntimeVariantSelect } from './BAIRuntimeVariantSelect';
105+
export type {
106+
BAIRuntimeVariantSelectProps,
107+
BAIRuntimeVariantSelectRef,
108+
RuntimeVariantNode,
109+
} from './BAIRuntimeVariantSelect';
104110
export { default as BAIUserSelect } from './BAIUserSelect';
105111
export type {
106112
BAIUserSelectProps,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@
268268
"TrafficRatio": "Traffic-Anteil",
269269
"TrafficStatus": "Datenverkehrsstatus"
270270
},
271+
"comp:BAIRuntimeVariantSelect": {
272+
"SelectRuntimeVariant": "Laufzeitvariante auswählen"
273+
},
271274
"comp:BAISchedulingHistoryNodes": {
272275
"Attempts": "Versuche",
273276
"CreatedAt": "Erstellt am",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@
268268
"TrafficRatio": "Αναλογία κυκλοφορίας",
269269
"TrafficStatus": "Κατάσταση δικτύου"
270270
},
271+
"comp:BAIRuntimeVariantSelect": {
272+
"SelectRuntimeVariant": "Επιλογή παραλλαγής χρόνου εκτέλεσης"
273+
},
271274
"comp:BAISchedulingHistoryNodes": {
272275
"Attempts": "Προσπάθειες",
273276
"CreatedAt": "Δημιουργήθηκε στις",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@
274274
"TrafficRatio": "Traffic Ratio",
275275
"TrafficStatus": "Traffic Status"
276276
},
277+
"comp:BAIRuntimeVariantSelect": {
278+
"SelectRuntimeVariant": "Select Runtime Variant"
279+
},
277280
"comp:BAISchedulingHistoryNodes": {
278281
"Attempts": "Attempts",
279282
"CreatedAt": "Created At",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@
268268
"TrafficRatio": "Proporción de tráfico",
269269
"TrafficStatus": "Estado del tráfico"
270270
},
271+
"comp:BAIRuntimeVariantSelect": {
272+
"SelectRuntimeVariant": "Seleccionar variante de tiempo de ejecución"
273+
},
271274
"comp:BAISchedulingHistoryNodes": {
272275
"Attempts": "Intentos",
273276
"CreatedAt": "Creado el",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@
268268
"TrafficRatio": "Liikenteen suhde",
269269
"TrafficStatus": "Liikenteen tila"
270270
},
271+
"comp:BAIRuntimeVariantSelect": {
272+
"SelectRuntimeVariant": "Valitse ajonaikainen variantti"
273+
},
271274
"comp:BAISchedulingHistoryNodes": {
272275
"Attempts": "Yritykset",
273276
"CreatedAt": "Luotu",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@
268268
"TrafficRatio": "Ratio de trafic",
269269
"TrafficStatus": "État du trafic"
270270
},
271+
"comp:BAIRuntimeVariantSelect": {
272+
"SelectRuntimeVariant": "Sélectionner la variante d'exécution"
273+
},
271274
"comp:BAISchedulingHistoryNodes": {
272275
"Attempts": "Tentatives",
273276
"CreatedAt": "Créé le",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@
268268
"TrafficRatio": "Rasio Lalu Lintas",
269269
"TrafficStatus": "Status Trafik"
270270
},
271+
"comp:BAIRuntimeVariantSelect": {
272+
"SelectRuntimeVariant": "Pilih Varian Runtime"
273+
},
271274
"comp:BAISchedulingHistoryNodes": {
272275
"Attempts": "Percobaan",
273276
"CreatedAt": "Dibuat pada",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@
268268
"TrafficRatio": "Rapporto di traffico",
269269
"TrafficStatus": "Stato del traffico"
270270
},
271+
"comp:BAIRuntimeVariantSelect": {
272+
"SelectRuntimeVariant": "Seleziona variante runtime"
273+
},
271274
"comp:BAISchedulingHistoryNodes": {
272275
"Attempts": "Tentativi",
273276
"CreatedAt": "Creato il",

0 commit comments

Comments
 (0)