diff --git a/src/components/Form/IpaDropdownSearch.tsx b/src/components/Form/IpaDropdownSearch.tsx index 49a6f877..f358e070 100644 --- a/src/components/Form/IpaDropdownSearch.tsx +++ b/src/components/Form/IpaDropdownSearch.tsx @@ -21,9 +21,11 @@ import { updateIpaObject } from "src/utils/ipaObjectUtils"; interface IPAParamDefinitionDropdown extends IPAParamDefinition { id?: string; setIpaObject?: (ipaObject: Record) => void; + onSelect?: (username: string) => void; // For non-ipaObjects options: string[]; ariaLabelledBy?: string; onSearch: (value: string) => void; + ipaObject?: Record; } const IpaDropdownSearch = (props: IPAParamDefinitionDropdown) => { @@ -40,6 +42,9 @@ const IpaDropdownSearch = (props: IPAParamDefinitionDropdown) => { if (ipaObject && props.setIpaObject !== undefined) { updateIpaObject(ipaObject, props.setIpaObject, selection, props.name); } + if (props.onSelect !== undefined) { + props.onSelect(selection as string); + } props.onSearch(""); setSearchValue(""); setIsOpen(false); diff --git a/src/components/layouts/DropdownSearch.tsx b/src/components/layouts/DropdownSearch.tsx new file mode 100644 index 00000000..61779529 --- /dev/null +++ b/src/components/layouts/DropdownSearch.tsx @@ -0,0 +1,120 @@ +import React from "react"; +// PatternFly +import { + Divider, + MenuToggle, + MenuToggleElement, + MenuSearch, + MenuSearchInput, + Dropdown, + DropdownItem, + DropdownList, + SearchInput, +} from "@patternfly/react-core"; +import { useGetIDListMutation, GenericPayload } from "src/services/rpc"; + +interface DropdownProps { + id?: string; + onSelect: (value: string) => void; + options: string[]; + ariaLabelledBy?: string; + searchType: string; + value: string; +} + +const DropdownSearch = (props: DropdownProps) => { + const [isOpen, setIsOpen] = React.useState(false); + const [searchValue, setSearchValue] = React.useState(""); + const [options, setOptions] = React.useState(props.options); + + const [retrieveUsers] = useGetIDListMutation({}); + + const onSelect = ( + _event: React.MouseEvent | undefined, + selection: string | number | undefined + ) => { + props.onSelect(selection as string); + setSearchValue(""); + setIsOpen(false); + setOptions(props.options); + }; + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + // Search for specifc users + const submitSearchValue = (value: string) => { + if (value === "") { + // Reset options + setOptions(props.options); + return; + } else { + // Searching + setOptions(["Searching ..."]); + } + retrieveUsers({ + searchValue: value, + sizeLimit: 200, + startIdx: 0, + stopIdx: 199, + entryType: props.searchType, + } as GenericPayload).then((result) => { + if (result && "data" in result) { + setOptions(result.data.list); + } else { + setOptions([]); + } + setIsOpen(true); + }); + }; + + // Removed selected value from options + const filteredOptions = options.filter((item) => item !== props.value); + + return ( + setIsOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + + {props.value} + + )} + ouiaId="BasicDropdown" + shouldFocusToggleOnSelect + isScrollable + > + + + setSearchValue(value)} + onSearch={() => submitSearchValue(searchValue)} + onClear={() => { + setSearchValue(""); + }} + /> + + + + + {filteredOptions.map((option, index) => ( + + {option} + + ))} + + + ); +}; + +export default DropdownSearch; diff --git a/src/components/modals/IdOverrideModals/AddIdOverrideUser.tsx b/src/components/modals/IdOverrideModals/AddIdOverrideUser.tsx new file mode 100644 index 00000000..4f4b99c2 --- /dev/null +++ b/src/components/modals/IdOverrideModals/AddIdOverrideUser.tsx @@ -0,0 +1,500 @@ +import React, { MutableRefObject, useEffect, useRef, useState } from "react"; +// PatternFly +import { + Button, + Spinner, + Text, + TextContent, + TextInput, + TextArea, + ValidatedOptions, +} from "@patternfly/react-core"; +// Layout +import SecondaryButton from "src/components/layouts/SecondaryButton"; +import ModalWithFormLayout from "src/components/layouts/ModalWithFormLayout"; +// Redux +import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query"; +import { SerializedError } from "@reduxjs/toolkit"; +// Modals +import ErrorModal from "src/components/modals/ErrorModal"; +// Forms +import DropdownSearch from "src/components/layouts/DropdownSearch"; +// Hooks +import useAlerts from "src/hooks/useAlerts"; +// Data types +import { IDViewOverrideUser } from "src/utils/datatypes/globalDataTypes"; +import { + useGetIDListMutation, + FindRPCResponse, + GenericPayload, +} from "src/services/rpc"; +import { + useAddIDOverrideUserMutation, + AddUserPayload, +} from "src/services/rpcIdOverrides"; + +export interface PropsToAddUser { + show: boolean; + idview: string; + users: IDViewOverrideUser[]; + handleModalToggle: () => void; + onOpenAddModal: () => void; + onCloseAddModal: () => void; + onRefresh: () => void; +} + +const AddIDOverrideUserModal = (props: PropsToAddUser) => { + // Alerts to show in the UI + const alerts = useAlerts(); + + // Define 'executeCommand' to add user data to IPA server + const [executeAddCommand] = useAddIDOverrideUserMutation(); + const [retrieveUsers] = useGetIDListMutation({}); + + // States for TextInputs + const [overrideUser, setOverrideUser] = useState(""); + const [login, setLogin] = useState(""); + const [gecos, setGecos] = useState(""); + const [uidnumber, setUidNumber] = useState(""); + const [gidnumber, setGidNumber] = useState(""); + const [usercertificate, setCertificate] = useState(""); + const [ipasshpubkey, setSSHKey] = useState(""); + const [loginshell, setShell] = useState(""); + const [homedirectory, setHomeDirectory] = useState(""); + const [description, setDescription] = useState(""); + + const [addSpinning, setAddBtnSpinning] = React.useState(false); + const [addAgainSpinning, setAddAgainBtnSpinning] = + React.useState(false); + const [loading, setLoading] = React.useState(false); + const [userNames, setUserNames] = useState([]); + + // Get our initial list of users + useEffect(() => { + if (!props.show) { + return; + } + setLoading(true); + retrieveUsers({ + searchValue: "", + sizeLimit: 200, + startIdx: 0, + stopIdx: 199, + entryType: "user", + } as GenericPayload).then((result) => { + if (result && "data" in result) { + // Filter out users that are already listed + const existing_users = props.users.map( + (user) => user["ipaanchoruuid"][0] + ); + const users = result.data.list.filter( + (item) => !existing_users.includes(item) + ); + setUserNames(users); + } else { + setUserNames([]); + } + setLoading(false); + }); + }, [props.show, props.users]); + + // Refs + const loginRef = useRef() as MutableRefObject; + const gecosRef = useRef() as MutableRefObject; + const uidNumberRef = useRef() as MutableRefObject; + const gidNumberRef = useRef() as MutableRefObject; + const shellRef = useRef() as MutableRefObject; + const homedirRef = useRef() as MutableRefObject; + + // List of fields + const fields = [ + { + id: "override-user", + name: "User to override", + pfComponent: ( + setOverrideUser(value)} + searchType="user" + value={overrideUser} + /> + ), + }, + { + id: "user-login", + name: "User login", + pfComponent: ( + setLogin(value)} + ref={loginRef} + /> + ), + }, + { + id: "user-gecos", + name: "GECOS", + pfComponent: ( + setGecos(value)} + ref={gecosRef} + /> + ), + }, + { + id: "user-uidnumber", + name: "UID", + pfComponent: ( + setUidNumber(value)} + ref={uidNumberRef} + validated={ + uidnumber !== "" && isNaN(Number(uidnumber)) + ? ValidatedOptions.error + : ValidatedOptions.default + } + /> + ), + }, + { + id: "user-gidnumber", + name: "GID", + pfComponent: ( + setGidNumber(value)} + ref={gidNumberRef} + validated={ + gidnumber !== "" && isNaN(Number(gidnumber)) + ? ValidatedOptions.error + : ValidatedOptions.default + } + /> + ), + }, + { + id: "user-shell", + name: "Login shell", + pfComponent: ( + setShell(value)} + ref={shellRef} + /> + ), + }, + { + id: "user-homedir", + name: "Home directory", + pfComponent: ( + setHomeDirectory(value)} + ref={homedirRef} + /> + ), + }, + { + id: "user-cert", + name: "Certificate", + pfComponent: ( +