diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 29c310d6aff..45ab6518453 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -206,6 +206,9 @@ export const versionedPages = { shareSnapshot: { '11.2.0': 'data-testid new share button share snapshot', }, + inviteUser: { + '11.5.0': 'data-testid new share button invite user', + }, }, }, NewExportButton: { diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx index 54b0f3bcabc..5d865ce2b4c 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx @@ -31,15 +31,17 @@ describe('ShareMenu', () => { Object.defineProperty(contextSrv, 'isSignedIn', { value: true, }); - grantUserPermissions([AccessControlAction.SnapshotsCreate]); + grantUserPermissions([AccessControlAction.SnapshotsCreate, AccessControlAction.OrgUsersAdd]); config.publicDashboardsEnabled = true; config.snapshotEnabled = true; + config.externalUserMngLinkUrl = 'http://localhost:3000'; setup({ meta: { canEdit: true } }); expect(await screen.findByTestId(selector.shareInternally)).toBeInTheDocument(); expect(await screen.findByTestId(selector.shareExternally)).toBeInTheDocument(); expect(await screen.findByTestId(selector.shareSnapshot)).toBeInTheDocument(); + expect(await screen.findByTestId(selector.inviteUser)).toBeInTheDocument(); }); it('should not share externally when public dashboard is disabled', async () => { @@ -49,6 +51,24 @@ describe('ShareMenu', () => { expect(screen.queryByTestId(selector.shareExternally)).not.toBeInTheDocument(); }); + it('should not render invite user when user does not have access', async () => { + Object.defineProperty(contextSrv, 'isSignedIn', { + value: true, + }); + + expect(await screen.queryByTestId(selector.inviteUser)).not.toBeInTheDocument(); + }); + + it('should not render invite user when externalUserMngLinkUrl is not provided', async () => { + Object.defineProperty(contextSrv, 'isSignedIn', { + value: true, + }); + grantUserPermissions([AccessControlAction.OrgUsersAdd]); + config.externalUserMngLinkUrl = ''; + + expect(await screen.queryByTestId(selector.inviteUser)).not.toBeInTheDocument(); + }); + describe('ShareSnapshot', () => { it('should not share snapshot when user is not signed in', async () => { config.snapshotEnabled = true; diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx index 0196ee754b8..c701e2a41ec 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx @@ -1,9 +1,12 @@ +import { css } from '@emotion/css'; import { useCallback } from 'react'; +import * as React from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { config, locationService } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; -import { IconName, Menu } from '@grafana/ui'; +import { Icon, IconName, Menu, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { t } from 'app/core/internationalization'; import { AccessControlAction } from 'app/types'; @@ -23,6 +26,9 @@ export interface ShareDrawerMenuItem { icon: IconName; renderCondition: boolean; onClick: (d: DashboardScene) => void; + renderDividerAbove?: boolean; + component?: React.ComponentType; + className?: string; } let customShareDrawerItems: ShareDrawerMenuItem[] = []; @@ -36,6 +42,7 @@ export function resetDashboardShareDrawerItems() { } export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardScene; panel?: VizPanel }) { + const styles = useStyles2(getStyles); const onMenuItemClick = (shareView: string) => { locationService.partial({ shareView }); }; @@ -63,8 +70,6 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc }, }); - customShareDrawerItems.forEach((d) => menuItems.push(d)); - menuItems.push({ shareId: shareDashboardType.snapshot, testId: newShareButtonSelector.shareSnapshot, @@ -79,8 +84,24 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc }, }); + customShareDrawerItems.forEach((d) => menuItems.push(d)); + + menuItems.push({ + shareId: shareDashboardType.inviteUser, + testId: newShareButtonSelector.inviteUser, + icon: 'add-user', + label: t('share-dashboard.menu.invite-user-title', 'Invite new member'), + renderCondition: !!config.externalUserMngLinkUrl && contextSrv.hasPermission(AccessControlAction.OrgUsersAdd), + onClick: () => { + window.open(config.externalUserMngLinkUrl, '_blank'); + }, + renderDividerAbove: true, + component: () => , + className: styles.inviteUserItem, + }); + return menuItems.filter((item) => item.renderCondition); - }, [panel]); + }, [panel, styles]); const onClick = (item: ShareDrawerMenuItem) => { DashboardInteractions.sharingCategoryClicked({ @@ -94,15 +115,33 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc return ( {buildMenuItems().map((item) => ( - onClick(item)} - /> + + {item.renderDividerAbove && } + onClick(item)} + /> + ))} ); } + +const getStyles = (theme: GrafanaTheme2) => { + return { + inviteUserItem: css({ + display: 'flex', + justifyContent: 'start', + flexDirection: 'row', + alignItems: 'center', + }), + inviteUserItemIcon: css({ + color: theme.colors.text.link, + }), + }; +}; diff --git a/public/app/features/dashboard/components/ShareModal/utils.ts b/public/app/features/dashboard/components/ShareModal/utils.ts index ce8c5f5b168..bcf6925da56 100644 --- a/public/app/features/dashboard/components/ShareModal/utils.ts +++ b/public/app/features/dashboard/components/ShareModal/utils.ts @@ -180,4 +180,5 @@ export const shareDashboardType: { pdf: 'pdf', report: 'report', publicDashboard: 'public_dashboard', + inviteUser: 'invite_user', }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 949a8a0ddee..9c85e1f761e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2919,6 +2919,7 @@ "share-dashboard": { "menu": { "export-json-title": "Export as JSON", + "invite-user-title": "Invite new member", "share-externally-title": "Share externally", "share-internally-title": "Share internally", "share-snapshot-title": "Share snapshot" diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 30a74c4eec8..59c48e08498 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2919,6 +2919,7 @@ "share-dashboard": { "menu": { "export-json-title": "Ēχpőřŧ äş ĴŜØŃ", + "invite-user-title": "Ĩʼnvįŧę ʼnęŵ męmþęř", "share-externally-title": "Ŝĥäřę ęχŧęřʼnäľľy", "share-internally-title": "Ŝĥäřę įʼnŧęřʼnäľľy", "share-snapshot-title": "Ŝĥäřę şʼnäpşĥőŧ"