From 642863501e415530364ed64b1c5c2067524494e0 Mon Sep 17 00:00:00 2001 From: Mark Reynolds Date: Thu, 5 Sep 2024 16:38:35 -0400 Subject: [PATCH] Implment netgroups members Fixes: https://github.com/freeipa/freeipa-webui/issues/546 Signed-off-by: Mark Reynolds --- src/components/MemberOf/MemberOfToolbar.tsx | 4 +- src/components/Members/MembersExternal.tsx | 7 +- src/components/Members/MembersHostGroups.tsx | 49 ++- src/components/Members/MembersHosts.tsx | 49 ++- src/components/Members/MembersNetgroups.tsx | 368 +++++++++++++++++++ src/components/Members/MembersServices.tsx | 7 +- src/components/Members/MembersUserGroups.tsx | 34 +- src/components/Members/MembersUsers.tsx | 35 +- src/navigation/AppRoutes.tsx | 20 + src/pages/Netgroups/NetgroupsMembers.tsx | 258 +++++++++++++ src/pages/Netgroups/NetgroupsTabs.tsx | 25 +- src/services/rpc.ts | 6 + src/services/rpcHostGroups.ts | 11 +- src/services/rpcNetgroups.ts | 61 ++- src/services/rpcUserGroups.ts | 19 +- src/utils/datatypes/globalDataTypes.ts | 2 + src/utils/netgroupsUtils.tsx | 2 + tests/features/hostgroup_members.feature | 22 ++ tests/features/netgroup_members.feature | 266 ++++++++++++++ tests/features/steps/common.ts | 2 +- 20 files changed, 1159 insertions(+), 88 deletions(-) create mode 100644 src/components/Members/MembersNetgroups.tsx create mode 100644 src/pages/Netgroups/NetgroupsMembers.tsx create mode 100644 tests/features/netgroup_members.feature diff --git a/src/components/MemberOf/MemberOfToolbar.tsx b/src/components/MemberOf/MemberOfToolbar.tsx index 7f0548ff..22274e46 100644 --- a/src/components/MemberOf/MemberOfToolbar.tsx +++ b/src/components/MemberOf/MemberOfToolbar.tsx @@ -115,8 +115,8 @@ const MemberOfToolbar = (props: MemberOfToolbarProps) => { Add - {/* Membership will show only if `membershipDirectionEnabled` is defined */} - {props.membershipDirectionEnabled !== undefined && ( + {/* Membership direction will show only if `membershipDirectionEnabled` is true */} + {props.membershipDirectionEnabled && ( <> { } const payload = { - userGroup: props.id, + entryName: props.id, entityType: "ipaexternalmember", idsToAdd: newExternalNames, } as MemberPayload; @@ -171,7 +170,7 @@ const MembersExternal = (props: PropsToMembersExternal) => { // Delete const onDeleteExternal = () => { const payload = { - userGroup: props.id, + entryName: props.id, entityType: "ipaexternalmember", idsToAdd: externalsSelected, } as MemberPayload; diff --git a/src/components/Members/MembersHostGroups.tsx b/src/components/Members/MembersHostGroups.tsx index f113052c..df347d1f 100644 --- a/src/components/Members/MembersHostGroups.tsx +++ b/src/components/Members/MembersHostGroups.tsx @@ -15,14 +15,17 @@ import useListPageSearchParams from "src/hooks/useListPageSearchParams"; // Utils import { API_VERSION_BACKUP, paginate } from "src/utils/utils"; // RPC -import { ErrorResult } from "src/services/rpc"; +import { ErrorResult, MemberPayload } from "src/services/rpc"; import { - MemberPayload, useAddAsMemberHGMutation, useGetHostGroupInfoByNameQuery, useGettingHostGroupsQuery, useRemoveAsMemberHGMutation, } from "src/services/rpcHostGroups"; +import { + useAddAsMemberNGMutation, + useRemoveAsMemberNGMutation, +} from "src/services/rpcNetgroups"; import { apiToHostGroup } from "src/utils/hostGroupUtils"; interface PropsToMembersHostGroups { @@ -128,11 +131,25 @@ const MembersHostGroups = (props: PropsToMembersHostGroups) => { } }, [fullHostGroupsQuery.data, fullHostGroupsQuery.isFetching]); + // Get type of the entity to show as text + const getEntityType = () => { + if (props.from === "host-groups") { + return "host group"; + } else if (props.from === "netgroup") { + return "netgroup"; + } else { + // Return 'group' as default + return "host group"; + } + }; + // Computed "states" const someItemSelected = hostGroupsSelected.length > 0; const showTableRows = hostGroups.length > 0; + const entityType = getEntityType(); const hostGroupColumnNames = ["Host group name", "Description"]; const hostGroupProperties = ["cn", "description"]; + const directionEnabled = props.from !== "netgroup" ? true : false; // Dialogs and actions const [showAddModal, setShowAddModal] = React.useState(false); @@ -147,8 +164,14 @@ const MembersHostGroups = (props: PropsToMembersHostGroups) => { // Add new member to 'HostGroup' // API calls - const [addMemberToHostGroups] = useAddAsMemberHGMutation(); - const [removeMembersFromHostGroups] = useRemoveAsMemberHGMutation(); + let [addMembers] = useAddAsMemberHGMutation(); + if (props.from === "netgroup") { + [addMembers] = useAddAsMemberNGMutation(); + } + let [removeMembers] = useRemoveAsMemberHGMutation(); + if (props.from === "netgroup") { + [removeMembers] = useRemoveAsMemberNGMutation(); + } const [adderSearchValue, setAdderSearchValue] = React.useState(""); const [availableHostGroups, setAvailableHostGroups] = React.useState< HostGroup[] @@ -209,19 +232,19 @@ const MembersHostGroups = (props: PropsToMembersHostGroups) => { } const payload = { - hostGroup: props.id, + entryName: props.id, entityType: "hostgroup", idsToAdd: newHostGroupNames, } as MemberPayload; setSpinning(true); - addMemberToHostGroups(payload).then((response) => { + addMembers(payload).then((response) => { if ("data" in response) { if (response.data.result) { // Set alert: success alerts.addAlert( "add-member-success", - "Assigned new host groups to host group '" + props.id + "'", + "Assigned new host groups to " + entityType + " '" + props.id + "'", "success" ); // Refresh data @@ -241,19 +264,19 @@ const MembersHostGroups = (props: PropsToMembersHostGroups) => { // Delete const onDeleteHostGroups = () => { const payload = { - hostGroup: props.id, + entryName: props.id, entityType: "hostgroup", idsToAdd: hostGroupsSelected, } as MemberPayload; setSpinning(true); - removeMembersFromHostGroups(payload).then((response) => { + removeMembers(payload).then((response) => { if ("data" in response) { if (response.data.result) { // Set alert: success alerts.addAlert( "remove-hostgroups-success", - "Removed host groups from host group '" + props.id + "'", + "Removed host groups from " + entityType + " '" + props.id + "'", "success" ); // Refresh @@ -324,7 +347,7 @@ const MembersHostGroups = (props: PropsToMembersHostGroups) => { onDeleteButtonClick={() => setShowDeleteModal(true)} addButtonEnabled={isAddButtonEnabled} onAddButtonClick={() => setShowAddModal(true)} - membershipDirectionEnabled={true} + membershipDirectionEnabled={directionEnabled} membershipDirection={membershipDirection} onMembershipDirectionChange={setMembershipDirection} helpIconEnabled={true} @@ -370,7 +393,7 @@ const MembersHostGroups = (props: PropsToMembersHostGroups) => { availableItems={availableItems} onAdd={onAddHostGroup} onSearchTextChange={setAdderSearchValue} - title={"Assign host groups to host group: " + props.id} + title={"Assign host groups to " + entityType + ": " + props.id} ariaLabel={"Add host groups modal"} spinning={spinning} /> @@ -379,7 +402,7 @@ const MembersHostGroups = (props: PropsToMembersHostGroups) => { setShowDeleteModal(false)} - title={"Delete host groups from host group: " + props.id} + title={"Delete host groups from " + entityType + ": " + props.id} onDelete={onDeleteHostGroups} spinning={spinning} > diff --git a/src/components/Members/MembersHosts.tsx b/src/components/Members/MembersHosts.tsx index c47b3bc1..bb14a15e 100644 --- a/src/components/Members/MembersHosts.tsx +++ b/src/components/Members/MembersHosts.tsx @@ -15,16 +15,19 @@ import useListPageSearchParams from "src/hooks/useListPageSearchParams"; // Utils import { API_VERSION_BACKUP, paginate } from "src/utils/utils"; // RPC -import { ErrorResult } from "src/services/rpc"; +import { ErrorResult, MemberPayload } from "src/services/rpc"; import { useGetHostInfoByNameQuery, useGettingHostQuery, } from "src/services/rpcHosts"; import { - MemberPayload, useAddAsMemberHGMutation, useRemoveAsMemberHGMutation, } from "src/services/rpcHostGroups"; +import { + useAddAsMemberNGMutation, + useRemoveAsMemberNGMutation, +} from "src/services/rpcNetgroups"; import { apiToHost } from "src/utils/hostUtils"; interface PropsToMembersHosts { @@ -126,11 +129,25 @@ const MembersHosts = (props: PropsToMembersHosts) => { } }, [fullHostsQuery.data, fullHostsQuery.isFetching]); + // Get type of the entity to show as text + const getEntityType = () => { + if (props.from === "hostgroups") { + return "host group"; + } else if (props.from === "netgroup") { + return "netgroup"; + } else { + // Return 'group' as default + return "host group"; + } + }; + // Computed "states" const someItemSelected = hostsSelected.length > 0; const showTableRows = hosts.length > 0; + const entityType = getEntityType(); const hostColumnNames = ["Host name", "Description"]; const hostProperties = ["fqdn", "description"]; + const directionEnabled = props.from !== "netgroup" ? true : false; // Dialogs and actions const [showAddModal, setShowAddModal] = React.useState(false); @@ -145,8 +162,14 @@ const MembersHosts = (props: PropsToMembersHosts) => { // Add new member to 'Host' // API calls - const [addMemberToHostGroups] = useAddAsMemberHGMutation(); - const [removeMembersFromHostGroups] = useRemoveAsMemberHGMutation(); + let [addMembers] = useAddAsMemberHGMutation(); + if (props.from === "netgroup") { + [addMembers] = useAddAsMemberNGMutation(); + } + let [removeMembers] = useRemoveAsMemberHGMutation(); + if (props.from === "netgroup") { + [removeMembers] = useRemoveAsMemberNGMutation(); + } const [adderSearchValue, setAdderSearchValue] = React.useState(""); const [availableHosts, setAvailableHosts] = React.useState([]); const [availableItems, setAvailableItems] = React.useState( @@ -205,19 +228,19 @@ const MembersHosts = (props: PropsToMembersHosts) => { } const payload = { - hostGroup: props.id, + entryName: props.id, entityType: "host", idsToAdd: newHostNames, } as MemberPayload; setSpinning(true); - addMemberToHostGroups(payload).then((response) => { + addMembers(payload).then((response) => { if ("data" in response) { if (response.data.result) { // Set alert: success alerts.addAlert( "add-member-success", - "Assigned new hosts to host group '" + props.id + "'", + "Assigned new hosts to " + entityType + " '" + props.id + "'", "success" ); // Refresh data @@ -237,19 +260,19 @@ const MembersHosts = (props: PropsToMembersHosts) => { // Delete const onDeleteHosts = () => { const payload = { - hostGroup: props.id, + entryName: props.id, entityType: "host", idsToAdd: hostsSelected, } as MemberPayload; setSpinning(true); - removeMembersFromHostGroups(payload).then((response) => { + removeMembers(payload).then((response) => { if ("data" in response) { if (response.data.result) { // Set alert: success alerts.addAlert( "remove-host-success", - "Removed hosts from host group '" + props.id + "'", + "Removed hosts from " + entityType + " '" + props.id + "'", "success" ); // Refresh @@ -316,7 +339,7 @@ const MembersHosts = (props: PropsToMembersHosts) => { onDeleteButtonClick={() => setShowDeleteModal(true)} addButtonEnabled={isAddButtonEnabled} onAddButtonClick={() => setShowAddModal(true)} - membershipDirectionEnabled={true} + membershipDirectionEnabled={directionEnabled} membershipDirection={membershipDirection} onMembershipDirectionChange={setMembershipDirection} helpIconEnabled={true} @@ -362,7 +385,7 @@ const MembersHosts = (props: PropsToMembersHosts) => { availableItems={availableItems} onAdd={onAddHost} onSearchTextChange={setAdderSearchValue} - title={"Assign hosts to host group: " + props.id} + title={"Assign hosts to " + entityType + ": " + props.id} ariaLabel={"Add hosts modal"} spinning={spinning} /> @@ -371,7 +394,7 @@ const MembersHosts = (props: PropsToMembersHosts) => { setShowDeleteModal(false)} - title={"Delete hosts from host group: " + props.id} + title={"Delete hosts from " + entityType + ": " + props.id} onDelete={onDeleteHosts} spinning={spinning} > diff --git a/src/components/Members/MembersNetgroups.tsx b/src/components/Members/MembersNetgroups.tsx new file mode 100644 index 00000000..ad894a3d --- /dev/null +++ b/src/components/Members/MembersNetgroups.tsx @@ -0,0 +1,368 @@ +import React from "react"; +// PatternFly +import { Pagination, PaginationVariant } from "@patternfly/react-core"; +// Components +import MemberOfToolbar from "../MemberOf/MemberOfToolbar"; +import MemberOfAddModal, { AvailableItems } from "../MemberOf/MemberOfAddModal"; +import MemberOfDeleteModal from "../MemberOf/MemberOfDeleteModal"; +import MemberTable from "src/components/tables/MembershipTable"; +import { MembershipDirection } from "src/components/MemberOf/MemberOfToolbar"; +// Data types +import { Netgroup } from "src/utils/datatypes/globalDataTypes"; +// Hooks +import useAlerts from "src/hooks/useAlerts"; +import useListPageSearchParams from "src/hooks/useListPageSearchParams"; +// Utils +import { API_VERSION_BACKUP, paginate } from "src/utils/utils"; +// RPC +import { ErrorResult, MemberPayload } from "src/services/rpc"; +import { + useAddAsMemberNGMutation, + useGetNetgroupInfoByNameQuery, + useGettingNetgroupsQuery, + useRemoveAsMemberNGMutation, +} from "src/services/rpcNetgroups"; +import { apiToNetgroup } from "src/utils/netgroupsUtils"; + +interface PropsToMembersNetgroups { + entity: Partial; + id: string; + from: string; + isDataLoading: boolean; + onRefreshData: () => void; + member_netgroup: string[]; + memberindirect_netgroup?: string[]; + setDirection: (direction: MembershipDirection) => void; + direction: MembershipDirection; +} + +const MembersNetgroups = (props: PropsToMembersNetgroups) => { + // Alerts to show in the UI + const alerts = useAlerts(); + + // Get parameters from URL + const { + page, + setPage, + perPage, + setPerPage, + searchValue, + setSearchValue, + membershipDirection, + setMembershipDirection, + } = useListPageSearchParams(); + + // Other states + const [netgroupsSelected, setNetgroupsSelected] = React.useState( + [] + ); + const [indirectNetgroupsSelected, setIndirectNetgroupsSelected] = + React.useState([]); + + // Loaded netGroups based on paging and member attributes + const [netgroups, setNetgroups] = React.useState([]); + + // Choose the correct netgroups based on the membership direction + const member_netgroup = props.member_netgroup || []; + const memberindirect_netgroup = props.memberindirect_netgroup || []; + let netgroupNames = + membershipDirection === "direct" + ? member_netgroup + : memberindirect_netgroup; + netgroupNames = [...netgroupNames]; + + const getNetgroupsNameToLoad = (): string[] => { + let toLoad = [...netgroupNames]; + toLoad.sort(); + + // Filter by search + if (searchValue) { + toLoad = toLoad.filter((name) => + name.toLowerCase().includes(searchValue.toLowerCase()) + ); + } + + // Apply paging + toLoad = paginate(toLoad, page, perPage); + + return toLoad; + }; + + const [netgroupNamesToLoad, setNetgroupNamesToLoad] = React.useState< + string[] + >(getNetgroupsNameToLoad()); + + // Load netgroups + const fullNetgroupsQuery = useGetNetgroupInfoByNameQuery({ + netgroupNamesList: netgroupNamesToLoad, + no_members: true, + version: API_VERSION_BACKUP, + }); + + // Refresh netgroups + React.useEffect(() => { + const netGroupsNames = getNetgroupsNameToLoad(); + setNetgroupNamesToLoad(netGroupsNames); + props.setDirection(membershipDirection); + }, [props.entity, membershipDirection, searchValue, page, perPage]); + + React.useEffect(() => { + setMembershipDirection(props.direction); + }, [props.entity]); + + React.useEffect(() => { + if (netgroupNamesToLoad.length > 0) { + fullNetgroupsQuery.refetch(); + } + }, [netgroupNamesToLoad]); + + // Update netgroups + React.useEffect(() => { + if (fullNetgroupsQuery.data && !fullNetgroupsQuery.isFetching) { + setNetgroups(fullNetgroupsQuery.data); + } + }, [fullNetgroupsQuery.data, fullNetgroupsQuery.isFetching]); + + // Computed "states" + const showTableRows = netgroups.length > 0; + const netgroupColumnNames = ["Netgroup name", "Description"]; + const netgroupProperties = ["cn", "description"]; + + // Dialogs and actions + const [showAddModal, setShowAddModal] = React.useState(false); + const [showDeleteModal, setShowDeleteModal] = React.useState(false); + const [spinning, setSpinning] = React.useState(false); + + // Buttons functionality + const isRefreshButtonEnabled = + !fullNetgroupsQuery.isFetching && !props.isDataLoading; + const isAddButtonEnabled = + membershipDirection !== "indirect" && isRefreshButtonEnabled; + + // API calls + const [addMemberToNetgroups] = useAddAsMemberNGMutation(); + const [removeMembersFromNetgroups] = useRemoveAsMemberNGMutation(); + const [adderSearchValue, setAdderSearchValue] = React.useState(""); + const [availableNetgroups, setAvailableNetgroups] = React.useState< + Netgroup[] + >([]); + const [availableItems, setAvailableItems] = React.useState( + [] + ); + + // Load available netgroups, delay the search for opening the modal + const netgroupsQuery = useGettingNetgroupsQuery({ + search: adderSearchValue, + apiVersion: API_VERSION_BACKUP, + sizelimit: 100, + startIdx: 0, + stopIdx: 100, + }); + + // Trigger available netgroups search + React.useEffect(() => { + if (showAddModal) { + netgroupsQuery.refetch(); + } + }, [showAddModal, adderSearchValue, props.entity]); + + // Update available netgroups + React.useEffect(() => { + if (netgroupsQuery.data && !netgroupsQuery.isFetching) { + // transform data to netgroups + const count = netgroupsQuery.data.result.count; + const results = netgroupsQuery.data.result.results; + let items: AvailableItems[] = []; + const avalNetgroups: Netgroup[] = []; + for (let i = 0; i < count; i++) { + const netgroup = apiToNetgroup(results[i].result); + avalNetgroups.push(netgroup); + items.push({ + key: netgroup.cn, + title: netgroup.cn, + }); + } + items = items.filter( + (item) => + !member_netgroup.includes(item.key) && + !memberindirect_netgroup.includes(item.key) && + item.key !== props.id + ); + + setAvailableNetgroups(avalNetgroups); + setAvailableItems(items); + } + }, [netgroupsQuery.data, netgroupsQuery.isFetching]); + + // Add + const onAddNetgroup = (items: AvailableItems[]) => { + const newHostGroupNames = items.map((item) => item.key); + if (props.id === undefined || newHostGroupNames.length == 0) { + return; + } + + const payload = { + entryName: props.id, + entityType: "netgroup", + idsToAdd: newHostGroupNames, + } as MemberPayload; + + setSpinning(true); + addMemberToNetgroups(payload).then((response) => { + if ("data" in response) { + if (response.data.result) { + // Set alert: success + alerts.addAlert( + "add-member-success", + "Assigned new netgroups to netgroup '" + props.id + "'", + "success" + ); + // Refresh data + props.onRefreshData(); + // Close modal + setShowAddModal(false); + } else if (response.data.error) { + // Set alert: error + const errorMessage = response.data.error as unknown as ErrorResult; + alerts.addAlert("add-member-error", errorMessage.message, "danger"); + } + } + setSpinning(false); + }); + }; + + // Delete + const onDeleteNetgroups = () => { + const payload = { + entryName: props.id, + entityType: "netgroup", + idsToAdd: netgroupsSelected, + } as MemberPayload; + + setSpinning(true); + removeMembersFromNetgroups(payload).then((response) => { + if ("data" in response) { + if (response.data.result) { + // Set alert: success + alerts.addAlert( + "remove-hnetgroups-success", + "Removed netgroups from netgroup '" + props.id + "'", + "success" + ); + // Refresh + props.onRefreshData(); + // Disable delete button + if (membershipDirection === "direct") { + setNetgroupsSelected([]); + } else { + setIndirectNetgroupsSelected([]); + } + // Close modal + setShowDeleteModal(false); + // Back to page 1 + setPage(1); + } else if (response.data.error) { + // Set alert: error + const errorMessage = response.data.error as unknown as ErrorResult; + alerts.addAlert( + "remove-netgroups-error", + errorMessage.message, + "danger" + ); + } + } + setSpinning(false); + }); + }; + + return ( + <> + + {}} + refreshButtonEnabled={isRefreshButtonEnabled} + onRefreshButtonClick={props.onRefreshData} + deleteButtonEnabled={ + membershipDirection === "direct" + ? netgroupsSelected.length > 0 + : indirectNetgroupsSelected.length > 0 + } + onDeleteButtonClick={() => setShowDeleteModal(true)} + addButtonEnabled={isAddButtonEnabled} + onAddButtonClick={() => setShowAddModal(true)} + membershipDirectionEnabled={true} + membershipDirection={membershipDirection} + onMembershipDirectionChange={setMembershipDirection} + helpIconEnabled={true} + totalItems={netgroupNames.length} + perPage={perPage} + page={page} + onPerPageChange={setPerPage} + onPageChange={setPage} + /> + + setPage(page)} + onPerPageSelect={(_e, perPage) => setPerPage(perPage)} + /> + setShowAddModal(false)} + availableItems={availableItems} + onAdd={onAddNetgroup} + onSearchTextChange={setAdderSearchValue} + title={"Assign netgroups to netgroup: " + props.id} + ariaLabel={"Add netgroups modal"} + spinning={spinning} + /> + setShowDeleteModal(false)} + title={"Delete netgroups from netgroup: " + props.id} + onDelete={onDeleteNetgroups} + spinning={spinning} + > + + membershipDirection === "direct" + ? netgroupsSelected.includes(hostGroup.cn) + : indirectNetgroupsSelected.includes(hostGroup.cn) + )} + from="netgroups" + idKey="cn" + columnNamesToShow={netgroupColumnNames} + propertiesToShow={netgroupProperties} + showTableRows + /> + + + ); +}; + +export default MembersNetgroups; diff --git a/src/components/Members/MembersServices.tsx b/src/components/Members/MembersServices.tsx index 94a44413..5ad46446 100644 --- a/src/components/Members/MembersServices.tsx +++ b/src/components/Members/MembersServices.tsx @@ -16,13 +16,12 @@ import useListPageSearchParams from "src/hooks/useListPageSearchParams"; import { API_VERSION_BACKUP, paginate } from "src/utils/utils"; import { apiToService } from "src/utils/serviceUtils"; // RPC -import { ErrorResult } from "src/services/rpc"; +import { ErrorResult, MemberPayload } from "src/services/rpc"; import { useGetServicesInfoByUidQuery, useGettingServicesQuery, } from "src/services/rpcServices"; import { - MemberPayload, useAddAsMemberMutation, useRemoveAsMemberMutation, } from "src/services/rpcUserGroups"; @@ -205,7 +204,7 @@ const MembersServices = (props: PropsToMembersServices) => { } const payload = { - userGroup: props.id, + entryName: props.id, entityType: "service", idsToAdd: newServiceNames, } as MemberPayload; @@ -237,7 +236,7 @@ const MembersServices = (props: PropsToMembersServices) => { // Delete const onDeleteService = () => { const payload = { - userGroup: props.id, + entryName: props.id, entityType: "service", idsToAdd: servicesSelected, } as MemberPayload; diff --git a/src/components/Members/MembersUserGroups.tsx b/src/components/Members/MembersUserGroups.tsx index 286abbdc..478512dc 100644 --- a/src/components/Members/MembersUserGroups.tsx +++ b/src/components/Members/MembersUserGroups.tsx @@ -15,14 +15,17 @@ import useListPageSearchParams from "src/hooks/useListPageSearchParams"; // Utils import { API_VERSION_BACKUP, paginate } from "src/utils/utils"; // RPC -import { ErrorResult } from "src/services/rpc"; +import { ErrorResult, MemberPayload } from "src/services/rpc"; import { - MemberPayload, useAddAsMemberMutation, useGetGroupInfoByNameQuery, useGettingGroupsQuery, useRemoveAsMemberMutation, } from "src/services/rpcUserGroups"; +import { + useAddAsMemberNGMutation, + useRemoveAsMemberNGMutation, +} from "src/services/rpcNetgroups"; import { apiToGroup } from "src/utils/groupUtils"; interface PropsToMembersUsergroups { @@ -130,9 +133,11 @@ const MembersUserGroups = (props: PropsToMembersUsergroups) => { const getEntityType = () => { if (props.from === "user-groups") { return "user group"; + } else if (props.from === "netgroup") { + return "netgroup"; } else { // Return 'group' as default - return "group"; + return "user group"; } }; @@ -142,6 +147,7 @@ const MembersUserGroups = (props: PropsToMembersUsergroups) => { const entityType = getEntityType(); const userGroupColumnNames = ["Group name", "GID", "Description"]; const userGroupProperties = ["cn", "gidnumber", "description"]; + const directionEnabled = props.from !== "netgroup" ? true : false; // Dialogs and actions const [showAddModal, setShowAddModal] = React.useState(false); @@ -156,8 +162,14 @@ const MembersUserGroups = (props: PropsToMembersUsergroups) => { // Add new member to 'UserGroup' // API calls - const [addMemberToUserGroups] = useAddAsMemberMutation(); - const [removeMembersFromUserGroups] = useRemoveAsMemberMutation(); + let [addMembers] = useAddAsMemberMutation(); + if (props.from === "netgroup") { + [addMembers] = useAddAsMemberNGMutation(); + } + let [removeMembers] = useRemoveAsMemberMutation(); + if (props.from === "netgroup") { + [removeMembers] = useRemoveAsMemberNGMutation(); + } const [adderSearchValue, setAdderSearchValue] = React.useState(""); const [availableUserGroups, setAvailableUserGroups] = React.useState< UserGroup[] @@ -218,13 +230,13 @@ const MembersUserGroups = (props: PropsToMembersUsergroups) => { } const payload = { - userGroup: props.id, + entryName: props.id, entityType: "group", idsToAdd: newUserGroupNames, } as MemberPayload; setSpinning(true); - addMemberToUserGroups(payload).then((response) => { + addMembers(payload).then((response) => { if ("data" in response) { if (response.data.result) { // Set alert: success @@ -250,13 +262,13 @@ const MembersUserGroups = (props: PropsToMembersUsergroups) => { // Delete const onDeleteUserGroups = () => { const payload = { - userGroup: props.id, + entryName: props.id, entityType: "group", idsToAdd: userGroupsSelected, } as MemberPayload; setSpinning(true); - removeMembersFromUserGroups(payload).then((response) => { + removeMembers(payload).then((response) => { if ("data" in response) { if (response.data.result) { // Set alert: success @@ -333,7 +345,7 @@ const MembersUserGroups = (props: PropsToMembersUsergroups) => { onDeleteButtonClick={() => setShowDeleteModal(true)} addButtonEnabled={isAddButtonEnabled} onAddButtonClick={() => setShowAddModal(true)} - membershipDirectionEnabled={true} + membershipDirectionEnabled={directionEnabled} membershipDirection={membershipDirection} onMembershipDirectionChange={setMembershipDirection} helpIconEnabled={true} @@ -388,7 +400,7 @@ const MembersUserGroups = (props: PropsToMembersUsergroups) => { setShowDeleteModal(false)} - title={"Delete " + entityType + "s from user group: " + props.id} + title={"Delete user groups from " + entityType + ": " + props.id} onDelete={onDeleteUserGroups} spinning={spinning} > diff --git a/src/components/Members/MembersUsers.tsx b/src/components/Members/MembersUsers.tsx index 551e8077..6f1beabf 100644 --- a/src/components/Members/MembersUsers.tsx +++ b/src/components/Members/MembersUsers.tsx @@ -16,16 +16,19 @@ import useListPageSearchParams from "src/hooks/useListPageSearchParams"; import { API_VERSION_BACKUP, paginate } from "src/utils/utils"; import { apiToUser } from "src/utils/userUtils"; // RPC -import { ErrorResult } from "src/services/rpc"; +import { ErrorResult, MemberPayload } from "src/services/rpc"; import { useGetUsersInfoByUidQuery, useGettingActiveUserQuery, } from "src/services/rpcUsers"; import { - MemberPayload, useAddAsMemberMutation, useRemoveAsMemberMutation, } from "src/services/rpcUserGroups"; +import { + useAddAsMemberNGMutation, + useRemoveAsMemberNGMutation, +} from "src/services/rpcNetgroups"; interface PropsToMembersUsers { entity: Partial; @@ -75,6 +78,15 @@ const MembersUsers = (props: PropsToMembersUsers) => { membershipDirection === "direct" ? member_user : memberindirect_user; userNames = [...userNames]; + let [addMembers] = useAddAsMemberMutation(); + if (props.from === "netgroup") { + [addMembers] = useAddAsMemberNGMutation(); + } + let [removeMembers] = useRemoveAsMemberMutation(); + if (props.from === "netgroup") { + [removeMembers] = useRemoveAsMemberNGMutation(); + } + const getUsersNameToLoad = (): string[] => { let toLoad = [...userNames]; toLoad.sort(); @@ -130,9 +142,11 @@ const MembersUsers = (props: PropsToMembersUsers) => { const getEntityType = () => { if (props.from === "user-groups") { return "user group"; + } else if (props.from === "netgroup") { + return "netgroup"; } else { // Return 'group' as default - return "group"; + return "user group"; } }; @@ -148,6 +162,7 @@ const MembersUsers = (props: PropsToMembersUsers) => { "Email address", ]; const userProperties = ["uid", "givenname", "sn", "nsaccountlock", "mail"]; + const directionEnabled = props.from !== "netgroup" ? true : false; // Dialogs and actions const [showAddModal, setShowAddModal] = React.useState(false); @@ -160,10 +175,6 @@ const MembersUsers = (props: PropsToMembersUsers) => { const isAddButtonEnabled = membershipDirection !== "indirect" && isRefreshButtonEnabled; - // Add new member to 'User' - // API calls - const [addMemberToUser] = useAddAsMemberMutation(); - const [removeMembersFromUsers] = useRemoveAsMemberMutation(); const [adderSearchValue, setAdderSearchValue] = React.useState(""); const [availableUsers, setAvailableUsers] = React.useState([]); const [availableItems, setAvailableItems] = React.useState( @@ -222,13 +233,13 @@ const MembersUsers = (props: PropsToMembersUsers) => { } const payload = { - userGroup: props.id, + entryName: props.id, entityType: "user", idsToAdd: newUserNames, } as MemberPayload; setSpinning(true); - addMemberToUser(payload).then((response) => { + addMembers(payload).then((response) => { if ("data" in response) { if (response.data.result) { // Set alert: success @@ -254,13 +265,13 @@ const MembersUsers = (props: PropsToMembersUsers) => { // Delete const onDeleteUser = () => { const payload = { - userGroup: props.id, + entryName: props.id, entityType: "user", idsToAdd: usersSelected, } as MemberPayload; setSpinning(true); - removeMembersFromUsers(payload).then((response) => { + removeMembers(payload).then((response) => { if ("data" in response) { if (response.data.result) { // Set alert: success @@ -333,7 +344,7 @@ const MembersUsers = (props: PropsToMembersUsers) => { onDeleteButtonClick={() => setShowDeleteModal(true)} addButtonEnabled={isAddButtonEnabled} onAddButtonClick={() => setShowAddModal(true)} - membershipDirectionEnabled={true} + membershipDirectionEnabled={directionEnabled} membershipDirection={membershipDirection} onMembershipDirectionChange={setMembershipDirection} helpIconEnabled={true} diff --git a/src/navigation/AppRoutes.tsx b/src/navigation/AppRoutes.tsx index 87277991..ca601ea8 100644 --- a/src/navigation/AppRoutes.tsx +++ b/src/navigation/AppRoutes.tsx @@ -223,6 +223,26 @@ export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => { path="" element={} /> + } + /> + } + /> + } + /> + } + /> + } + /> diff --git a/src/pages/Netgroups/NetgroupsMembers.tsx b/src/pages/Netgroups/NetgroupsMembers.tsx new file mode 100644 index 00000000..bdc7816d --- /dev/null +++ b/src/pages/Netgroups/NetgroupsMembers.tsx @@ -0,0 +1,258 @@ +import React, { useState } from "react"; +// PatternFly +import { Badge, Tab, Tabs, TabTitleText } from "@patternfly/react-core"; +// Data types +import { Netgroup } from "src/utils/datatypes/globalDataTypes"; +import { MembershipDirection } from "src/components/MemberOf/MemberOfToolbar"; +// Layouts +import TabLayout from "src/components/layouts/TabLayout"; +// Navigation +import { useNavigate } from "react-router-dom"; +// Hooks +import useUpdateRoute from "src/hooks/useUpdateRoute"; +// RPC +import { useGetNetgroupByIdQuery } from "src/services/rpcNetgroups"; +// 'Members' sections +import MembersUsers from "src/components/Members/MembersUsers"; +import MembersUserGroups from "src/components/Members/MembersUserGroups"; +import MembersHosts from "src/components/Members/MembersHosts"; +import MembersHostGroups from "src/components/Members/MembersHostGroups"; +import MembersNetgroups from "src/components/Members/MembersNetgroups"; + +interface PropsToNetgroupsMembers { + netgroup: Netgroup; + tabSection: string; +} + +const NetgroupsMembers = (props: PropsToNetgroupsMembers) => { + const navigate = useNavigate(); + + const netgroupQuery = useGetNetgroupByIdQuery(props.netgroup.cn); + const netgroupData = netgroupQuery.data || {}; + const [netgroup, setNetgroup] = useState>({}); + + React.useEffect(() => { + if (!netgroupQuery.isFetching && netgroupData) { + setNetgroup({ ...netgroupData }); + } + }, [netgroupData, netgroupQuery.isFetching]); + + const onRefreshNetgroupData = () => { + netgroupQuery.refetch(); + }; + + // Update current route data to Redux and highlight the current page in the Nav bar + useUpdateRoute({ pathname: "netgroups", noBreadcrumb: true }); + + // Tab counters + const [netgroupCount, setNetgroupCount] = React.useState(0); + const [userCount, setUserCount] = React.useState(0); + const [groupCount, setGroupCount] = React.useState(0); + const [hostCount, setHostCount] = React.useState(0); + const [hostGroupCount, setHostGroupCount] = React.useState(0); + + // group Directions + const [netgroupDirection, setNetgroupDirection] = React.useState( + "direct" as MembershipDirection + ); + + const updateNetgroupDirection = (direction: MembershipDirection) => { + if (direction === "direct") { + setNetgroupCount( + netgroup && netgroup.member_netgroup + ? netgroup.member_netgroup.length + : 0 + ); + } else { + setNetgroupCount( + netgroup && netgroup.memberindirect_netgroup + ? netgroup.memberindirect_netgroup.length + : 0 + ); + } + setNetgroupDirection(direction); + }; + + React.useEffect(() => { + if (netgroupDirection === "direct") { + setNetgroupCount( + netgroup && netgroup.member_netgroup + ? netgroup.member_netgroup.length + : 0 + ); + } else { + setNetgroupCount( + netgroup && netgroup.memberindirect_netgroup + ? netgroup.memberindirect_netgroup.length + : 0 + ); + } + + setUserCount( + netgroup && netgroup.memberuser_user ? netgroup.memberuser_user.length : 0 + ); + setGroupCount( + netgroup && netgroup.memberuser_group + ? netgroup.memberuser_group.length + : 0 + ); + setHostCount( + netgroup && netgroup.memberhost_host ? netgroup.memberhost_host.length : 0 + ); + setHostGroupCount( + netgroup && netgroup.memberhost_hostgroup + ? netgroup.memberhost_hostgroup.length + : 0 + ); + }, [netgroup]); + + // Tab + const [activeTabKey, setActiveTabKey] = useState("member_user"); + + const handleTabClick = ( + _event: React.MouseEvent, + tabIndex: number | string + ) => { + setActiveTabKey(tabIndex as string); + navigate("/netgroups/" + props.netgroup.cn + "/" + tabIndex); + }; + + React.useEffect(() => { + setActiveTabKey(props.tabSection); + }, [props.tabSection]); + + return ( + + + + Users{" "} + + {userCount} + + + } + > + {}} + direction={"direct"} + /> + + + User groups{" "} + + {groupCount} + + + } + > + {}} + direction={"direct"} + /> + + + Hosts{" "} + + {hostCount} + + + } + > + {}} + direction={"direct"} + /> + + + Host groups{" "} + + {hostGroupCount} + + + } + > + {}} + direction={"direct"} + /> + + + Netgroups{" "} + + {netgroupCount} + + + } + > + + + + + ); +}; + +export default NetgroupsMembers; diff --git a/src/pages/Netgroups/NetgroupsTabs.tsx b/src/pages/Netgroups/NetgroupsTabs.tsx index 5ab302ef..d75c252f 100644 --- a/src/pages/Netgroups/NetgroupsTabs.tsx +++ b/src/pages/Netgroups/NetgroupsTabs.tsx @@ -14,12 +14,14 @@ import { URL_PREFIX } from "src/navigation/NavRoutes"; import BreadCrumb, { BreadCrumbItem } from "src/components/layouts/BreadCrumb"; import TitleLayout from "src/components/layouts/TitleLayout"; import DataSpinner from "src/components/layouts/DataSpinner"; +import { partialNetgroupToNetgroup } from "src/utils/netgroupsUtils"; // Hooks import { useNetgroupSettings } from "src/hooks/useNetgroupSettingsData"; // Redux import { useAppDispatch } from "src/store/hooks"; import { updateBreadCrumbPath } from "src/store/Global/routes-slice"; import { NotFound } from "src/components/errors/PageErrors"; +import NetgroupsMembers from "./NetgroupsMembers"; import NetgroupsSettings from "./NetgroupsSettings"; // eslint-disable-next-line react/prop-types @@ -43,10 +45,10 @@ const NetgroupsTabs = ({ section }) => { setActiveTabKey(tabIndex as string); if (tabIndex === "settings") { navigate("/netgroups/" + cn); + } else if (tabIndex === "member") { + navigate("/netgroups/" + cn + "/member_user"); } else if (tabIndex === "memberof") { // navigate("/netgroups/" + cn + "/memberof_netgroup"); - } else if (tabIndex === "members") { - // navigate("/netgroups/" + cn + "/members_netgroup"); } }; @@ -78,7 +80,12 @@ const NetgroupsTabs = ({ section }) => { if (!section) { navigate(URL_PREFIX + "/netgroups/" + cn); } - setActiveTabKey(section); + const section_string = section as string; + if (section_string.startsWith("memberof_")) { + setActiveTabKey("memberof"); + } else if (section_string.startsWith("member_")) { + setActiveTabKey("member"); + } }, [section]); if ( @@ -96,6 +103,8 @@ const NetgroupsTabs = ({ section }) => { return ; } + const netgroup = partialNetgroupToNetgroup(netgroupSettingsData.netgroup); + return ( <> @@ -138,13 +147,15 @@ const NetgroupsTabs = ({ section }) => { /> Members} - > + > + + Is a member of} > diff --git a/src/services/rpc.ts b/src/services/rpc.ts index 71acf5a4..d8468de4 100644 --- a/src/services/rpc.ts +++ b/src/services/rpc.ts @@ -155,6 +155,12 @@ export interface GetEntriesPayload { entryType?: "user" | "group" | "host" | "hostgroup" | "netgroup"; } +export interface MemberPayload { + entryName: string; + idsToAdd: string[]; + entityType: string; +} + // Body data to perform the calls export const getCommand = (commandData: Command) => { const payloadWithParams = { diff --git a/src/services/rpcHostGroups.ts b/src/services/rpcHostGroups.ts index c1cd2af7..f6699ab8 100644 --- a/src/services/rpcHostGroups.ts +++ b/src/services/rpcHostGroups.ts @@ -12,6 +12,7 @@ import { apiToHostGroup } from "src/utils/hostGroupUtils"; import { API_VERSION_BACKUP } from "../utils/utils"; // Data types import { HostGroup } from "src/utils/datatypes/globalDataTypes"; +import { MemberPayload } from "./rpc"; /** * User Group-related endpoints: addToGroups, removeFromGroups, @@ -42,12 +43,6 @@ export type GroupFullData = { hostGroup?: Partial; }; -export interface MemberPayload { - hostGroup: string; - idsToAdd: string[]; - entityType: string; -} - const extendedApi = api.injectEndpoints({ endpoints: (build) => ({ getHostGroupsFullData: build.query({ @@ -237,7 +232,7 @@ const extendedApi = api.injectEndpoints({ */ addAsMemberHG: build.mutation({ query: (payload) => { - const hostGroup = payload.hostGroup; + const hostGroup = payload.entryName; const idsToAdd = payload.idsToAdd; const memberType = payload.entityType; @@ -256,7 +251,7 @@ const extendedApi = api.injectEndpoints({ */ removeAsMemberHG: build.mutation({ query: (payload) => { - const hostGroup = payload.hostGroup; + const hostGroup = payload.entryName; const idsToAdd = payload.idsToAdd; const memberType = payload.entityType; diff --git a/src/services/rpcNetgroups.ts b/src/services/rpcNetgroups.ts index 4c98a199..018cc8b7 100644 --- a/src/services/rpcNetgroups.ts +++ b/src/services/rpcNetgroups.ts @@ -7,6 +7,7 @@ import { BatchResponse, FindRPCResponse, useGettingGenericQuery, + MemberPayload, } from "./rpc"; import { API_VERSION_BACKUP } from "../utils/utils"; import { apiToNetgroup } from "src/utils/netgroupsUtils"; @@ -14,7 +15,7 @@ import { Netgroup } from "../utils/datatypes/globalDataTypes"; /** * Netgroup-related endpoints: addToNetgroups, removeFromNetgroups, getNetgroupInfoByName, - * saveNetgroup, saveAndCleanNetgroup + * saveNetgroup, saveAndCleanNetgroup, getNetgroupByID * * API commands: * - netgroup_add: https://freeipa.readthedocs.io/en/latest/api/netgroup_add.html @@ -341,6 +342,61 @@ const extendedApi = api.injectEndpoints({ return getBatchCommand(actions, API_VERSION_BACKUP); }, }), + /** + * Get user group info by name + * + */ + getNetgroupById: build.query({ + query: (groupId) => { + return getCommand({ + method: "netgroup_show", + params: [ + [groupId], + { all: true, rights: true, version: API_VERSION_BACKUP }, + ], + }); + }, + transformResponse: (response: FindRPCResponse): Netgroup => + apiToNetgroup(response.result.result), + }), + /** + * Given a list of IDs, add them as members + * @param {MemberPayload} - Payload with IDs and options + */ + addAsMemberNG: build.mutation({ + query: (payload) => { + const netgroup = payload.entryName; + const idsToAdd = payload.idsToAdd; + const memberType = payload.entityType; + + return getCommand({ + method: "netgroup_add_member", + params: [ + [netgroup], + { all: true, [memberType]: idsToAdd, version: API_VERSION_BACKUP }, + ], + }); + }, + }), + /** + * Remove members + * @param {MemberPayload} - Payload with IDs and options + */ + removeAsMemberNG: build.mutation({ + query: (payload) => { + const netgroup = payload.entryName; + const idsToAdd = payload.idsToAdd; + const memberType = payload.entityType; + + return getCommand({ + method: "netgroup_remove_member", + params: [ + [netgroup], + { all: true, [memberType]: idsToAdd, version: API_VERSION_BACKUP }, + ], + }); + }, + }), }), overrideExisting: false, }); @@ -362,4 +418,7 @@ export const { useSaveNetgroupMutation, useRemoveMemberFromNetgroupsMutation, useSaveAndCleanNetgroupMutation, + useGetNetgroupByIdQuery, + useAddAsMemberNGMutation, + useRemoveAsMemberNGMutation, } = extendedApi; diff --git a/src/services/rpcUserGroups.ts b/src/services/rpcUserGroups.ts index d73a127f..e8c53bd8 100644 --- a/src/services/rpcUserGroups.ts +++ b/src/services/rpcUserGroups.ts @@ -7,6 +7,7 @@ import { BatchRPCResponse, FindRPCResponse, useGettingGenericQuery, + MemberPayload, } from "./rpc"; import { apiToGroup } from "src/utils/groupUtils"; import { apiToPwPolicy } from "src/utils/pwPolicyUtils"; @@ -60,12 +61,6 @@ export type GroupFullData = { pwPolicy?: Partial; }; -export interface MemberPayload { - userGroup: string; - idsToAdd: string[]; - entityType: string; -} - const extendedApi = api.injectEndpoints({ endpoints: (build) => ({ getUserGroupsFullData: build.query({ @@ -327,12 +322,12 @@ const extendedApi = api.injectEndpoints({ apiToGroup(response.result.result), }), /** - * Given a list of user IDs, add them as members to a group - * @param {MemberPayload} - Payload with user IDs and options + * Given a list of IDs, add them as members + * @param {MemberPayload} - Payload with IDs and options */ addAsMember: build.mutation({ query: (payload) => { - const userGroup = payload.userGroup; + const userGroup = payload.entryName; const idsToAdd = payload.idsToAdd; const memberType = payload.entityType; @@ -346,12 +341,12 @@ const extendedApi = api.injectEndpoints({ }, }), /** - * Remove a user group from some user members - * @param {MemberPayload} - Payload with user IDs and options + * Remove members + * @param {MemberPayload} - Payload with IDs and options */ removeAsMember: build.mutation({ query: (payload) => { - const userGroup = payload.userGroup; + const userGroup = payload.entryName; const idsToAdd = payload.idsToAdd; const memberType = payload.entityType; diff --git a/src/utils/datatypes/globalDataTypes.ts b/src/utils/datatypes/globalDataTypes.ts index af3f8871..15d66839 100644 --- a/src/utils/datatypes/globalDataTypes.ts +++ b/src/utils/datatypes/globalDataTypes.ts @@ -184,10 +184,12 @@ export interface Netgroup { description: string; dn: string; memberof_netgroup: string[]; + member_netgroup: string[]; memberhost_host: string[]; memberhost_hostgroup: string[]; memberuser_user: string[]; memberuser_group: string[]; + memberindirect_netgroup: string[]; externalhost: string[]; usercategory: string; hostcategory: string; diff --git a/src/utils/netgroupsUtils.tsx b/src/utils/netgroupsUtils.tsx index fff603ec..b0164ca3 100644 --- a/src/utils/netgroupsUtils.tsx +++ b/src/utils/netgroupsUtils.tsx @@ -41,10 +41,12 @@ export function createEmptyNetgroup(): Netgroup { usercategory: "", hostcategory: "", memberof_netgroup: [], + member_netgroup: [], memberhost_host: [], memberhost_hostgroup: [], memberuser_user: [], memberuser_group: [], + memberindirect_netgroup: [], externalhost: [], }; diff --git a/tests/features/hostgroup_members.feature b/tests/features/hostgroup_members.feature index d189f262..0bc4e950 100644 --- a/tests/features/hostgroup_members.feature +++ b/tests/features/hostgroup_members.feature @@ -117,3 +117,25 @@ Feature: Hostgroup members And I should not see "testgroup" entry in the data table Then I should see the "hostgroup" tab count is "0" + # + # Cleanup + # + Scenario: Cleanup - remove the test host + Given I am on "hosts" page + Then I select partial entry "myhost.dom-server.ipa.demo" in the data table + When I click on "Delete" button + * I see "Remove hosts" modal + * I should see partial "myhost.dom-server.ipa.demo" entry in the data table + When in the modal dialog I click on "Delete" button + * I should see "success" alert with text "Hosts removed" + Then I should not see "myhost.dom-server.ipa.demo" entry in the data table + + Scenario: Cleanup - delete the test host group + Given I should see "testgroup" entry in the data table + Then I select entry "testgroup" in the data table + When I click on "Delete" button + * I see "Remove host groups" modal + * I should see "testgroup" 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" + Then I should not see "testgroup" entry in the data table diff --git a/tests/features/netgroup_members.feature b/tests/features/netgroup_members.feature new file mode 100644 index 00000000..05cfcdbf --- /dev/null +++ b/tests/features/netgroup_members.feature @@ -0,0 +1,266 @@ +Feature: Netgroup members + Work with usergroup Members section and its operations in all the available + tabs (Users, User groups, Hosts, Host groups, And netgroups) + + Background: + Given I am logged in as "Administrator" + Given I am on "netgroups" page + + Scenario: Add test user + Given I am on "active-users" page + Given sample testing user "armadillo" exists + + Scenario: Add two netgroup + Given I am on "netgroups" page + When I click on "Add" button + * I type in the field "Netgroup name" text "mynetgroup" + When in the modal dialog I click on "Add and add another" button + * I should see "success" alert with text "New netgroup added" + Then I type in the field "Netgroup name" text "mynetgroup2" + * in the modal dialog I click on "Add" button + * I should see "success" alert with text "New netgroup added" + Then I should see "mynetgroup" entry in the data table + Then I should see "mynetgroup2" 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 "myhostgroup" + When in the modal dialog I click on "Add" button + * I should see "success" alert with text "New host group added" + Then I should see "myhostgroup" entry in the data table + + # + # Test "User" members + # + Scenario: Add a User member + Given I click on "mynetgroup" entry in the data table + Given I click on "Members" page tab + Given I am on "mynetgroup" group > Members > "Users" section + Then I should see the "user" tab count is "0" + When I click on "Add" button located in the toolbar + Then I should see the dialog with title "Assign users to netgroup: mynetgroup" + When I move user "armadillo" 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 users to netgroup 'mynetgroup'" + * I close the alert + Then I should see the element "armadillo" in the table + Then I should see the "user" tab count is "1" + + Scenario: Search for a user + When I type "armadillo" in the search field + Then I should see the "armadillo" text in the search input field + When I click on the arrow icon to perform search + Then I should see the element "armadillo" 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 + When I click on the arrow icon to perform search + Then I should not see the element "armadillo" in the table + * I click on the X icon to clear the search field + Then I click on the arrow icon to perform search + + Scenario: Remove User member + When I select entry "armadillo" in the data table + And I click on "Delete" button located in the toolbar + Then I should see the dialog with title "Delete users from netgroup: mynetgroup" + And the "armadillo" element should be in the dialog table + When in the modal dialog I click on "Delete" button + Then I should see "success" alert with text "Removed users from netgroup 'mynetgroup'" + * I close the alert + And I should not see the element "armadillo" in the table + Then I should see the "user" tab count is "0" + + # + # Test "UserGroup" members + # + Scenario: Add a Usergroup member + Given I click on "User groups" page tab + Given I am on "mynetgroup" group > Members > "User groups" section + Then I should see the "group" tab count is "0" + When I click on "Add" button located in the toolbar + Then I should see the dialog with title "Assign user groups to netgroup: mynetgroup" + When I move user "editors" 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 user groups to netgroup 'mynetgroup'" + * I close the alert + Then I should see the element "editors" in the table + Then I should see the "group" tab count is "1" + + Scenario: Search for a usergroup + When I type "editors" in the search field + Then I should see the "editors" text in the search input field + When I click on the arrow icon to perform search + Then I should see the element "editors" 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 + When I click on the arrow icon to perform search + Then I should not see the element "editors" in the table + Then I click on the X icon to clear the search field + Then I click on the arrow icon to perform search + Then I should see the element "editors" in the table + + Scenario: Remove Usergroup member + When I select entry "editors" in the data table + And I click on "Delete" button located in the toolbar + Then I should see the dialog with title "Delete user groups from netgroup: mynetgroup" + And the "editors" element should be in the dialog table + When in the modal dialog I click on "Delete" button + Then I should see "success" alert with text "Removed user groups from netgroup 'mynetgroup'" + * I close the alert + And I should not see the element "editors" in the table + Then I should see the "group" tab count is "0" + + # + # Test "Host" members + # + Scenario: Add a Host member into the netgroup + Given I click on "Hosts" page tab + Given I am on "mynetgroup" group > Members > "Hosts" section + Then I should see the "host" tab count is "0" + When I click on "Add" button located in the toolbar + Then I should see the dialog with title "Assign hosts to netgroup: mynetgroup" + When I move user "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 netgroup 'mynetgroup'" + * I close the alert + Then I should see "server.ipa.demo" entry in the data table + Then I should see the "host" tab count is "1" + + Scenario: Search for a host + When I type "server.ipa.demo" in the search field + Then I should see the "server.ipa.demo" text in the search input field + When I click on the arrow icon to perform search + Then I should see "server.ipa.demo" entry in the data 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 + When I click on the arrow icon to perform search + Then I should not see "server.ipa.demo" entry in the data table + Then I click on the X icon to clear the search field + Then I click on the arrow icon to perform search + Then I should see "server.ipa.demo" entry in the data table + + Scenario: Remove host member + When I select partial entry "server.ipa.demo" in the data table + And I click on "Delete" button located in the toolbar + Then I should see the dialog with title "Delete hosts from netgroup: mynetgroup" + And the "server.ipa.demo" element should be in the dialog table + When in the modal dialog I click on "Delete" button + Then I should see "success" alert with text "Removed hosts from netgroup 'mynetgroup'" + * I close the alert + And I should not see the element "server.ipa.demo" in the table + Then I should see the "host" tab count is "0" + + # + # Test "Host group" members + # + Scenario: Add a Host group member into the netgroup + Given I click on "Host groups" page tab + Given I am on "mynetgroup" group > Members > "Host groups" section + Then I should see the "hostgroup" tab count is "0" + When I click on "Add" button located in the toolbar + Then I should see the dialog with title "Assign host groups to netgroup: mynetgroup" + When I move user "myhostgroup" 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 host groups to netgroup 'mynetgroup'" + * I close the alert + Then I should see "myhostgroup" entry in the data table + Then I should see the "hostgroup" tab count is "1" + + Scenario: Search for a host group + When I type "myhostgroup" in the search field + Then I should see the "myhostgroup" text in the search input field + When I click on the arrow icon to perform search + Then I should see "myhostgroup" entry in the data 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 + When I click on the arrow icon to perform search + Then I should not see "myhostgroup" entry in the data table + Then I click on the X icon to clear the search field + Then I click on the arrow icon to perform search + Then I should see "myhostgroup" entry in the data table + + Scenario: Remove host group member + When I select partial entry "myhostgroup" in the data table + And I click on "Delete" button located in the toolbar + Then I should see the dialog with title "Delete host groups from netgroup: mynetgroup" + And the "myhostgroup" element should be in the dialog table + When in the modal dialog I click on "Delete" button + Then I should see "success" alert with text "Removed host groups from netgroup 'mynetgroup'" + * I close the alert + And I should not see the element "myhostgroup" in the table + Then I should see the "hostgroup" tab count is "0" + + # + # Test "Netgroup" members + # + Scenario: Add a netgroup into the netgroup + Given I click on "Netgroups" page tab + Given I am on "mynetgroup" group > Members > "Netgroups" section + Then I should see the "netgroup" tab count is "0" + When I click on "Add" button located in the toolbar + Then I should see the dialog with title "Assign netgroups to netgroup: mynetgroup" + When I move user "mynetgroup2" 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 netgroups to netgroup 'mynetgroup'" + * I close the alert + Then I should see "mynetgroup2" entry in the data table + Then I should see the "netgroup" tab count is "1" + + Scenario: Search for a netgroup + When I type "mynetgroup2" in the search field + Then I should see the "mynetgroup2" text in the search input field + When I click on the arrow icon to perform search + Then I should see "mynetgroup2" entry in the data 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 + When I click on the arrow icon to perform search + Then I should not see "mynetgroup2" entry in the data table + Then I click on the X icon to clear the search field + Then I click on the arrow icon to perform search + Then I should see "mynetgroup2" entry in the data table + + Scenario: Switch between direct and indirect memberships + When I click on the "indirect" button + Then I should see the "netgroup" tab count is "0" + When I click on the "direct" button + Then I should see the "netgroup" tab count is "1" + + Scenario: Remove netgroup member + When I select entry "mynetgroup2" in the data table + And I click on "Delete" button located in the toolbar + Then I should see the dialog with title "Delete netgroups from netgroup: mynetgroup" + And the "mynetgroup2" element should be in the dialog table + When in the modal dialog I click on "Delete" button + Then I should see "success" alert with text "Removed netgroups from netgroup 'mynetgroup'" + * I close the alert + And I should not see the element "mynetgroup2" in the table + Then I should see the "netgroup" tab count is "0" + + # + # Cleanup + # + Scenario: clean up and delete new host group + Given I am on "host-groups" page + Then I select entry "myhostgroup" in the data table + When I click on "Delete" button + * I see "Remove host groups" modal + * I should see "myhostgroup" 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" + + Scenario: clean up and delete new netgroup + Given I am on "netgroups" page + Then I select entry "mynetgroup" in the data table + Then I select entry "mynetgroup2" in the data table + When I click on "Delete" button + * I see "Remove netgroups" modal + * I should see "mynetgroup" entry in the data table + * I should see "mynetgroup2" entry in the data table + When in the modal dialog I click on "Delete" button + * I should see "success" alert with text "Netgroups removed" + diff --git a/tests/features/steps/common.ts b/tests/features/steps/common.ts index e8991bfc..2d96d712 100644 --- a/tests/features/steps/common.ts +++ b/tests/features/steps/common.ts @@ -547,7 +547,7 @@ When("I scroll down", () => { }); }); -// Get tab badge count +// Get tab badge count. This expects the tab has an id that ends with "_count" Then( "I should see the {string} tab count is {string}", (count_id: string, value: string) => {