From e471b9355c9f6298db5ceb0654a4b3aee9b98d49 Mon Sep 17 00:00:00 2001 From: Mark Reynolds Date: Tue, 15 Oct 2024 17:51:23 -0400 Subject: [PATCH] id views - implement Applied to tab Also expanded alerts to accept react nodes for the alert title Fixes: https://github.com/freeipa/freeipa-webui/issues/585 Signed-off-by: Mark Reynolds --- src/hooks/useAlerts.tsx | 8 +- src/navigation/AppRoutes.tsx | 4 + src/pages/IDViews/IDViews.tsx | 8 +- src/pages/IDViews/IDViewsAppliedTo.tsx | 729 ++++++++++++++++++ src/pages/IDViews/IDViewsAppliedToTable.tsx | 120 +++ src/pages/IDViews/IDViewsOverrideGroups.tsx | 47 +- .../IDViews/IDViewsOverrideGroupsTable.tsx | 1 - src/pages/IDViews/IDViewsOverrideUsers.tsx | 48 +- .../IDViews/IDViewsOverrideUsersTable.tsx | 1 - src/pages/IDViews/IDViewsTabs.tsx | 18 +- src/services/rpcIDViews.ts | 31 +- src/utils/datatypes/globalDataTypes.ts | 2 + src/utils/idOverrideUtils.tsx | 1 + src/utils/idViewUtils.tsx | 1 + tests/features/id_view_appliedto.feature | 185 +++++ tests/features/id_view_handling.feature | 4 +- .../idoverride_users_handling.feature | 2 +- tests/features/steps/common.ts | 14 + tests/features/steps/user_handling.ts | 2 +- 19 files changed, 1116 insertions(+), 110 deletions(-) create mode 100644 src/pages/IDViews/IDViewsAppliedTo.tsx create mode 100644 src/pages/IDViews/IDViewsAppliedToTable.tsx create mode 100644 tests/features/id_view_appliedto.feature diff --git a/src/hooks/useAlerts.tsx b/src/hooks/useAlerts.tsx index 7fcbca20..67186809 100644 --- a/src/hooks/useAlerts.tsx +++ b/src/hooks/useAlerts.tsx @@ -11,7 +11,7 @@ export type AlertVariant = "custom" | "danger" | "warning" | "success" | "info"; export interface AlertInfo { name: string; - title: string; + title: string | React.ReactNode; variant: AlertVariant; } @@ -22,7 +22,11 @@ export function useAlerts() { return alerts.filter((alert) => alert.name !== name); }; - const addAlert = (name: string, title: string, variant: AlertVariant) => { + const addAlert = ( + name: string, + title: string | React.ReactNode, + variant: AlertVariant + ) => { const alert: AlertInfo = { name: name, title: title, diff --git a/src/navigation/AppRoutes.tsx b/src/navigation/AppRoutes.tsx index cea06006..a61bbeb8 100644 --- a/src/navigation/AppRoutes.tsx +++ b/src/navigation/AppRoutes.tsx @@ -292,6 +292,10 @@ export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => { path="override-groups" element={} /> + } + /> diff --git a/src/pages/IDViews/IDViews.tsx b/src/pages/IDViews/IDViews.tsx index 9eecdccf..fbe62b03 100644 --- a/src/pages/IDViews/IDViews.tsx +++ b/src/pages/IDViews/IDViews.tsx @@ -376,7 +376,9 @@ const IDViews = () => { if (response.data.result) { alerts.addAlert( "unapply-id-views-hosts-success", - "ID views unapplied from hosts", + "ID views unapplied from " + + response.data.result["completed"] + + " hosts", "success" ); // Refresh data @@ -406,7 +408,9 @@ const IDViews = () => { if (response.data.result) { alerts.addAlert( "unapply-id-views-hosts-success", - "ID views unapplied from host groups", + "ID views unapplied from " + + response.data.result["completed"] + + " hosts", "success" ); // Refresh data diff --git a/src/pages/IDViews/IDViewsAppliedTo.tsx b/src/pages/IDViews/IDViewsAppliedTo.tsx new file mode 100644 index 00000000..3bd66134 --- /dev/null +++ b/src/pages/IDViews/IDViewsAppliedTo.tsx @@ -0,0 +1,729 @@ +import React, { useEffect, useState } from "react"; +// PatternFly +import { + Button, + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, + MenuToggleElement, + Page, + PageSection, + PageSectionVariants, + PaginationVariant, + SearchInput, + TextVariants, +} from "@patternfly/react-core"; +import { + InnerScrollContainer, + OuterScrollContainer, +} from "@patternfly/react-table"; +// Layouts +import DualListLayout from "src/components/layouts/DualListLayout"; +import ToolbarLayout, { + ToolbarItem, +} from "src/components/layouts/ToolbarLayout"; +import HelpTextWithIconLayout from "src/components/layouts/HelpTextWithIconLayout"; +// Components +import PaginationLayout from "src/components/layouts/PaginationLayout"; +// Tables +import IDViewsAppliedToTable from "src/pages/IDViews/IDViewsAppliedToTable"; +// Utils +import { partialViewToView } from "src/utils/idViewUtils"; +// Data types +import { IDView } from "src/utils/datatypes/globalDataTypes"; +// Hooks +import { useAlerts } from "src/hooks/useAlerts"; +import useUpdateRoute from "src/hooks/useUpdateRoute"; +import useListPageSearchParams from "src/hooks/useListPageSearchParams"; +// Errors +import useApiError from "src/hooks/useApiError"; +import ModalErrors from "src/components/errors/ModalErrors"; +// Icons +import OutlinedQuestionCircleIcon from "@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon"; +// RPC client +import { ErrorResult } from "../../services/rpc"; +import { + ViewApplyPayload, + useGetIDViewsFullDataQuery, + useApplyHostsMutation, + useApplyHostGroupsMutation, + useUnapplyHostsMutation, + useUnapplyHostgroupsMutation, +} from "../../services/rpcIDViews"; + +export interface AppliesToProps { + idView: IDView; + onRefresh: () => void; +} + +const IDViewsAppliedTo = (props: AppliesToProps) => { + // Update current route data to Redux and highlight the current page in the Nav bar + const { browserTitle } = useUpdateRoute({ + pathname: "id-views", + noBreadcrumb: true, + }); + + // API + const [executeApplyHosts] = useApplyHostsMutation(); + const [executeApplyHostgroups] = useApplyHostGroupsMutation(); + const [executeUnapplyHosts] = useUnapplyHostsMutation(); + const [executeUnapplyHostgroups] = useUnapplyHostgroupsMutation(); + + // Set the page title to be shown in the browser tab + React.useEffect(() => { + document.title = browserTitle; + }, [browserTitle]); + + // Initialize views (Redux) + const [hostsList, setHostsList] = useState([]); + const [shownHostsList, setShownHostsList] = useState([]); + + // Alerts to show in the UI + const alerts = useAlerts(); + + // URL parameters: page number, page size, search value + const { page, setPage, perPage, setPerPage, searchValue, setSearchValue } = + useListPageSearchParams(); + + // Handle API calls errors + const modalErrors = useApiError([]); + + // Table comps + const [selectedPerPage, setSelectedPerPage] = useState(0); + const [totalCount, setTotalCount] = useState(0); + + const updateSelectedPerPage = (selected: number) => { + setSelectedPerPage(selected); + }; + const updatePage = (newPage: number) => { + setPage(newPage); + }; + const updatePerPage = (newSetPerPage: number) => { + setPerPage(newSetPerPage); + }; + + // 'Delete' button state + const [isDeleteButtonDisabled, setIsDeleteButtonDisabled] = + useState(true); + + const updateIsUnapplyButtonDisabled = (value: boolean) => { + setIsDeleteButtonDisabled(value); + }; + + const [isUnapply, setIsUnapply] = useState(false); + const updateIsUnapply = (value: boolean) => { + setIsUnapply(value); + }; + + const updateShownHosts = (newShownHostsList: string[]) => { + setShownHostsList(newShownHostsList); + }; + + // Page indexes + const firstIdx = (page - 1) * perPage; + const lastIdx = page * perPage; + + const idViewFullDataQuery = useGetIDViewsFullDataQuery(props.idView.cn); + const idViewFullData = idViewFullDataQuery.data; + + useEffect(() => { + if ( + idViewFullData && + idViewFullData.idView && + !idViewFullDataQuery.isFetching + ) { + const idView = partialViewToView(idViewFullData.idView); + const length = idView["appliedtohosts"].length; + const shownHosts: string[] = []; + for (let i = firstIdx; i < length && i < lastIdx; i++) { + shownHosts.push(idView["appliedtohosts"][i]); + } + setHostsList(idView["appliedtohosts"]); + setShownHostsList(shownHosts); + } + }, [idViewFullData, idViewFullDataQuery.isFetching]); + + // Always refetch data when the component is loaded. + // This ensures the data is always up-to-date. + useEffect(() => { + idViewFullDataQuery.refetch(); + }, [page, perPage]); + + // Refresh button handling + const refreshViewsData = () => { + setTotalCount(0); + clearSelectedHosts(); + setPage(1); + props.onRefresh(); + }; + + const updateSearchValue = (value: string) => { + setSearchValue(value); + }; + + useEffect(() => { + const searchResults: string[] = []; + for (let i = 0; i < hostsList.length; i++) { + if (hostsList[i].toLowerCase().includes(searchValue.toLowerCase())) { + searchResults.push(hostsList[i]); + } + } + setShownHostsList(searchResults); + }, [searchValue]); + + const [selectedHosts, setSelectedHosts] = useState([]); + const clearSelectedHosts = () => { + const emptyList: string[] = []; + setSelectedHosts(emptyList); + }; + + // Unapply functions + const [showUnapplyHostGroupModal, setShowUnapplyHostGroupModal] = + useState(false); + const [applySpinning, setApplySpinning] = useState(false); + + const openUnapplyHostgroupModal = () => { + setShowUnapplyHostGroupModal(true); + }; + const closeUnapplyHostgroupModal = () => { + setShowUnapplyHostGroupModal(false); + }; + + const onUnapplyHosts = () => { + setApplySpinning(true); + + // unapply views from hosts + executeUnapplyHosts(selectedHosts).then((response) => { + if ("data" in response) { + if (response.data.result) { + const failed_response = response.data.result["failed"]; + if (failed_response.memberhost.host.length > 0) { + const failed_hosts = failed_response.memberhost.host; + const failed_msgs: string[] = []; + for (let i = 0; i < failed_hosts.length; i++) { + failed_msgs.push(failed_hosts[i][0]); + } + const alert_msg = ( + <> + ID View cannot be unapplied to IPA master + {failed_msgs.length > 1 ? "s" : ""}: +
    + {failed_msgs.map((item) => ( +
  • + • {item} +
  • + ))} +
