From 5e21b9e2d1ebad0dcb5fce5fa1bc48b74cea4b45 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Fri, 7 Mar 2025 16:09:59 -0300 Subject: [PATCH] Invite User: Add invite user button in top bar (#101809) --- .../src/types/featureToggles.gen.ts | 1 + .../src/selectors/pages.ts | 3 -- pkg/services/featuremgmt/registry.go | 9 ++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/featuremgmt/toggles_gen.json | 15 ++++++ .../AppChrome/TopBar/InviteUserButton.tsx | 35 ++++++++++++++ .../AppChrome/TopBar/SingleTopBar.tsx | 2 + .../sharing/ShareButton/ShareMenu.test.tsx | 47 ------------------- .../sharing/ShareButton/ShareMenu.tsx | 38 +-------------- public/locales/en-US/grafana.json | 5 +- public/locales/pseudo-LOCALE/grafana.json | 5 +- 12 files changed, 77 insertions(+), 88 deletions(-) create mode 100644 public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index f262e6aaf2f..350a9bf7b5a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -257,4 +257,5 @@ export interface FeatureToggles { assetSriChecks?: boolean; alertRuleRestore?: boolean; grafanaManagedRecordingRulesDatasources?: boolean; + inviteUserExperimental?: boolean; } diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index ae0a8e08d3f..75d7c8dd4d0 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -206,9 +206,6 @@ 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/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f3834bb5757..dfb8405c2ea 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1799,6 +1799,15 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, + { + Name: "inviteUserExperimental", + Description: "Renders invite user button along the app", + Stage: FeatureStageExperimental, + Owner: grafanaSharingSquad, + HideFromAdminPage: true, + HideFromDocs: true, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ff29806e399..8e0710c30ef 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -238,3 +238,4 @@ rendererDisableAppPluginsPreload,experimental,@grafana/sharing-squad,false,false assetSriChecks,experimental,@grafana/frontend-ops,false,false,true alertRuleRestore,preview,@grafana/alerting-squad,false,false,false grafanaManagedRecordingRulesDatasources,experimental,@grafana/alerting-squad,false,false,false +inviteUserExperimental,experimental,@grafana/sharing-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 957b81e842c..d9dcfd8603a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -962,4 +962,8 @@ const ( // FlagGrafanaManagedRecordingRulesDatasources // Enables writing to data sources for Grafana-managed recording rules. FlagGrafanaManagedRecordingRulesDatasources = "grafanaManagedRecordingRulesDatasources" + + // FlagInviteUserExperimental + // Renders invite user button along the app + FlagInviteUserExperimental = "inviteUserExperimental" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0b40ecb9551..b42c54bc438 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2204,6 +2204,21 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "inviteUserExperimental", + "resourceVersion": "1741358664069", + "creationTimestamp": "2025-03-07T14:44:24Z" + }, + "spec": { + "description": "Renders invite user button along the app", + "stage": "experimental", + "codeowner": "@grafana/sharing-squad", + "frontend": true, + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "jaegerBackendMigration", diff --git a/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx b/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx new file mode 100644 index 00000000000..a788f798fc6 --- /dev/null +++ b/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx @@ -0,0 +1,35 @@ +import { reportInteraction } from '@grafana/runtime'; +import { Button, Stack } from '@grafana/ui'; +import { config } from 'app/core/config'; +import { t } from 'app/core/internationalization'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getExternalUserMngLinkUrl } from 'app/features/users/utils'; +import { AccessControlAction } from 'app/types'; + +import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; + +export function InviteUserButton() { + return config.externalUserMngLinkUrl && contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) ? ( + + + + + + ) : null; +} diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx index 225db772a11..64d67fa326d 100644 --- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx +++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx @@ -19,6 +19,7 @@ import { enrichHelpItem } from '../MegaMenu/utils'; import { QuickAdd } from '../QuickAdd/QuickAdd'; import { TOP_BAR_LEVEL_HEIGHT } from '../types'; +import { InviteUserButton } from './InviteUserButton'; import { ProfileButton } from './ProfileButton'; import { SignInLink } from './SignInLink'; import { TopNavBarMenu } from './TopNavBarMenu'; @@ -87,6 +88,7 @@ export const SingleTopBar = memo(function SingleTopBar({ tooltip="Enable kiosk mode" /> {!contextSrv.user.isSignedIn && } + {config.featureToggles.inviteUserExperimental && } {profileNode && } 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 403e76d2c7f..91439fe662a 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx @@ -41,7 +41,6 @@ describe('ShareMenu', () => { 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 () => { @@ -51,52 +50,6 @@ 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 render invite user with analytics when config is provided', async () => { - Object.defineProperty(contextSrv, 'isSignedIn', { - value: true, - }); - grantUserPermissions([AccessControlAction.OrgUsersAdd]); - - config.externalUserMngLinkUrl = 'http://localhost:3000/users'; - config.externalUserMngAnalytics = true; - config.externalUserMngAnalyticsParams = 'src=grafananet&other=value1'; - setup({ meta: { canEdit: true } }); - - const inviteUser = await screen.findByTestId(selector.inviteUser); - // Mock window.open - const windowOpenMock = jest.spyOn(window, 'open').mockImplementation(() => null); - - // Simulate click event - inviteUser.click(); - - // Assert window.open was called with the correct URL - expect(windowOpenMock).toHaveBeenCalledWith( - 'http://localhost:3000/users?src=grafananet&other=value1&cnt=share-invite', - '_blank' - ); - - // Restore the original implementation - windowOpenMock.mockRestore(); - }); - - 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 fb97d7cd8e8..9223b071492 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx @@ -1,19 +1,16 @@ -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 { Icon, IconName, Menu, useStyles2 } from '@grafana/ui'; +import { IconName, Menu } from '@grafana/ui'; import { contextSrv } from 'app/core/core'; import { t } from 'app/core/internationalization'; import { AccessControlAction } from 'app/types'; import { isPublicDashboardsEnabled } from '../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { getTrackingSource, shareDashboardType } from '../../../dashboard/components/ShareModal/utils'; -import { getExternalUserMngLinkUrl } from '../../../users/utils'; import { DashboardScene } from '../../scene/DashboardScene'; import { DashboardInteractions } from '../../utils/interactions'; @@ -43,7 +40,6 @@ export function resetDashboardShareDrawerItems() { } export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardScene; panel?: VizPanel }) { - const styles = useStyles2(getStyles); const onMenuItemClick = (shareView: string) => { locationService.partial({ shareView }); }; @@ -87,24 +83,8 @@ 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: () => { - const url = getExternalUserMngLinkUrl('share-invite'); - - window.open(url.toString(), '_blank'); - }, - renderDividerAbove: true, - component: () => , - className: styles.inviteUserItem, - }); - return menuItems.filter((item) => item.renderCondition); - }, [panel, styles]); + }, [panel]); const onClick = (item: ShareDrawerMenuItem) => { DashboardInteractions.sharingCategoryClicked({ @@ -134,17 +114,3 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc ); } - -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/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 025e79c6e97..cef94684d01 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2920,6 +2920,10 @@ } }, "navigation": { + "invite-user": { + "invite-button": "Invite", + "invite-tooltip": "Invite new member" + }, "item": { "add-bookmark": "Add to Bookmarks", "remove-bookmark": "Remove from Bookmarks" @@ -3534,7 +3538,6 @@ "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 63bb823a2d0..a2f227ebf67 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -2920,6 +2920,10 @@ } }, "navigation": { + "invite-user": { + "invite-button": "Ĩʼnvįŧę", + "invite-tooltip": "Ĩʼnvįŧę ʼnęŵ męmþęř" + }, "item": { "add-bookmark": "Åđđ ŧő ßőőĸmäřĸş", "remove-bookmark": "Ŗęmővę ƒřőm ßőőĸmäřĸş" @@ -3534,7 +3538,6 @@ "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şĥőŧ"