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şĥőŧ"