Skip to content

Commit 1730cfd

Browse files
committed
Fix Owner filtering on ProjectEdit (#628)
* Use an object for UserFilter options Provide a display name for the filter options instead of the api parameter key * Update ProjectEdit, useUserFilter hook Change how owner selection works in the form. Use the UserFilter hook to provide name and email filtering specifically, instead of only name filtering via typeahead. Having typeahead is nice, but assumes all users have SSO names set properly, which they may not. I think this layout works alright, and the default API params should populate the list with the first 25 users with no filters applied * variable rename
1 parent 6e5d16c commit 1730cfd

File tree

4 files changed

+77
-116
lines changed

4 files changed

+77
-116
lines changed

frontend/src/components/user-filter.js

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const DEFAULT_OP = Object.keys(STRING_OPERATIONS)[0];
2323
const UserFilterComponent = ({
2424
applyFilter,
2525
isFieldOpen,
26-
fieldSelection,
26+
selectedField,
2727
userToggle,
2828
onFieldSelect,
2929
isOperationOpen,
@@ -44,14 +44,19 @@ const UserFilterComponent = ({
4444
aria-label="user-filter-field"
4545
variant={SelectVariant.single}
4646
isOpen={isFieldOpen}
47-
selections={fieldSelection}
47+
selected={selectedField}
4848
onToggle={(_, change) => setIsFieldOpen(change)}
4949
toggle={userToggle}
5050
onSelect={onFieldSelect}
5151
>
52-
{STRING_USER_FIELDS.map((option, index) => (
53-
<SelectOption key={index} value={option}>
54-
{option}
52+
{STRING_USER_FIELDS.map((option) => (
53+
<SelectOption
54+
key={option.value}
55+
id={option.value}
56+
value={option}
57+
ref={null}
58+
>
59+
{option.children}
5560
</SelectOption>
5661
))}
5762
</Select>
@@ -68,8 +73,8 @@ const UserFilterComponent = ({
6873
toggle={operationToggle}
6974
>
7075
<SelectList>
71-
{Object.keys(STRING_OPERATIONS).map((option, index) => (
72-
<SelectOption key={index} value={option}>
76+
{Object.keys(STRING_OPERATIONS).map((option) => (
77+
<SelectOption key={option} value={option}>
7378
{option}
7479
</SelectOption>
7580
))}
@@ -90,7 +95,9 @@ const UserFilterComponent = ({
9095
{filterValue && (
9196
<Flex>
9297
<FlexItem>
93-
<Button onClick={applyFilter}>Apply Filter</Button>
98+
<Button onClick={applyFilter} variant="control">
99+
Apply Filter
100+
</Button>
94101
</FlexItem>
95102
</Flex>
96103
)}
@@ -101,7 +108,7 @@ const UserFilterComponent = ({
101108
UserFilterComponent.propTypes = {
102109
applyFilter: PropTypes.func,
103110
isFieldOpen: PropTypes.bool,
104-
fieldSelection: PropTypes.string,
111+
selectedField: PropTypes.string,
105112
userToggle: PropTypes.func,
106113
onFieldSelect: PropTypes.func,
107114
isOperationOpen: PropTypes.bool,
@@ -117,7 +124,7 @@ UserFilterComponent.propTypes = {
117124
const useUserFilter = () => {
118125
// Provide a rich user filter, like meta filter but not as dynamic in the fields
119126

120-
const [fieldSelection, setFieldSelection] = useState(DEFAULT_FIELD);
127+
const [selectedField, setSelectedField] = useState(DEFAULT_FIELD);
121128
const [isFieldOpen, setIsFieldOpen] = useState(false);
122129

123130
const [operationSelection, setOperationSelection] = useState(DEFAULT_OP);
@@ -128,7 +135,7 @@ const useUserFilter = () => {
128135
const [activeFilters, setActiveFilters] = useState({});
129136

130137
const onFieldSelect = useCallback((_, selection) => {
131-
setFieldSelection(selection);
138+
setSelectedField(selection);
132139
setFilterValue('');
133140
setIsFieldOpen(false);
134141
}, []);
@@ -154,16 +161,16 @@ const useUserFilter = () => {
154161

155162
const applyFilter = useCallback(() => {
156163
updateFilters(
157-
fieldSelection,
164+
selectedField.value,
158165
operationSelection,
159166
filterValue.trim(),
160167
() => {
161-
setFieldSelection(DEFAULT_FIELD);
168+
setSelectedField(DEFAULT_FIELD);
162169
setOperationSelection(DEFAULT_OP);
163170
setFilterValue('');
164171
},
165172
);
166-
}, [fieldSelection, filterValue, operationSelection, updateFilters]);
173+
}, [selectedField, filterValue, operationSelection, updateFilters]);
167174

168175
const userToggle = useCallback(
169176
(toggleRef) => (
@@ -172,10 +179,10 @@ const useUserFilter = () => {
172179
isExpanded={isFieldOpen}
173180
ref={toggleRef}
174181
>
175-
{fieldSelection}
182+
{selectedField.children}
176183
</MenuToggle>
177184
),
178-
[fieldSelection, isFieldOpen],
185+
[selectedField, isFieldOpen],
179186
);
180187

181188
const operationToggle = useCallback(
@@ -195,7 +202,7 @@ const useUserFilter = () => {
195202
<UserFilterComponent
196203
applyFilter={applyFilter}
197204
userToggle={userToggle}
198-
fieldSelection={fieldSelection}
205+
selectedField={selectedField.children}
199206
onFieldSelect={onFieldSelect}
200207
isFieldOpen={isFieldOpen}
201208
setIsFieldOpen={setIsFieldOpen}

frontend/src/constants.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,16 @@ export const STRING_JJV_FIELDS = ['job_name', 'source', 'build_number', 'env'];
100100
export const NUMERIC_JJV_FIELDS = ['start_time'];
101101
export const JJV_FIELDS = [...STRING_JJV_FIELDS, ...NUMERIC_JJV_FIELDS];
102102

103-
export const STRING_USER_FIELDS = ['name', 'email'];
103+
export const STRING_USER_FIELDS = [
104+
{
105+
value: 'name',
106+
children: 'Display Name',
107+
},
108+
{
109+
value: 'email',
110+
children: 'Email Address',
111+
},
112+
];
104113

105114
export const USER_COLUMNS = {
106115
name: 'Display Name',

frontend/src/pages/admin/project-edit.js

Lines changed: 42 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import { TimesIcon } from '@patternfly/react-icons';
3030

3131
import { HttpClient } from '../../services/http';
3232
import { Settings } from '../../settings';
33-
import { dashboardToOption } from '../../utilities.js';
33+
import { dashboardToOption, toAPIFilter } from '../../utilities.js';
34+
import useUserFilter from '../../components/user-filter.js';
3435

3536
const userToOption = (user) => {
3637
if (!user) {
@@ -65,11 +66,8 @@ const ProjectEdit = () => {
6566

6667
// owner selection state
6768
const [filteredUsers, setFilteredUsers] = useState([]);
68-
const [users, setUsers] = useState([]);
6969
const [isOwnerOpen, setIsOwnerOpen] = useState(false);
7070
const [selectedOwner, setSelectedOwner] = useState({});
71-
const [filterValueOwner, setFilterValueOwner] = useState('');
72-
const [inputValueOwner, setInputValueOwner] = useState('');
7371

7472
// dashboard selection state
7573
const [filteredDashboards, setFilteredDashboards] = useState([]);
@@ -88,8 +86,8 @@ const ProjectEdit = () => {
8886
let project = {
8987
title: title,
9088
name: name,
91-
owner_id: selectedOwner ? selectedOwner.id : null,
92-
default_dashboard_id: selectedDashboard ? selectedDashboard.id : null,
89+
owner_id: selectedOwner?.id || null,
90+
default_dashboard_id: selectedDashboard?.id || null,
9391
};
9492

9593
let request = null;
@@ -111,25 +109,12 @@ const ProjectEdit = () => {
111109
.catch((error) => console.error(error));
112110
};
113111

114-
const onOwnerInputChange = (_, value) => {
115-
setInputValueOwner(value);
116-
setFilterValueOwner(value);
117-
};
118-
119-
const onOwnerSelect = (event, value) => {
112+
const onOwnerSelect = (_, value) => {
120113
setSelectedOwner(value.user);
121114
setIsOwnerOpen(false);
122-
setFilterValueOwner(value.user.name);
123-
setInputValueOwner(value.user.name);
124-
};
125-
126-
const onOwnerClear = () => {
127-
setSelectedOwner(null);
128-
setFilterValueOwner('');
129-
setInputValueOwner('');
130115
};
131116

132-
const onDashboardSelect = (event, value) => {
117+
const onDashboardSelect = (_, value) => {
133118
setSelectedDashboard(value.dashboard);
134119
setIsDashboardOpen(false);
135120
setFilterValueDashboard(value.dashboard.title);
@@ -147,16 +132,22 @@ const ProjectEdit = () => {
147132
setFilterValueDashboard(value);
148133
};
149134

150-
// fetch the admin users once
135+
const { filterComponents, activeFilterComponents, activeFilters } =
136+
useUserFilter();
137+
138+
// fetch the admin users with the active filter
151139
useEffect(() => {
152-
HttpClient.get([Settings.serverUrl, 'admin', 'user'])
140+
HttpClient.get([Settings.serverUrl, 'admin', 'user'], {
141+
...(Object.keys(activeFilters)?.length === 0
142+
? {}
143+
: { filter: toAPIFilter(activeFilters) }),
144+
})
153145
.then((response) => HttpClient.handleResponse(response))
154146
.then((data) => {
155-
setUsers(data.users);
156147
setFilteredUsers(data.users);
157148
})
158149
.catch((error) => console.error(error));
159-
}, []);
150+
}, [activeFilters]);
160151

161152
// fetch the project if needed
162153
useEffect(() => {
@@ -171,7 +162,6 @@ const ProjectEdit = () => {
171162
setCrumbTitle(data.title);
172163
setName(data.name);
173164
setSelectedOwner(data.owner);
174-
setInputValueOwner(data.owner?.name);
175165
setSelectedDashboard(data.defaultDashboard);
176166
setInputValueDashboard(data.defaultDashboard?.title);
177167
})
@@ -229,65 +219,19 @@ const ProjectEdit = () => {
229219
setFilteredDashboards(newSelectOptionsDashboard);
230220
}, [dashboards, filterValueDashboard, inputValueDashboard, isDashboardOpen]);
231221

232-
// update owner filtering and selection items
233-
useEffect(() => {
234-
let newSelectOptionsUser = [...users];
235-
if (inputValueOwner) {
236-
newSelectOptionsUser = users.filter((menuItem) =>
237-
String(menuItem.name)
238-
.toLowerCase()
239-
.includes(filterValueOwner.toLowerCase()),
240-
);
241-
if (newSelectOptionsUser.length === 0) {
242-
newSelectOptionsUser = [
243-
{
244-
isDisabled: true,
245-
value: {},
246-
name: `No results found for "${filterValueOwner}"`,
247-
},
248-
];
249-
}
250-
}
251-
setFilteredUsers(newSelectOptionsUser);
252-
}, [filterValueOwner, inputValueOwner, isOwnerOpen, users]);
253-
254222
const toggleOwner = (toggleRef) => (
255223
<MenuToggle
256224
innerRef={toggleRef}
257-
variant="typeahead"
258-
aria-label="Typeahead menu toggle"
225+
variant="secondary"
226+
aria-label="Owner selection toggle"
259227
onClick={() => {
260228
setIsOwnerOpen(!isOwnerOpen);
261229
}}
262230
isExpanded={isOwnerOpen}
263-
isFullWidth
264231
>
265-
<TextInputGroup isPlain>
266-
<TextInputGroupMain
267-
value={inputValueOwner}
268-
onClick={() => {
269-
setIsOwnerOpen(!isOwnerOpen);
270-
}}
271-
onChange={onOwnerInputChange}
272-
id="typeahead-select-input"
273-
autoComplete="off"
274-
placeholder="Select project owner"
275-
role="combobox"
276-
isExpanded={isOwnerOpen}
277-
aria-controls="select-typeahead-listbox"
278-
/>
279-
<TextInputGroupUtilities>
280-
{!!inputValueOwner && (
281-
<Button
282-
variant="plain"
283-
onClick={onOwnerClear}
284-
aria-label="Clear input value"
285-
>
286-
<TimesIcon aria-hidden />
287-
</Button>
288-
)}
289-
</TextInputGroupUtilities>
290-
</TextInputGroup>
232+
{selectedOwner?.name ||
233+
selectedOwner?.email ||
234+
'Use a filter to select an owner'}
291235
</MenuToggle>
292236
);
293237

@@ -300,7 +244,6 @@ const ProjectEdit = () => {
300244
setIsDashboardOpen(!isDashboardOpen);
301245
}}
302246
isExpanded={isDashboardOpen}
303-
isFullWidth
304247
isDisabled={filteredDashboards.length === 0 ? true : false}
305248
>
306249
<TextInputGroup isPlain>
@@ -335,7 +278,7 @@ const ProjectEdit = () => {
335278
return (
336279
<React.Fragment>
337280
<PageSection variant={PageSectionVariants.light}>
338-
<Title headingLevel="h1" size="2xl" className="pf-v5-c-title">
281+
<Title headingLevel="h1" size="2xl">
339282
Projects / {crumbTitle}
340283
</Title>
341284
</PageSection>
@@ -388,34 +331,36 @@ const ProjectEdit = () => {
388331
</FormGroup>
389332
<FormGroup fieldId="owner" label="Owner">
390333
<Select
391-
id="typeahead-select-owner"
334+
id="projectOwner"
335+
ouiaId="project-edit-owner-select"
392336
isOpen={isOwnerOpen}
393337
selected={selectedOwner}
394338
onSelect={onOwnerSelect}
395339
onOpenChange={() => setIsOwnerOpen(false)}
396340
toggle={toggleOwner}
397341
isScrollable={true}
398-
maxMenuHeight="300px"
342+
variant="default"
399343
>
400-
<SelectList id="select-typeahead-listbox">
401-
{filteredUsers?.map((user, index) => (
402-
<SelectOption
403-
key={user.id || index}
404-
onClick={() => setSelectedOwner(user)}
405-
value={userToOption(user)}
406-
description={user.email}
407-
isDisabled={user.isDisabled}
408-
ref={null}
409-
>
410-
{user.name}
411-
</SelectOption>
412-
))}
413-
</SelectList>
344+
{filteredUsers?.map((user, index) => (
345+
<SelectOption
346+
key={user.id || index}
347+
onClick={() => setSelectedOwner(user)}
348+
value={userToOption(user)}
349+
description={user.email}
350+
isDisabled={user.isDisabled}
351+
ref={null}
352+
>
353+
{user.name || user.email}
354+
</SelectOption>
355+
))}
414356
</Select>
415357
<FormHelperText>
416358
<HelperText>
417359
<HelperTextItem>
418-
The user who owns the project
360+
The user who owns the project. Use the filter to narrow
361+
the selection options above.
362+
{filterComponents}
363+
{activeFilterComponents}
419364
</HelperTextItem>
420365
</HelperText>
421366
</FormHelperText>
@@ -429,7 +374,7 @@ const ProjectEdit = () => {
429374
onOpenChange={() => setIsDashboardOpen(false)}
430375
toggle={toggleDashboard}
431376
isScrollable={true}
432-
maxMenuHeight="300px"
377+
variant="typeahead"
433378
>
434379
<SelectList id="select-typeahead-listbox">
435380
{filteredDashboards.map((dashboard, index) => (

frontend/src/pages/admin/project-list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const ProjectList = () => {
4848
cells: [
4949
{ title: project.title },
5050
{ title: project.name },
51-
{ title: project.owner && project.owner.name },
51+
{ title: project.owner?.name || project.owner?.email },
5252
{
5353
title: (
5454
<div style={{ textAlign: 'right' }}>

0 commit comments

Comments
 (0)