Feature Highlights: update upgrade components UI (#47885) (#47948)

* 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>
(cherry picked from commit 9c0aa09a85)

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
Grot (@grafanabot)
2022-04-20 14:24:51 +02:00
committed by GitHub
co-authored by Alex Khomenko
parent f682ad8c60
commit 0a4b6dccfc
8 changed files with 144 additions and 62 deletions
@@ -7,16 +7,17 @@ import { reportExperimentView } from '@grafana/runtime';
export interface Props extends HTMLAttributes<HTMLSpanElement> { export interface Props extends HTMLAttributes<HTMLSpanElement> {
text?: string; text?: string;
experimentId?: 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); const styles = useStyles2(getStyles);
useEffect(() => { useEffect(() => {
if (experimentId) { if (experimentId) {
reportExperimentView(experimentId, 'test', ''); reportExperimentView(experimentId, 'test', eventVariant);
} }
}, [experimentId]); }, [experimentId, eventVariant]);
return ( return (
<span className={cx(styles.badge, className)} {...htmlProps}> <span className={cx(styles.badge, className)} {...htmlProps}>
@@ -1,24 +1,41 @@
import React, { HTMLAttributes } from 'react'; import React, { HTMLAttributes, useEffect } from 'react';
import { css, cx } from '@emotion/css'; 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 { GrafanaTheme2 } from '@grafana/data';
import { reportExperimentView } from '@grafana/runtime/src';
type ComponentSize = 'sm' | 'md'; type ComponentSize = 'sm' | 'md';
export interface Props extends HTMLAttributes<HTMLOrSVGElement> { export interface Props extends HTMLAttributes<HTMLOrSVGElement> {
featureName: string; featureName: string;
size?: ComponentSize; 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)); const styles = useStyles2((theme) => getUpgradeBoxStyles(theme, size));
useEffect(() => {
reportExperimentView(`feature-highlights-${featureId}`, 'test', eventVariant);
}, [eventVariant, featureId]);
return ( return (
<div className={cx(styles.box, className)} {...htmlProps}> <div className={cx(styles.box, className)} {...htmlProps}>
<Icon name={'rocket'} className={styles.icon} /> <Icon name={'rocket'} className={styles.icon} />
<div className={styles.inner}> <div className={styles.inner}>
<p className={styles.text}> <p className={styles.text}>
You’ve discovered a Pro feature! Get the Grafana Pro plan to access {featureName}. You’ve discovered a Pro feature! {text || `Get the Grafana Pro plan to access ${featureName}.`}
</p> </p>
<LinkButton <LinkButton
variant="secondary" variant="secondary"
@@ -92,6 +109,11 @@ export interface UpgradeContentProps {
description?: string; description?: string;
listItems: string[]; listItems: string[];
caption?: string; caption?: string;
action?: {
text: string;
link?: string;
onClick?: () => void;
};
} }
export const UpgradeContent = ({ export const UpgradeContent = ({
@@ -101,6 +123,7 @@ export const UpgradeContent = ({
featureName, featureName,
description, description,
caption, caption,
action,
}: UpgradeContentProps) => { }: UpgradeContentProps) => {
const styles = useStyles2(getUpgradeContentStyles); const styles = useStyles2(getUpgradeContentStyles);
return ( return (
@@ -115,6 +138,16 @@ export const UpgradeContent = ({
</li> </li>
))} ))}
</ul> </ul>
{action?.link && (
<LinkButton variant={'primary'} href={action.link}>
{action.text}
</LinkButton>
)}
{action?.onClick && (
<Button variant={'primary'} onClick={action.onClick}>
{action.text}
</Button>
)}
{featureUrl && ( {featureUrl && (
<LinkButton fill={'text'} href={featureUrl} className={styles.link} target="_blank" rel="noreferrer noopener"> <LinkButton fill={'text'} href={featureUrl} className={styles.link} target="_blank" rel="noreferrer noopener">
Learn more Learn more
@@ -146,6 +179,9 @@ const getUpgradeContentStyles = (theme: GrafanaTheme2) => {
width: 100%; width: 100%;
} }
`, `,
title: css`
color: ${theme.colors.text.maxContrast};
`,
description: css` description: css`
color: ${theme.colors.text.primary}; color: ${theme.colors.text.primary};
font-weight: ${theme.typography.fontWeightLight}; font-weight: ${theme.typography.fontWeightLight};
@@ -168,9 +204,6 @@ const getUpgradeContentStyles = (theme: GrafanaTheme2) => {
link: css` link: css`
margin-left: ${theme.spacing(2)}; margin-left: ${theme.spacing(2)};
`, `,
title: css`
color: ${theme.colors.text.maxContrast};
`,
caption: css` caption: css`
font-weight: ${theme.typography.fontWeightLight}; font-weight: ${theme.typography.fontWeightLight};
margin: ${theme.spacing(1, 0, 0)}; margin: ${theme.spacing(1, 0, 0)};
+8
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;
@@ -5,6 +5,7 @@ import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import { ProBadge } from 'app/core/components/Upgrade/ProBadge';
import { GenericDataSourcePlugin } from '../settings/PluginSettings'; import { GenericDataSourcePlugin } from '../settings/PluginSettings';
import { highlightTrial } from '../../admin/utils';
const loadingDSType = 'Loading'; const loadingDSType = 'Loading';
@@ -53,7 +54,8 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
const isLoadingNav = dataSource.type === loadingDSType; const isLoadingNav = dataSource.type === loadingDSType;
const dsPermissions = { const permissionsExperimentId = 'feature-highlights-data-source-permissions-badge';
const dsPermissions: NavModelItem = {
active: false, active: false,
icon: 'lock', icon: 'lock',
id: `datasource-permissions-${dataSource.uid}`, id: `datasource-permissions-${dataSource.uid}`,
@@ -61,6 +63,10 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
url: `datasources/edit/${dataSource.uid}/permissions`, url: `datasources/edit/${dataSource.uid}/permissions`,
}; };
if (highlightTrial() && !isLoadingNav) {
dsPermissions.tabSuffix = () => ProBadge({ experimentId: permissionsExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('dspermissions')) { if (featureEnabled('dspermissions')) {
if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) { if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) {
navModel.children!.push(dsPermissions); navModel.children!.push(dsPermissions);
@@ -69,11 +75,12 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
navModel.children!.push({ navModel.children!.push({
...dsPermissions, ...dsPermissions,
url: dsPermissions.url + '/upgrade', 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, active: false,
icon: 'info-circle', icon: 'info-circle',
id: `datasource-insights-${dataSource.uid}`, id: `datasource-insights-${dataSource.uid}`,
@@ -81,17 +88,23 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
url: `datasources/edit/${dataSource.uid}/insights`, url: `datasources/edit/${dataSource.uid}/insights`,
}; };
if (highlightTrial() && !isLoadingNav) {
analytics.tabSuffix = () => ProBadge({ experimentId: analyticsExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('analytics')) { if (featureEnabled('analytics')) {
navModel.children!.push(analytics); navModel.children!.push(analytics);
} else if (highlightsEnabled && !isLoadingNav) { } else if (highlightsEnabled && !isLoadingNav) {
navModel.children!.push({ navModel.children!.push({
...analytics, ...analytics,
url: analytics.url + '/upgrade', 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, active: false,
icon: 'database', icon: 'database',
id: `datasource-cache-${dataSource.uid}`, id: `datasource-cache-${dataSource.uid}`,
@@ -100,13 +113,17 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
hideFromTabs: !pluginMeta.isBackend || !config.caching.enabled, hideFromTabs: !pluginMeta.isBackend || !config.caching.enabled,
}; };
if (highlightTrial() && !isLoadingNav) {
caching.tabSuffix = () => ProBadge({ experimentId: cachingExperimentId, eventVariant: 'trial' });
}
if (featureEnabled('caching')) { if (featureEnabled('caching')) {
navModel.children!.push(caching); navModel.children!.push(caching);
} else if (highlightsEnabled && !isLoadingNav) { } else if (highlightsEnabled && !isLoadingNav) {
navModel.children!.push({ navModel.children!.push({
...caching, ...caching,
url: caching.url + '/upgrade', url: caching.url + '/upgrade',
tabSuffix: () => ProBadge({ experimentId: 'feature-highlights-query-caching-badge' }), tabSuffix: () => ProBadge({ experimentId: cachingExperimentId }),
}); });
} }
+54 -19
View File
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { SlideDown } from 'app/core/components/Animations/SlideDown'; 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; const { Input } = LegacyForms;
import { StoreState, TeamGroup } from '../../types'; import { StoreState, TeamGroup } from '../../types';
@@ -10,6 +10,8 @@ import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
import { getTeamGroups } from './state/selectors'; import { getTeamGroups } from './state/selectors';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; 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) { function mapStateToProps(state: StoreState) {
return { return {
@@ -90,14 +92,25 @@ export class TeamGroupSync extends PureComponent<Props, State> {
render() { render() {
const { isAdding, newGroupId } = this.state; const { isAdding, newGroupId } = this.state;
const { groups, isReadOnly } = this.props; const { groups, isReadOnly } = this.props;
return ( return (
<div> <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"> <div className="page-action-bar">
<h3 className="page-sub-heading">External group sync</h3> {(!highlightTrial() || groups.length > 0) && (
<Tooltip placement="auto" content={headerTooltip}> <>
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" /> <h3 className="page-sub-heading">External group sync</h3>
</Tooltip> <Tooltip placement="auto" content={headerTooltip}>
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
</Tooltip>
</>
)}
<div className="page-action-bar__spacer" /> <div className="page-action-bar__spacer" />
{groups.length > 0 && ( {groups.length > 0 && (
<Button className="pull-right" onClick={this.onToggleAdding} disabled={isReadOnly}> <Button className="pull-right" onClick={this.onToggleAdding} disabled={isReadOnly}>
@@ -131,19 +144,23 @@ export class TeamGroupSync extends PureComponent<Props, State> {
</div> </div>
</SlideDown> </SlideDown>
{groups.length === 0 && !isAdding && ( {groups.length === 0 &&
<EmptyListCTA !isAdding &&
onClick={this.onToggleAdding} (highlightTrial() ? (
buttonIcon="users-alt" <TeamSyncUpgradeContent action={{ onClick: this.onToggleAdding, text: 'Add group' }} />
title="There are no external groups to sync with" ) : (
buttonTitle="Add Group" <EmptyListCTA
proTip={headerTooltip} onClick={this.onToggleAdding}
proTipLinkTitle="Learn more" buttonIcon="users-alt"
proTipLink="http://docs.grafana.org/auth/enhanced_ldap/" title="There are no external groups to sync with"
proTipTarget="_blank" buttonTitle="Add group"
buttonDisabled={isReadOnly} proTip={headerTooltip}
/> proTipLinkTitle="Learn more"
)} proTipLink="https://docs.grafana.org/auth/enhanced_ldap/"
proTipTarget="_blank"
buttonDisabled={isReadOnly}
/>
))}
{groups.length > 0 && ( {groups.length > 0 && (
<div className="admin-list-table"> <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 user’s Grafana teams and permissions based on their single sign-on group membership, instead of when users sign in.'
}
/>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync); export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync);
+6 -24
View File
@@ -7,7 +7,7 @@ import Page from 'app/core/components/Page/Page';
import TeamMembers from './TeamMembers'; import TeamMembers from './TeamMembers';
import TeamPermissions from './TeamPermissions'; import TeamPermissions from './TeamPermissions';
import TeamSettings from './TeamSettings'; import TeamSettings from './TeamSettings';
import TeamGroupSync from './TeamGroupSync'; import TeamGroupSync, { TeamSyncUpgradeContent } from './TeamGroupSync';
import { AccessControlAction, StoreState } from 'app/types'; import { AccessControlAction, StoreState } from 'app/types';
import { loadTeam, loadTeamMembers } from './state/actions'; import { loadTeam, loadTeamMembers } from './state/actions';
import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors'; import { getTeam, getTeamMembers, isSignedInUserTeamAdmin } from './state/selectors';
@@ -15,9 +15,9 @@ import { getTeamLoadingNav } from './state/navModel';
import { getNavModel } from 'app/core/selectors/navModel'; import { getNavModel } from 'app/core/selectors/navModel';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { NavModel } from '@grafana/data'; import { NavModel } from '@grafana/data';
import { featureEnabled, reportExperimentView } from '@grafana/runtime'; import { featureEnabled } from '@grafana/runtime';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; 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 { interface TeamPageRouteParams {
id: string; id: string;
@@ -84,13 +84,6 @@ export class TeamPages extends PureComponent<Props, State> {
async componentDidMount() { async componentDidMount() {
await this.fetchTeam(); 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() { async fetchTeam() {
@@ -141,7 +134,7 @@ export class TeamPages extends PureComponent<Props, State> {
renderPage(isSignedInUserTeamAdmin: boolean): React.ReactNode { renderPage(isSignedInUserTeamAdmin: boolean): React.ReactNode {
const { isSyncEnabled } = this.state; const { isSyncEnabled } = this.state;
const { members, team, theme } = this.props; const { members, team } = this.props;
const currentPage = this.getCurrentPage(); const currentPage = this.getCurrentPage();
const canReadTeam = contextSrv.hasAccessInMetadata( const canReadTeam = contextSrv.hasAccessInMetadata(
@@ -177,19 +170,8 @@ export class TeamPages extends PureComponent<Props, State> {
} else if (config.featureToggles.featureHighlights) { } else if (config.featureToggles.featureHighlights) {
return ( return (
<> <>
<UpgradeBox featureName={'team sync'} /> <UpgradeBox featureName={'team sync'} featureId={'team-sync'} />
<UpgradeContent <TeamSyncUpgradeContent />
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 user’s Grafana teams and permissions based on their single sign-on group membership, instead of when users sign in.'
}
/>
</> </>
); );
} }
@@ -67,10 +67,10 @@ exports[`Render should render component 1`] = `
<EmptyListCTA <EmptyListCTA
buttonDisabled={false} buttonDisabled={false}
buttonIcon="users-alt" buttonIcon="users-alt"
buttonTitle="Add Group" buttonTitle="Add group"
onClick={[Function]} onClick={[Function]}
proTip="Sync LDAP or OAuth groups with your Grafana teams." 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" proTipLinkTitle="Learn more"
proTipTarget="_blank" proTipTarget="_blank"
title="There are no external groups to sync with" title="There are no external groups to sync with"
+7 -1
View File
@@ -4,6 +4,7 @@ import { NavModelItem, NavModel } from '@grafana/data';
import config from 'app/core/config'; import config from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import { ProBadge } from 'app/core/components/Upgrade/ProBadge';
import { highlightTrial } from 'app/features/admin/utils';
const loadingTeam = { const loadingTeam = {
avatarUrl: 'public/img/user_profile.png', avatarUrl: 'public/img/user_profile.png',
@@ -51,7 +52,7 @@ export function buildNavModel(team: Team): NavModelItem {
}); });
} }
const teamGroupSync = { const teamGroupSync: NavModelItem = {
active: false, active: false,
icon: 'sync', icon: 'sync',
id: `team-groupsync-${team.id}`, id: `team-groupsync-${team.id}`,
@@ -61,6 +62,11 @@ export function buildNavModel(team: Team): NavModelItem {
const isLoadingTeam = team === loadingTeam; 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 // With both Legacy and FGAC the tab is protected being featureEnabled
// While team is loading we leave the teamsync tab // 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 // With FGAC the External Group Sync tab is available when user has ActionTeamsPermissionsRead for this team