PublicDashboards: Email sharing users page (#67124)

This commit is contained in:
Juan Cabanas 2023-04-27 14:20:03 -03:00 committed by GitHub
parent 35407142d0
commit fc3737bf4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 553 additions and 30 deletions

View File

@ -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}`,
},
},
},
};

View File

@ -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<FilterProps>) => {
extraFilters.push(filter);
};
const selectors = e2eSelectors.pages.UserListPage.UserListAdminPage;
const mapDispatchToProps = {
fetchUsers,
changeQuery,
@ -78,7 +81,7 @@ const UserListAdminPageUnConnected = ({
return (
<Page.Contents>
<div className="page-action-bar">
<div className="page-action-bar" data-testid={selectors.container}>
<div className="gf-form gf-form--grow">
<FilterInput
placeholder="Search user by login, email, or name."

View File

@ -0,0 +1,202 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { GrafanaBootConfig } from '@grafana/runtime/src';
import config from 'app/core/config';
import { TestProvider } from '../../../test/helpers/TestProvider';
import { contextSrv } from '../../core/services/context_srv';
import UserListPage from './UserListPage';
const selectors = e2eSelectors.pages.UserListPage;
const tabsSelector = selectors.tabs;
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({ get: jest.fn().mockResolvedValue([]) }),
}));
jest.mock('./UserListAdminPage', () => ({
UserListAdminPageContent: () => <div data-testid={selectors.UserListAdminPage.container} />,
}));
jest.mock('../users/UsersListPage', () => ({
UsersListPageContent: () => <div data-testid={selectors.UsersListPage.container} />,
}));
jest.mock('./UserListPublicDashboardPage/UserListPublicDashboardPage', () => ({
UserListPublicDashboardPage: () => <div data-testid={selectors.UsersListPublicDashboardsPage.container} />,
}));
const renderPage = () => {
render(
<TestProvider>
<UserListPage />
</TestProvider>
);
};
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();
});
});

View File

@ -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 }) => (
<Tab
label="Public dashboard users"
active={view === TabView.PUBLIC_DASHBOARDS}
onChangeTab={() => setView(TabView.PUBLIC_DASHBOARDS)}
data-testid={selectors.tabs.publicDashboardsUsers}
/>
);
const TAB_PAGE_MAP: Record<TabView, React.ReactElement> = {
[TabView.ADMIN]: <UserListAdminPageContent />,
[TabView.ORG]: <UsersListPageContent />,
[TabView.PUBLIC_DASHBOARDS]: <UserListPublicDashboardPage />,
};
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 (
<Page navId={'global-users'}>
{showToggle && (
{showAdminAndOrgTabs ? (
<TabsBar className={styles.tabsMargin}>
<Tab label="All users" active={view === 'admin'} onChangeTab={() => setView('admin')} />
<Tab label="Organization users" active={view === 'org'} onChangeTab={() => setView('org')} />
<Tab
label="All users"
active={view === TabView.ADMIN}
onChangeTab={() => setView(TabView.ADMIN)}
data-testid={selectors.tabs.allUsers}
/>
<Tab
label="Organization users"
active={view === TabView.ORG}
onChangeTab={() => setView(TabView.ORG)}
data-testid={selectors.tabs.orgUsers}
/>
{hasEmailSharingEnabled && <PublicDashboardsTab view={view} setView={setView} />}
</TabsBar>
) : (
hasEmailSharingEnabled && (
<TabsBar className={styles.tabsMargin}>
<Tab
label="Users"
active={view === TabView.ORG}
onChangeTab={() => setView(TabView.ORG)}
data-testid={selectors.tabs.users}
/>
<PublicDashboardsTab view={view} setView={setView} />
</TabsBar>
)
)}
{view === 'admin' ? <UserListAdminPageContent /> : <UsersListPageContent />}
{view ? TAB_PAGE_MAP[view] : <UsersListPageContent />}
</Page>
);
}

View File

@ -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 (
<Modal className={styles.modal} isOpen title="Public dashboards" onDismiss={onDismiss}>
{isLoading ? (
<div className={styles.loading}>
<LoadingPlaceholder text="Loading..." />
</div>
) : (
dashboards?.map((dash) => (
<div key={dash.dashboardUid} className={styles.listItem} data-testid={selectors.listItem(dash.dashboardUid)}>
<p className={styles.dashboardTitle}>{dash.dashboardTitle}</p>
<div className={styles.urlsContainer}>
<a
rel="noreferrer"
target="_blank"
className={cx('external-link', styles.url)}
href={generatePublicDashboardUrl(dash.publicDashboardAccessToken)}
onClick={onDismiss}
>
Public dashboard URL
</a>
<span className={styles.urlsDivider}></span>
<a
className={cx('external-link', styles.url)}
href={`/d/${dash.dashboardUid}?shareView=share`}
onClick={onDismiss}
>
Public dashboard settings
</a>
</div>
<hr className={styles.divider} />
</div>
))
)}
</Modal>
);
};
export const DashboardsListModalButton = ({ email }: { email: string }) => (
<ModalsController>
{({ showModal, hideModal }) => (
<Button
variant="secondary"
size="sm"
icon="question-circle"
title="Open dashboards list"
aria-label="Open dashboards list"
onClick={() => showModal(DashboardsListModal, { email, onDismiss: hideModal })}
/>
)}
</ModalsController>
);
const getStyles = (theme: GrafanaTheme2) => ({
modal: css`
width: 590px;
`,
loading: css`
display: flex;
justify-content: center;
`,
listItem: css`
display: flex;
flex-direction: column;
gap: ${theme.spacing(0.5)};
`,
divider: css`
margin: ${theme.spacing(1.5, 0)};
color: ${theme.colors.text.secondary};
`,
urlsContainer: css`
display: flex;
gap: ${theme.spacing(0.5)};
${theme.breakpoints.down('sm')} {
flex-direction: column;
}
`,
urlsDivider: css`
color: ${theme.colors.text.secondary};
${theme.breakpoints.down('sm')} {
display: none;
}
`,
dashboardTitle: css`
font-size: ${theme.typography.body.fontSize};
font-weight: ${theme.typography.fontWeightBold};
margin-bottom: 0;
`,
url: css`
font-size: ${theme.typography.body.fontSize};
`,
});

View File

@ -0,0 +1,49 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { Button, Modal, ModalsController, useStyles2 } from '@grafana/ui/src';
import { SessionUser } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
const DeleteUserModal = ({ user, onDismiss }: { user: SessionUser; onDismiss: () => void }) => {
const styles = useStyles2(getStyles);
return (
<Modal className={styles.modal} isOpen title="Delete" onDismiss={onDismiss}>
<p className={styles.description}>
The user {user.email} is currently present in {user.totalDashboards} public dashboard(s). If you wish to remove
this user, please navigate to the settings of the corresponding public dashboard.
</p>
<Modal.ButtonRow>
<Button type="button" variant="secondary" onClick={onDismiss} fill="outline">
Close
</Button>
</Modal.ButtonRow>
</Modal>
);
};
export const DeleteUserModalButton = ({ user }: { user: SessionUser }) => (
<ModalsController>
{({ showModal, hideModal }) => (
<Button
size="sm"
variant="destructive"
onClick={() => showModal(DeleteUserModal, { user, onDismiss: hideModal })}
icon="times"
aria-label="Delete user"
title="Delete user"
/>
)}
</ModalsController>
);
const getStyles = (theme: GrafanaTheme2) => ({
modal: css`
width: 500px;
`,
description: css`
font-size: ${theme.typography.body.fontSize};
margin: 0;
`,
});

View File

@ -0,0 +1,61 @@
import React from 'react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { HorizontalGroup, Icon, Tag, Tooltip } from '@grafana/ui/src';
import { Page } from 'app/core/components/Page/Page';
import { useGetActiveUsersQuery } from '../../dashboard/api/publicDashboardApi';
import { DashboardsListModalButton } from './DashboardsListModalButton';
import { DeleteUserModalButton } from './DeleteUserModalButton';
const selectors = e2eSelectors.pages.UserListPage.publicDashboards;
export const UserListPublicDashboardPage = () => {
const { data: users, isLoading } = useGetActiveUsersQuery();
return (
<Page.Contents isLoading={isLoading}>
<table className="filter-table form-inline" data-testid={selectors.container}>
<thead>
<tr>
<th>Email</th>
<th>
<span>Activated </span>
<Tooltip placement="top" content={'Earliest time user has been an active user to a dashboard'}>
<Icon name="question-circle" />
</Tooltip>
</th>
<th>Origin</th>
<th>Role</th>
<th></th>
</tr>
</thead>
<tbody>
{users?.map((user) => (
<tr key={user.email}>
<td className="max-width-10">
<span className="ellipsis" title={user.email}>
{user.email}
</span>
</td>
<td className="max-width-10">{user.firstSeenAtAge}</td>
<td className="max-width-10">
<HorizontalGroup spacing="sm">
<span>{user.totalDashboards} dashboard(s)</span>
<DashboardsListModalButton email={user.email} />
</HorizontalGroup>
</td>
<td className="max-width-10">
<Tag name="Viewer" colorIndex={19} />
</td>
<td className="text-right">
<DeleteUserModalButton user={user} />
</td>
</tr>
))}
</tbody>
</table>
</Page.Contents>
);
};

View File

@ -7,6 +7,8 @@ import { createErrorNotification, createSuccessNotification } from 'app/core/cop
import {
PublicDashboard,
PublicDashboardSettings,
SessionDashboard,
SessionUser,
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { DashboardModel } from 'app/features/dashboard/state';
import { ListPublicDashboardResponse } from 'app/features/manage-dashboards/types';
@ -42,7 +44,7 @@ const getConfigError = (err: unknown) => ({ error: isFetchError(err) && err.stat
export const publicDashboardApi = createApi({
reducerPath: 'publicDashboardApi',
baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }),
tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard'],
tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard', 'UsersWithActiveSessions', 'ActiveUserDashboards'],
refetchOnMountOrArgChange: true,
endpoints: (builder) => ({
getPublicDashboard: builder.query<PublicDashboard | undefined, string>({
@ -116,6 +118,18 @@ export const publicDashboardApi = createApi({
url: '',
}),
}),
getActiveUsers: builder.query<SessionUser[], void>({
query: () => ({
url: '/',
}),
providesTags: ['UsersWithActiveSessions'],
}),
getActiveUserDashboards: builder.query<SessionDashboard[], string>({
query: () => ({
url: '',
}),
providesTags: (result, _, email) => [{ type: 'ActiveUserDashboards', id: email }],
}),
listPublicDashboards: builder.query<ListPublicDashboardResponse[], void>({
query: () => ({
url: '/dashboards/public-dashboards',
@ -139,6 +153,8 @@ export const publicDashboardApi = createApi({
invalidatesTags: (result, error, { dashboardUid }) => [
{ type: 'PublicDashboard', id: dashboardUid },
'AuditTablePublicDashboard',
'UsersWithActiveSessions',
'ActiveUserDashboards',
],
}),
}),
@ -153,4 +169,6 @@ export const {
useAddRecipientMutation,
useDeleteRecipientMutation,
useReshareAccessToRecipientMutation,
useGetActiveUsersQuery,
useGetActiveUserDashboardsQuery,
} = publicDashboardApi;

View File

@ -120,7 +120,7 @@ const ConfigPublicDashboard = () => {
{hasEmailSharingEnabled && <EmailSharingConfiguration />}
<Field label="Dashboard URL" className={styles.publicUrl}>
<Input
value={generatePublicDashboardUrl(publicDashboard!)}
value={generatePublicDashboardUrl(publicDashboard!.accessToken!)}
readOnly
disabled={!publicDashboard?.isEnabled}
data-testid={selectors.CopyUrlInput}
@ -129,7 +129,7 @@ const ConfigPublicDashboard = () => {
data-testid={selectors.CopyUrlButton}
variant="primary"
disabled={!publicDashboard?.isEnabled}
getText={() => generatePublicDashboardUrl(publicDashboard!)}
getText={() => generatePublicDashboardUrl(publicDashboard!.accessToken!)}
>
Copy
</ClipboardButton>

View File

@ -31,7 +31,7 @@ describe('generatePublicDashboardUrl', () => {
updateConfig({ appUrl });
let pubdash = { accessToken } as PublicDashboard;
expect(generatePublicDashboardUrl(pubdash)).toEqual(`${appUrl}public-dashboards/${accessToken}`);
expect(generatePublicDashboardUrl(pubdash.accessToken!)).toEqual(`${appUrl}public-dashboards/${accessToken}`);
});
});

View File

@ -25,6 +25,18 @@ export interface PublicDashboard extends PublicDashboardSettings {
recipients?: Array<{ uid: string; recipient: string }>;
}
export interface SessionDashboard {
dashboardTitle: string;
dashboardUid: string;
publicDashboardAccessToken: string;
}
export interface SessionUser {
email: string;
firstSeenAtAge: string;
totalDashboards: number;
}
// Instance methods
export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => {
return variables.length > 0;
@ -58,10 +70,10 @@ export const getUnsupportedDashboardDatasources = (panels: PanelModel[]): string
*
* All app urls from the Grafana boot config end with a slash.
*
* @param publicDashboard
* @param accessToken
*/
export const generatePublicDashboardUrl = (publicDashboard: PublicDashboard): string => {
return `${getConfig().appUrl}public-dashboards/${publicDashboard.accessToken}`;
export const generatePublicDashboardUrl = (accessToken: string): string => {
return `${getConfig().appUrl}public-dashboards/${accessToken}`;
};
export const validEmailRegex = /^[A-Z\d._%+-]+@[A-Z\d.-]+\.[A-Z]{2,}$/i;

View File

@ -26,7 +26,7 @@ export const DeletePublicDashboardModal = ({
onDismiss: () => void;
}) => (
<ConfirmModal
isOpen={true}
isOpen
body={<Body title={dashboardTitle} />}
onConfirm={onConfirm}
onDismiss={onDismiss}

View File

@ -13,7 +13,7 @@ import { configureStore } from 'app/store/configureStore';
import { ListPublicDashboardResponse } from '../../types';
import { PublicDashboardListTable, viewPublicDashboardUrl } from './PublicDashboardListTable';
import { PublicDashboardListTable } from './PublicDashboardListTable';
const publicDashboardListResponse: ListPublicDashboardResponse[] = [
{
@ -90,12 +90,6 @@ const renderPublicDashboardTable = async (waitForListRendering?: boolean) => {
waitForListRendering && (await waitForElementToBeRemoved(screen.getByTestId('Spinner'), { timeout: 3000 }));
};
describe('viewPublicDashboardUrl', () => {
it('has the correct url', () => {
expect(viewPublicDashboardUrl('abcd')).toEqual('public-dashboards/abcd');
});
});
describe('Show table', () => {
it('renders loader spinner while loading', async () => {
await renderPublicDashboardTable();

View File

@ -6,9 +6,9 @@ import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { Link, ButtonGroup, LinkButton, Icon, Tag, useStyles2, Tooltip, useTheme2, Spinner } from '@grafana/ui/src';
import { Page } from 'app/core/components/Page/Page';
import { getConfig } from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv';
import { useListPublicDashboardsQuery } from 'app/features/dashboard/api/publicDashboardApi';
import { generatePublicDashboardUrl } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { isOrgAdmin } from 'app/features/plugins/admin/permissions';
import { AccessControlAction } from 'app/types';
@ -16,9 +16,6 @@ import { ListPublicDashboardResponse } from '../../types';
import { DeletePublicDashboardButton } from './DeletePublicDashboardButton';
export const viewPublicDashboardUrl = (accessToken: string): string =>
`${getConfig().appUrl}public-dashboards/${accessToken}`;
export const PublicDashboardListTable = () => {
const { width } = useWindowSize();
const isMobile = width <= 480;
@ -72,7 +69,7 @@ export const PublicDashboardListTable = () => {
<td>
<ButtonGroup className={styles.buttonGroup}>
<LinkButton
href={viewPublicDashboardUrl(pd.accessToken)}
href={generatePublicDashboardUrl(pd.accessToken)}
fill="text"
size={responsiveSize}
title={pd.isEnabled ? 'View public dashboard' : 'Public dashboard is disabled'}

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { renderMarkdown } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { HorizontalGroup, Pagination, VerticalGroup } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
@ -46,6 +47,8 @@ export interface State {
showInvites: boolean;
}
const selectors = e2eSelectors.pages.UserListPage.UsersListPage;
export const UsersListPageUnconnected = ({
users,
page,
@ -80,7 +83,7 @@ export const UsersListPageUnconnected = ({
return <InviteesTable invitees={invitees} />;
} else {
return (
<VerticalGroup spacing="md">
<VerticalGroup spacing="md" data-testid={selectors.container}>
<UsersTable
users={users}
orgId={contextSrv.user.orgId}