+ {response.data.result["completed"] > 0 + ? "However " + + response.data.result["completed"] + + " hosts were unapplied" + : ""} + + ); + alerts.addAlert("unapply-id-view-hosts-error", alert_msg, "danger"); + } else { + alerts.addAlert( + "unapply-id-view-hosts-success", + "ID view unapplied from " + + response.data.result["completed"] + + " hosts", + "success" + ); + } + // Refresh data + refreshViewsData(); + } else if (response.data.error) { + // Show toast notification: error + const errorMessage = response.data.error as ErrorResult; + alerts.addAlert( + "unapply-id-view-hosts-error", + "ID view unapplied from hosts failed: " + errorMessage.message, + "danger" + ); + } + } + setApplySpinning(false); + }); + }; + + const onUnapplyHostgroups = (hostGroups: string[]) => { + setApplySpinning(true); + + // unapply views from host groups + executeUnapplyHostgroups(hostGroups).then((response) => { + if ("data" in response) { + if (response.data.result) { + const failed_response = response.data.result["failed"]; + if (failed_response.memberhost.host.length > 0) { + const failed_hosts = failed_response.memberhost.host; + const failed_msgs: string[] = []; + for (let i = 0; i < failed_hosts.length; i++) { + failed_msgs.push(failed_hosts[i][0]); + } + const alert_msg = ( + <> + ID View cannot be unapplied to IPA master + {failed_msgs.length > 1 ? "s" : ""}: +
    + {failed_msgs.map((item) => ( +
  • + • {item} +
  • + ))} +
