Skip to content

Commit 35e3bff

Browse files
committed
Add 'Is managed by' tab to OTP tokens sub-page
Add the "Is managed by" tab section to the OTP tokens sub-page, allowing users to view and manage which users can manage a given OTP token. Changes: - Create `ManagedByUsers` reusable component in src/components/ManagedBy/ - Create `OtpTokensManagedBy` wrapper with `TabLayout` and badge count - Add `otptoken_add_managedby` / `otptoken_remove_managedby` RPC mutations - Update `OtpToken.managedby_user` type from `string` to `string[]` - Add `managedby_user` route in `AppRoutes.tsx` Assisted-by: Claude <noreply@anthropic.com> Signed-off-by: Carla Martinez <carlmart@redhat.com>
1 parent a360610 commit 35e3bff

7 files changed

Lines changed: 475 additions & 4 deletions

File tree

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import React from "react";
2+
// PatternFly
3+
import { Pagination, PaginationVariant } from "@patternfly/react-core";
4+
// Components
5+
import MemberOfToolbar from "../MemberOf/MemberOfToolbar";
6+
import MemberOfAddModal, { AvailableItems } from "../MemberOf/MemberOfAddModal";
7+
import MemberOfDeleteModal from "../MemberOf/MemberOfDeleteModal";
8+
import MemberTable from "src/components/tables/MembershipTable";
9+
// Data types
10+
import { OtpToken, User } from "src/utils/datatypes/globalDataTypes";
11+
// Redux
12+
import { useAppDispatch } from "src/store/hooks";
13+
// Hooks
14+
import { addAlert } from "src/store/Global/alerts-slice";
15+
import useListPageSearchParams from "src/hooks/useListPageSearchParams";
16+
import { toggleHelpPanel } from "src/store/Global/contextual-help-slice";
17+
// Utils
18+
import { API_VERSION_BACKUP, paginate } from "src/utils/utils";
19+
import { apiToUser } from "src/utils/userUtils";
20+
// RPC
21+
import { ErrorResult } from "src/services/rpc";
22+
import {
23+
useGetUsersInfoByUidQuery,
24+
useGettingActiveUserQuery,
25+
} from "src/services/rpcUsers";
26+
import {
27+
OtpTokenManagedByPayload,
28+
useAddManagedByOtpTokenMutation,
29+
useRemoveManagedByOtpTokenMutation,
30+
} from "src/services/rpcOtpTokens";
31+
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
32+
import { SerializedError } from "@reduxjs/toolkit";
33+
34+
interface ManagedByUsersProps {
35+
entity: Partial<OtpToken>;
36+
id: string;
37+
from: string;
38+
isDataLoading: boolean;
39+
onRefreshData: () => void;
40+
}
41+
42+
const ManagedByUsers = (props: ManagedByUsersProps) => {
43+
const dispatch = useAppDispatch();
44+
45+
// Get parameters from URL
46+
const { page, setPage, perPage, setPerPage, searchValue, setSearchValue } =
47+
useListPageSearchParams();
48+
49+
// Other states
50+
const [usersSelected, setUsersSelected] = React.useState<string[]>([]);
51+
52+
const managedby_user = props.entity.managedby_user || [];
53+
54+
const filteredUsers = React.useMemo(() => {
55+
let toLoad = [...managedby_user].sort();
56+
57+
if (searchValue) {
58+
const q = searchValue.toLowerCase();
59+
toLoad = toLoad.filter((name) => name.toLowerCase().includes(q));
60+
}
61+
62+
return toLoad;
63+
}, [props.entity.managedby_user, searchValue]);
64+
65+
const userNamesToLoad = React.useMemo(
66+
() => paginate(filteredUsers, page, perPage),
67+
[filteredUsers, page, perPage]
68+
);
69+
70+
const fullUsersQuery = useGetUsersInfoByUidQuery({
71+
uidsList: userNamesToLoad,
72+
noMembers: true,
73+
});
74+
75+
const users = React.useMemo<User[]>(
76+
() => fullUsersQuery.data ?? [],
77+
[fullUsersQuery.data]
78+
);
79+
80+
const updateSearchValue = (value: string) => {
81+
setPage(1);
82+
setSearchValue(value);
83+
};
84+
85+
const submitSearchValue = () => {
86+
setPage(1);
87+
};
88+
89+
// Computed "states"
90+
const someItemSelected = usersSelected.length > 0;
91+
const showTableRows = users.length > 0;
92+
const userColumnNames = ["User login", "First name", "Last name", "Status"];
93+
const userProperties = ["uid", "givenname", "sn", "nsaccountlock"];
94+
95+
// Dialogs and actions
96+
const [showAddModal, setShowAddModal] = React.useState(false);
97+
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
98+
const [spinning, setSpinning] = React.useState(false);
99+
100+
// Buttons functionality
101+
const isRefreshButtonEnabled =
102+
!fullUsersQuery.isFetching && !props.isDataLoading;
103+
const isDeleteEnabled = someItemSelected;
104+
const isAddButtonEnabled = isRefreshButtonEnabled;
105+
106+
// API calls
107+
const [addManagedBy] = useAddManagedByOtpTokenMutation();
108+
const [removeManagedBy] = useRemoveManagedByOtpTokenMutation();
109+
110+
const [adderSearchValue, setAdderSearchValue] = React.useState("");
111+
const [availableUsers, setAvailableUsers] = React.useState<User[]>([]);
112+
const [availableItems, setAvailableItems] = React.useState<AvailableItems[]>(
113+
[]
114+
);
115+
116+
// Load available Users
117+
const usersQuery = useGettingActiveUserQuery({
118+
search: adderSearchValue,
119+
apiVersion: API_VERSION_BACKUP,
120+
sizelimit: 100,
121+
startIdx: 0,
122+
stopIdx: 100,
123+
});
124+
125+
// Trigger available Users search
126+
React.useEffect(() => {
127+
if (showAddModal) {
128+
usersQuery.refetch();
129+
}
130+
}, [showAddModal, adderSearchValue, props.entity]);
131+
132+
// Update available Users
133+
React.useEffect(() => {
134+
if (usersQuery.data && !usersQuery.isFetching) {
135+
const count = usersQuery.data.result.count;
136+
const results = usersQuery.data.result.results;
137+
let items: AvailableItems[] = [];
138+
const avalUsers: User[] = [];
139+
for (let i = 0; i < count; i++) {
140+
const user = apiToUser(results[i].result);
141+
avalUsers.push(user);
142+
items.push({
143+
key: user.uid,
144+
title: user.uid,
145+
});
146+
}
147+
items = items.filter((item) => !managedby_user.includes(item.key));
148+
149+
setAvailableUsers(avalUsers);
150+
setAvailableItems(items);
151+
}
152+
}, [usersQuery.data, usersQuery.isFetching, managedby_user]);
153+
154+
// Add
155+
const onAddUser = (items: AvailableItems[]) => {
156+
const newUserNames = items.map((item) => item.key);
157+
if (props.id === undefined || newUserNames.length === 0) {
158+
return;
159+
}
160+
161+
const payload: OtpTokenManagedByPayload = {
162+
otpTokenId: props.id,
163+
users: newUserNames,
164+
};
165+
166+
setSpinning(true);
167+
addManagedBy(payload).then(
168+
(
169+
response:
170+
| { data: { result?: unknown; error?: unknown } }
171+
| { error: FetchBaseQueryError | SerializedError }
172+
) => {
173+
if ("data" in response) {
174+
if (response.data?.result) {
175+
dispatch(
176+
addAlert({
177+
name: "add-managedby-success",
178+
title: "Assigned new managers to OTP token '" + props.id + "'",
179+
variant: "success",
180+
})
181+
);
182+
props.onRefreshData();
183+
setShowAddModal(false);
184+
} else if (response.data?.error) {
185+
const errorMessage = response.data.error as unknown as ErrorResult;
186+
dispatch(
187+
addAlert({
188+
name: "add-managedby-error",
189+
title: errorMessage.message,
190+
variant: "danger",
191+
})
192+
);
193+
}
194+
} else if ("error" in response) {
195+
dispatch(
196+
addAlert({
197+
name: "add-managedby-error",
198+
title: "Failed to assign managers",
199+
variant: "danger",
200+
})
201+
);
202+
}
203+
setSpinning(false);
204+
}
205+
);
206+
};
207+
208+
// Delete
209+
const onDeleteUser = () => {
210+
if (props.id === undefined) return;
211+
212+
const payload: OtpTokenManagedByPayload = {
213+
otpTokenId: props.id,
214+
users: usersSelected,
215+
};
216+
217+
setSpinning(true);
218+
removeManagedBy(payload).then(
219+
(
220+
response:
221+
| { data: unknown }
222+
| { error: FetchBaseQueryError | SerializedError }
223+
) => {
224+
if ("data" in response) {
225+
dispatch(
226+
addAlert({
227+
name: "remove-managedby-success",
228+
title: "Removed managers from OTP token '" + props.id + "'",
229+
variant: "success",
230+
})
231+
);
232+
props.onRefreshData();
233+
setUsersSelected([]);
234+
setShowDeleteModal(false);
235+
} else if ("error" in response) {
236+
dispatch(
237+
addAlert({
238+
name: "remove-managedby-error",
239+
title: "Failed to remove managers",
240+
variant: "danger",
241+
})
242+
);
243+
}
244+
setSpinning(false);
245+
}
246+
);
247+
};
248+
249+
return (
250+
<>
251+
<MemberOfToolbar
252+
searchText={searchValue}
253+
onSearchTextChange={updateSearchValue}
254+
onSearch={submitSearchValue}
255+
searchPlaceholder="Search users"
256+
searchAriaLabel="Search users"
257+
refreshButtonEnabled={isRefreshButtonEnabled}
258+
onRefreshButtonClick={props.onRefreshData}
259+
deleteButtonEnabled={isDeleteEnabled}
260+
onDeleteButtonClick={() => setShowDeleteModal(true)}
261+
addButtonEnabled={isAddButtonEnabled}
262+
onAddButtonClick={() => setShowAddModal(true)}
263+
helpIconEnabled={true}
264+
onHelpIconClick={() => dispatch(toggleHelpPanel())}
265+
totalItems={filteredUsers.length}
266+
perPage={perPage}
267+
page={page}
268+
onPerPageChange={setPerPage}
269+
onPageChange={setPage}
270+
/>
271+
<MemberTable
272+
entityList={users}
273+
idKey="uid"
274+
from="active-users"
275+
columnNamesToShow={userColumnNames}
276+
propertiesToShow={userProperties}
277+
checkedItems={usersSelected}
278+
onCheckItemsChange={setUsersSelected}
279+
showTableRows={showTableRows}
280+
/>
281+
<Pagination
282+
className="pf-v6-u-pb-0 pf-v6-u-pr-md"
283+
itemCount={filteredUsers.length}
284+
widgetId="pagination-options-menu-bottom"
285+
perPage={perPage}
286+
page={page}
287+
variant={PaginationVariant.bottom}
288+
onSetPage={(_e, page) => setPage(page)}
289+
onPerPageSelect={(_e, perPage) => setPerPage(perPage)}
290+
/>
291+
{showAddModal && (
292+
<MemberOfAddModal
293+
showModal={showAddModal}
294+
onCloseModal={() => setShowAddModal(false)}
295+
availableItems={availableItems}
296+
onAdd={onAddUser}
297+
onSearchTextChange={setAdderSearchValue}
298+
title={`Assign users managing OTP token: ${props.id}`}
299+
ariaLabel="Add managed by user modal"
300+
spinning={spinning}
301+
/>
302+
)}
303+
{showDeleteModal && someItemSelected && (
304+
<MemberOfDeleteModal
305+
showModal={showDeleteModal}
306+
onCloseModal={() => setShowDeleteModal(false)}
307+
title={`Remove managers from OTP token: ${props.id}`}
308+
onDelete={onDeleteUser}
309+
spinning={spinning}
310+
>
311+
<MemberTable
312+
entityList={availableUsers.filter((user) =>
313+
usersSelected.includes(user.uid)
314+
)}
315+
from="active-users"
316+
idKey="uid"
317+
columnNamesToShow={userColumnNames}
318+
propertiesToShow={userProperties}
319+
showTableRows
320+
/>
321+
</MemberOfDeleteModal>
322+
)}
323+
</>
324+
);
325+
};
326+
327+
export default ManagedByUsers;

src/navigation/AppRoutes.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,10 @@ export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
464464
path=""
465465
element={<OtpTokensTabs section="settings" />}
466466
/>
467+
<Route
468+
path="managedby_user"
469+
element={<OtpTokensTabs section="managedby_user" />}
470+
/>
467471
</Route>
468472
</Route>
469473
<Route path="identity-provider-references">

0 commit comments

Comments
 (0)