diff --git a/src/components/BulkSelectorServicesPrep.tsx b/src/components/BulkSelectorServicesPrep.tsx index 0c83bd27..0133dfc3 100644 --- a/src/components/BulkSelectorServicesPrep.tsx +++ b/src/components/BulkSelectorServicesPrep.tsx @@ -110,13 +110,13 @@ const BulkSelectorServicesPrep = (props: PropsToBulkSelectorPrep) => { selectableServicesList: Service[] ) => { props.elementData.changeSelectedServiceIds( - isSelecting ? selectableServicesList.map((r) => r.id) : [] + isSelecting ? selectableServicesList.map((r) => r.krbcanonicalname) : [] ); // Update selected elements const serviceNamesArray: string[] = []; selectableServicesList.map((service) => { - serviceNamesArray.push(service.id); + serviceNamesArray.push(service.krbcanonicalname); }); props.elementData.updateSelectedServices(serviceNamesArray); @@ -133,7 +133,7 @@ const BulkSelectorServicesPrep = (props: PropsToBulkSelectorPrep) => { servicesIdArray.push(service); }); selectableServicesList.map((service) => { - servicesIdArray.push(service.id); + servicesIdArray.push(service.krbcanonicalname); }); } @@ -165,20 +165,22 @@ const BulkSelectorServicesPrep = (props: PropsToBulkSelectorPrep) => { selectableServicesList: Service[] ) => { props.elementData.changeSelectedServiceIds( - isSelecting ? selectableServicesList.map((r) => r.id) : [] + isSelecting ? selectableServicesList.map((r) => r.krbcanonicalname) : [] ); // Update selected elements const serviceNamesArray: string[] = []; selectableServicesList.map((service) => { - serviceNamesArray.push(service.id); + serviceNamesArray.push(service.krbcanonicalname); }); props.elementData.updateSelectedServices(serviceNamesArray); // Enable/disable 'Delete' button if (isSelecting) { const servicesIdArray: string[] = []; - selectableServicesList.map((service) => servicesIdArray.push(service.id)); + selectableServicesList.map((service) => + servicesIdArray.push(service.krbcanonicalname) + ); props.elementData.changeSelectedServiceIds(servicesIdArray); props.elementData.updateSelectedServices(servicesIdArray); props.buttonsData.updateIsDeleteButtonDisabled(false); @@ -194,8 +196,9 @@ const BulkSelectorServicesPrep = (props: PropsToBulkSelectorPrep) => { // - Some rows selected: null (-) // - None selected: false (empty) const areAllElementsSelected: boolean | null = + props.elementData.selectedServices.length > 0 && props.elementData.selectedServices.length === - props.elementData.selectableServicesTable.length + props.elementData.selectableServicesTable.length ? true : props.elementData.selectedServices.length > 0 ? null @@ -241,7 +244,9 @@ const BulkSelectorServicesPrep = (props: PropsToBulkSelectorPrep) => { // The 'currentPageAlreadySelected' should be set when elements are selected useEffect(() => { const found = props.shownElementsList.every( - (service) => props.elementData.selectedServices.indexOf(service.id) >= 0 + (service) => + props.elementData.selectedServices.indexOf(service.krbcanonicalname) >= + 0 ); if (found) { @@ -254,7 +259,9 @@ const BulkSelectorServicesPrep = (props: PropsToBulkSelectorPrep) => { if ( !props.shownElementsList.some( (service) => - props.elementData.selectedServices.indexOf(service.id) >= 0 + props.elementData.selectedServices.indexOf( + service.krbcanonicalname + ) >= 0 ) ) { props.selectedPerPageData.updateSelectedPerPage(0); diff --git a/src/components/ManagedBy/ManagedByDeleteModal.tsx b/src/components/ManagedBy/ManagedByDeleteModal.tsx index c72de183..b258ac41 100644 --- a/src/components/ManagedBy/ManagedByDeleteModal.tsx +++ b/src/components/ManagedBy/ManagedByDeleteModal.tsx @@ -58,6 +58,7 @@ const ManagedByDeleteModal = (props: PropsToDeleteModal) => { elementsToDelete={props.groupNamesToDelete} columnNames={["Host"]} elementType={props.tabData.tabName} + idAttr="fqdn" /> ), }, diff --git a/src/components/ServicesSections/AllowedCreateKeytab.tsx b/src/components/ServicesSections/AllowedCreateKeytab.tsx index 2f7985d5..f2eee213 100644 --- a/src/components/ServicesSections/AllowedCreateKeytab.tsx +++ b/src/components/ServicesSections/AllowedCreateKeytab.tsx @@ -7,7 +7,7 @@ import CreateKeytabUserGroupsTable from "../tables/HostsSettings/CreateKeytabUse import CreateKeytabHostsTable from "../tables/HostsSettings/CreateKeytabHostsTable"; import CreateKeytabHostGroupsTable from "../tables/HostsSettings/CreateKeytabHostGroupsTable"; // Data types -import { Service } from "src/utils/datatypes/globalDataTypes"; +import { Service } from "../../utils/datatypes/globalDataTypes"; interface PropsToAllowCreateKeytab { service: Service; @@ -17,12 +17,12 @@ const AllowedCreateKeytab = (props: PropsToAllowCreateKeytab) => { return ( - - + + - - + + ); diff --git a/src/components/ServicesSections/AllowedRetrieveKeytab.tsx b/src/components/ServicesSections/AllowedRetrieveKeytab.tsx index 52da4e96..c906db6f 100644 --- a/src/components/ServicesSections/AllowedRetrieveKeytab.tsx +++ b/src/components/ServicesSections/AllowedRetrieveKeytab.tsx @@ -7,7 +7,7 @@ import RetrieveKeytabUserGroupsTable from "../tables/HostsSettings/RetrieveKeyta import RetrieveKeytabHostsTable from "../tables/HostsSettings/RetrieveKeytabHostsTable"; import RetrieveKeytabHostGroupsTable from "../tables/HostsSettings/RetrieveKeytabHostGroupTable"; // Data types -import { Service } from "src/utils/datatypes/globalDataTypes"; +import { Service } from "../../utils/datatypes/globalDataTypes"; interface PropsToAllowCreateKeytab { service: Service; @@ -17,12 +17,12 @@ const AllowedRetrieveKeytab = (props: PropsToAllowCreateKeytab) => { return ( - - + + - - + + ); diff --git a/src/components/ServicesSections/ServiceSettings.tsx b/src/components/ServicesSections/ServiceSettings.tsx index 2269914c..2da4f678 100644 --- a/src/components/ServicesSections/ServiceSettings.tsx +++ b/src/components/ServicesSections/ServiceSettings.tsx @@ -39,7 +39,7 @@ const ServiceSettings = (props: PropsToServiceSettings) => { >([ { id: 0, - alias: props.service.id, + alias: props.service.krbcanonicalname, }, ]); @@ -261,7 +261,7 @@ const ServiceSettings = (props: PropsToServiceSettings) => { void; + onOpenAddModal?: () => void; + onCloseAddModal?: () => void; + onRefresh?: () => void; } const AddService = (props: PropsToAddService) => { // Set dispatch (Redux) const dispatch = useAppDispatch(); + // Alerts to show in the UI + const alerts = useAlerts(); + + const [executeServiceAddCommand] = useAddServiceMutation(); + // Set host names list - const hostsList = useAppSelector((state) => state.hosts.hostsList); - const hostNamesList = hostsList.map((hostName) => hostName.fqdn); + const [addSpinning, setAddBtnSpinning] = React.useState(false); + const [addAgainSpinning, setAddAgainBtnSpinning] = + React.useState(false); // 'Service' select const [isServiceOpen, setIsServiceOpen] = useState(false); @@ -75,10 +97,16 @@ const AddService = (props: PropsToAddService) => { setIsServiceOpen(false); }; + // Validation fields + const [hostNameValidation, setHostNameValidation] = useState({ + isError: false, + message: "", + pfError: ValidatedOptions.default, + }); + // 'Host name' select const [isHostNameOpen, setIsHostNameOpen] = useState(false); const [hostNameSelected, setHostNameSelected] = useState(""); - const hostNameOptions = hostNamesList; const hostNameOnToggle = () => { setIsHostNameOpen(!isHostNameOpen); @@ -105,12 +133,6 @@ const AddService = (props: PropsToAddService) => { ); - // 'Force' checkbox - const [isForceChecked, setIsForceChecked] = useState(false); - - // 'Skip host check' checkbox - const [isSkipHostChecked, setIsSkipHostChecked] = useState(false); - // Validation fields const [serviceValidation, setServiceValidation] = useState({ isError: false, @@ -118,12 +140,6 @@ const AddService = (props: PropsToAddService) => { pfError: ValidatedOptions.default, }); - const [hostNameValidation, setHostNameValidation] = useState({ - isError: false, - message: "", - pfError: ValidatedOptions.default, - }); - const serviceValidationHandler = () => { if (serviceSelected === "") { const serviceVal = { @@ -189,13 +205,25 @@ const AddService = (props: PropsToAddService) => { resetHostNameError(); }; + // Checklbox handlers + const [forceCheckbox, setForceCheckbox] = useState(false); + const [skipHostCheckbox, setSkipHostCheckbox] = useState(false); + // Force + const handleForceCheckbox = () => { + setForceCheckbox(!forceCheckbox); + }; + // Skip host check + const handleSkipHostCheckCheckbox = () => { + setSkipHostCheckbox(!skipHostCheckbox); + }; + // Add button is disabled until the user fills the required fields const [buttonDisabled, setButtonDisabled] = useState(true); useEffect(() => { if (serviceSelected !== "" && hostNameSelected !== "") { setButtonDisabled(false); } else { - setButtonDisabled(false); + setButtonDisabled(true); } }, [serviceSelected, hostNameSelected]); @@ -250,7 +278,7 @@ const AddService = (props: PropsToAddService) => { isOpen={isHostNameOpen} aria-labelledby="host name" > - {hostNameOptions.map((option, index) => ( + {props.hostsList.map((option, index) => ( {option} @@ -275,11 +303,12 @@ const AddService = (props: PropsToAddService) => { pfComponent: ( ), }, @@ -289,11 +318,12 @@ const AddService = (props: PropsToAddService) => { pfComponent: ( ), }, @@ -303,8 +333,12 @@ const AddService = (props: PropsToAddService) => { const cleanAllFields = () => { setServiceSelected(""); setHostNameSelected(""); - setIsForceChecked(false); - setIsSkipHostChecked(false); + setForceCheckbox(false); + setSkipHostCheckbox(false); + setIsHostNameOpen(false); + setIsServiceOpen(false); + setAddBtnSpinning(false); + setAddAgainBtnSpinning(false); }; // Clean fields and close modal (To prevent data persistence when reopen modal) @@ -324,56 +358,176 @@ const AddService = (props: PropsToAddService) => { } else return true; }; - // Add new 'Service' - const addServiceHandler = () => { + // Define status flags to determine user added successfully or error + let isAdditionSuccess = true; + + // Track which button has been clicked ('onAddUser' or 'onAddAndAddAnother') + // to better handle the 'retry' function and its behavior + let onAddServiceClicked = true; + + // Add host data + const addServiceData = async () => { + const newServicePayload = { + service: serviceSelected + "/" + hostNameSelected, + force: forceCheckbox, + skip_host_check: skipHostCheckbox, + } as ServiceAddPayload; + + // Add host via API call + await executeServiceAddCommand(newServicePayload).then((service) => { + if ("data" in service) { + const data = service.data as FindRPCResponse; + const error = data.error as FetchBaseQueryError | SerializedError; + const result = data.result; + + if (error) { + // Set status flag: error + isAdditionSuccess = false; + // Handle error + handleAPIError(error); + } else { + // Set alert: success + alerts.addAlert( + "add-service-success", + "New service added", + "success" + ); + + // Dispatch host data to redux + const updatedServiceList = result.result as unknown as Service; + dispatch(addService(updatedServiceList)); + // Set status flag: success + isAdditionSuccess = true; + // Refresh data + if (props.onRefresh !== undefined) { + props.onRefresh(); + } + } + } + }); + }; + + const addAndAddAnotherHandler = () => { + onAddServiceClicked = false; const validation = validateFields(); if (validation) { - const newService: Service = { - id: serviceSelected + "/" + hostNameSelected, - serviceType: serviceSelected, - host: hostNameSelected, - }; - // TODO: Manage 'Force' and 'Skip host check' behaviors - dispatch(addService(newService)); - cleanAndCloseModal(); + setAddAgainBtnSpinning(true); + addServiceData().then(() => { + if (isAdditionSuccess) { + // Do not close the modal, but clean fields & reset validations + cleanAllFields(); + resetValidations(); + } else { + // Close the modal without cleaning fields + if (props.onCloseAddModal !== undefined) { + props.onCloseAddModal(); + } + setAddAgainBtnSpinning(false); + } + }); } }; - const addAndAddAnotherServiceHandler = () => { + const addServiceHandler = () => { + onAddServiceClicked = true; const validation = validateFields(); if (validation) { - const newService: Service = { - id: serviceSelected + "/" + hostNameSelected, - serviceType: serviceSelected, - host: hostNameSelected, - }; - // TODO: Manage 'Force' and 'Skip host check' behaviors - dispatch(addService(newService)); - // Do not close the modal, but clean fields & reset validations - cleanAllFields(); - resetValidations(); + setAddBtnSpinning(true); + addServiceData().then(() => { + if (!isAdditionSuccess) { + // Close the modal without cleaning fields + if (props.onCloseAddModal !== undefined) { + props.onCloseAddModal(); + } + setAddBtnSpinning(false); + } else { + // Clean data and close modal + cleanAndCloseModal(); + } + }); } }; + // Error handling + const [isModalErrorOpen, setIsModalErrorOpen] = useState(false); + const [errorTitle, setErrorTitle] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const closeAndCleanErrorParameters = () => { + setIsModalErrorOpen(false); + setErrorTitle(""); + setErrorMessage(""); + }; + + const onCloseErrorModal = () => { + closeAndCleanErrorParameters(); + // Show Add modal + if (props.onOpenAddModal !== undefined) { + props.onOpenAddModal(); + } + }; + + const onRetry = () => { + // Keep the add modal closed until the operation is done... + if (props.onCloseAddModal !== undefined) { + props.onCloseAddModal(); + } + + // Close the error modal + closeAndCleanErrorParameters(); + + // Repeats the same previous operation + if (onAddServiceClicked) { + addServiceHandler(); + } else { + addAndAddAnotherHandler(); + } + }; + + const errorModalActions = [ + + Retry + , + , + ]; + + const handleAPIError = (error: FetchBaseQueryError | SerializedError) => { + if ("code" in error) { + setErrorTitle("IPA error " + error.code + ": " + error.name); + if (error.message !== undefined) { + setErrorMessage(error.message); + } + } + setIsModalErrorOpen(true); + }; + // Buttons that will be shown at the end of the form const modalActions = [ - Add + {addSpinning ? "Adding" : "Add"} , - Add and add another + {addAgainSpinning ? "Adding" : "Add and add another"} , , + ]; + + const handleAPIError = (error: FetchBaseQueryError | SerializedError) => { + if ("code" in error) { + setErrorTitle("IPA error " + error.code + ": " + error.name); + if (error.message !== undefined) { + setErrorMessage(error.message); + } + } else if ("data" in error) { + const errorData = error.data as ErrorData; + const errorCode = errorData.code as string; + const errorName = errorData.name as string; + const errorMessage = errorData.error as string; + + setErrorTitle("IPA error " + errorCode + ": " + errorName); + setErrorMessage(errorMessage); + } + setIsModalErrorOpen(true); + }; + const deleteServices = () => { - props.selectedElementsData.selectedElements.map((service) => { - dispatch(removeService(service)); - }); - props.selectedElementsData.updateSelectedElements([]); - props.buttonsData.updateIsDeleteButtonDisabled(true); - props.buttonsData.updateIsDeletion(true); - closeModal(); + setBtnSpinning(true); + + // Delete elements + executeServicesDelCommand(props.selectedServicesData.selectedElements).then( + (response) => { + if ("data" in response) { + const data = response.data as BatchRPCResponse; + const result = data.result; + + if (result) { + if ("error" in result.results[0] && result.results[0].error) { + const errorData = { + code: result.results[0].error_code, + name: result.results[0].error_name, + error: result.results[0].error, + } as ErrorData; + + const error = { + status: "CUSTOM_ERROR", + data: errorData, + } as FetchBaseQueryError; + + // Handle error + handleAPIError(error); + } else { + // Update data from Redux + props.selectedServicesData.selectedElements.map((service) => { + dispatch(removeService(service)); + }); + + props.selectedServicesData.updateSelectedElements([]); + props.buttonsData.updateIsDeleteButtonDisabled(true); + props.buttonsData.updateIsDeletion(true); + + alerts.addAlert( + "remove-services-success", + "Services removed", + "success" + ); + + setBtnSpinning(false); + closeModal(); + // Refresh data + if (props.onRefresh !== undefined) { + props.onRefresh(); + } + } + } + } + } + ); }; // Set the Modal and Action buttons for 'Delete' option @@ -94,8 +196,12 @@ const DeleteServices = (props: PropsToDeleteServices) => { variant="danger" onClick={deleteServices} form="delete-services-modal" + spinnerAriaValueText="Deleting" + spinnerAriaLabel="Deleting" + isLoading={spinning} + isDisabled={spinning} > - Delete + {spinning ? "Deleting" : "Delete"} ,