+ {response.data.result["completed"] > 0 + ? "However " + + response.data.result["completed"] + + " hosts were unapplied" + : ""} + + ); + alerts.addAlert( + "unapply-id-view-host-groups-error", + alert_msg, + "danger" + ); + } else { + alerts.addAlert( + "unapply-id-view-hosts-success", + "ID view unapplied from " + + response.data.result["completed"] + + " hosts", + "success" + ); + } + // Refresh data + refreshViewsData(); + } else if (response.data.error) { + // Show toast notification: error + const errorMessage = response.data.error as ErrorResult; + alerts.addAlert( + "unapply-id-view-hosts-error", + "ID view unapplied from host groups failed: " + + errorMessage.message, + "danger" + ); + } + } + setApplySpinning(false); + closeUnapplyHostgroupModal(); + }); + }; + + // Apply functions + const [showApplyHostModal, setShowApplyHostModal] = useState(false); + const [showApplyHostGroupModal, setShowApplyHostGroupModal] = useState(false); + const openApplyHostModal = () => { + setShowApplyHostModal(true); + }; + const closeApplyHostModal = () => { + setShowApplyHostModal(false); + }; + const openApplyHostGroupModal = () => { + setShowApplyHostGroupModal(true); + }; + const closeApplyHostGroupModal = () => { + setShowApplyHostGroupModal(false); + }; + + const onApplyHosts = (hosts: string[]) => { + setApplySpinning(true); + + const payload = { + viewName: props.idView.cn, + items: hosts, + } as ViewApplyPayload; + + // Apply views to hosts + executeApplyHosts(payload).then((response) => { + if ("data" in response) { + if (response.data.result) { + const failed_response = response.data.result["failed"]; + if (failed_response.memberhost.host.length > 0) { + const failed_hosts = failed_response.memberhost.host; + const failed_msgs: string[] = []; + for (let i = 0; i < failed_hosts.length; i++) { + failed_msgs.push(failed_hosts[i][0]); + } + const alert_msg = ( + <> + ID View cannot be applied to IPA master + {failed_msgs.length > 1 ? "s" : ""}: +
    + {failed_msgs.map((item) => ( +
  • + • {item} +
  • + ))} +
+ {response.data.result["completed"] > 0 + ? "However " + + response.data.result["completed"] + + " hosts were applied" + : ""} + + ); + alerts.addAlert("apply-id-view-hosts-error", alert_msg, "danger"); + } else { + alerts.addAlert( + "apply-id-view-hosts-success", + "ID view applied to " + + response.data.result["completed"] + + " hosts", + "success" + ); + } + // Refresh data + refreshViewsData(); + } else if (response.data.error) { + // Show toast notification: error + const errorMessage = response.data.error as ErrorResult; + alerts.addAlert( + "apply-id-view-hosts-error", + "ID view applied to hosts failed: " + errorMessage.message, + "danger" + ); + } + } + setApplySpinning(false); + closeApplyHostModal(); + }); + }; + + const onApplyHostGroups = (hostGroups: string[]) => { + setApplySpinning(true); + + const payload = { + viewName: props.idView.cn, + items: hostGroups, + } as ViewApplyPayload; + + // Apply views to host groups + executeApplyHostgroups(payload).then((response) => { + if ("data" in response) { + if (response.data.result) { + const failed_response = response.data.result["failed"]; + if (failed_response.memberhost.host.length > 0) { + const failed_hosts = failed_response.memberhost.host; + const failed_msgs: string[] = []; + for (let i = 0; i < failed_hosts.length; i++) { + failed_msgs.push(failed_hosts[i][0]); + } + const alert_msg = ( + <> + ID View cannot be applied to IPA master + {failed_msgs.length > 1 ? "s" : ""}: +
    + {failed_msgs.map((item) => ( +
  • + • {item} +
  • + ))} +
