From ac95e4e7dea5e4698eef68e2c3d2988b957ae7f2 Mon Sep 17 00:00:00 2001 From: pwillis77 Date: Thu, 12 Sep 2024 12:40:36 +0000 Subject: [PATCH 1/2] feat(NET-1545): add tour to user management page --- .../modals/add-user-modal/AddUserModal.tsx | 5 +- .../invite-user-modal/InviteUserModal.tsx | 17 +- src/pages/users/CreateNetworkRolePage.tsx | 74 ++- src/pages/users/CreateUserGroupPage.tsx | 73 ++- src/pages/users/GroupsPage.tsx | 40 +- src/pages/users/RolesPage.tsx | 39 +- src/pages/users/UsersPage.tsx | 473 ++++++++++++++---- 7 files changed, 594 insertions(+), 127 deletions(-) diff --git a/src/components/modals/add-user-modal/AddUserModal.tsx b/src/components/modals/add-user-modal/AddUserModal.tsx index 2d9b9186..b6425235 100644 --- a/src/components/modals/add-user-modal/AddUserModal.tsx +++ b/src/components/modals/add-user-modal/AddUserModal.tsx @@ -33,7 +33,7 @@ interface AddUserModalProps { addUserButtonRef?: Ref; addUserNameInputRef?: Ref; addUserPasswordInputRef?: Ref; - addUserSetAsAdminCheckboxRef?: Ref; + createUserModalPlatformAccessLevelRef?: Ref; } type CreateUserForm = User & { @@ -62,6 +62,7 @@ export default function AddUserModal({ onCancel, addUserNameInputRef, addUserPasswordInputRef, + createUserModalPlatformAccessLevelRef, }: AddUserModalProps) { const [form] = Form.useForm(); const [notify, notifyCtx] = notification.useNotification(); @@ -343,7 +344,7 @@ export default function AddUserModal({ - + any; onCancel?: (e: MouseEvent) => void; onClose?: () => void; + inviteUserModalPlatformAccessLevelRef: Ref; + inviteUserModalEmailAddressesInputRef: Ref; } interface UserInviteForm { @@ -70,7 +72,14 @@ const groupsTabKey = 'groups'; const customRolesTabKey = 'custom-roles'; const defaultTabKey = groupsTabKey; -export default function InviteUserModal({ isOpen, onInviteFinish, onClose, onCancel }: InviteUserModalProps) { +export default function InviteUserModal({ + isOpen, + onInviteFinish, + onClose, + onCancel, + inviteUserModalPlatformAccessLevelRef, + inviteUserModalEmailAddressesInputRef, +}: InviteUserModalProps) { const [form] = Form.useForm(); const [notify, notifyCtx] = notification.useNotification(); const { isServerEE } = useServerLicense(); @@ -372,7 +381,7 @@ export default function InviteUserModal({ isOpen, onInviteFinish, onClose, onCan <>
- + - + networkRoleName.current, + placement: 'bottom', + }, + { + title: 'Network', + description: 'Select the network this role will apply to', + target: () => networkName.current, + placement: 'bottom', + }, + { + title: 'Admin Access', + description: 'Assign admin access to the network', + target: () => networkAdminAccess.current, + placement: 'bottom', + }, + { + title: 'Role Permissions', + description: 'Set role permissions for the network, turn on the permissions you want to grant', + target: () => networkRolePermissions.current, + placement: 'bottom', + }, + { + title: 'Create Role', + description: 'Click here to create the network role', + target: () => createRoleButton.current, + placement: 'bottom', + }, + ]; + useEffect(() => { loadNetworks(); }, [isServerEE, loadNetworks]); @@ -509,6 +551,16 @@ export default function CreateNetworkRolePage(props: PageProps) { Create a Network Role + + + @@ -519,7 +571,7 @@ export default function CreateNetworkRolePage(props: PageProps) { - + - + - + - + Role Permissions @@ -583,14 +635,20 @@ export default function CreateNetworkRolePage(props: PageProps) { }} > - - {/* misc */} + setIsTourOpen(false)} /> {notifyCtx} ); diff --git a/src/pages/users/CreateUserGroupPage.tsx b/src/pages/users/CreateUserGroupPage.tsx index 5013c04c..e97545a8 100644 --- a/src/pages/users/CreateUserGroupPage.tsx +++ b/src/pages/users/CreateUserGroupPage.tsx @@ -1,5 +1,5 @@ import { useStore } from '@/store/store'; -import { PlusOutlined } from '@ant-design/icons'; +import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { Button, Card, @@ -14,9 +14,11 @@ import { Table, TableColumnProps, theme, + Tour, + TourProps, Typography, } from 'antd'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { PageProps } from '../../models/Page'; import './UsersPage.scss'; import { extractErrorMsg } from '@/utils/ServiceUtils'; @@ -65,6 +67,13 @@ export default function CreateUserGroupPage(props: PageProps) { const [groupMembers, setGroupMembers] = useState([]); const [isCreateNetworkRoleModalOpen, setIsCreateNetworkRoleModalOpen] = useState(false); const [currentNetworkId, setCurrentNetworkId] = useState(''); + const [isTourOpen, setIsTourOpen] = useState(false); + + const groupNameRef = useRef(null); + const groupDescRef = useRef(null); + const groupNetworkAccessRef = useRef(null); + const groupMembersRef = useRef(null); + const createGroupButtonRef = useRef(null); const filteredMembers = useMemo(() => { return groupMembers.filter((m) => m.username?.toLowerCase().includes(membersSearch.trim().toLowerCase())); @@ -199,6 +208,39 @@ export default function CreateUserGroupPage(props: PageProps) { const platformRoleVal = Form.useWatch('platformRole', metadataForm); + const createUserGroupTourSteps: TourProps['steps'] = [ + { + title: 'Group Name', + description: 'Set group name', + target: () => groupNameRef.current, + placement: 'bottom', + }, + { + title: 'Group Description', + description: 'Set group description', + target: () => groupDescRef.current, + placement: 'bottom', + }, + { + title: 'Associated Network Roles', + description: 'Set the network roles for this group', + target: () => groupNetworkAccessRef.current, + placement: 'bottom', + }, + { + title: 'Group Members', + description: 'Add group members', + target: () => groupMembersRef.current, + placement: 'bottom', + }, + { + title: 'Create Group', + description: 'Click to create group', + target: () => createGroupButtonRef.current, + placement: 'bottom', + }, + ]; + useEffect(() => { setGroupMembers([]); }, [platformRoleVal]); @@ -216,7 +258,7 @@ export default function CreateUserGroupPage(props: PageProps) { {/* top bar */} - + View All Groups @@ -228,6 +270,16 @@ export default function CreateUserGroupPage(props: PageProps) { + + + @@ -236,7 +288,7 @@ export default function CreateUserGroupPage(props: PageProps) { - + */} - + @@ -277,7 +329,7 @@ export default function CreateUserGroupPage(props: PageProps) { - +
@@ -297,7 +349,11 @@ export default function CreateUserGroupPage(props: PageProps) { - + - @@ -351,6 +407,7 @@ export default function CreateUserGroupPage(props: PageProps) { {/* misc */} + setIsTourOpen(false)} /> {notifyCtx} {/* modals */} diff --git a/src/pages/users/GroupsPage.tsx b/src/pages/users/GroupsPage.tsx index e411b701..05589745 100644 --- a/src/pages/users/GroupsPage.tsx +++ b/src/pages/users/GroupsPage.tsx @@ -4,7 +4,14 @@ import { AppRoutes } from '@/routes'; import { UsersService } from '@/services/UsersService'; import { getUserGroupRoute, resolveAppRoute } from '@/utils/RouteUtils'; import { useServerLicense } from '@/utils/Utils'; -import { DeleteOutlined, MoreOutlined, PlusOutlined, QuestionCircleOutlined, SearchOutlined } from '@ant-design/icons'; +import { + DeleteOutlined, + InfoCircleOutlined, + MoreOutlined, + PlusOutlined, + QuestionCircleOutlined, + SearchOutlined, +} from '@ant-design/icons'; import { Button, Card, @@ -19,16 +26,29 @@ import { Typography, notification, } from 'antd'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { RefObject, useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface GroupPageProps { users: User[]; triggerDataRefresh?: () => void; + setIsTourOpen: (isOpen: boolean) => void; + groupsHelpButtonRef: RefObject; + groupsTableRef: RefObject; + groupsSearchInputRef: RefObject; + groupsCreateGroupButtonRef: RefObject; } -export default function GroupsPage({ users, triggerDataRefresh }: GroupPageProps) { +export default function GroupsPage({ + users, + triggerDataRefresh, + setIsTourOpen, + groupsHelpButtonRef, + groupsTableRef, + groupsSearchInputRef, + groupsCreateGroupButtonRef, +}: GroupPageProps) { const [notify, notifyCtx] = notification.useNotification(); const navigate = useNavigate(); const { isServerEE } = useServerLicense(); @@ -221,7 +241,7 @@ export default function GroupsPage({ users, triggerDataRefresh }: GroupPageProps {!isEmpty && ( <> - + } + ref={groupsHelpButtonRef} /> + @@ -262,6 +293,7 @@ export default function GroupsPage({ users, triggerDataRefresh }: GroupPageProps rowKey="id" size="small" scroll={{ x: true }} + ref={groupsTableRef} // rowClassName={(role) => { // return role.id === selectedRole?.id ? 'selected-row' : ''; // }} diff --git a/src/pages/users/RolesPage.tsx b/src/pages/users/RolesPage.tsx index ae6d3878..ab25bd0e 100644 --- a/src/pages/users/RolesPage.tsx +++ b/src/pages/users/RolesPage.tsx @@ -5,7 +5,14 @@ import { UsersService } from '@/services/UsersService'; import { getNetworkRoleRoute, getPlatformRoleRoute, resolveAppRoute } from '@/utils/RouteUtils'; import { deriveUserRoleType } from '@/utils/UserMgmtUtils'; import { useServerLicense } from '@/utils/Utils'; -import { DeleteOutlined, MoreOutlined, PlusOutlined, QuestionCircleOutlined, SearchOutlined } from '@ant-design/icons'; +import { + DeleteOutlined, + InfoCircleOutlined, + MoreOutlined, + PlusOutlined, + QuestionCircleOutlined, + SearchOutlined, +} from '@ant-design/icons'; import { Button, Card, @@ -20,15 +27,27 @@ import { Typography, notification, } from 'antd'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { RefObject, useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface RolesPageProps { triggerDataRefresh?: () => void; + setIsTourOpen: (isOpen: boolean) => void; + networkRolesHelpButtonRef: RefObject; + networkRolesTableRef: RefObject; + networkRolesSearchInputRef: RefObject; + networkRolesCreateRoleButtonRef: RefObject; } -export default function RolesPage({ triggerDataRefresh }: RolesPageProps) { +export default function RolesPage({ + triggerDataRefresh, + setIsTourOpen, + networkRolesCreateRoleButtonRef, + networkRolesHelpButtonRef, + networkRolesSearchInputRef, + networkRolesTableRef, +}: RolesPageProps) { const [notify, notifyCtx] = notification.useNotification(); const navigate = useNavigate(); const { isServerEE } = useServerLicense(); @@ -246,7 +265,7 @@ export default function RolesPage({ triggerDataRefresh }: RolesPageProps) { {!isEmpty && ( <> - + } + ref={networkRolesHelpButtonRef} /> + {/* { navigate(resolveAppRoute(AppRoutes.CREATE_NETWORK_ROLE_ROUTE)); }} + ref={networkRolesCreateRoleButtonRef} > Create Network Role @@ -326,6 +356,7 @@ export default function RolesPage({ triggerDataRefresh }: RolesPageProps) { }, }; }} + ref={networkRolesTableRef} // rowSelection={{ // type: 'radio', // hideSelectAll: true, diff --git a/src/pages/users/UsersPage.tsx b/src/pages/users/UsersPage.tsx index 13e89d82..d0368df7 100644 --- a/src/pages/users/UsersPage.tsx +++ b/src/pages/users/UsersPage.tsx @@ -3,6 +3,7 @@ import { CheckOutlined, CopyOutlined, DeleteOutlined, + InfoCircleOutlined, MoreOutlined, PlusOutlined, QuestionCircleOutlined, @@ -26,6 +27,8 @@ import { TableColumnsType, Tabs, TabsProps, + Tour, + TourProps, Typography, } from 'antd'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -46,6 +49,7 @@ import UserDetailsModal from '@/components/modals/user-details-modal/UserDetails import InviteUserModal from '@/components/modals/invite-user-modal/InviteUserModal'; import { useNavigate } from 'react-router-dom'; import { AppRoutes } from '@/routes'; +import { set } from 'lodash'; const USERS_DOCS_URL = 'https://docs.netmaker.io/pro/pro-users.html'; @@ -74,8 +78,7 @@ export default function UsersPage(props: PageProps) { const [isUpdateUserModalOpen, setIsUpdateUserModalOpen] = useState(false); const [isTransferSuperAdminRightsModalOpen, setIsTransferSuperAdminRightsModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); - // const [isTourOpen, setIsTourOpen] = useState(false); - // const [tourStep, setTourStep] = useState(0); + const [isTourOpen, setIsTourOpen] = useState(false); const [invites, setInvites] = useState([]); const [isLoadingInvites, setIsLoadingInvites] = useState(true); const [userInvitesSearch, setUserInvitesSearch] = useState(''); @@ -84,6 +87,7 @@ export default function UsersPage(props: PageProps) { const [pendingUsers, setPendingUsers] = useState([]); const [isLoadingPendingUsers, setIsLoadingPendingUsers] = useState(true); const [pendingUsersSearch, setPendingUsersSearch] = useState(''); + const [currentTourStep, setCurrentTourStep] = useState(0); const usersTableRef = useRef(null); const addUserButtonRef = useRef(null); @@ -91,7 +95,31 @@ export default function UsersPage(props: PageProps) { const addUserPasswordInputRef = useRef(null); const addUserSetAsAdminCheckboxRef = useRef(null); const denyAllUsersButtonRef = useRef(null); + const reloadUsersButtonRef = useRef(null); + const usersHelpButtonRef = useRef(null); + const searchUsersInputRef = useRef(null); + const inviteUserModalEmailAddressesInputRef = useRef(null); + const inviteUserModalPlatformAccessLevelRef = useRef(null); + const createUserModalPlatformAccessLevelRef = useRef(null); + const createUserModalCreateUserButtonRef = useRef(null); + const networkRolesHelpButtonRef = useRef(null); + const networkRolesTableRef = useRef(null); + const networkRolesSearchInputRef = useRef(null); + const networkRolesCreateRoleButtonRef = useRef(null); + const groupsHelpButtonRef = useRef(null); + const groupsTableRef = useRef(null); + const groupsSearchInputRef = useRef(null); + const groupsCreateGroupButtonRef = useRef(null); + const invitesHelpButtonRef = useRef(null); + const invitesTableRef = useRef(null); + const invitesSearchInputRef = useRef(null); + const invitesCreateInviteButtonRef = useRef(null); + const invitesClearAllInvitesButtonRef = useRef(null); + const pendingUsersHelpButtonRef = useRef(null); const pendingUsersTableRef = useRef(null); + const pendingUsersDenyAllUsersButtonRef = useRef(null); + const pendingUsersSearchInputRef = useRef(null); + const reloadPendingUsersButtonRef = useRef(null); const loadUsers = useCallback( async (showLoading = true) => { @@ -581,12 +609,244 @@ export default function UsersPage(props: PageProps) { } }; + // tours + const nextTourStep = useCallback(() => { + setCurrentTourStep(currentTourStep + 1); + }, [currentTourStep]); + + const prevTourStep = useCallback(() => { + setCurrentTourStep(currentTourStep - 1); + }, [currentTourStep]); + + const handleTourOnChange = useCallback( + (current: number) => { + setCurrentTourStep(current); + }, + [setCurrentTourStep], + ); + + const usersTabTourSteps: TourProps['steps'] = useMemo( + () => [ + { + title: 'Users', + description: 'View users and their roles, you can also edit or delete users and transfer super admin rights', + target: () => usersTableRef.current, + placement: 'bottom', + }, + { + title: 'Search Users', + description: 'Search for users by username', + target: () => searchUsersInputRef.current, + placement: 'bottom', + }, + { + title: 'Get Help', + description: 'Click here to view the documentation for users', + target: () => usersHelpButtonRef.current, + placement: 'bottom', + }, + { + title: 'Reload Users', + description: 'Click here to reload users', + target: () => reloadUsersButtonRef.current, + placement: 'bottom', + }, + { + title: 'Add a User', + description: 'Click here to add a user either by creating a new user or inviting a user', + target: () => addUserButtonRef.current, + placement: 'bottom', + onNext: () => { + setIsAddUserModalOpen(true); + nextTourStep(); + }, + }, + { + title: 'Username', + description: 'Enter a username for the user', + target: () => addUserNameInputRef.current, + placement: 'bottom', + onPrev: () => { + setIsAddUserModalOpen(false); + prevTourStep(); + }, + }, + { + title: 'Password', + description: 'Enter a password for the user', + target: () => addUserPasswordInputRef.current, + placement: 'bottom', + }, + { + title: 'Set user user platform access level', + description: 'Set the platform access level for the user', + target: () => createUserModalPlatformAccessLevelRef.current, + placement: 'bottom', + onNext: () => { + setIsAddUserModalOpen(false); + setIsInviteModalOpen(true); + nextTourStep(); + }, + }, + { + title: 'Invite a User', + description: 'Enter email addresses to invite users', + target: () => inviteUserModalEmailAddressesInputRef.current, + placement: 'bottom', + onPrev: () => { + setIsAddUserModalOpen(true); + setIsInviteModalOpen(false); + prevTourStep(); + }, + }, + { + title: 'Set user platform access level', + description: 'Set the platform access level for the users', + target: () => inviteUserModalPlatformAccessLevelRef.current, + placement: 'bottom', + }, + ], + [nextTourStep, prevTourStep], + ); + + const networkRolesTabTourSteps: TourProps['steps'] = [ + { + title: 'Network Roles', + description: 'View and manage network roles', + target: () => networkRolesTableRef.current, + placement: 'bottom', + }, + { + title: 'Search Network Roles', + description: 'Search for network roles by name', + target: () => networkRolesSearchInputRef.current, + placement: 'bottom', + }, + { + title: 'Get Help', + description: 'Click here to view the documentation for network roles', + target: () => networkRolesHelpButtonRef.current, + placement: 'bottom', + }, + // { + // title: 'Reload Network Roles', + // description: 'Click here to reload network roles', + // target: () => reloadUsersButtonRef.current, + // placement: 'bottom', + // }, + { + title: 'Create a Network Role', + description: 'Click here to create a new network role', + target: () => networkRolesCreateRoleButtonRef.current, + placement: 'bottom', + }, + ]; + + const groupsTabTourSteps: TourProps['steps'] = [ + { + title: 'Groups', + description: 'View and manage groups', + target: () => groupsTableRef.current, + placement: 'bottom', + }, + { + title: 'Search Groups', + description: 'Search for groups by name', + target: () => groupsSearchInputRef.current, + placement: 'bottom', + }, + { + title: 'Get Help', + description: 'Click here to view the documentation for groups', + target: () => groupsHelpButtonRef.current, + placement: 'bottom', + }, + { + title: 'Create a Group', + description: 'Click here to create a new group', + target: () => groupsCreateGroupButtonRef.current, + placement: 'bottom', + }, + ]; + + const invitesTabTourSteps: TourProps['steps'] = [ + { + title: 'Invites', + description: 'View and manage user invites', + target: () => invitesTableRef.current, + placement: 'bottom', + }, + { + title: 'Search Invites', + description: 'Search for invites by email', + target: () => invitesSearchInputRef.current, + placement: 'bottom', + }, + { + title: 'Get Help', + description: 'Click here to view the documentation for invites', + target: () => invitesHelpButtonRef.current, + placement: 'bottom', + }, + { + title: 'Reload Invites', + description: 'Click here to reload invites', + target: () => reloadUsersButtonRef.current, + placement: 'bottom', + }, + { + title: 'Create an Invite', + description: 'Click here to create a new invite', + target: () => invitesCreateInviteButtonRef.current, + placement: 'bottom', + }, + { + title: 'Clear All Invites', + description: 'Click here to clear all invites', + target: () => invitesClearAllInvitesButtonRef.current, + placement: 'bottom', + }, + ]; + + const pendingUsersTabTourSteps: TourProps['steps'] = [ + { + title: 'Pending Users', + description: 'View and manage pending users', + target: () => pendingUsersTableRef.current, + placement: 'bottom', + }, + { + title: 'Search Pending Users', + description: 'Search for pending users by username', + target: () => pendingUsersSearchInputRef.current, + placement: 'bottom', + }, + { + title: 'Get Help', + description: 'Click here to view the documentation for pending users', + target: () => pendingUsersHelpButtonRef.current, + placement: 'bottom', + }, + { + title: 'Reload Pending Users', + description: 'Click here to reload pending users', + target: () => reloadPendingUsersButtonRef.current, + placement: 'bottom', + }, + { + title: 'Deny All Users', + description: 'Click here to deny all pending users', + target: () => pendingUsersDenyAllUsersButtonRef.current, + placement: 'bottom', + }, + ]; + // ui components const getUsersContent = useCallback(() => { return ( <> - + } style={{ marginRight: '0.5rem' }} + ref={usersHelpButtonRef} /> - {/* */} - + {isServerEE && ( // we dont have CE on SaaS @@ -641,7 +906,12 @@ export default function UsersPage(props: PageProps) { ), }} > - @@ -684,7 +954,7 @@ export default function UsersPage(props: PageProps) { return ( <> - + } + ref={invitesHelpButtonRef} /> - {/* */} + - @@ -761,7 +1037,7 @@ export default function UsersPage(props: PageProps) { return ( <> - + - {/* */} - + - -