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>
@ -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": [
|
||||
|
@ -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)};
|
||||
`,
|
||||
|
@ -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>You’ve found a Pro feature!</h4>
|
||||
{text && <p className={styles.text}>{text}</p>}
|
||||
{children}
|
||||
<div className={styles.inner}>
|
||||
<p className={styles.text}>
|
||||
You’ve 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;
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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,
|
||||
|
@ -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 user’s 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));
|
||||
|
BIN
public/img/enterprise/highlights/datasource-insights-dark.png
Normal file
After Width: | Height: | Size: 193 KiB |
BIN
public/img/enterprise/highlights/datasource-insights-light.png
Normal file
After Width: | Height: | Size: 134 KiB |
BIN
public/img/enterprise/highlights/ds-permissions-dark.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
public/img/enterprise/highlights/ds-permissions-light.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
public/img/enterprise/highlights/query-caching-dark.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
public/img/enterprise/highlights/query-caching-light.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
public/img/enterprise/highlights/recorded-queries-dark.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
public/img/enterprise/highlights/recorded-queries-light.png
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
public/img/enterprise/highlights/reporting-email.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
public/img/enterprise/highlights/team-sync-dark.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
public/img/enterprise/highlights/team-sync-light.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
public/img/enterprise/highlights/usage-insights-dark.png
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
public/img/enterprise/highlights/usage-insights-light.png
Normal file
After Width: | Height: | Size: 139 KiB |