Skip to content

Commit bb55ac1

Browse files
feat(ui): add support for dropdown, heading, and readonly (#2996)
* add support for dropdown and heading * add comment * address feedback
1 parent a06929b commit bb55ac1

File tree

5 files changed

+567
-13
lines changed

5 files changed

+567
-13
lines changed

e2e/kots-release-install-v3/config.yaml

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,12 +268,9 @@ spec:
268268
required: true
269269
help_text: "Help text of a **_Checkbox_** config item type **_marked required_**"
270270

271-
# TODO: uncomment when we support dropdowns in the UI
272-
# - name: label_dropdown
273-
# type: label
274-
# title: |
275-
# ## Dropdown Items
276-
# ---
271+
- name: heading_dropdown
272+
type: heading
273+
title: Dropdown Configuration
277274

278275
- name: dropdown_simple
279276
title: Simple Dropdown
@@ -306,7 +303,35 @@ spec:
306303
title: Option 1
307304
- name: dropdown_required_opt2
308305
title: Option 2
309-
default: dropdown_required_opt1 # temporarily set a default to get past the required validation until we support dropdowns in the UI
306+
307+
- name: heading_selectone
308+
type: heading
309+
title: Select One Configuration
310+
311+
- name: selectone_simple
312+
title: Simple Select One (renders as radio)
313+
type: select_one
314+
help_text: "Help text of a simple **_Select One_** config item type (backward compatible with radio)"
315+
items:
316+
- name: selectone_simple_opt1
317+
title: Option 1
318+
- name: selectone_simple_opt2
319+
title: Option 2
320+
321+
- name: selectone_with_default
322+
title: Select One with default value
323+
type: select_one
324+
help_text: "Help text of a **_Select One_** config item type **_with default value_**"
325+
items:
326+
- name: selectone_with_default_opt1
327+
title: Option 1
328+
- name: selectone_with_default_opt2
329+
title: Option 2
330+
default: selectone_with_default_opt2
331+
332+
- name: heading_radio
333+
type: heading
334+
title: Radio Configuration
310335

311336
- name: label_radio
312337
type: label
@@ -370,6 +395,55 @@ spec:
370395
required: true
371396
help_text: "Help text of a **_File_** config item type **_marked required_**"
372397

398+
- name: heading_readonly
399+
type: heading
400+
title: ReadOnly Configuration
401+
402+
- name: readonly_text
403+
title: ReadOnly Text Field
404+
type: text
405+
value: "This value cannot be changed"
406+
readonly: true
407+
help_text: "This is a **_readonly text field_** - you cannot edit it"
408+
409+
- name: readonly_textarea
410+
title: ReadOnly Textarea
411+
type: textarea
412+
value: "This is a readonly textarea.\nIt has multiple lines.\nBut you cannot edit it."
413+
readonly: true
414+
help_text: "This is a **_readonly textarea_**"
415+
416+
- name: readonly_checkbox
417+
title: ReadOnly Checkbox
418+
type: bool
419+
value: "1"
420+
readonly: true
421+
help_text: "This checkbox is checked and **_readonly_**"
422+
423+
- name: readonly_radio
424+
title: ReadOnly Radio
425+
type: radio
426+
value: radio_readonly_opt2
427+
readonly: true
428+
help_text: "This radio group is **_readonly_** with option 2 selected"
429+
items:
430+
- name: radio_readonly_opt1
431+
title: Option 1
432+
- name: radio_readonly_opt2
433+
title: Option 2
434+
435+
- name: readonly_dropdown
436+
title: ReadOnly Dropdown
437+
type: dropdown
438+
value: dropdown_readonly_opt2
439+
readonly: true
440+
help_text: "This dropdown is **_readonly_** with Option 2 selected"
441+
items:
442+
- name: dropdown_readonly_opt1
443+
title: Option 1
444+
- name: dropdown_readonly_opt2
445+
title: Option 2
446+
373447
- name: hidden_required
374448
title: Hidden required
375449
type: text

web/src/components/common/Select.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import { useSettings } from "../../contexts/SettingsContext";
3+
import HelpText from "./HelpText";
34

45
interface SelectOption {
56
value: string;
@@ -16,13 +17,14 @@ interface SelectProps {
1617
disabled?: boolean;
1718
error?: string;
1819
helpText?: string;
20+
defaultValue?: string;
1921
className?: string;
2022
labelClassName?: string;
2123
placeholder?: string;
2224
dataTestId?: string;
2325
}
2426

25-
const Select: React.FC<SelectProps> = ({
27+
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(({
2628
id,
2729
label,
2830
value,
@@ -32,11 +34,12 @@ const Select: React.FC<SelectProps> = ({
3234
disabled = false,
3335
error,
3436
helpText,
37+
defaultValue,
3538
className = "",
3639
labelClassName = "",
3740
placeholder,
3841
dataTestId,
39-
}) => {
42+
}, ref) => {
4043
const { settings } = useSettings();
4144
const themeColor = settings.themeColor;
4245

@@ -47,6 +50,7 @@ const Select: React.FC<SelectProps> = ({
4750
{required && <span className="text-red-500 ml-1">*</span>}
4851
</label>
4952
<select
53+
ref={ref}
5054
id={id}
5155
value={value}
5256
onChange={onChange}
@@ -75,9 +79,11 @@ const Select: React.FC<SelectProps> = ({
7579
))}
7680
</select>
7781
{error && <p className="mt-1 text-sm text-red-500">{error}</p>}
78-
{helpText && !error && <p className="mt-1 text-sm text-gray-500">{helpText}</p>}
82+
<HelpText helpText={helpText} defaultValue={defaultValue} error={error} />
7983
</div>
8084
);
81-
};
85+
});
86+
87+
Select.displayName = 'Select';
8288

8389
export default Select;

web/src/components/wizard/config/ConfigurationStep.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Input from '../../common/Input';
66
import Textarea from '../../common/Textarea';
77
import Checkbox from '../../common/Checkbox';
88
import Radio from '../../common/Radio';
9+
import Select from '../../common/Select';
910
import Label from '../../common/Label';
1011
import Markdown from '../../common/Markdown';
1112
import FileInput from '../../common/file';
@@ -304,16 +305,21 @@ const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ onNext }) => {
304305
updateConfigValue(itemName, value, filename);
305306
};
306307

308+
const handleDropdownChange = (itemName: string, e: React.ChangeEvent<HTMLSelectElement>) => {
309+
updateConfigValue(itemName, e.target.value);
310+
};
311+
307312
const renderConfigItem = (item: AppConfigItem) => {
308-
// Display item validation errors with priority over initial config errors
313+
// Display item validation errors with priority over initial config errors
309314
const displayError = itemErrors[item.name] || item.error;
310-
315+
311316
const sharedProps = {
312317
id: item.name,
313318
label: item.title,
314319
helpText: item.help_text,
315320
error: displayError,
316321
required: item.required,
322+
disabled: item.readonly,
317323
ref: setRef(item.name),
318324
}
319325

@@ -369,6 +375,7 @@ const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ onNext }) => {
369375
);
370376

371377
case 'radio':
378+
case 'select_one': // select_one renders as radio for backward compatibility
372379
if (item.items) {
373380
return (
374381
<Radio
@@ -394,6 +401,41 @@ const ConfigurationStep: React.FC<ConfigurationStepProps> = ({ onNext }) => {
394401
/>
395402
);
396403

404+
case 'dropdown':
405+
if (item.items) {
406+
const options = item.items.map(child => ({
407+
value: child.name,
408+
label: child.title
409+
}));
410+
return (
411+
<Select
412+
{...sharedProps}
413+
defaultValue={item.default}
414+
value={getEffectiveValue(item)}
415+
options={options}
416+
placeholder="Select an option"
417+
onChange={(e) => handleDropdownChange(item.name, e)}
418+
dataTestId={`dropdown-input-${item.name}`}
419+
className="w-96"
420+
/>
421+
);
422+
}
423+
return null;
424+
425+
case 'heading':
426+
return (
427+
<div className="pt-4 pb-2 border-b border-gray-200" role="group">
428+
<h3
429+
className="text-lg font-semibold text-gray-900"
430+
data-testid={`heading-${item.name}`}
431+
role="heading"
432+
aria-level={3}
433+
>
434+
{item.title}
435+
</h3>
436+
</div>
437+
);
438+
397439
case 'label':
398440
return (
399441
<Label

0 commit comments

Comments
 (0)