From fc3737bf4f00681cc1b3cc0d4829bfa7a1e4cb31 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Thu, 27 Apr 2023 14:20:03 -0300 Subject: [PATCH] PublicDashboards: Email sharing users page (#67124) --- .../src/selectors/pages.ts | 21 ++ .../app/features/admin/UserListAdminPage.tsx | 5 +- .../app/features/admin/UserListPage.test.tsx | 202 ++++++++++++++++++ public/app/features/admin/UserListPage.tsx | 71 +++++- .../DashboardsListModalButton.tsx | 108 ++++++++++ .../DeleteUserModalButton.tsx | 49 +++++ .../UserListPublicDashboardPage.tsx | 61 ++++++ .../dashboard/api/publicDashboardApi.ts | 20 +- .../ConfigPublicDashboard.tsx | 4 +- .../SharePublicDashboardUtils.test.tsx | 2 +- .../SharePublicDashboardUtils.ts | 18 +- .../DeletePublicDashboardModal.tsx | 2 +- .../PublicDashboardListTable.test.tsx | 8 +- .../PublicDashboardListTable.tsx | 7 +- public/app/features/users/UsersListPage.tsx | 5 +- 15 files changed, 553 insertions(+), 30 deletions(-) create mode 100644 public/app/features/admin/UserListPage.test.tsx create mode 100644 public/app/features/admin/UserListPublicDashboardPage/DashboardsListModalButton.tsx create mode 100644 public/app/features/admin/UserListPublicDashboardPage/DeleteUserModalButton.tsx create mode 100644 public/app/features/admin/UserListPublicDashboardPage/UserListPublicDashboardPage.tsx diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 95cd03e5b25..fd3412b8559 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -289,11 +289,32 @@ export const Pages = { }, }, UserListPage: { + tabs: { + allUsers: 'data-testid all-users-tab', + orgUsers: 'data-testid org-users-tab', + publicDashboardsUsers: 'data-testid public-dashboards-users-tab', + users: 'data-testid users-tab', + }, org: { url: '/org/users', }, admin: { url: '/admin/users', }, + publicDashboards: { + container: 'data-testid public-dashboards-users-list', + }, + UserListAdminPage: { + container: 'data-testid user-list-admin-page', + }, + UsersListPage: { + container: 'data-testid users-list-page', + }, + UsersListPublicDashboardsPage: { + container: 'data-testid users-list-public-dashboards-page', + DashboardsListModal: { + listItem: (uid: string) => `data-testid dashboards-list-item-${uid}`, + }, + }, }, }; diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index a1c6e1f0327..66fa7929bc0 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -3,6 +3,7 @@ import React, { ComponentType, useEffect, useMemo, memo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { Icon, IconName, @@ -32,6 +33,8 @@ export const addExtraFilters = (filter: ComponentType) => { extraFilters.push(filter); }; +const selectors = e2eSelectors.pages.UserListPage.UserListAdminPage; + const mapDispatchToProps = { fetchUsers, changeQuery, @@ -78,7 +81,7 @@ const UserListAdminPageUnConnected = ({ return ( -
+
({ + ...jest.requireActual('@grafana/runtime'), + getBackendSrv: () => ({ get: jest.fn().mockResolvedValue([]) }), +})); + +jest.mock('./UserListAdminPage', () => ({ + UserListAdminPageContent: () =>
, +})); +jest.mock('../users/UsersListPage', () => ({ + UsersListPageContent: () =>
, +})); +jest.mock('./UserListPublicDashboardPage/UserListPublicDashboardPage', () => ({ + UserListPublicDashboardPage: () =>
, +})); + +const renderPage = () => { + render( + + + + ); +}; + +const enableEmailSharing = () => { + config.featureToggles.publicDashboardsEmailSharing = true; + config.featureToggles.publicDashboards = true; + config.licenseInfo = { ...config.licenseInfo, enabledFeatures: { publicDashboardsEmailSharing: true } }; +}; + +let originalConfigData: GrafanaBootConfig; + +beforeEach(() => { + originalConfigData = { ...config }; +}); + +afterEach(() => { + config.featureToggles = originalConfigData.featureToggles; + config.licenseInfo = originalConfigData.licenseInfo; +}); + +describe('Tabs rendering', () => { + it('should render All and Org Users tabs when user has permissions to read to org users and is admin', async () => { + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + + renderPage(); + + expect(screen.getByTestId(tabsSelector.allUsers)).toBeInTheDocument(); + expect(screen.getByTestId(tabsSelector.orgUsers)).toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.publicDashboardsUsers)).not.toBeInTheDocument(); + }); + it('should render All, Org and Public dashboard tabs when user has permissions to read org users, is admin and has email sharing enabled', async () => { + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + + enableEmailSharing(); + renderPage(); + + expect(screen.getByTestId(tabsSelector.allUsers)).toBeInTheDocument(); + expect(screen.getByTestId(tabsSelector.orgUsers)).toBeInTheDocument(); + expect(screen.getByTestId(tabsSelector.publicDashboardsUsers)).toBeInTheDocument(); + }); + describe('No permissions to read org users or not admin', () => { + [ + { + hasOrgReadPermissions: false, + isAdmin: true, + }, + { + hasOrgReadPermissions: true, + isAdmin: false, + }, + ].forEach((scenario) => { + it('should render no tabs when user has no permissions to read org users or is not admin', async () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(scenario.hasOrgReadPermissions); + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(scenario.isAdmin); + + renderPage(); + + expect(screen.queryByTestId(tabsSelector.allUsers)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.orgUsers)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.publicDashboardsUsers)).not.toBeInTheDocument(); + }); + }); + }); + describe('No permissions to read org users or not admin but email sharing enabled', () => { + [ + { + title: 'user has no permissions to read org users', + hasOrgReadPermissions: false, + isAdmin: true, + }, + { + title: 'user is not admin', + hasOrgReadPermissions: true, + isAdmin: false, + }, + ].forEach((scenario) => { + it(`should render User and Public dashboard tabs when ${scenario.title} but has email sharing enabled`, async () => { + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(scenario.hasOrgReadPermissions); + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(scenario.isAdmin); + + enableEmailSharing(); + renderPage(); + + expect(screen.queryByTestId(tabsSelector.allUsers)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.orgUsers)).not.toBeInTheDocument(); + + expect(screen.getByTestId(tabsSelector.users)).toBeInTheDocument(); + expect(screen.getByTestId(tabsSelector.publicDashboardsUsers)).toBeInTheDocument(); + }); + }); + }); +}); + +describe('Tables rendering', () => { + it('should render UserListAdminPage when user is admin', () => { + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + + renderPage(); + + expect(screen.getByTestId(tabsSelector.allUsers).className.includes('activeTabStyle')).toBeTruthy(); + expect(screen.getByTestId(tabsSelector.orgUsers)).toBeInTheDocument(); + + expect(screen.getByTestId(selectors.UserListAdminPage.container)).toBeInTheDocument(); + }); + it('should render UsersListPage when user is admin and has org read permissions', async () => { + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true); + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + + renderPage(); + + expect(screen.getByTestId(tabsSelector.allUsers).className.includes('activeTabStyle')).toBeTruthy(); + expect(screen.getByTestId(tabsSelector.orgUsers)).toBeInTheDocument(); + expect(screen.getByTestId(selectors.UserListAdminPage.container)).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId(tabsSelector.orgUsers)); + expect(screen.getByTestId(tabsSelector.orgUsers).className.includes('activeTabStyle')).toBeTruthy(); + expect(screen.getByTestId(selectors.UsersListPage.container)).toBeInTheDocument(); + }); + it('should render UsersListPage when user has org read permissions and is not admin', async () => { + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false); + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + + renderPage(); + + expect(screen.queryByTestId(tabsSelector.allUsers)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.orgUsers)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.users)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.publicDashboardsUsers)).not.toBeInTheDocument(); + + expect(screen.getByTestId(selectors.UsersListPage.container)).toBeInTheDocument(); + }); + it('should render UserListPublicDashboardPage when user has email sharing enabled and is not admin', async () => { + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false); + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true); + + enableEmailSharing(); + renderPage(); + + expect(screen.queryByTestId(tabsSelector.allUsers)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.orgUsers)).not.toBeInTheDocument(); + + expect(screen.queryByTestId(tabsSelector.users)).toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.publicDashboardsUsers)).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId(tabsSelector.publicDashboardsUsers)); + expect(screen.getByTestId(tabsSelector.publicDashboardsUsers).className.includes('activeTabStyle')).toBeTruthy(); + expect(screen.getByTestId(selectors.UsersListPublicDashboardsPage.container)).toBeInTheDocument(); + }); + it('should render UsersListPage when user is not admin and does not have nor org read perms neither email sharing enabled', async () => { + jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false); + jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false); + + renderPage(); + + expect(screen.queryByTestId(tabsSelector.allUsers)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.orgUsers)).not.toBeInTheDocument(); + + expect(screen.queryByTestId(tabsSelector.users)).not.toBeInTheDocument(); + expect(screen.queryByTestId(tabsSelector.publicDashboardsUsers)).not.toBeInTheDocument(); + + expect(screen.getByTestId(selectors.UsersListPage.container)).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/admin/UserListPage.tsx b/public/app/features/admin/UserListPage.tsx index e3bbbb65ff7..90764f9b40a 100644 --- a/public/app/features/admin/UserListPage.tsx +++ b/public/app/features/admin/UserListPage.tsx @@ -2,6 +2,8 @@ import { css } from '@emotion/css'; import React, { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; +import { config, featureEnabled } from '@grafana/runtime'; import { useStyles2, TabsBar, Tab } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; @@ -10,31 +12,84 @@ import { AccessControlAction } from '../../types'; import { UsersListPageContent } from '../users/UsersListPage'; import { UserListAdminPageContent } from './UserListAdminPage'; +import { UserListPublicDashboardPage } from './UserListPublicDashboardPage/UserListPublicDashboardPage'; + +enum TabView { + ADMIN = 'admin', + ORG = 'org', + PUBLIC_DASHBOARDS = 'public-dashboards', +} + +const selectors = e2eSelectors.pages.UserListPage; + +const PublicDashboardsTab = ({ view, setView }: { view: TabView | null; setView: (v: TabView | null) => void }) => ( + setView(TabView.PUBLIC_DASHBOARDS)} + data-testid={selectors.tabs.publicDashboardsUsers} + /> +); + +const TAB_PAGE_MAP: Record = { + [TabView.ADMIN]: , + [TabView.ORG]: , + [TabView.PUBLIC_DASHBOARDS]: , +}; export default function UserListPage() { + const styles = useStyles2(getStyles); + const hasAccessToAdminUsers = contextSrv.hasAccess(AccessControlAction.UsersRead, contextSrv.isGrafanaAdmin); const hasAccessToOrgUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); - const styles = useStyles2(getStyles); + const hasEmailSharingEnabled = + Boolean(config.featureToggles.publicDashboards) && + Boolean(config.featureToggles.publicDashboardsEmailSharing) && + featureEnabled('publicDashboardsEmailSharing'); + const [view, setView] = useState(() => { if (hasAccessToAdminUsers) { - return 'admin'; + return TabView.ADMIN; } else if (hasAccessToOrgUsers) { - return 'org'; + return TabView.ORG; } return null; }); - const showToggle = hasAccessToOrgUsers && hasAccessToAdminUsers; + const showAdminAndOrgTabs = hasAccessToOrgUsers && hasAccessToAdminUsers; return ( - {showToggle && ( + {showAdminAndOrgTabs ? ( - setView('admin')} /> - setView('org')} /> + setView(TabView.ADMIN)} + data-testid={selectors.tabs.allUsers} + /> + setView(TabView.ORG)} + data-testid={selectors.tabs.orgUsers} + /> + {hasEmailSharingEnabled && } + ) : ( + hasEmailSharingEnabled && ( + + setView(TabView.ORG)} + data-testid={selectors.tabs.users} + /> + + + ) )} - {view === 'admin' ? : } + {view ? TAB_PAGE_MAP[view] : } ); } diff --git a/public/app/features/admin/UserListPublicDashboardPage/DashboardsListModalButton.tsx b/public/app/features/admin/UserListPublicDashboardPage/DashboardsListModalButton.tsx new file mode 100644 index 00000000000..f18c05d711c --- /dev/null +++ b/public/app/features/admin/UserListPublicDashboardPage/DashboardsListModalButton.tsx @@ -0,0 +1,108 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data/src'; +import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; +import { Button, LoadingPlaceholder, Modal, ModalsController, useStyles2 } from '@grafana/ui/src'; +import { generatePublicDashboardUrl } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; + +import { useGetActiveUserDashboardsQuery } from '../../dashboard/api/publicDashboardApi'; + +const selectors = e2eSelectors.pages.UserListPage.UsersListPublicDashboardsPage.DashboardsListModal; +export const DashboardsListModal = ({ email, onDismiss }: { email: string; onDismiss: () => void }) => { + const styles = useStyles2(getStyles); + + const { data: dashboards, isLoading } = useGetActiveUserDashboardsQuery(email); + + return ( + + {isLoading ? ( +
+ +
+ ) : ( + dashboards?.map((dash) => ( +
+

{dash.dashboardTitle}

+ +
+
+ )) + )} +
+ ); +}; + +export const DashboardsListModalButton = ({ email }: { email: string }) => ( + + {({ showModal, hideModal }) => ( + + + + ); +}; + +export const DeleteUserModalButton = ({ user }: { user: SessionUser }) => ( + + {({ showModal, hideModal }) => ( +