Feature Highlights: update components (#46920)

* Apply new upgrade styles

* Add caption/update pro badge color

* Update team sync upgrade page

* Add UpgradeContentVertical

* Update public/app/core/components/Upgrade/UpgradeBox.tsx

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

* Update screenshots

* Update screenshots[2]

* Scroll screenshot if doesnt fit

* Add missing target prop

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>
This commit is contained in:
Alex Khomenko 2022-03-30 15:46:56 +03:00 committed by GitHub
parent 42e090cda8
commit 114704e2e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 197 additions and 77 deletions

View File

@ -284,7 +284,7 @@ exports[`no enzyme tests`] = {
"public/app/features/teams/TeamMembers.test.tsx:3993436101": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/teams/TeamPages.test.tsx:2493626076": [
"public/app/features/teams/TeamPages.test.tsx:1265047708": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/features/teams/TeamSettings.test.tsx:2628968507": [

View File

@ -32,7 +32,7 @@ const getStyles = (theme: GrafanaTheme2) => {
border-radius: ${theme.shape.borderRadius(5)};
background-color: ${theme.colors.success.main};
padding: ${theme.spacing(0.25, 0.75)};
color: ${theme.colors.text.maxContrast};
color: white; // use the same color for both themes
font-weight: ${theme.typography.fontWeightMedium};
font-size: ${theme.typography.pxToRem(10)};
`,

View File

@ -1,4 +1,4 @@
import React, { ReactNode, HTMLAttributes } from 'react';
import React, { HTMLAttributes } from 'react';
import { css, cx } from '@emotion/css';
import { Icon, LinkButton, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
@ -6,48 +6,30 @@ import { GrafanaTheme2 } from '@grafana/data';
type ComponentSize = 'sm' | 'md';
export interface Props extends HTMLAttributes<HTMLOrSVGElement> {
text?: string;
featureName: string;
size?: ComponentSize;
children?: ReactNode;
secondaryAction?: {
url: string;
text: string;
};
}
export const UpgradeBox = ({ text, className, children, secondaryAction, size = 'md', ...htmlProps }: Props) => {
export const UpgradeBox = ({ featureName, className, children, size = 'md', ...htmlProps }: Props) => {
const styles = useStyles2((theme) => getUpgradeBoxStyles(theme, size));
return (
<div className={cx(styles.box, className)} {...htmlProps}>
<Icon name={'rocket'} className={styles.icon} />
<div>
<h4>Youve found a Pro feature!</h4>
{text && <p className={styles.text}>{text}</p>}
{children}
<div className={styles.inner}>
<p className={styles.text}>
Youve discovered a Pro feature! Get the Grafana Pro plan to access {featureName}.
</p>
<LinkButton
variant="primary"
variant="secondary"
size={size}
className={styles.button}
href="https://grafana.com/profile/org/subscription"
target="__blank"
rel="noopener noreferrer"
>
Upgrade to Pro
Upgrade
</LinkButton>
{secondaryAction && (
<LinkButton
variant="link"
size={size}
className={cx(styles.button, styles.buttonSecondary)}
href={secondaryAction.url}
target="__blank"
rel="noopener noreferrer"
>
{secondaryAction.text}
</LinkButton>
)}
</div>
</div>
);
@ -60,26 +42,35 @@ const getUpgradeBoxStyles = (theme: GrafanaTheme2, size: ComponentSize) => {
return {
box: css`
display: flex;
align-items: center;
position: relative;
border-radius: ${borderRadius};
background: ${theme.colors.primary.transparent};
border: 1px solid ${theme.colors.primary.shade};
background: ${theme.colors.success.transparent};
padding: ${theme.spacing(2)};
color: ${theme.colors.primary.text};
color: ${theme.colors.success.text};
font-size: ${theme.typography[fontBase].fontSize};
text-align: left;
line-height: 16px;
margin: ${theme.spacing(0, 'auto', 3, 'auto')};
max-width: ${theme.breakpoints.values.xxl}px;
width: 100%;
`,
inner: css`
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
`,
text: css`
margin-bottom: 0;
padding: ${theme.spacing(2, 0)};
line-height: 1.5;
margin: 0;
`,
button: css`
margin-top: ${theme.spacing(2)};
background-color: ${theme.colors.success.main};
font-weight: ${theme.typography.fontWeightLight};
color: white;
&:first-of-type {
margin-right: ${theme.spacing(1)};
&:hover {
background-color: ${theme.colors.success.main};
}
&:focus-visible {
@ -88,11 +79,154 @@ const getUpgradeBoxStyles = (theme: GrafanaTheme2, size: ComponentSize) => {
outline: 2px solid ${theme.colors.primary.main};
}
`,
buttonSecondary: css`
color: ${theme.colors.text.secondary};
`,
icon: css`
margin: ${theme.spacing(0.5, 1, 0.5, 0.5)};
`,
};
};
export interface UpgradeContentProps {
image: string;
featureUrl?: string;
featureName: string;
description?: string;
listItems: string[];
caption?: string;
}
export const UpgradeContent = ({
listItems,
image,
featureUrl,
featureName,
description,
caption,
}: UpgradeContentProps) => {
const styles = useStyles2(getUpgradeContentStyles);
return (
<div className={styles.container}>
<div className={styles.content}>
<h3 className={styles.title}>Get started with {featureName}</h3>
{description && <h6 className={styles.description}>{description}</h6>}
<ul className={styles.list}>
{listItems.map((item, index) => (
<li key={index}>
<Icon name={'check'} size={'xl'} className={styles.icon} /> {item}
</li>
))}
</ul>
{featureUrl && (
<LinkButton fill={'text'} href={featureUrl} className={styles.link} target="_blank" rel="noreferrer noopener">
Learn more
</LinkButton>
)}
</div>
<div className={styles.media}>
<img src={getImgUrl(image)} alt={'Feature screenshot'} />
{caption && <p className={styles.caption}>{caption}</p>}
</div>
</div>
);
};
const getUpgradeContentStyles = (theme: GrafanaTheme2) => {
return {
container: css`
display: flex;
justify-content: space-between;
`,
content: css`
width: 45%;
margin-right: ${theme.spacing(4)};
`,
media: css`
width: 55%;
img {
width: 100%;
}
`,
description: css`
color: ${theme.colors.text.primary};
font-weight: ${theme.typography.fontWeightLight};
`,
list: css`
list-style: none;
margin: ${theme.spacing(4, 0, 2, 0)};
li {
display: flex;
align-items: flex-start;
color: ${theme.colors.text.primary};
padding: ${theme.spacing(1, 0)};
}
`,
icon: css`
color: ${theme.colors.success.main};
margin-right: ${theme.spacing(1)};
`,
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)};
`,
};
};
export const UpgradeContentVertical = ({
featureName,
description,
featureUrl,
image,
}: Omit<UpgradeContentProps, 'listItems' | 'caption'>) => {
const styles = useStyles2(getContentVerticalStyles);
return (
<div className={styles.container}>
<h3 className={styles.title}>Get started with {featureName}</h3>
{description && <h6 className={styles.description}>{description}</h6>}
<LinkButton fill={'text'} href={featureUrl} target="_blank" rel="noreferrer noopener">
Learn more
</LinkButton>
<div className={styles.media}>
<img src={getImgUrl(image)} alt={'Feature screenshot'} />
</div>
</div>
);
};
const getContentVerticalStyles = (theme: GrafanaTheme2) => {
return {
container: css`
overflow: auto;
height: 100%;
`,
title: css`
color: ${theme.colors.text.maxContrast};
`,
description: css`
color: ${theme.colors.text.primary};
font-weight: ${theme.typography.fontWeightLight};
`,
media: css`
width: 100%;
margin-top: ${theme.spacing(2)};
img {
width: 100%;
}
`,
};
};
const getImgUrl = (urlOrId: string) => {
if (urlOrId.startsWith('http')) {
return urlOrId;
}
return '/public/img/enterprise/highlights/' + urlOrId;
};

View File

@ -1,26 +0,0 @@
import React, { useEffect } from 'react';
import { Modal } from '@grafana/ui';
import { reportExperimentView } from '@grafana/runtime';
import { UpgradeBox } from './UpgradeBox';
export interface Props {
title: string;
text: string;
isOpen?: boolean;
onDismiss?: () => void;
experimentId?: string;
}
export const UpgradeModal = ({ title, text, isOpen, onDismiss, experimentId }: Props) => {
useEffect(() => {
if (experimentId) {
reportExperimentView(experimentId, 'test', '');
}
}, [experimentId]);
return (
<Modal title={title} isOpen={isOpen} onDismiss={onDismiss}>
<UpgradeBox text={text} />
</Modal>
);
};

View File

@ -4,7 +4,7 @@ import { Props, TeamPages } from './TeamPages';
import { OrgRole, Team, TeamMember } from '../../types';
import { getMockTeam } from './__mocks__/teamMocks';
import { User } from 'app/core/services/context_srv';
import { NavModel } from '@grafana/data';
import { NavModel, createTheme } from '@grafana/data';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
jest.mock('@grafana/runtime/src/config', () => ({
@ -35,6 +35,7 @@ const setup = (propOverrides?: object) => {
team: {} as Team,
members: [] as TeamMember[],
editorsCanAdmin: false,
theme: createTheme(),
signedInUser: {
id: 1,
isGrafanaAdmin: false,

View File

@ -1,6 +1,7 @@
import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { includes } from 'lodash';
import { Themeable2, withTheme2 } from '@grafana/ui';
import config from 'app/core/config';
import Page from 'app/core/components/Page/Page';
import TeamMembers from './TeamMembers';
@ -16,14 +17,14 @@ import { contextSrv } from 'app/core/services/context_srv';
import { NavModel } from '@grafana/data';
import { featureEnabled, reportExperimentView } from '@grafana/runtime';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { UpgradeBox } from 'app/core/components/Upgrade/UpgradeBox';
import { UpgradeBox, UpgradeContent } from 'app/core/components/Upgrade/UpgradeBox';
interface TeamPageRouteParams {
id: string;
page: string | null;
}
export interface OwnProps extends GrafanaRouteComponentProps<TeamPageRouteParams> {}
export interface OwnProps extends GrafanaRouteComponentProps<TeamPageRouteParams>, Themeable2 {}
interface State {
isSyncEnabled: boolean;
@ -140,7 +141,7 @@ export class TeamPages extends PureComponent<Props, State> {
renderPage(isSignedInUserTeamAdmin: boolean): React.ReactNode {
const { isSyncEnabled } = this.state;
const { members, team } = this.props;
const { members, team, theme } = this.props;
const currentPage = this.getCurrentPage();
const canReadTeam = contextSrv.hasAccessInMetadata(
@ -175,11 +176,21 @@ export class TeamPages extends PureComponent<Props, State> {
}
} else if (config.featureToggles.featureHighlights) {
return (
<UpgradeBox
text={
"Team Sync immediately updates each user's Grafana teams and permissions based on their LDAP or Oauth group membership, instead of updating when users sign in."
<>
<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.'
}
/>
</>
);
}
}
@ -201,4 +212,4 @@ export class TeamPages extends PureComponent<Props, State> {
}
}
export default connector(TeamPages);
export default connector(withTheme2(TeamPages));

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB