Skip to content

Commit 981f8e8

Browse files
committed
feat(FR-2547): add reset button to runtime parameter form section (#6733)
Resolves #6734 ([FR-2547](https://lablup.atlassian.net/browse/FR-2547)) > [!CAUTION] > The i18n for runtime parameter isn't included. Because it renders dynamically using server data. I'll update it after server supported. ## Changes - Convert runtime parameter form from card-based layout to collapsible section with "Optional" label - Add reset button with undo icon that restores all parameters to default values and clears touched state - Reset button is disabled when no parameters have been modified - Replace card tabs with standard Ant Design tabs for better visual hierarchy - Fix useEffect dependency to only reinitialize on runtime variant or groups changes, not on initial value changes - Update parameter cleanup logic to always strip managed keys during reset operations, ensuring proper cleanup of previously set values - Switch from BAICard to Collapse component for better space utilization [FR-2547]: https://lablup.atlassian.net/browse/FR-2547?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 03cfac4 commit 981f8e8

2 files changed

Lines changed: 106 additions & 49 deletions

File tree

react/src/components/RuntimeParameterFormSection.tsx

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,20 @@ import {
1212
buildEnvPresetKeySet,
1313
} from '../hooks/useRuntimeParameterSchema';
1414
import InputNumberWithSlider from './InputNumberWithSlider';
15-
import { Checkbox, Form, InputNumber, Select, Input, theme, Alert } from 'antd';
16-
import { BAICard, BAIFlex } from 'backend.ai-ui';
15+
import { UndoOutlined } from '@ant-design/icons';
16+
import {
17+
Checkbox,
18+
Collapse,
19+
Form,
20+
InputNumber,
21+
Select,
22+
Input,
23+
Tooltip,
24+
theme,
25+
Alert,
26+
Tabs,
27+
} from 'antd';
28+
import { BAIButton, BAIFlex } from 'backend.ai-ui';
1729
import React, { useCallback, useEffect, useEffectEvent, useState } from 'react';
1830
import { useTranslation } from 'react-i18next';
1931

@@ -141,9 +153,12 @@ const RuntimeParameterFormSection: React.FC<
141153
}
142154
});
143155

156+
// Initialize only when runtimeVariant or groups change — NOT when initialExtraArgs/initialEnvVars
157+
// change (e.g., due to Relay store updates after mutation). The initial* props are read inside
158+
// initializeValues via useEffectEvent, so they always reflect the latest closure.
144159
useEffect(() => {
145160
initializeValues();
146-
}, [runtimeVariant, initialExtraArgs, initialEnvVars, groups]);
161+
}, [runtimeVariant, groups]);
147162

148163
const handleParamChange = useCallback(
149164
(key: string, newValue: string) => {
@@ -165,6 +180,14 @@ const RuntimeParameterFormSection: React.FC<
165180
[onChange, onTouchedKeysChange],
166181
);
167182

183+
const handleReset = useCallback(() => {
184+
if (!groups) return;
185+
const defaults = buildDefaultsMap(groups);
186+
setValues(defaults);
187+
setTouchedKeys(new Set());
188+
onTouchedKeysChange?.(new Set());
189+
}, [groups, setValues, onTouchedKeysChange]);
190+
168191
if (!groups) return null;
169192

170193
// Build tab list from available categories (dynamically from API)
@@ -178,32 +201,70 @@ const RuntimeParameterFormSection: React.FC<
178201
const effectiveActiveTab = availableCategories.includes(activeTab)
179202
? activeTab
180203
: (availableCategories[0] ?? '');
181-
const activeGroup = groups.find((g) => g.category === effectiveActiveTab);
182204

183205
return (
184-
<Form.Item label={t('modelService.RuntimeParamTitle')}>
185-
<BAICard
186-
size="small"
187-
tabList={tabList}
188-
activeTabKey={effectiveActiveTab}
189-
onTabChange={(key) => setActiveTab(key)}
190-
>
191-
<Alert
192-
type="warning"
193-
showIcon
194-
title={t('modelService.RuntimeParamUnchangedHint')}
195-
style={{ marginBottom: token.marginSM }}
196-
/>
197-
{activeGroup && (
198-
<ParameterGroupContent
199-
group={activeGroup}
200-
values={values}
201-
touchedKeys={touchedKeys}
202-
onParamChange={handleParamChange}
203-
/>
204-
)}
205-
</BAICard>
206-
</Form.Item>
206+
<Collapse
207+
size="small"
208+
defaultActiveKey={['runtime-params']}
209+
items={[
210+
{
211+
key: 'runtime-params',
212+
label: (
213+
<BAIFlex justify="between" align="center" style={{ flex: 1 }}>
214+
<span>
215+
{t('modelService.RuntimeParamTitle')}{' '}
216+
<span style={{ color: token.colorTextSecondary }}>
217+
({t('general.Optional')})
218+
</span>
219+
</span>
220+
<Tooltip title={t('button.Reset')}>
221+
<BAIButton
222+
type="link"
223+
size="small"
224+
icon={<UndoOutlined />}
225+
aria-label={t('button.Reset')}
226+
onClick={(e) => {
227+
e.stopPropagation();
228+
handleReset();
229+
}}
230+
disabled={touchedKeys.size === 0}
231+
/>
232+
</Tooltip>
233+
</BAIFlex>
234+
),
235+
children: (
236+
<>
237+
<Alert
238+
type="warning"
239+
showIcon
240+
title={t('modelService.RuntimeParamUnchangedHint')}
241+
style={{ marginBottom: token.marginSM }}
242+
/>
243+
<Tabs
244+
size="small"
245+
activeKey={effectiveActiveTab}
246+
onChange={(key) => setActiveTab(key)}
247+
items={tabList.map((tab) => {
248+
const group = groups.find((g) => g.category === tab.key);
249+
return {
250+
key: tab.key,
251+
label: tab.label,
252+
children: group ? (
253+
<ParameterGroupContent
254+
group={group}
255+
values={values}
256+
touchedKeys={touchedKeys}
257+
onParamChange={handleParamChange}
258+
/>
259+
) : null,
260+
};
261+
})}
262+
/>
263+
</>
264+
),
265+
},
266+
]}
267+
/>
207268
);
208269
};
209270

react/src/components/ServiceLauncherPageContent.tsx

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -316,31 +316,27 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
316316
}
317317
}
318318

319-
// Strip managed ARGS keys from existing EXTRA_ARGS before merging
320-
// Only when there are touched ARGS values to merge
321-
if (Object.keys(argsValues).length > 0) {
322-
const argsSchemaKeys = buildArgsSchemaKeySet(groups);
323-
if (environ[extraArgsEnvVar] && argsSchemaKeys.size > 0) {
324-
const { unmappedText } = reverseMapExtraArgs(
325-
environ[extraArgsEnvVar],
326-
argsSchemaKeys,
327-
);
328-
if (unmappedText) {
329-
environ[extraArgsEnvVar] = unmappedText;
330-
} else {
331-
delete environ[extraArgsEnvVar];
332-
}
319+
// Always strip managed ARGS keys from existing EXTRA_ARGS.
320+
// This ensures reset (empty touchedKeys) properly cleans up previously set values.
321+
const argsSchemaKeys = buildArgsSchemaKeySet(groups);
322+
if (environ[extraArgsEnvVar] && argsSchemaKeys.size > 0) {
323+
const { unmappedText } = reverseMapExtraArgs(
324+
environ[extraArgsEnvVar],
325+
argsSchemaKeys,
326+
);
327+
if (unmappedText) {
328+
environ[extraArgsEnvVar] = unmappedText;
329+
} else {
330+
delete environ[extraArgsEnvVar];
333331
}
334332
}
335333

336-
// Strip only touched ENV-preset keys from environ before re-adding
337-
// (prevents erasing user-provided env vars that happen to share preset keys)
338-
const touchedEnvKeys = Object.keys(touchedValues).filter((key) => {
339-
const preset = presetMap.get(key);
340-
return preset?.presetTarget === 'ENV';
341-
});
342-
for (const envKey of touchedEnvKeys) {
343-
delete environ[envKey];
334+
// Always strip all ENV-preset keys from environ.
335+
// This ensures reset (empty touchedKeys) properly cleans up previously set values.
336+
for (const preset of presets) {
337+
if (preset.presetTarget === 'ENV') {
338+
delete environ[preset.key];
339+
}
344340
}
345341

346342
// Merge ARGS-type values into EXTRA_ARGS env var

0 commit comments

Comments
 (0)