Feature Highlights: update upgrade components UI (#47885)

* Highlights: add action prop

* Highlight team sync for trial users

* Add badges for trial highlights

* Move events to UpgradeBox

* Fix undefined license settings

* Update snapshot

* Update public/app/features/datasources/state/navModel.ts

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Update public/app/features/datasources/state/navModel.ts

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Update public/app/features/datasources/state/navModel.ts

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Update copy and event handling

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>
This commit is contained in:
Alex Khomenko 2022-04-20 15:08:49 +03:00 committed by GitHub
parent 68aac0bd90
commit 9c0aa09a85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 144 additions and 62 deletions

View File

@ -7,16 +7,17 @@ import { reportExperimentView } from '@grafana/runtime';
export interface Props extends HTMLAttributes<HTMLSpanElement> {
text?: string;
experimentId?: string;
eventVariant?: string;
}
export const ProBadge = ({ text = 'PRO', className, experimentId, ...htmlProps }: Props) => {
export const ProBadge = ({ text = 'PRO', className, experimentId, eventVariant = '', ...htmlProps }: Props) => {
const styles = useStyles2(getStyles);
useEffect(() => {
if (experimentId) {
reportExperimentView(experimentId, 'test', '');
reportExperimentView(experimentId, 'test', eventVariant);
}
}, [experimentId]);
}, [experimentId, eventVariant]);
return (
<span className={cx(styles.badge, className)} {...htmlProps}>

View File

@ -1,24 +1,41 @@
import React, { HTMLAttributes } from 'react';
import React, { HTMLAttributes, useEffect } from 'react';
import { css, cx } from '@emotion/css';
import { Icon, LinkButton, useStyles2 } from '@grafana/ui';
import { Button, Icon, LinkButton, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { reportExperimentView } from '@grafana/runtime/src';
type ComponentSize = 'sm' | 'md';
export interface Props extends HTMLAttributes<HTMLOrSVGElement> {
featureName: string;
size?: ComponentSize;
text?: string;
eventVariant?: string;
featureId: string;
}
export const UpgradeBox = ({ featureName, className, children, size = 'md', ...htmlProps }: Props) => {
export const UpgradeBox = ({
featureName,
className,
children,
text,
featureId,
eventVariant = '',
size = 'md',
...htmlProps
}: Props) => {
const styles = useStyles2((theme) => getUpgradeBoxStyles(theme, size));
useEffect(() => {
reportExperimentView(`feature-highlights-${featureId}`, 'test', eventVariant);
}, [eventVariant, featureId]);
return (
<div className={cx(styles.box, className)} {...htmlProps}>
<Icon name={'rocket'} className={styles.icon} />
<div className={styles.inner}>
<p className={styles.text}>
Youve discovered a Pro feature! Get the Grafana Pro plan to access {featureName}.
Youve discovered a Pro feature! {text || `Get the Grafana Pro plan to access ${featureName}.`}
</p>
<LinkButton
variant="secondary"
@ -92,6 +109,11 @@ export interface UpgradeContentProps {
description?: string;
listItems: string[];
caption?: string;
action?: {
text: string;
link?: string;
onClick?: () => void;
};
}
export const UpgradeContent = ({
@ -101,6 +123,7 @@ export const UpgradeContent = ({
featureName,
description,
caption,
action,
}: UpgradeContentProps) => {
const styles = useStyles2(getUpgradeContentStyles);
return (
@ -115,6 +138,16 @@ export const UpgradeContent = ({
</li>
))}
</ul>
{action?.link && (
<LinkButton variant={'primary'} href={action.link}>
{action.text}
</LinkButton>
)}
{action?.onClick && (
<Button variant={'primary'} onClick={action.onClick}>
{action.text}
</Button>
)}
{featureUrl && (
<LinkButton fill={'text'} href={featureUrl} className={styles.link} target="_blank" rel="noreferrer noopener">
Learn more
@ -146,6 +179,9 @@ const getUpgradeContentStyles = (theme: GrafanaTheme2) => {
width: 100%;
}
`,
title: css`
color: ${theme.colors.text.maxContrast};
`,
description: css`
color: ${theme.colors.text.primary};
font-weight: ${theme.typography.fontWeightLight};
@ -168,9 +204,6 @@ const getUpgradeContentStyles = (theme: GrafanaTheme2) => {
link: css`
margin-left: ${theme.spacing(2)};
`,
title: css`
color: ${theme.colors.text.maxContrast};
`,
caption: css`
font-weight: ${theme.typography.fontWeightLight};
margin: ${theme.spacing(1, 0, 0)};

View File

@ -0,0 +1,8 @@
import { config } from '@grafana/runtime/src';
export function isTrial() {
const settings = (config as any).licensing;
return settings?.isTrial;
}
export const highlightTrial = () => isTrial() && config.featureToggles.featureHighlights;

View File

@ -5,6 +5,7 @@ import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types';
import { ProBadge } from 'app/core/components/Upgrade/ProBadge';
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
import { highlightTrial } from '../../admin/utils';
const loadingDSType = 'Loading';
@ -53,7 +54,8 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
const isLoadingNav = dataSource.type === loadingDSType;
const dsPermissions = {
const permissionsExperimentId = 'feature-highlights-data-source-permissions-badge';
const dsPermissions: NavModelItem = {
active: false,
icon: 'lock',
id: `datasource-permissions-${dataSource.uid}`,
@ -61,6 +63,10 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
url: `datasources/edit/${dataSource.uid}/permissions`,
};
if (highlightTrial() && !isLoadingNav) {
dsPermissions.tabSuffix = () => ProBadge({ experimentId: permissionsExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('dspermissions')) {
if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) {
navModel.children!.push(dsPermissions);
@ -69,11 +75,12 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
navModel.children!.push({
...dsPermissions,
url: dsPermissions.url + '/upgrade',
tabSuffix: () => ProBadge({ experimentId: 'feature-highlights-data-source-permissions-badge' }),
tabSuffix: () => ProBadge({ experimentId: permissionsExperimentId }),
});
}
const analytics = {
const analyticsExperimentId = 'feature-highlights-data-source-insights-badge';
const analytics: NavModelItem = {
active: false,
icon: 'info-circle',
id: `datasource-insights-${dataSource.uid}`,
@ -81,17 +88,23 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
url: `datasources/edit/${dataSource.uid}/insights`,
};
if (highlightTrial() && !isLoadingNav) {
analytics.tabSuffix = () => ProBadge({ experimentId: analyticsExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('analytics')) {
navModel.children!.push(analytics);
} else if (highlightsEnabled && !isLoadingNav) {
navModel.children!.push({
...analytics,
url: analytics.url + '/upgrade',
tabSuffix: () => ProBadge({ experimentId: 'feature-highlights-data-source-insights-badge' }),
tabSuffix: () => ProBadge({ experimentId: analyticsExperimentId }),
});
}
const caching = {
const cachingExperimentId = 'feature-highlights-query-caching-badge';
const caching: NavModelItem = {
active: false,
icon: 'database',
id: `datasource-cache-${dataSource.uid}`,
@ -100,13 +113,17 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
hideFromTabs: !pluginMeta.isBackend || !config.caching.enabled,
};
if (highlightTrial() && !isLoadingNav) {
caching.tabSuffix = () => ProBadge({ experimentId: cachingExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('caching')) {
navModel.children!.push(caching);
} else if (highlightsEnabled && !isLoadingNav) {
navModel.children!.push({
...caching,
url: caching.url + '/upgrade',
tabSuffix: () => ProBadge({ experimentId: 'feature-highlights-query-caching-badge' }),
tabSuffix: () => ProBadge({ experimentId: cachingExperimentId }),
});
}

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { LegacyForms, Tooltip, Icon, Button } from '@grafana/ui';
import { LegacyForms, Tooltip, Icon, Button, useTheme2 } from '@grafana/ui';
const { Input } = LegacyForms;
import { StoreState, TeamGroup } from '../../types';
@ -10,6 +10,8 @@ import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
import { getTeamGroups } from './state/selectors';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { UpgradeBox, UpgradeContent, UpgradeContentProps } from 'app/core/components/Upgrade/UpgradeBox';
import { highlightTrial } from 'app/features/admin/utils';
function mapStateToProps(state: StoreState) {
return {
@ -90,14 +92,25 @@ export class TeamGroupSync extends PureComponent<Props, State> {
render() {
const { isAdding, newGroupId } = this.state;
const { groups, isReadOnly } = this.props;
return (
<div>
{highlightTrial() && (
<UpgradeBox
featureId={'team-sync'}
eventVariant={'trial'}
featureName={'team sync'}
text={'Add a group to enable team sync for free during your trial of Grafana Pro.'}
/>
)}
<div className="page-action-bar">
<h3 className="page-sub-heading">External group sync</h3>
<Tooltip placement="auto" content={headerTooltip}>
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
</Tooltip>
{(!highlightTrial() || groups.length > 0) && (
<>
<h3 className="page-sub-heading">External group sync</h3>
<Tooltip placement="auto" content={headerTooltip}>
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
</Tooltip>
</>
)}
<div className="page-action-bar__spacer" />
{groups.length > 0 && (
<Button className="pull-right" onClick={this.onToggleAdding} disabled={isReadOnly}>
@ -131,19 +144,23 @@ export class TeamGroupSync extends PureComponent<Props, State> {
</div>
</SlideDown>
{groups.length === 0 && !isAdding && (
<EmptyListCTA
onClick={this.onToggleAdding}
buttonIcon="users-alt"
title="There are no external groups to sync with"
buttonTitle="Add Group"
proTip={headerTooltip}
proTipLinkTitle="Learn more"
proTipLink="http://docs.grafana.org/auth/enhanced_ldap/"
proTipTarget="_blank"
buttonDisabled={isReadOnly}
/>
)}
{groups.length === 0 &&
!isAdding &&
(highlightTrial() ? (
<TeamSyncUpgradeContent action={{ onClick: this.onToggleAdding, text: 'Add group' }} />
) : (
<EmptyListCTA
onClick={this.onToggleAdding}
buttonIcon="users-alt"
title="There are no external groups to sync with"
buttonTitle="Add group"
proTip={headerTooltip}
proTipLinkTitle="Learn more"
proTipLink="https://docs.grafana.org/auth/enhanced_ldap/"
proTipTarget="_blank"
buttonDisabled={isReadOnly}
/>
))}
{groups.length > 0 && (
<div className="admin-list-table">
@ -163,4 +180,22 @@ export class TeamGroupSync extends PureComponent<Props, State> {
}
}
export const TeamSyncUpgradeContent = ({ action }: { action?: UpgradeContentProps['action'] }) => {
const theme = useTheme2();
return (
<UpgradeContent
action={action}
listItems={[
'Stop managing user access in two places - assign users to groups in SAML, LDAP or Oauth, and manage access at a Team level in Grafana',
'Update users permissions immediately when you add or remove them from an LDAP group, with no need for them to sign out and back in',
]}
image={`team-sync-${theme.isLight ? 'light' : 'dark'}.png`}
featureName={'team sync'}
featureUrl={'https://grafana.com/docs/grafana/latest/enterprise/team-sync'}
description={
'Team Sync makes it easier for you to manage users access in Grafana, by immediately updating each users Grafana teams and permissions based on their single sign-on group membership, instead of when users sign in.'
}
/>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync);

View File

@ -7,7 +7,7 @@ import Page from 'app/core/components/Page/Page';
import TeamMembers from './TeamMembers';
import TeamPermissions from './TeamPermissions';
import TeamSettings from './TeamSettings';
import TeamGroupSync from './TeamGroupSync';
import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync';
import { AccessControlAction, StoreState } from 'app/types';
import { loadTeam, loadTeamMembers } from './state/actions';
import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors';
@ -15,9 +15,9 @@ import { getTeamLoadingNav } from './state/navModel';
import { getNavModel } from 'app/core/selectors/navModel';
import { contextSrv } from 'app/core/services/context_srv';
import { NavModel } from '@grafana/data';
import { featureEnabled, reportExperimentView } from '@grafana/runtime';
import { featureEnabled } from '@grafana/runtime';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { UpgradeBox, UpgradeContent } from 'app/core/components/Upgrade/UpgradeBox';
import { UpgradeBox } from 'app/core/components/Upgrade/UpgradeBox';
interface TeamPageRouteParams {
id: string;
@ -84,13 +84,6 @@ export class TeamPages extends PureComponent<Props, State> {
async componentDidMount() {
await this.fetchTeam();
const { isSyncEnabled } = this.state;
const currentPage = this.getCurrentPage();
if (currentPage === PageTypes.GroupSync && !isSyncEnabled && config.featureToggles.featureHighlights) {
reportExperimentView('feature-highlights-team-sync', 'test', '');
}
}
async fetchTeam() {
@ -141,7 +134,7 @@ export class TeamPages extends PureComponent<Props, State> {
renderPage(isSignedInUserTeamAdmin: boolean): React.ReactNode {
const { isSyncEnabled } = this.state;
const { members, team, theme } = this.props;
const { members, team } = this.props;
const currentPage = this.getCurrentPage();
const canReadTeam = contextSrv.hasAccessInMetadata(
@ -177,19 +170,8 @@ export class TeamPages extends PureComponent<Props, State> {
} else if (config.featureToggles.featureHighlights) {
return (
<>
<UpgradeBox featureName={'team sync'} />
<UpgradeContent
listItems={[
'Stop managing user access in two places - assign users to groups in SAML, LDAP or Oauth, and manage access at a Team level in Grafana',
'Update users permissions immediately when you add or remove them from an LDAP group, with no need for them to sign out and back in',
]}
image={`team-sync-${theme.isLight ? 'light' : 'dark'}.png`}
featureName={'team sync'}
featureUrl={'https://grafana.com/docs/grafana/latest/enterprise/team-sync'}
description={
'Team Sync makes it easier for you to manage users access in Grafana, by immediately updating each users Grafana teams and permissions based on their single sign-on group membership, instead of when users sign in.'
}
/>
<UpgradeBox featureName={'team sync'} featureId={'team-sync'} />
<TeamSyncUpgradeContent />
</>
);
}

View File

@ -67,10 +67,10 @@ exports[`Render should render component 1`] = `
<EmptyListCTA
buttonDisabled={false}
buttonIcon="users-alt"
buttonTitle="Add Group"
buttonTitle="Add group"
onClick={[Function]}
proTip="Sync LDAP or OAuth groups with your Grafana teams."
proTipLink="http://docs.grafana.org/auth/enhanced_ldap/"
proTipLink="https://docs.grafana.org/auth/enhanced_ldap/"
proTipLinkTitle="Learn more"
proTipTarget="_blank"
title="There are no external groups to sync with"

View File

@ -4,6 +4,7 @@ import { NavModelItem, NavModel } from '@grafana/data';
import config from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv';
import { ProBadge } from 'app/core/components/Upgrade/ProBadge';
import { highlightTrial } from 'app/features/admin/utils';
const loadingTeam = {
avatarUrl: 'public/img/user_profile.png',
@ -51,7 +52,7 @@ export function buildNavModel(team: Team): NavModelItem {
});
}
const teamGroupSync = {
const teamGroupSync: NavModelItem = {
active: false,
icon: 'sync',
id: `team-groupsync-${team.id}`,
@ -61,6 +62,11 @@ export function buildNavModel(team: Team): NavModelItem {
const isLoadingTeam = team === loadingTeam;
if (highlightTrial()) {
teamGroupSync.tabSuffix = () =>
ProBadge({ experimentId: isLoadingTeam ? '' : 'feature-highlights-team-sync-badge', eventVariant: 'trial' });
}
// With both Legacy and FGAC the tab is protected being featureEnabled
// While team is loading we leave the teamsync tab
// With FGAC the External Group Sync tab is available when user has ActionTeamsPermissionsRead for this team