Skip to content

Commit b62a901

Browse files
committed
Wizard: use placeholder styling for profile selector in dark mode
The profile selector's "Select a profile" text appeared white in dark mode instead of grey like the policy selector's "Select a policy". The policy selector renders placeholder text directly inside MenuToggle, which PatternFly styles grey via pf-m-placeholder. The profile selector used a typeahead input whose ::placeholder pseudo-element doesn't inherit PatternFly's dark mode color. Render the placeholder as a plain text node with pf-m-placeholder when the dropdown is closed and no profile is selected. Switch to the typeahead input only when the dropdown is open or a value exists.
1 parent 0e153cd commit b62a901

6 files changed

Lines changed: 69 additions & 181 deletions

File tree

playwright/Customizations/FipsMode.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ test('FIPS switch toggles and persists through save', async ({
7171
.getByRole('radio', { name: 'Use a default OpenSCAP profile' })
7272
.check();
7373

74-
const typeToFilter = frame.getByRole('textbox', { name: 'Type to filter' });
75-
76-
await typeToFilter.fill('stig');
74+
await frame.getByTestId('profileSelect').click();
7775
await frame
7876
.getByRole('option', {
7977
name: /Red Hat STIG for Red Hat Enterprise Linux 10/i,

playwright/Customizations/OpenSCAP.spec.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,14 @@ test('Create a blueprint with OpenSCAP customization', async ({
6060
await expect(
6161
frame.getByText('WSL: customization is not supported'),
6262
).toBeVisible();
63-
await expect(
64-
frame.getByRole('textbox', { name: 'Type to filter' }),
65-
).toBeEnabled();
63+
await expect(frame.getByTestId('profileSelect')).toBeEnabled();
6664
});
6765

6866
await test.step('Select a CIS profile then switch to None', async () => {
6967
await frame
7068
.getByRole('radio', { name: 'Use a default OpenSCAP profile' })
7169
.click();
72-
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
70+
await frame.getByTestId('profileSelect').click();
7371
await frame
7472
.getByRole('option', {
7573
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server',
@@ -95,7 +93,7 @@ test('Create a blueprint with OpenSCAP customization', async ({
9593
.getByRole('radio', { name: 'Use a default OpenSCAP profile' })
9694
.click();
9795

98-
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
96+
await frame.getByTestId('profileSelect').click();
9997
await frame
10098
.getByRole('option', {
10199
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server',
@@ -147,14 +145,10 @@ test('Create a blueprint with OpenSCAP customization', async ({
147145
await test.step('Edit BP', async () => {
148146
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
149147
await frame.getByRole('button', { name: 'Base settings' }).click();
150-
await frame.getByRole('textbox', { name: 'Type to filter' }).click();
151-
await expect(
152-
frame.getByText(
153-
'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server',
154-
),
155-
).toBeVisible();
156-
await frame.getByRole('textbox', { name: 'Type to filter' }).clear();
157-
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
148+
await expect(frame.getByTestId('profileSelect')).toHaveTextContent(
149+
'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server',
150+
);
151+
await frame.getByTestId('profileSelect').click();
158152
await frame
159153
.getByRole('option', {
160154
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 2 - Server',

src/Components/CreateImageWizard/steps/Oscap/components/PolicySelector.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ const PolicySelector = ({ isDisabled = false }: PolicySelectorProps) => {
232232
<MenuToggle
233233
ouiaId='compliancePolicySelect'
234234
ref={toggleRef}
235+
isPlaceholder={!policyTitle && !isApplying}
235236
onClick={() => setIsOpen(!isOpen)}
236237
isExpanded={isOpen}
237238
isDisabled={isDisabled || isFetchingPolicies || isApplying}

src/Components/CreateImageWizard/steps/Oscap/components/ProfileSelector.tsx

Lines changed: 44 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import React, { useEffect, useState } from 'react';
22

33
import {
4-
Button,
54
FormGroup,
65
MenuToggle,
76
MenuToggleElement,
87
Select,
98
SelectList,
109
SelectOption,
1110
Spinner,
12-
TextInputGroup,
13-
TextInputGroupMain,
14-
TextInputGroupUtilities,
1511
} from '@patternfly/react-core';
16-
import { TimesIcon } from '@patternfly/react-icons';
1712

1813
import {
1914
DistributionProfileItem,
@@ -31,7 +26,6 @@ import {
3126
changeFscMode,
3227
clearKernelAppend,
3328
selectComplianceProfileID,
34-
selectComplianceType,
3529
selectDistribution,
3630
setOscapProfile,
3731
} from '@/store/slices/wizard';
@@ -68,21 +62,13 @@ const ProfileSelector = ({
6862
);
6963
const dispatch = useAppDispatch();
7064
const [isOpen, setIsOpen] = useState(false);
71-
const [inputValue, setInputValue] = useState<string>('');
72-
const [filterValue, setFilterValue] = useState<string>('');
73-
const [selectOptions, setSelectOptions] = useState<
74-
{
75-
id: DistributionProfileItem;
76-
name: string | undefined;
77-
}[]
78-
>([]);
65+
const [isApplying, setIsApplying] = useState(false);
7966
const [profileDetails, setProfileDetails] = useState<
8067
{
8168
id: DistributionProfileItem;
8269
name: string | undefined;
8370
}[]
8471
>([]);
85-
const complianceType = useAppSelector(selectComplianceType);
8672
const prefetchProfile = useBackendPrefetch('getOscapCustomizations');
8773
const {
8874
clearCompliancePackages,
@@ -103,14 +89,6 @@ const ProfileSelector = ({
10389

10490
const [trigger] = useLazyGetOscapCustomizationsQuery();
10591

106-
useEffect(() => {
107-
if (!profileID) {
108-
setInputValue('');
109-
setFilterValue('');
110-
setIsOpen(false);
111-
}
112-
}, [profileID]);
113-
11492
// prefetch the profiles customizations for on-prem
11593
// and save the results to the cache, since the request
11694
// is quite slow
@@ -148,30 +126,13 @@ const ProfileSelector = ({
148126

149127
const resolvedProfiles = await Promise.all(promises);
150128
setProfileDetails(resolvedProfiles);
151-
setSelectOptions(resolvedProfiles);
152129
};
153130

154131
fetchProfileDetails();
155132
}, [profiles, release, trigger]);
156133

157-
useEffect(() => {
158-
if (!filterValue) {
159-
setSelectOptions(profileDetails);
160-
return;
161-
}
162-
const trimmedFilter = filterValue.toLowerCase().trim();
163-
const filtered = profileDetails.filter(({ name }) =>
164-
name?.toLowerCase().includes(trimmedFilter),
165-
);
166-
167-
setSelectOptions(filtered);
168-
if (!isOpen && filtered.length > 0) {
169-
setIsOpen(true);
170-
}
171-
}, [filterValue, profileDetails, isOpen]);
172-
173134
const handleToggle = () => {
174-
if (!isOpen && complianceType === 'openscap') {
135+
if (!isOpen) {
175136
refetch();
176137
}
177138
setIsOpen(!isOpen);
@@ -184,53 +145,13 @@ const ProfileSelector = ({
184145
handleServices(undefined);
185146
dispatch(clearKernelAppend());
186147
dispatch(changeFips(false));
187-
setInputValue('');
188-
setFilterValue('');
189-
};
190-
191-
const onInputClick = () => {
192-
if (!isOpen) {
193-
setIsOpen(true);
194-
} else if (!inputValue) {
195-
setIsOpen(false);
196-
}
197-
};
198-
199-
const onTextInputChange = (_event: React.FormEvent, value: string) => {
200-
setInputValue(value);
201-
setFilterValue(value);
202-
203-
if (value !== profileID) {
204-
dispatch(setOscapProfile(undefined));
205-
}
206-
};
207-
208-
const onKeyDown = (event: React.KeyboardEvent) => {
209-
if (event.key === 'Enter') {
210-
event.preventDefault();
211-
212-
if (!isOpen) {
213-
setIsOpen(true);
214-
} else if (selectOptions.length === 1) {
215-
const singleProfile = selectOptions[0];
216-
const selection: OScapSelectOptionValueType = {
217-
profileID: singleProfile.id,
218-
toString: () => singleProfile.name || '',
219-
};
220-
221-
setInputValue(singleProfile.name || '');
222-
setFilterValue('');
223-
applyChanges(selection);
224-
setIsOpen(false);
225-
}
226-
}
227148
};
228149

229150
const applyChanges = (selection: OScapSelectOptionValueType) => {
230151
if (selection.profileID === undefined) {
231-
// handle user has selected 'None' case
232152
handleClear();
233153
} else {
154+
setIsApplying(true);
234155
const oldOscapPackages = currentProfileData?.packages || [];
235156
trigger(
236157
{
@@ -253,7 +174,8 @@ const ProfileSelector = ({
253174
handleKernelAppend(response.kernel?.append);
254175
dispatch(setOscapProfile(selection.profileID));
255176
dispatch(changeFips(response.fips?.enabled || false));
256-
});
177+
})
178+
.finally(() => setIsApplying(false));
257179
}
258180
};
259181

@@ -263,49 +185,56 @@ const ProfileSelector = ({
263185
) => {
264186
if (selection === undefined) return;
265187

266-
setInputValue((selection as OScapSelectOptionValueType['profileID']) || '');
267-
setFilterValue('');
268188
applyChanges(selection as unknown as OScapSelectOptionValueType);
269189
setIsOpen(false);
270190
};
271191

192+
const selectedProfileName = profileID
193+
? profileDetails.find(({ id }) => id === profileID)?.name || profileID
194+
: undefined;
195+
196+
const profileOptions = () => {
197+
if (isFetching) {
198+
return [
199+
<SelectOption key='oscap-loader' value='loader'>
200+
<Spinner size='lg' />
201+
</SelectOption>,
202+
];
203+
}
204+
205+
const res = profileDetails.map(({ id, name }) => (
206+
<SelectOption
207+
key={id}
208+
value={{
209+
profileID: id,
210+
toString: () => name,
211+
}}
212+
isSelected={profileID === id}
213+
>
214+
{name}
215+
</SelectOption>
216+
));
217+
return res;
218+
};
219+
272220
const toggleOpenSCAP = (toggleRef: React.Ref<MenuToggleElement>) => (
273221
<MenuToggle
274222
data-testid='profileSelect'
275223
ref={toggleRef}
276-
variant='typeahead'
224+
isPlaceholder={!selectedProfileName && !isApplying}
277225
onClick={() => setIsOpen(!isOpen)}
278226
isExpanded={isOpen}
279-
isDisabled={isDisabled || !isSuccess}
227+
isDisabled={isDisabled || !isSuccess || isApplying}
280228
isFullWidth
229+
style={{ maxWidth: 'none' }}
281230
>
282-
<TextInputGroup isPlain>
283-
<TextInputGroupMain
284-
value={
285-
profileID
286-
? profileDetails.find(({ id }) => id === profileID)?.name ||
287-
profileID
288-
: inputValue
289-
}
290-
onClick={onInputClick}
291-
onChange={onTextInputChange}
292-
onKeyDown={onKeyDown}
293-
autoComplete='off'
294-
placeholder='Select a profile'
295-
isExpanded={isOpen}
296-
/>
297-
298-
{profileID && (
299-
<TextInputGroupUtilities>
300-
<Button
301-
icon={<TimesIcon />}
302-
variant='plain'
303-
onClick={handleClear}
304-
aria-label='Clear input'
305-
/>
306-
</TextInputGroupUtilities>
307-
)}
308-
</TextInputGroup>
231+
{isApplying ? (
232+
<>
233+
<Spinner size='sm' /> Applying profile...
234+
</>
235+
) : (
236+
selectedProfileName || 'Select a profile'
237+
)}
309238
</MenuToggle>
310239
);
311240

@@ -319,35 +248,8 @@ const ProfileSelector = ({
319248
onOpenChange={handleToggle}
320249
toggle={toggleOpenSCAP}
321250
shouldFocusFirstItemOnOpen={false}
322-
popperProps={{
323-
maxWidth: '50vw',
324-
}}
325251
>
326-
<SelectList>
327-
{isFetching && (
328-
<SelectOption value='loader'>
329-
<Spinner size='lg' />
330-
</SelectOption>
331-
)}
332-
{selectOptions.length > 0 &&
333-
selectOptions.map(({ id, name }) => (
334-
<SelectOption
335-
key={id}
336-
value={{
337-
profileID: id,
338-
toString: () => name,
339-
}}
340-
isSelected={profileID === id}
341-
>
342-
{name}
343-
</SelectOption>
344-
))}
345-
{isSuccess && selectOptions.length === 0 && (
346-
<SelectOption isDisabled>
347-
{`No results found for "${filterValue}"`}
348-
</SelectOption>
349-
)}
350-
</SelectList>
252+
<SelectList>{profileOptions()}</SelectList>
351253
</Select>
352254
</FormGroup>
353255
);

0 commit comments

Comments
 (0)