diff --git a/public/app/core/components/AppChrome/TopBar/InviteUserButton.test.tsx b/public/app/core/components/AppChrome/TopBar/InviteUserButton.test.tsx
new file mode 100644
index 00000000000..d867c17488b
--- /dev/null
+++ b/public/app/core/components/AppChrome/TopBar/InviteUserButton.test.tsx
@@ -0,0 +1,174 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { config, reportInteraction } from '@grafana/runtime';
+import { contextSrv } from 'app/core/core';
+import { getExternalUserMngLinkUrl } from 'app/features/users/utils';
+
+import { InviteUserButton } from './InviteUserButton';
+
+// Mock dependencies
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ config: {
+ externalUserMngLinkUrl: 'https://example.com/invite',
+ },
+ reportInteraction: jest.fn(),
+}));
+
+jest.mock('app/core/core', () => ({
+ contextSrv: {
+ hasPermission: jest.fn(),
+ },
+}));
+
+jest.mock('app/features/users/utils', () => ({
+ getExternalUserMngLinkUrl: jest.fn(),
+}));
+
+const mockContextSrv = jest.mocked(contextSrv);
+const mockConfig = jest.mocked(config);
+const mockReportInteraction = jest.mocked(reportInteraction);
+const mockGetExternalUserMngLinkUrl = jest.mocked(getExternalUserMngLinkUrl);
+
+// Mock window.open
+const mockWindowOpen = jest.fn();
+Object.defineProperty(window, 'open', {
+ value: mockWindowOpen,
+ writable: true,
+});
+
+// Mock window.matchMedia for responsive testing
+const mockMatchMedia = (matches: boolean) => {
+ Object.defineProperty(window, 'matchMedia', {
+ value: jest.fn().mockImplementation(() => ({
+ matches,
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ })),
+ writable: true,
+ });
+};
+
+describe('InviteUserButton', () => {
+ const mockInviteUrl = 'https://example.com/invite?cnt=invite-user-top-bar';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockGetExternalUserMngLinkUrl.mockReturnValue(mockInviteUrl);
+ });
+
+ describe('Business Logic - When button should appear', () => {
+ it('should not render when user lacks permission', () => {
+ mockConfig.externalUserMngLinkUrl = 'https://example.com/invite';
+ mockContextSrv.hasPermission.mockReturnValue(false);
+ mockMatchMedia(true);
+
+ render();
+
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+
+ it('should not render when external user management URL is not configured', () => {
+ mockConfig.externalUserMngLinkUrl = '';
+ mockContextSrv.hasPermission.mockReturnValue(true);
+ mockMatchMedia(true);
+
+ render();
+
+ expect(screen.queryByRole('button')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('User Experience - Responsive behavior', () => {
+ beforeEach(() => {
+ mockConfig.externalUserMngLinkUrl = 'https://example.com/invite';
+ mockContextSrv.hasPermission.mockReturnValue(true);
+ });
+
+ it('should show text on large screens', () => {
+ mockMatchMedia(true); // Large screen (≥lg)
+
+ render();
+
+ const button = screen.getByRole('button', { name: /invite user/i });
+ expect(button).toHaveTextContent('Invite');
+ });
+
+ it('should show icon only on small screens', () => {
+ mockMatchMedia(false); // Small screen ();
+
+ const button = screen.getByRole('button', { name: /invite user/i });
+ expect(button).not.toHaveTextContent('Invite');
+ });
+ });
+
+ describe('Core Functionality - Click behavior', () => {
+ beforeEach(() => {
+ mockConfig.externalUserMngLinkUrl = 'https://example.com/invite';
+ mockContextSrv.hasPermission.mockReturnValue(true);
+ mockMatchMedia(true);
+ });
+
+ it('should track analytics and open invite URL when clicked', async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ await user.click(screen.getByRole('button', { name: /invite user/i }));
+
+ // Verify the complete user flow
+ expect(mockReportInteraction).toHaveBeenCalledWith('invite_user_button_clicked', {
+ placement: 'top_bar_right',
+ });
+ expect(mockGetExternalUserMngLinkUrl).toHaveBeenCalledWith('invite-user-top-bar');
+ expect(mockWindowOpen).toHaveBeenCalledWith(mockInviteUrl, '_blank');
+ });
+ });
+
+ describe('Error Handling - Preventing crashes', () => {
+ beforeEach(() => {
+ mockConfig.externalUserMngLinkUrl = 'https://example.com/invite';
+ mockContextSrv.hasPermission.mockReturnValue(true);
+ mockMatchMedia(true);
+ });
+
+ it('should handle URL generation errors gracefully', async () => {
+ mockGetExternalUserMngLinkUrl.mockImplementation(() => {
+ throw new Error('URL generation failed');
+ });
+
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+ const user = userEvent.setup();
+
+ render();
+
+ // Should not crash when URL generation fails
+ await user.click(screen.getByRole('button', { name: /invite user/i }));
+
+ expect(consoleSpy).toHaveBeenCalledWith('Failed to handle invite user click:', expect.any(Error));
+
+ consoleSpy.mockRestore();
+ });
+
+ it('should handle popup blocking gracefully', async () => {
+ mockWindowOpen.mockImplementation(() => {
+ throw new Error('Popup blocked');
+ });
+
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+ const user = userEvent.setup();
+
+ render();
+
+ // Should not crash when popup is blocked
+ await user.click(screen.getByRole('button', { name: /invite user/i }));
+
+ expect(consoleSpy).toHaveBeenCalledWith('Failed to handle invite user click:', expect.any(Error));
+
+ consoleSpy.mockRestore();
+ });
+ });
+});
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..deec044c42e
--- /dev/null
+++ b/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx
@@ -0,0 +1,43 @@
+import { t } from '@grafana/i18n';
+import { config, reportInteraction } from '@grafana/runtime';
+import { ToolbarButton } from '@grafana/ui';
+import { contextSrv } from 'app/core/core';
+import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
+import { getExternalUserMngLinkUrl } from 'app/features/users/utils';
+import { AccessControlAction } from 'app/types/accessControl';
+
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
+
+export function InviteUserButton() {
+ const isLargeScreen = useMediaQueryMinWidth('lg');
+
+ const handleClick = () => {
+ try {
+ reportInteraction('invite_user_button_clicked', {
+ placement: 'top_bar_right',
+ });
+
+ const url = getExternalUserMngLinkUrl('invite-user-top-bar');
+ window.open(url.toString(), '_blank');
+ } catch (error) {
+ console.error('Failed to handle invite user click:', error);
+ }
+ };
+
+ const shouldRender = config.externalUserMngLinkUrl && contextSrv.hasPermission(AccessControlAction.OrgUsersAdd);
+
+ return shouldRender ? (
+ <>
+
+ {isLargeScreen ? t('navigation.invite-user.invite-button', 'Invite') : undefined}
+
+
+ >
+ ) : null;
+}
diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
index 6766d757b49..e5033c75c1c 100644
--- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
+++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
@@ -24,6 +24,7 @@ import { enrichHelpItem } from '../MegaMenu/utils';
import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
import { QuickAdd } from '../QuickAdd/QuickAdd';
+import { InviteUserButton } from './InviteUserButton';
import { ProfileButton } from './ProfileButton';
import { SignInLink } from './SignInLink';
import { SingleTopBarActions } from './SingleTopBarActions';
@@ -106,6 +107,7 @@ export const SingleTopBar = memo(function SingleTopBar({
{config.featureToggles.extensionSidebar && !isSmallScreen && }
{!showToolbarLevel && actions}
{!contextSrv.user.isSignedIn && }
+ {config.featureToggles.inviteUserExperimental && }
{profileNode && }
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index aeeb66f2cac..abf40fc23fe 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -9759,7 +9759,9 @@
"aria-label": "Help"
},
"invite-user": {
- "invite-new-member-button": "Invite new member"
+ "invite-button": "Invite",
+ "invite-new-member-button": "Invite new member",
+ "invite-tooltip": "Invite user"
},
"item": {
"add-bookmark": "Add to Bookmarks",