mirror of https://github.com/grafana/grafana.git
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:
parent
68aac0bd90
commit
9c0aa09a85
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
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>
|
||||
<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)};
|
||||
|
|
|
|||
|
|
@ -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 { 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 }),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 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);
|
||||
|
|
|
|||
|
|
@ -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 user’s 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue