From 08d4b41a31d79b83f08dcd43d9334c729d8ebb2c Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 09:27:38 +0200 Subject: [PATCH 1/8] Fix openMenuOnFocus always false This was already false per default, but was set to false explicitly to fix a tab navigation issue in the metadata tab of events/series dialog. So this sets openMenuOnFocus to false in RenderFields, so that it may be true in other components again --- src/components/shared/DropDown.tsx | 1 - src/components/shared/wizard/RenderField.tsx | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 03dd4a8078..372c7a8b75 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -229,7 +229,6 @@ const DropDown = ({ t("SELECT_NO_MATCHING_RESULTS")} /> ); diff --git a/src/components/shared/wizard/RenderField.tsx b/src/components/shared/wizard/RenderField.tsx index d2c72ea52f..eb6800cdae 100644 --- a/src/components/shared/wizard/RenderField.tsx +++ b/src/components/shared/wizard/RenderField.tsx @@ -422,7 +422,9 @@ const EditableSingleSelectDropDown = ({ } customCSS={{ isMetadataStyle: focused ? false : true, width: "100%" }} handleMenuIsOpen={(open: boolean) => setFocused(open)} - openMenuOnFocus + // Deliberately false: with the menu open, Tab breaks keyboard + // navigation through the metadata form fields (see f44c9b7). + openMenuOnFocus={false} autoFocus={isFirstField} skipTranslate={!metadataField.translatable} /> From 901effaf48e5b634d96b4d120942124e1e9de3d5 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 09:44:38 +0200 Subject: [PATCH 2/8] Always copy options array Before we only made a copy if skipTranslate was true. This could have cause errors with immutable arrays, so it is probably better to copy everytime. --- src/components/shared/DropDown.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 372c7a8b75..2c7d1c0481 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -91,10 +91,11 @@ const DropDown = ({ required: boolean, ) => { // Translate - // Translating is expensive, skip it if it is not required - if (!skipTranslate) { - unformattedOptions = unformattedOptions.map(option => ({ ...option, label: t(option.label as ParseKeys) })); - } + // Translating is expensive, skip it if it is not required. + // Either way, copy the array so the input is not transmuted later. + unformattedOptions = skipTranslate + ? [...unformattedOptions] + : unformattedOptions.map(option => ({ ...option, label: t(option.label as ParseKeys) })); // Add "No value" option if (!required) { From 9a0e313117a96ac190319ef8272553e7eaac2548 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 13:08:38 +0200 Subject: [PATCH 3/8] Remove unused auto check menuPlacement defaults to this anyway, no need to check here again --- src/components/shared/DropDown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 2c7d1c0481..9de3f9300b 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -217,7 +217,7 @@ const DropDown = ({ onMenuClose: () => openMenu(false), isDisabled: disabled, openMenuOnFocus: openMenuOnFocus, - menuPlacement: menuPlacement ?? "auto", + menuPlacement: menuPlacement, components: { MenuList }, }; From da7f8d953c28d4206df47f7fce92c9d0173c8e9e Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 13:18:40 +0200 Subject: [PATCH 4/8] Make default Dropdown ref stable By using useRef instead of createRef we can avoid recreating the ref all the time. Should result in a very, very minor performance boost. --- src/components/shared/DropDown.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 9de3f9300b..41d5509283 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { dropDownSpacingTheme, @@ -20,7 +20,7 @@ export type DropDownOption = { * This component renders a dropdown menu using react-select */ const DropDown = ({ - ref = React.createRef, boolean, GroupBase>>>(), + ref, value, text, options, @@ -69,7 +69,8 @@ const DropDown = ({ }) => { const { t } = useTranslation(); - const selectRef = ref; + const internalRef = useRef, boolean, GroupBase>> | null>(null); + const selectRef = ref ?? internalRef; const style = dropDownStyle(customCSS ?? {}); From 272b96254efc5b7c5cb6deae7dd0f36be3795d07 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 13:32:14 +0200 Subject: [PATCH 5/8] Keep virtualization components more stable Avoid potential rerenders for virtualized options which can end up quite costly for larger lists. Could also help with flickering and focus reset issues somewhat. --- src/components/shared/DropDown.tsx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 41d5509283..823d536452 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { dropDownSpacingTheme, @@ -16,6 +16,17 @@ export type DropDownOption = { order?: number } +function MenuListRow({ + index, + names, + style, +}: RowComponentProps<{ + names: string[]; +}>) { + const name = names[index]; + return
{name}
; +} + /** * This component renders a dropdown menu using react-select */ @@ -132,7 +143,7 @@ const DropDown = ({ /** * Custom component for list virtualization */ - const MenuList = (props: MenuListProps, false>) => { + const MenuList = useCallback((props: MenuListProps, false>) => { const { children, maxHeight } = props; return Array.isArray(children) ? ( @@ -151,18 +162,7 @@ const DropDown = ({ /> ) : null; - }; - - function MenuListRow({ - index, - names, - style, - }: RowComponentProps<{ - names: string[]; - }>) { - const name = names[index]; - return
{name}
; - } + }, [itemHeight]); const filterOptions = (inputValue: string) => { if (options) { From 5000c54d9a46dfd634276f97e9471e88128d89d5 Mon Sep 17 00:00:00 2001 From: Arnei Date: Thu, 20 Aug 2026 13:47:04 +0200 Subject: [PATCH 6/8] Properly debounce async option load Previous code was just delaying the fetch by a second. It would still fire all accumulated request anyway (e.g. requests for "C", "Co, "Cou", "Cour", "Cours" and "Course", instead of just "Course". Implements debouncing, which should make for less load and cleaner rendering steps when searching in an async dropdown. --- src/components/shared/DropDown.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 823d536452..be062e5c5b 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -173,14 +173,23 @@ const DropDown = ({ return []; }; + const debounceTimeoutRef = useRef>(undefined); + + useEffect(() => { + return () => clearTimeout(debounceTimeoutRef.current); + }, []); + const loadOptionsAsync = (inputValue: string, callback: (options: DropDownOption[]) => void) => { - const timeout = async () => { - callback(formatOptions( - fetchOptions ? await fetchOptions(inputValue) : filterOptions(inputValue), - required, - )); - }; - setTimeout(() => { timeout(); }, 1000); + clearTimeout(debounceTimeoutRef.current); + debounceTimeoutRef.current = setTimeout(() => { + const timeout = async () => { + callback(formatOptions( + fetchOptions ? await fetchOptions(inputValue) : filterOptions(inputValue), + required, + )); + }; + void timeout(); + }, 1000); }; const loadOptions = ( From bb354c980c50b61b951f8f0df9d02618564a66b7 Mon Sep 17 00:00:00 2001 From: Arnei Date: Mon, 24 Aug 2026 08:54:24 +0200 Subject: [PATCH 7/8] Reduce async search debounce from 1000ms to 300ms The 1s debounce delay predates the backend performance work on the ACL role picker (see the opencast20-side fixes for /admin-ng/acl/roles.json): when a search could take several seconds regardless, an extra second of debounce was a small fraction of the total wait. Now that a real search responds in roughly 100ms, the fixed 1s delay is the dominant, and now clearly excessive, part of the perceived latency -- pull it down to 300ms, in line with typical search-as-you-type debounce intervals, and give it a name instead of a bare literal so it's easy to find and retune later. --- src/components/shared/DropDown.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index be062e5c5b..d902251173 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -16,6 +16,9 @@ export type DropDownOption = { order?: number } +// How long to wait after the user stops typing before firing a fetchOptions() search request. +const SEARCH_DEBOUNCE_MS = 300; + function MenuListRow({ index, names, @@ -189,7 +192,7 @@ const DropDown = ({ )); }; void timeout(); - }, 1000); + }, SEARCH_DEBOUNCE_MS); }; const loadOptions = ( From 95ed00624b035f4d28bd5ab27d5afb25cbf7ad08 Mon Sep 17 00:00:00 2001 From: Arnei Date: Mon, 24 Aug 2026 10:53:10 +0200 Subject: [PATCH 8/8] Search ACL roles on the server instead of fetching them all upfront The Access Policy tab (and the corresponding wizards) fetched every role in the system (limit: -1) on open to populate each role dropdown client-side. On instances with many thousands of roles this was a multi-second, multi-megabyte request fired once per open. fetchRolesWithTarget now accepts query/limit/offset/hasUser, and AccessPolicyTable's role dropdowns search the backend as the user types instead. The parent components only fetch a single role up front, just to read the isSanitize flag that decides whether users and non-user roles are split into separate tables. DropDown gains a loadOptionsOnMount flag so a fetchOptions-based dropdown can skip its eager default-option fetch on mount (used here since AccessPolicyTable renders one dropdown per existing policy row, which would otherwise fire one identical request per row on tab open) and instead fetch its default option list lazily, the first time that particular instance is opened. This also fixes the virtualized MenuList silently rendering nothing instead of react-select's no-options/loading message when there are no options yet. --- .../ModalTabsAndPages/NewAccessPage.tsx | 26 +++--- src/components/shared/DropDown.tsx | 29 ++++++- .../modals/ResourceDetailsAccessPolicyTab.tsx | 80 +++++++++++-------- .../users/partials/wizard/AclAccessPage.tsx | 26 +++--- src/slices/aclSlice.ts | 12 ++- 5 files changed, 105 insertions(+), 68 deletions(-) diff --git a/src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx b/src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx index b2a7d9961b..07ad6d0a24 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx @@ -2,14 +2,13 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Notifications from "../../../shared/Notifications"; import { - Role, checkAcls, fetchAclActions, fetchAclTemplates, fetchRolesWithTarget, } from "../../../../slices/aclSlice"; import { FormikProps } from "formik"; -import { policiesFiltered, rolesFiltered } from "../../../../utils/aclUtils"; +import { policiesFiltered } from "../../../../utils/aclUtils"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { fetchSeriesDetailsAcls } from "../../../../slices/seriesDetailsSlice"; import { getSeriesDetailsAcl } from "../../../../selectors/seriesDetailsSelectors"; @@ -55,21 +54,23 @@ const NewAccessPage = ({ // States containing response from server concerning acl templates, actions and roles const [aclTemplates, setAclTemplates] = useState<{ id: string, value: string}[]>([]); const [aclActions, setAclActions] = useState<{ id: string, value: string}[]>([]); - const [roles, setRoles] = useState([]); + const [isSanitize, setIsSanitize] = useState(undefined); const [loading, setLoading] = useState(false); const seriesAcl = useAppSelector(state => getSeriesDetailsAcl(state)); const user = useAppSelector(state => getUserInformation(state)); useEffect(() => { - // fetch data about roles, acl templates and actions from backend + // fetch data about acl templates and actions from backend async function fetchData() { setLoading(true); const [responseTemplates, responseActions, responseRoles] = await Promise.all([ - fetchAclTemplates(), fetchAclActions(), fetchRolesWithTarget("ACL")]); + fetchAclTemplates(), fetchAclActions(), fetchRolesWithTarget("ACL", { limit: 1 })]); setAclTemplates(responseTemplates); setAclActions(responseActions); - setRoles(responseRoles); + if (responseRoles.length > 0) { + setIsSanitize(responseRoles[0].isSanitize); + } setLoading(false); } @@ -118,13 +119,13 @@ const NewAccessPage = ({ defaultUser={user} /> - {roles.length > 0 && !roles[0].isSanitize && + {isSanitize === false && <> {hasAccess(viewUsersAccessRole, user) && ({ hasActions={aclActions.length > 0} transactions={{ readOnly: false }} aclActions={aclActions} - roles={roles} editAccessRole={editAccessRole} /> } @@ -141,7 +141,7 @@ const NewAccessPage = ({ ({ hasActions={aclActions.length > 0} transactions={{ readOnly: false }} aclActions={aclActions} - roles={roles} editAccessRole={editAccessRole} /> } } - {roles.length > 0 && roles[0].isSanitize && + {isSanitize === true && <> 0} transactions={{ readOnly: false }} aclActions={aclActions} - roles={roles} editAccessRole={editAccessRole} />
diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index d902251173..5a2ed29700 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { dropDownSpacingTheme, @@ -54,6 +54,7 @@ const DropDown = ({ optionHeight = 25, customCSS, fetchOptions, + loadOptionsOnMount = true, }: { ref?: React.RefObject, boolean, GroupBase>> | null> value: T @@ -80,12 +81,23 @@ const DropDown = ({ optionLineHeight?: string }, fetchOptions?: (inputValue: string) => Promise[]> + // Whether an async (fetchOptions-based) dropdown should eagerly fetch its default option list on + // mount, before the user has interacted with it. Set to false when many instances of this dropdown + // may be mounted at once (e.g. one per table row), to avoid firing one request per instance on + // render; the default option list is then fetched lazily, the first time this instance's menu is + // opened, instead of unconditionally on mount. + loadOptionsOnMount?: boolean }) => { const { t } = useTranslation(); const internalRef = useRef, boolean, GroupBase>> | null>(null); const selectRef = ref ?? internalRef; + // Holds the result of the one-off default-option fetch below, once it has completed. + const [preloadedOptions, setPreloadedOptions] = useState[] | undefined>(undefined); + const [isPreloading, setIsPreloading] = useState(false); + const hasStartedPreload = useRef(false); + const style = dropDownStyle(customCSS ?? {}); useEffect(() => { @@ -96,6 +108,14 @@ const DropDown = ({ }, [menuIsOpen, selectRef]); const openMenu = (open: boolean) => { + // If loadOptionsOnMount === false, fetch option list ourselves here + if (open && fetchOptions && !loadOptionsOnMount && !hasStartedPreload.current) { + hasStartedPreload.current = true; + setIsPreloading(true); + fetchOptions("") + .then(fetched => setPreloadedOptions(formatOptions(fetched, required))) + .finally(() => setIsPreloading(false)); + } if (handleMenuIsOpen !== undefined) { handleMenuIsOpen(open); } @@ -164,7 +184,9 @@ const DropDown = ({ overscanCount={4} />
- ) : null; + // react-select passes a single NoOptionsMessage/LoadingMessage node here (not an array) when + // there are no options to list, e.g. before the user has typed anything into an async dropdown. + ) : children; }, [itemHeight]); const filterOptions = (inputValue: string) => { @@ -220,7 +242,8 @@ const DropDown = ({ options, required, ) - : true, + : loadOptionsOnMount || (preloadedOptions ?? []), + isLoading: isPreloading, cacheOptions: true, loadOptions: fetchOptions ? loadOptionsAsync : loadOptions, placeholder: placeholder, diff --git a/src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx b/src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx index b45978c763..8cc406e747 100644 --- a/src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx +++ b/src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useRef } from "react"; import RenderMultiField from "../wizard/RenderMultiField"; import { Acl, @@ -17,8 +17,8 @@ import { } from "../../../utils/resourceUtils"; import { getUserInformation } from "../../../selectors/userInfoSelectors"; import { hasAccess } from "../../../utils/utils"; -import DropDown from "../DropDown"; -import { getAclTemplateText, handleTemplateChange, policiesFiltered, rolesFiltered } from "../../../utils/aclUtils"; +import DropDown, { DropDownOption } from "../DropDown"; +import { getAclTemplateText, handleTemplateChange, policiesFiltered } from "../../../utils/aclUtils"; import { useAppDispatch, useAppSelector } from "../../../store"; import { removeNotificationWizardForm, addNotification } from "../../../slices/notificationSlice"; import { useTranslation } from "react-i18next"; @@ -101,8 +101,10 @@ const ResourceDetailsAccessPolicyTab = ({ // shows, whether a resource has additional actions on top of normal read and write rights const [hasActions, setHasActions] = useState(false); - // list of possible roles - const [roles, setRoles] = useState([]); + // Whether per-role user info is sanitized (hidden) by this Opencast instance. Determines whether the roles + // list is split into a "Users" table and a "Groups & other roles" table, or shown as a single combined table. + // undefined until the initial, minimal check below has resolved. + const [isSanitize, setIsSanitize] = useState(undefined); // this state is used, because the policies should be read-only, if a transaction is currently being performed on a resource const [transactions, setTransactions] = useState({ readOnly: false }); @@ -122,7 +124,12 @@ const ResourceDetailsAccessPolicyTab = ({ setAclTemplates(responseTemplates); setAclActions(responseActions); setHasActions(responseActions.length > 0); - fetchRolesWithTarget("ACL").then(roles => setRoles(roles)); + // Fetch a single role just to read the isSanitize flag off it, rather than fetching every role. + fetchRolesWithTarget("ACL", { limit: 1 }).then(roles => { + if (roles.length > 0) { + setIsSanitize(roles[0].isSanitize); + } + }); if (fetchHasActiveTransactions) { const fetchTransactionResult = await dispatch(fetchHasActiveTransactions(resourceId)).then(unwrapResult); if (fetchTransactionResult.active !== undefined) { @@ -306,13 +313,13 @@ const ResourceDetailsAccessPolicyTab = ({ defaultUser={user} /> - {roles.length > 0 && !roles[0].isSanitize && + {isSanitize === false && <> {hasAccess(viewUsersAccessRole, user) && } @@ -329,7 +335,7 @@ const ResourceDetailsAccessPolicyTab = ({ } } - {roles.length > 0 && roles[0].isSanitize && + {isSanitize === true && <>
@@ -400,7 +404,7 @@ type AccessPolicyTabFormikProps = { export const AccessPolicyTable = ({ isUserTable, policiesFiltered, - rolesFilteredbyPolicies, + hasUser, header, firstColumnHeader, createLabel, @@ -408,12 +412,13 @@ export const AccessPolicyTable = ({ hasActions, transactions, aclActions, - roles, editAccessRole, }: { isUserTable: boolean policiesFiltered: TransformedAcl[] - rolesFilteredbyPolicies: Role[] + // If set, only search roles that do (true) or don't (false) resolve to a user account. + // undefined means no filter (single combined table, sanitized instances). + hasUser: boolean | undefined header?: ParseKeys firstColumnHeader: ParseKeys createLabel: ParseKeys, @@ -421,7 +426,6 @@ export const AccessPolicyTable = ({ hasActions: boolean transactions: { readOnly: boolean } aclActions: { id: string, value: string }[] - roles: Role[] editAccessRole: string }) => { const { t } = useTranslation(); @@ -435,11 +439,26 @@ export const AccessPolicyTable = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const dropdownOptions = useMemo(() => { - return roles.length > 0 - ? formatAclRolesForDropdown(rolesFilteredbyPolicies) - : []; - }, [roles, rolesFilteredbyPolicies]); + // Roles seen in the most recent search, keyed by name, so a selection can be enriched with its + // full Role (including .user) without having to hold or re-fetch the entire roles list. + const roleCacheRef = useRef>(new Map()); + + const fetchRoleOptions = async (inputValue: string): Promise[]> => { + let fetchedRoles = await fetchRolesWithTarget("ACL", { query: inputValue, limit: 50, hasUser }); + + if (aclDefaults && aclDefaults["display_role_filter_blacklist_prefixes"]) { + const prefixes = aclDefaults["display_role_filter_blacklist_prefixes"].split(","); + fetchedRoles = fetchedRoles.filter(role => + !prefixes.some(prefix => role.name.startsWith(prefix)), + ); + } + + for (const role of fetchedRoles) { + roleCacheRef.current.set(role.name, role); + } + + return formatAclRolesForDropdown(fetchedRoles); + }; const createPolicy = (role: string, withUser: boolean): TransformedAcl => { const user = withUser ? { username: "", name: "", email: "" } : undefined; @@ -472,16 +491,6 @@ export const AccessPolicyTable = ({ return newRole; }; - // Filter available options by custom prefixes from the config - if (aclDefaults) { - if (aclDefaults["display_role_filter_blacklist_prefixes"]) { - const prefixes = aclDefaults["display_role_filter_blacklist_prefixes"].split(","); - rolesFilteredbyPolicies = rolesFilteredbyPolicies.filter(role => - !prefixes.some(prefix => role.name.startsWith(prefix)), - ); - } - } - return ( <> {/* list of policy details and interface for changing them */} @@ -554,12 +563,13 @@ export const AccessPolicyTable = ({ { if (element) { - const matchingRole = roles.find(role => role.name === element.value); + const matchingRole = roleCacheRef.current.get(element.value); arrayHelpers.replace(formik.values.policies.findIndex(p => p === policy), { ...policy, role: element.value, diff --git a/src/components/users/partials/wizard/AclAccessPage.tsx b/src/components/users/partials/wizard/AclAccessPage.tsx index c591b7ac82..35d1d9363d 100644 --- a/src/components/users/partials/wizard/AclAccessPage.tsx +++ b/src/components/users/partials/wizard/AclAccessPage.tsx @@ -3,13 +3,12 @@ import { useTranslation } from "react-i18next"; import { FormikProps } from "formik"; import Notifications from "../../../shared/Notifications"; import { - Role, checkAcls, fetchAclActions, fetchAclTemplates, fetchRolesWithTarget, } from "../../../../slices/aclSlice"; -import { policiesFiltered, rolesFiltered } from "../../../../utils/aclUtils"; +import { policiesFiltered } from "../../../../utils/aclUtils"; import { useAppDispatch } from "../../../../store"; import { TransformedAcl } from "../../../../slices/aclDetailsSlice"; import { AccessPolicyTable, TemplateSelector } from "../../../shared/modals/ResourceDetailsAccessPolicyTab"; @@ -40,20 +39,22 @@ const AclAccessPage = ({ const [aclTemplates, setAclTemplates] = useState<{ id: string, value: string }[]>([]); const [aclActions, setAclActions] = useState<{ id: string, value: string }[]>([]); - const [roles, setRoles] = useState([]); + const [isSanitize, setIsSanitize] = useState(undefined); const [loading, setLoading] = useState(false); const editAccessRole = "ROLE_UI_SERIES_DETAILS_ACL_EDIT"; useEffect(() => { - // fetch data about roles, acl templates and actions from backend + // fetch data about acl templates and actions from backend async function fetchData() { setLoading(true); const [responseTemplates, responseActions, responseRoles] = await Promise.all([ - fetchAclTemplates(), fetchAclActions(), fetchRolesWithTarget("ACL")]); + fetchAclTemplates(), fetchAclActions(), fetchRolesWithTarget("ACL", { limit: 1 })]); setAclTemplates(responseTemplates); setAclActions(responseActions); - setRoles(responseRoles); + if (responseRoles.length > 0) { + setIsSanitize(responseRoles[0].isSanitize); + } setLoading(false); } @@ -83,12 +84,12 @@ const AclAccessPage = ({ aclTemplates={aclTemplates} /> - {roles.length > 0 && !roles[0].isSanitize && + {isSanitize === false && <> ({ hasActions={aclActions.length > 0} transactions={{ readOnly: false }} aclActions={aclActions} - roles={roles} editAccessRole={editAccessRole} /> ({ hasActions={aclActions.length > 0} transactions={{ readOnly: false }} aclActions={aclActions} - roles={roles} editAccessRole={editAccessRole} /> } - {roles.length > 0 && roles[0].isSanitize && + {isSanitize === true && <> 0} transactions={{ readOnly: false }} aclActions={aclActions} - roles={roles} editAccessRole={editAccessRole} />
diff --git a/src/slices/aclSlice.ts b/src/slices/aclSlice.ts index 01f6e13052..4ab5a59a60 100644 --- a/src/slices/aclSlice.ts +++ b/src/slices/aclSlice.ts @@ -187,10 +187,18 @@ export const fetchAclTemplateByName = async (name: string) => { }; // fetch roles for select dialogs and access policy pages -export const fetchRolesWithTarget = async (target: string) => { +export const fetchRolesWithTarget = async (target: string, options?: { + query?: string, + limit?: number, + offset?: number, + hasUser?: boolean, // If set, only return roles that do (true) or don't (false) resolve to an actual user account. +}) => { const params = { - limit: -1, + limit: options?.limit ?? -1, + offset: options?.offset, target: target, + query: options?.query, + hasUser: options?.hasUser, }; const response = await axios.get("/admin-ng/acl/roles.json", { params: params });