Invite User: Add invite user button in top bar (#101809)

pull/101824/head^2
Juan Cabanas 2 months ago committed by GitHub
parent b73c59547c
commit 5e21b9e2d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  2. 3
      packages/grafana-e2e-selectors/src/selectors/pages.ts
  3. 9
      pkg/services/featuremgmt/registry.go
  4. 1
      pkg/services/featuremgmt/toggles_gen.csv
  5. 4
      pkg/services/featuremgmt/toggles_gen.go
  6. 15
      pkg/services/featuremgmt/toggles_gen.json
  7. 35
      public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx
  8. 2
      public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
  9. 47
      public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.test.tsx
  10. 38
      public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
  11. 5
      public/locales/en-US/grafana.json
  12. 5
      public/locales/pseudo-LOCALE/grafana.json

@ -257,4 +257,5 @@ export interface FeatureToggles {
assetSriChecks?: boolean;
alertRuleRestore?: boolean;
grafanaManagedRecordingRulesDatasources?: boolean;
inviteUserExperimental?: boolean;
}

@ -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: {

@ -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,
},
}
)

@ -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

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
238 assetSriChecks experimental @grafana/frontend-ops false false true
239 alertRuleRestore preview @grafana/alerting-squad false false false
240 grafanaManagedRecordingRulesDatasources experimental @grafana/alerting-squad false false false
241 inviteUserExperimental experimental @grafana/sharing-squad false false true

@ -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"
)

@ -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",

@ -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) ? (
<Stack gap={2} alignItems="center">
<NavToolbarSeparator />
<Button
icon="add-user"
size="sm"
variant="secondary"
fill="solid"
onClick={() => {
reportInteraction('invite_user_button_clicked', {
placement: 'top_bar_right',
});
const url = getExternalUserMngLinkUrl('invite-user-top-bar');
window.open(url.toString(), '_blank');
}}
tooltip={t('navigation.invite-user.invite-tooltip', 'Invite new member')}
>
{t('navigation.invite-user.invite-button', 'Invite')}
</Button>
<NavToolbarSeparator />
</Stack>
) : null;
}

@ -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 && <SignInLink />}
{config.featureToggles.inviteUserExperimental && <InviteUserButton />}
{profileNode && <ProfileButton profileNode={profileNode} />}
</Stack>
</div>

@ -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;

@ -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: () => <Icon name="external-link-alt" className={styles.inviteUserItemIcon} />,
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
</Menu>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
inviteUserItem: css({
display: 'flex',
justifyContent: 'start',
flexDirection: 'row',
alignItems: 'center',
}),
inviteUserItemIcon: css({
color: theme.colors.text.link,
}),
};
};

@ -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"

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

Loading…
Cancel
Save