Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -55,21 +54,23 @@ const NewAccessPage = <T extends RequiredFormProps>({
// 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<Role[]>([]);
const [isSanitize, setIsSanitize] = useState<boolean | undefined>(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);
}

Expand Down Expand Up @@ -118,21 +119,20 @@ const NewAccessPage = <T extends RequiredFormProps>({
defaultUser={user}
/>

{roles.length > 0 && !roles[0].isSanitize &&
{isSanitize === false &&
<>
{hasAccess(viewUsersAccessRole, user) &&
<AccessPolicyTable
isUserTable={true}
policiesFiltered={policiesFiltered(formik.values.policies, true)}
rolesFilteredbyPolicies={rolesFiltered(roles, true)}
hasUser={true}
header={"EVENTS.EVENTS.DETAILS.ACCESS.ACCESS_POLICY.USERS"}
firstColumnHeader={"EVENTS.EVENTS.DETAILS.ACCESS.ACCESS_POLICY.USER"}
createLabel={"EVENTS.EVENTS.DETAILS.ACCESS.ACCESS_POLICY.NEW_USER"}
formik={formik}
hasActions={aclActions.length > 0}
transactions={{ readOnly: false }}
aclActions={aclActions}
roles={roles}
editAccessRole={editAccessRole}
/>
}
Expand All @@ -141,34 +141,32 @@ const NewAccessPage = <T extends RequiredFormProps>({
<AccessPolicyTable
isUserTable={false}
policiesFiltered={policiesFiltered(formik.values.policies, false)}
rolesFilteredbyPolicies={rolesFiltered(roles, false)}
hasUser={false}
header={"USERS.ACLS.NEW.ACCESS.ACCESS_POLICY.NON_USER_ROLES"}
firstColumnHeader={"EVENTS.EVENTS.DETAILS.ACCESS.ACCESS_POLICY.ROLE"}
createLabel={"EVENTS.EVENTS.DETAILS.ACCESS.ACCESS_POLICY.NEW"}
formik={formik}
hasActions={aclActions.length > 0}
transactions={{ readOnly: false }}
aclActions={aclActions}
roles={roles}
editAccessRole={editAccessRole}
/>
}
</>
}

{roles.length > 0 && roles[0].isSanitize &&
{isSanitize === true &&
<>
<AccessPolicyTable
isUserTable={false}
policiesFiltered={formik.values.policies}
rolesFilteredbyPolicies={roles}
hasUser={undefined}
firstColumnHeader={"EVENTS.EVENTS.DETAILS.ACCESS.ACCESS_POLICY.ROLE"}
createLabel={"EVENTS.EVENTS.DETAILS.ACCESS.ACCESS_POLICY.NEW"}
formik={formik}
hasActions={aclActions.length > 0}
transactions={{ readOnly: false }}
aclActions={aclActions}
roles={roles}
editAccessRole={editAccessRole}
/>
<div className="obj-container">
Expand Down
98 changes: 67 additions & 31 deletions src/components/shared/DropDown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import {
dropDownSpacingTheme,
Expand All @@ -16,11 +16,25 @@ export type DropDownOption<T> = {
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,
style,
}: RowComponentProps<{
names: string[];
}>) {
const name = names[index];
return <div style={style}>{name}</div>;
}

/**
* This component renders a dropdown menu using react-select
*/
const DropDown = <T, >({
ref = React.createRef<SelectInstance<DropDownOption<T>, boolean, GroupBase<DropDownOption<T>>>>(),
ref,
value,
text,
options,
Expand All @@ -40,6 +54,7 @@ const DropDown = <T, >({
optionHeight = 25,
customCSS,
fetchOptions,
loadOptionsOnMount = true,
}: {
ref?: React.RefObject<SelectInstance<DropDownOption<T>, boolean, GroupBase<DropDownOption<T>>> | null>
value: T
Expand All @@ -66,10 +81,22 @@ const DropDown = <T, >({
optionLineHeight?: string
},
fetchOptions?: (inputValue: string) => Promise<DropDownOption<T>[]>
// 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 selectRef = ref;
const internalRef = useRef<SelectInstance<DropDownOption<T>, boolean, GroupBase<DropDownOption<T>>> | 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<DropDownOption<T>[] | undefined>(undefined);
const [isPreloading, setIsPreloading] = useState(false);
const hasStartedPreload = useRef(false);

const style = dropDownStyle<T>(customCSS ?? {});

Expand All @@ -81,6 +108,14 @@ const DropDown = <T, >({
}, [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);
}
Expand All @@ -91,10 +126,11 @@ const DropDown = <T, >({
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) {
Expand Down Expand Up @@ -130,7 +166,7 @@ const DropDown = <T, >({
/**
* Custom component for list virtualization
*/
const MenuList = (props: MenuListProps<DropDownOption<T>, false>) => {
const MenuList = useCallback((props: MenuListProps<DropDownOption<T>, false>) => {
const { children, maxHeight } = props;

return Array.isArray(children) ? (
Expand All @@ -148,19 +184,10 @@ const DropDown = <T, >({
overscanCount={4}
/>
</div>
) : null;
};

function MenuListRow({
index,
names,
style,
}: RowComponentProps<{
names: string[];
}>) {
const name = names[index];
return <div style={style}>{name}</div>;
}
// 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) => {
if (options) {
Expand All @@ -171,14 +198,23 @@ const DropDown = <T, >({
return [];
};

const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);

useEffect(() => {
return () => clearTimeout(debounceTimeoutRef.current);
}, []);

const loadOptionsAsync = (inputValue: string, callback: (options: DropDownOption<T>[]) => 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();
}, SEARCH_DEBOUNCE_MS);
};

const loadOptions = (
Expand Down Expand Up @@ -206,7 +242,8 @@ const DropDown = <T, >({
options,
required,
)
: true,
: loadOptionsOnMount || (preloadedOptions ?? []),
isLoading: isPreloading,
cacheOptions: true,
loadOptions: fetchOptions ? loadOptionsAsync : loadOptions,
placeholder: placeholder,
Expand All @@ -216,7 +253,7 @@ const DropDown = <T, >({
onMenuClose: () => openMenu(false),
isDisabled: disabled,
openMenuOnFocus: openMenuOnFocus,
menuPlacement: menuPlacement ?? "auto",
menuPlacement: menuPlacement,
components: { MenuList },
};

Expand All @@ -229,7 +266,6 @@ const DropDown = <T, >({
<AsyncSelect
ref={selectRef}
{...commonProps}
openMenuOnFocus={false}
noOptionsMessage={() => t("SELECT_NO_MATCHING_RESULTS")}
/>
);
Expand Down
Loading
Loading