+ {response.data.result["completed"] > 0 + ? "However " + + response.data.result["completed"] + + " hosts were applied" + : ""} + + ); + alerts.addAlert("apply-id-view-hosts-error", alert_msg, "danger"); + } else { + alerts.addAlert( + "apply-id-view-host-groups-success", + "ID view applied to " + + response.data.result["completed"] + + " hosts", + "success" + ); + } + // Refresh data + refreshViewsData(); + } else if (response.data.error) { + // Show toast notification: error + const errorMessage = response.data.error as ErrorResult; + alerts.addAlert( + "apply-id-view-host-groups-error", + "ID view applied to host groups failed: " + errorMessage.message, + "danger" + ); + } + } + setApplySpinning(false); + closeApplyHostModal(); + }); + }; + + // Data wrappers + // - 'PaginationLayout' + const paginationData = { + page, + perPage, + updatePage, + updatePerPage, + updateSelectedPerPage, + updateShownElementsList: updateShownHosts, + totalCount, + }; + + const selectedPerPageData = { + selectedPerPage, + updateSelectedPerPage, + }; + + const hostsTableData = { + selectedHosts, + hostsList, + setSelectedHosts, + clearSelectedHosts, + }; + + const viewsTableButtonsData = { + updateIsUnapplyButtonDisabled, + isUnapply, + updateIsUnapply, + }; + + // Dropdown select feature + const [isApplyOpen, setIsApplyOpen] = React.useState(false); + const onToggleClickApply = () => { + setIsApplyOpen(!isApplyOpen); + }; + const onSelectApply = () => { + setIsApplyOpen(false); + }; + + const [isUnapplyOpen, setIsUnapplyOpen] = React.useState(false); + const onToggleClickUnapply = () => { + setIsUnapplyOpen(!isUnapplyOpen); + }; + const onSelectUnapply = () => { + setIsUnapplyOpen(false); + }; + + // List of toolbar items + const toolbarItems: ToolbarItem[] = [ + { + key: 1, + element: ( + updateSearchValue(value)} + onClear={() => setSearchValue("")} + /> + ), + toolbarItemVariant: "search-filter", + toolbarItemSpacer: { default: "spacerMd" }, + }, + { + key: 2, + toolbarItemVariant: "separator", + }, + { + key: 3, + element: ( + + ), + }, + { + key: 4, + element: ( + setIsApplyOpen(isApplyOpen)} + toggle={(toggleRef: React.Ref) => ( + + Apply + + )} + ouiaId="ApplyDropdown" + shouldFocusToggleOnSelect + className="pf-m-small" + > + + setShowApplyHostModal(true)} + > + Apply hosts + + setShowApplyHostGroupModal(true)} + > + Apply host groups + + + + ), + }, + { + key: 5, + element: ( + + setIsUnapplyOpen(isUnapplyOpen) + } + toggle={(toggleRef: React.Ref) => ( + + Unapply + + )} + ouiaId="UnapplyDropdown" + shouldFocusToggleOnSelect + > + + + Unapply hosts + + setShowUnapplyHostGroupModal(true)} + > + Unapply host groups + + + + ), + }, + { + key: 6, + toolbarItemVariant: "separator", + }, + { + key: 7, + element: ( + + } + /> + ), + }, + { + key: 8, + element: ( + + ), + toolbarItemAlignment: { default: "alignRight" }, + }, + ]; + + return ( + + + + +
+ + + + + +
+ +
+ + + + +
+ ); +}; + +export default IDViewsAppliedTo; diff --git a/src/pages/IDViews/IDViewsAppliedToTable.tsx b/src/pages/IDViews/IDViewsAppliedToTable.tsx new file mode 100644 index 00000000..798159f0 --- /dev/null +++ b/src/pages/IDViews/IDViewsAppliedToTable.tsx @@ -0,0 +1,120 @@ +import React, { useEffect } from "react"; +// PatternFly +import { Td, Th, Tr } from "@patternfly/react-table"; +// Tables +import TableLayout from "src/components/layouts/TableLayout"; +// Layouts +import EmptyBodyTable from "src/components/tables/EmptyBodyTable"; +// React Router DOM +import { Link } from "react-router-dom"; +// Hooks +import useShifting from "src/hooks/useShifting"; + +interface HostsData { + selectedHosts: string[]; + hostsList: string[]; + setSelectedHosts: (hosts: string[]) => void; + clearSelectedHosts: () => void; +} + +interface ButtonsData { + updateIsUnapplyButtonDisabled: (value: boolean) => void; + isUnapply: boolean; + updateIsUnapply: (value: boolean) => void; +} + +interface PaginationData { + selectedPerPage: number; + updateSelectedPerPage: (selected: number) => void; +} + +export interface PropsToTable { + hosts: string[]; + shownHosts: string[]; + hostsData: HostsData; + buttonsData: ButtonsData; + paginationData: PaginationData; +} + +const IDViewsAppliedToTable = (props: PropsToTable) => { + // Retrieve views data from props + const shownHostsList = [...props.shownHosts]; + + const isHostSelected = (host: string) => { + if ( + props.hostsData.selectedHosts.find( + (selectedHost) => selectedHost === host + ) + ) { + return true; + } else { + return false; + } + }; + + // On selecting one single row + const onSelectHost = useShifting( + shownHostsList, + props.hostsData.setSelectedHosts + ); + + // Reset 'selectedHosts' array if an apply operation has been done + useEffect(() => { + if (props.buttonsData.isUnapply) { + props.hostsData.clearSelectedHosts(); + props.buttonsData.updateIsUnapply(false); + } + }, [props.buttonsData.isUnapply]); + + // Enable 'Un-apply' button (if any host selected) + useEffect(() => { + if (props.hostsData.selectedHosts.length > 0) { + props.buttonsData.updateIsUnapplyButtonDisabled(false); + } + + if (props.hostsData.selectedHosts.length === 0) { + props.buttonsData.updateIsUnapplyButtonDisabled(true); + } + }, [props.hostsData.selectedHosts]); + + // Defining table header and body from here to avoid passing specific names to the Table Layout + const header = ( + + + Host + + ); + + const body = shownHostsList.map((host, rowIndex) => ( + + + onSelectHost(host, rowIndex, isSelecting), + isSelected: isHostSelected(host), + }} + /> + + + {host} + + + + )); + + return ( + : body} + /> + ); +}; + +export default IDViewsAppliedToTable; diff --git a/src/pages/IDViews/IDViewsOverrideGroups.tsx b/src/pages/IDViews/IDViewsOverrideGroups.tsx index 8e73669b..68c8eb47 100644 --- a/src/pages/IDViews/IDViewsOverrideGroups.tsx +++ b/src/pages/IDViews/IDViewsOverrideGroups.tsx @@ -101,16 +101,11 @@ const IDViewsOverrideGroups = (props: PropsToOverrides) => { }; const selectableTable = groupsList.filter(isGroupOverrideSelectable); - const setGroupSelected = (group: IDViewOverrideGroup, isSelecting = true) => { - if (isGroupOverrideSelectable(group)) { - updateSelectedGroups([group], isSelecting); - } - }; + const groupsTableData = { isSelectable: isGroupOverrideSelectable, selected: selectedGroups, selectableTable, - setSelected: setGroupSelected, setSelectedGroups: setSelectedGroupsList, clearSelected: clearSelectedGroups, }; @@ -249,44 +244,6 @@ const IDViewsOverrideGroups = (props: PropsToOverrides) => { // Show table rows const [showTableRows, setShowTableRows] = useState(!isBatchLoading); - const updateSelectedGroups = ( - groups: IDViewOverrideGroup[], - isSelected: boolean - ) => { - let newSelected: string[] = []; - if (isSelected) { - newSelected = JSON.parse(JSON.stringify(selectedGroups)); - for (let i = 0; i < groups.length; i++) { - if ( - selectedGroups.find( - (selectedGroup) => selectedGroup === groups[i].ipaanchoruuid[0] - ) - ) { - // Already in the list - continue; - } - newSelected.push(groups[i].ipaanchoruuid[0]); - } - } else { - // Remove view - for (let i = 0; i < selectedGroups.length; i++) { - let found = false; - for (let ii = 0; ii < groups.length; ii++) { - if (selectedGroups[i] === groups[ii].ipaanchoruuid[0]) { - found = true; - break; - } - } - if (!found) { - // Keep this valid selected entry - newSelected.push(selectedGroups[i]); - } - } - } - setSelectedGroupsList(newSelected); - setIsDeleteButtonDisabled(newSelected.length === 0); - }; - // Always refetch data when the component is loaded. // This ensures the data is always up-to-date. useEffect(() => { @@ -427,7 +384,7 @@ const IDViewsOverrideGroups = (props: PropsToOverrides) => { contentClassName="pf-v5-u-p-0" toolbarItems={toolbarItems} /> -
+
{batchError !== undefined && batchError ? ( diff --git a/src/pages/IDViews/IDViewsOverrideGroupsTable.tsx b/src/pages/IDViews/IDViewsOverrideGroupsTable.tsx index a1a9fbf1..83cab695 100644 --- a/src/pages/IDViews/IDViewsOverrideGroupsTable.tsx +++ b/src/pages/IDViews/IDViewsOverrideGroupsTable.tsx @@ -14,7 +14,6 @@ interface IDViewsOverrideData { isSelectable: (view: IDViewOverrideGroup) => boolean; selected: string[]; selectableTable: IDViewOverrideGroup[]; - setSelected: (group: IDViewOverrideGroup, isSelecting?: boolean) => void; setSelectedGroups: (group: string[]) => void; clearSelected: () => void; } diff --git a/src/pages/IDViews/IDViewsOverrideUsers.tsx b/src/pages/IDViews/IDViewsOverrideUsers.tsx index c9140294..629d2e27 100644 --- a/src/pages/IDViews/IDViewsOverrideUsers.tsx +++ b/src/pages/IDViews/IDViewsOverrideUsers.tsx @@ -97,16 +97,11 @@ const IDViewsOverrideUsers = (props: PropsToOverrides) => { }; const selectableTable = usersList.filter(isUserOverrideSelectable); - const setUserSelected = (user: IDViewOverrideUser, isSelecting = true) => { - if (isUserOverrideSelectable(user)) { - updateSelectedUsers([user], isSelecting); - } - }; + const usersTableData = { isSelectable: isUserOverrideSelectable, selected: selectedUsers, selectableTable, - setSelected: setUserSelected, setSelectedUsers: setSelectedUsersList, clearSelected: clearSelectedUsers, }; @@ -245,45 +240,6 @@ const IDViewsOverrideUsers = (props: PropsToOverrides) => { // Show table rows const [showTableRows, setShowTableRows] = useState(!isBatchLoading); - const updateSelectedUsers = ( - users: IDViewOverrideUser[], - isSelected: boolean - ) => { - let newSelected: string[] = []; - if (isSelected) { - newSelected = JSON.parse(JSON.stringify(selectedUsers)); - for (let i = 0; i < users.length; i++) { - if ( - selectedUsers.find( - (selectedUser) => selectedUser === users[i].ipaanchoruuid[0] - ) - ) { - // Already in the list - continue; - } - // user view to list - newSelected.push(users[i].ipaanchoruuid[0]); - } - } else { - // Remove view - for (let i = 0; i < selectedUsers.length; i++) { - let found = false; - for (let ii = 0; ii < users.length; ii++) { - if (selectedUsers[i] === users[ii].ipaanchoruuid[0]) { - found = true; - break; - } - } - if (!found) { - // Keep this valid selected entry - newSelected.push(selectedUsers[i]); - } - } - } - setSelectedUsersList(newSelected); - setIsDeleteButtonDisabled(newSelected.length === 0); - }; - // Always refetch data when the component is loaded. // This ensures the data is always up-to-date. useEffect(() => { @@ -405,7 +361,7 @@ const IDViewsOverrideUsers = (props: PropsToOverrides) => { contentClassName="pf-v5-u-p-0" toolbarItems={toolbarItems} /> -
+
{batchError !== undefined && batchError ? ( diff --git a/src/pages/IDViews/IDViewsOverrideUsersTable.tsx b/src/pages/IDViews/IDViewsOverrideUsersTable.tsx index ef97fb22..0cd9740d 100644 --- a/src/pages/IDViews/IDViewsOverrideUsersTable.tsx +++ b/src/pages/IDViews/IDViewsOverrideUsersTable.tsx @@ -14,7 +14,6 @@ interface IDViewsOverrideData { isSelectable: (view: IDViewOverrideUser) => boolean; selected: string[]; selectableTable: IDViewOverrideUser[]; - setSelected: (user: IDViewOverrideUser, isSelecting?: boolean) => void; setSelectedUsers: (users: string[]) => void; clearSelected: () => void; } diff --git a/src/pages/IDViews/IDViewsTabs.tsx b/src/pages/IDViews/IDViewsTabs.tsx index 8fc604aa..889ecaa7 100644 --- a/src/pages/IDViews/IDViewsTabs.tsx +++ b/src/pages/IDViews/IDViewsTabs.tsx @@ -24,6 +24,7 @@ import { updateBreadCrumbPath } from "src/store/Global/routes-slice"; import { NotFound } from "src/components/errors/PageErrors"; import IDViewsSettings from "./IDViewsSettings"; import IDViewsOverrides from "./IDViewsOverrides"; +import IDViewsAppliedTo from "./IDViewsAppliedTo"; // eslint-disable-next-line react/prop-types const IDViewsTabs = ({ section }) => { @@ -49,8 +50,8 @@ const IDViewsTabs = ({ section }) => { navigate("/id-views/" + cn); } else if (tabName.startsWith("override")) { navigate("/id-views/" + cn + "/override-users"); - } else if (tabName === "appliesto") { - // navigate("/id-views/" + cn + "/appliesto_idview"); + } else if (tabIndex === "appliedto") { + navigate("/id-views/" + cn + "/appliedto"); } }; @@ -160,10 +161,15 @@ const IDViewsTabs = ({ section }) => { /> Applies to} - > + eventKey={"appliedto"} + name="appliedto-details" + title={Applied to} + > + + diff --git a/src/services/rpcIDViews.ts b/src/services/rpcIDViews.ts index e769ed4c..840f97cc 100644 --- a/src/services/rpcIDViews.ts +++ b/src/services/rpcIDViews.ts @@ -41,6 +41,11 @@ export type ViewFullData = { idView?: Partial; }; +export interface ViewApplyPayload { + viewName: string; + items: string[]; +} + const extendedApi = api.injectEndpoints({ endpoints: (build) => ({ getIDViewsFullData: build.query({ @@ -121,7 +126,7 @@ const extendedApi = api.injectEndpoints({ }, }), /** - * Given a list of view names, show the full data of those grouviewsps + * Given a list of view names, show the full data of those views * @param {string[]} groupNames - List of group names * @param {boolean} noMembers - Whether to show members or not * @returns {BatchRPCResponse} - Batch response @@ -180,14 +185,32 @@ const extendedApi = api.injectEndpoints({ * @param {string[]} hostgroupNames - List of hostgroup names */ unapplyHostgroups: build.mutation({ - query: (hostgroups) => { + query: (hostGroups) => { return getCommand({ method: "idview_unapply", - params: [[], { hostgroup: hostgroups }], + params: [[], { hostgroup: hostGroups }], }); }, invalidatesTags: ["FullIDViewHostgroups"], }), + applyHosts: build.mutation({ + query: (payload) => { + return getCommand({ + method: "idview_apply", + params: [[payload.viewName], { host: payload.items }], + }); + }, + invalidatesTags: ["FullIDViewHosts"], + }), + applyHostGroups: build.mutation({ + query: (payload) => { + return getCommand({ + method: "idview_apply", + params: [[payload.viewName], { hostgroup: payload.items }], + }); + }, + invalidatesTags: ["FullIDViewHosts"], + }), }), overrideExisting: false, }); @@ -207,4 +230,6 @@ export const { useSaveIDViewMutation, useUnapplyHostsMutation, useUnapplyHostgroupsMutation, + useApplyHostsMutation, + useApplyHostGroupsMutation, } = extendedApi; diff --git a/src/utils/datatypes/globalDataTypes.ts b/src/utils/datatypes/globalDataTypes.ts index d29ad349..80c0aca1 100644 --- a/src/utils/datatypes/globalDataTypes.ts +++ b/src/utils/datatypes/globalDataTypes.ts @@ -395,6 +395,7 @@ export interface IDView { ipadomainresolutionorder: string; useroverrides: string[]; groupoverrides: string[]; + appliedtohosts: string[]; } export interface IDViewOverrideUser { @@ -424,6 +425,7 @@ export interface IDViewOverrideGroup { description: string; gidnumber: string; ipaanchoruuid: string; + appliedtohosts: string[]; } export interface Config { diff --git a/src/utils/idOverrideUtils.tsx b/src/utils/idOverrideUtils.tsx index 8e1bba9b..e12994be 100644 --- a/src/utils/idOverrideUtils.tsx +++ b/src/utils/idOverrideUtils.tsx @@ -108,6 +108,7 @@ export function createEmptyOverrideGroup(): IDViewOverrideGroup { description: "", gidnumber: "", ipaanchoruuid: "", + appliedtohosts: [], }; return override; diff --git a/src/utils/idViewUtils.tsx b/src/utils/idViewUtils.tsx index 6e3a256c..4977574c 100644 --- a/src/utils/idViewUtils.tsx +++ b/src/utils/idViewUtils.tsx @@ -36,6 +36,7 @@ export function createEmptyView(): IDView { ipadomainresolutionorder: "", useroverrides: [], groupoverrides: [], + appliedtohosts: [], }; return view; diff --git a/tests/features/id_view_appliedto.feature b/tests/features/id_view_appliedto.feature new file mode 100644 index 00000000..b9118f06 --- /dev/null +++ b/tests/features/id_view_appliedto.feature @@ -0,0 +1,185 @@ +Feature: ID View applied to manipulation + Apply and unapply hosts and hostgroups to an ID view + + Background: + Given I am logged in as "Administrator" + Given I am on "id-views" page + + # + # Create sample entries + # + Scenario: Add new hosts + Given I am on "hosts" page + When I click on "Add" button + Then I type in the field "Host name" text "idviewhost1" + * in the modal dialog I click on "Add" button + * I should see "success" alert with text "New host added" + Then I should see partial "idviewhost1" entry in the data table + When I click on "Add" button + Then I type in the field "Host name" text "idviewhost2" + * in the modal dialog I click on "Add" button + * I should see "success" alert with text "New host added" + Then I should see partial "idviewhost2" entry in the data table + + Scenario: Add a new host group + Given I am on "host-groups" page + When I click on "Add" button + * I type in the field "Group name" text "idviewhostgroup1" + When in the modal dialog I click on "Add" button + * I should see "success" alert with text "New host group added" + * I close the alert + Then I should see "idviewhostgroup1" entry in the data table + When I click on "Add" button + * I type in the field "Group name" text "idviewhostgroup2" + When in the modal dialog I click on "Add" button + * I should see "success" alert with text "New host group added" + * I close the alert + Then I should see "idviewhostgroup2" entry in the data table + + Scenario: Add a Host members into the host group 1 + Given I am on "host-groups" page + And I click on "idviewhostgroup1" entry in the data table + And I click on "Members" page tab + And I am on "idviewhostgroup1" group > Members > "Hosts" section + When I click on "Add" button located in the toolbar + Then I should see the dialog with title "Assign hosts to host group: idviewhostgroup1" + When I move user "idviewhost1.dom-server.ipa.demo" from the available list and move it to the chosen options + And in the modal dialog I click on "Add" button + * I should see "success" alert with text "Assigned new hosts to host group 'idviewhostgroup1'" + * I close the alert + Then I should see the element "idviewhost1.dom-server.ipa.demo" in the table + + Scenario: Add a Host members into the host group 2 + Given I am on "host-groups" page + Given I click on "idviewhostgroup2" entry in the data table + And I click on "Members" page tab + And I am on "idviewhostgroup2" group > Members > "Hosts" section + When I click on "Add" button located in the toolbar + Then I should see the dialog with title "Assign hosts to host group: idviewhostgroup2" + When I move user "idviewhost2.dom-server.ipa.demo" from the available list and move it to the chosen options + And in the modal dialog I click on "Add" button + * I should see "success" alert with text "Assigned new hosts to host group 'idviewhostgroup2'" + * I close the alert + Then I should see the element "idviewhost2.dom-server.ipa.demo" in the table + + Scenario: Add a new view + Given I am on "id-views" page + When I click on "Add" button + * I type in the field "ID view name" text "a_new_view" + When in the modal dialog I click on "Add" button + * I should see "success" alert with text "New ID view added" + * I close the alert + Then I should see "a_new_view" entry in the data table + + # + # Start testing "applied to" hosts + # + Scenario: Apply ID view to the host + When I click on "a_new_view" entry in the data table + Then I click on "Applied to" page tab + * I click toolbar dropdown "Apply" + * I click toolbar dropdown item "Apply hosts" + * I click on the arrow icon to perform search + Then I click on the dual list item "idviewhost1" + And I click on the dual list item "idviewhost2" + * I click on the dual list add selected button + * in the modal dialog I click on "Apply" button + Then I should see "success" alert with text "ID view applied to 2 hosts" + * I close the alert + + Scenario: Search for a host + When I type "idviewhost2" in the search field + Then I should see the "idviewhost2" text in the search input field + Then I should see the element "idviewhost2" in the table + Then I should not see the element "idviewhost1" in the table + * I click on the X icon to clear the search field + When I type "notthere" in the search field + Then I should see the "notthere" text in the search input field + Then I should not see the element "idviewhost1" in the table + Then I should not see the element "idviewhost2" in the table + * I click on the X icon to clear the search field + And I should see the element "idviewhost1" in the table + And I should see the element "idviewhost2" in the table + + # + # Test unapply hosts + # + Scenario: Unapply a host from the ID view + Given I select partial entry "idviewhost1" in the data table + Given I select partial entry "idviewhost2" in the data table + When I click toolbar dropdown "Unapply" + Then I click toolbar dropdown item "Unapply hosts" + And I should see "success" alert with text "ID view unapplied from 2 hosts" + * I close the alert + + # + # Test "applied to" hostgroups + # + Scenario: Apply ID view to a host group + When I click toolbar dropdown "Apply" + And I click toolbar dropdown item "Apply host groups" + * I click on the arrow icon to perform search + Then I click on the dual list item "idviewhostgroup1" + And I click on the dual list item "idviewhostgroup2" + * I click on the dual list add selected button + * in the modal dialog I click on "Apply" button + Then I should see "success" alert with text "ID view applied to 2 hosts" + * I close the alert + + # + # Test unapply host groups + # + Scenario: Unapply host groups from the ID view + When I click toolbar dropdown "Unapply" + Then I click toolbar dropdown item "Unapply host groups" + * I click on the arrow icon to perform search + Then I click on the dual list item "idviewhostgroup1" + * I click on the dual list add selected button + * in the modal dialog I click on "Unapply" button + Then I should see "success" alert with text "ID view unapplied from 1 hosts" + * I close the alert + + # Cleanup + Scenario: Delete the ID view + When I click on the breadcrump link "ID views" + Then I should see "a_new_view" entry in the data table + Then I select entry "a_new_view" in the data table + When I click on "Delete" button + * I see "Remove ID views" modal + * I should see "a_new_view" entry in the data table + When in the modal dialog I click on "Delete" button + * I should see "success" alert with text "ID views removed" + * I close the alert + Then I should not see "a_new_view" entry in the data table + + Scenario: Delete hosts + Given I am on "hosts" page + And I should see partial "idviewhost1" entry in the data table + And I should see partial "idviewhost2" entry in the data table + When I select partial entry "idviewhost1" in the data table + And I select partial entry "idviewhost2" in the data table + And I click on "Delete" button + Then I see "Remove hosts" modal + * I should see partial "idviewhost1" entry in the data table + * I should see partial "idviewhost2" entry in the data table + When in the modal dialog I click on "Delete" button + * I should see "success" alert with text "Hosts removed" + * I close the alert + Then I should not see partial "idviewhost1" entry in the data table + Then I should not see partial "idviewhost2" entry in the data table + + Scenario: Delete host groups + Given I am on "host-groups" page + And I should see "idviewhostgroup1" entry in the data table + And I should see "idviewhostgroup2" entry in the data table + Then I select entry "idviewhostgroup1" in the data table + And I select entry "idviewhostgroup2" in the data table + When I click on "Delete" button + * I see "Remove host groups" modal + * I should see "idviewhostgroup1" entry in the data table + * I should see "idviewhostgroup2" entry in the data table + When in the modal dialog I click on "Delete" button + * I should see "success" alert with text "Host groups removed" + * I close the alert + Then I should not see "idviewhostgroup1" entry in the data table + And I should not see "idviewhostgroup2" entry in the data table diff --git a/tests/features/id_view_handling.feature b/tests/features/id_view_handling.feature index 36351e26..51afa0c7 100644 --- a/tests/features/id_view_handling.feature +++ b/tests/features/id_view_handling.feature @@ -40,7 +40,7 @@ Feature: ID View manipulation * I click on the first dual list item * I click on the dual list add selected button When in the modal dialog I click on "Unapply" button - Then I should see "success" alert with text "ID views unapplied from hosts" + Then I should see "success" alert with text "ID views unapplied from 0 hosts" * I close the alert Scenario: Unapply views from host groups @@ -49,7 +49,7 @@ Feature: ID View manipulation * I click on the first dual list item * I click on the dual list add selected button When in the modal dialog I click on "Unapply" button - Then I should see "success" alert with text "ID views unapplied from host groups" + Then I should see "success" alert with text "ID views unapplied from 0 hosts" * I close the alert Scenario: Delete a view diff --git a/tests/features/idoverride_users_handling.feature b/tests/features/idoverride_users_handling.feature index c6f4d14a..48d5e3af 100644 --- a/tests/features/idoverride_users_handling.feature +++ b/tests/features/idoverride_users_handling.feature @@ -93,7 +93,7 @@ Feature: ID Override user manipulation Scenario: Delete users Given I am on "active-users" page Given I should see "overrideuser1" entry in the data table - Given I should see "overrideuser1" entry in the data table + Given I should see "overrideuser2" entry in the data table When I select entry "overrideuser1" in the data table And I select entry "overrideuser2" in the data table And I click on "Delete" button diff --git a/tests/features/steps/common.ts b/tests/features/steps/common.ts index bd3e184b..bac15f3d 100644 --- a/tests/features/steps/common.ts +++ b/tests/features/steps/common.ts @@ -375,6 +375,20 @@ When( } ); +When("I click toolbar dropdown {string}", (name: string) => { + cy.get("div.pf-v5-c-toolbar__item") + .find("button.pf-v5-c-menu-toggle") + .contains(name) + .click(); +}); + +When("I click toolbar dropdown item {string}", (name: string) => { + cy.get("li.pf-v5-c-menu__list-item") + .find("button.pf-v5-c-menu__item") + .contains(name) + .click(); +}); + Then( "I should see the option {string} selected in the {string} selector", (option: string, selectorName: string) => { diff --git a/tests/features/steps/user_handling.ts b/tests/features/steps/user_handling.ts index 34a55a83..1639a0b4 100644 --- a/tests/features/steps/user_handling.ts +++ b/tests/features/steps/user_handling.ts @@ -429,7 +429,7 @@ Then( When( "I click on {string} button located in the toolbar", (buttonName: string) => { - cy.wait(1000); + cy.wait(2000); cy.get("div.pf-v5-c-toolbar").find("button").contains(buttonName).click(); } );