mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 01:41:24 -06:00
Onboarding: New getting started panel (#23826)
* image and card component * change height of getting started panel * progress * setup basic step * advanced steps * step forward and backward * do checks * fix button size * minor styling on butttons * add correct links * save tutorial click in localstorage * types and gradients * fix gradients * use spacing variable * lots of responsiveness * add links to help * Getting started work * redo according to split panel design * minor touch ups * new background images * split up docs card to different hrefs * welcome bar touch ups * hide icon on small screens * transparent false on welcome banner * fix urls * source tag in welcome urls * move images to panel dir, removed unused images * Nicer loading message * make the cards look nicer on wide screens * append utm tag on render instead * replace width with margin * new background image for light * remove target on a element * removing buttonselect, add tag to href * more polishing Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
cab066f8ce
commit
36fa54a288
@ -12,6 +12,7 @@ export interface GrafanaThemeCommons {
|
||||
md: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
xxl: string;
|
||||
};
|
||||
typography: {
|
||||
fontFamily: {
|
||||
|
@ -72,6 +72,7 @@ const theme: GrafanaThemeCommons = {
|
||||
md: '769px', // 1 more than regular ipad in portrait
|
||||
lg: '992px',
|
||||
xl: '1200px',
|
||||
xxl: '1440px',
|
||||
},
|
||||
spacing: {
|
||||
insetSquishMd: '4px 8px',
|
||||
|
@ -366,7 +366,7 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
"w": 24,
|
||||
"h": 4,
|
||||
"h": 9,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -1,124 +1,68 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { Icon, IconName } from '@grafana/ui';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { Button, Spinner, stylesFactory } from '@grafana/ui';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { css, cx } from 'emotion';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
interface Step {
|
||||
title: string;
|
||||
cta?: string;
|
||||
icon: IconName;
|
||||
href: string;
|
||||
target?: string;
|
||||
note?: string;
|
||||
check: () => Promise<boolean>;
|
||||
done?: boolean;
|
||||
}
|
||||
import { Step } from './components/Step';
|
||||
import imageDark from './img/Onboarding_Panel_dark.svg';
|
||||
import imageLight from './img/Onboarding_Panel_light.svg';
|
||||
import { getSteps } from './steps';
|
||||
import { Card, SetupStep } from './types';
|
||||
|
||||
interface State {
|
||||
checksDone: boolean;
|
||||
currentStep: number;
|
||||
steps: SetupStep[];
|
||||
}
|
||||
|
||||
export class GettingStarted extends PureComponent<PanelProps, State> {
|
||||
stepIndex = 0;
|
||||
readonly steps: Step[];
|
||||
state = {
|
||||
checksDone: false,
|
||||
currentStep: 0,
|
||||
steps: getSteps(),
|
||||
};
|
||||
|
||||
constructor(props: PanelProps) {
|
||||
super(props);
|
||||
async componentDidMount() {
|
||||
const { steps } = this.state;
|
||||
|
||||
this.state = {
|
||||
checksDone: false,
|
||||
};
|
||||
const checkedStepsPromises: Array<Promise<SetupStep>> = steps.map(async (step: SetupStep) => {
|
||||
const checkedCardsPromises: Array<Promise<Card>> = step.cards.map((card: Card) => {
|
||||
return card.check().then(passed => {
|
||||
return { ...card, done: passed };
|
||||
});
|
||||
});
|
||||
const checkedCards = await Promise.all(checkedCardsPromises);
|
||||
return {
|
||||
...step,
|
||||
done: checkedCards.every(c => c.done),
|
||||
cards: checkedCards,
|
||||
};
|
||||
});
|
||||
|
||||
this.steps = [
|
||||
{
|
||||
title: 'Install Grafana',
|
||||
icon: 'check',
|
||||
href: 'http://docs.grafana.org/',
|
||||
target: '_blank',
|
||||
note: 'Review the installation docs',
|
||||
check: () => Promise.resolve(true),
|
||||
},
|
||||
{
|
||||
title: 'Create a data source',
|
||||
cta: 'Add data source',
|
||||
icon: 'database',
|
||||
href: 'datasources/new?gettingstarted',
|
||||
check: () => {
|
||||
return new Promise(resolve => {
|
||||
resolve(
|
||||
getDatasourceSrv()
|
||||
.getMetricSources()
|
||||
.filter(item => {
|
||||
return item.meta.builtIn !== true;
|
||||
}).length > 0
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Build a dashboard',
|
||||
cta: 'New dashboard',
|
||||
icon: 'apps',
|
||||
href: 'dashboard/new?gettingstarted',
|
||||
check: () => {
|
||||
return backendSrv.search({ limit: 1 }).then(result => {
|
||||
return result.length > 0;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Invite your team',
|
||||
cta: 'Add Users',
|
||||
icon: 'users-alt',
|
||||
href: 'org/users?gettingstarted',
|
||||
check: () => {
|
||||
return backendSrv.get('/api/org/users/lookup').then((res: any) => {
|
||||
/* return res.length > 1; */
|
||||
return false;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Install apps & plugins',
|
||||
cta: 'Explore plugin repository',
|
||||
icon: 'plug',
|
||||
href: 'https://grafana.com/plugins?utm_source=grafana_getting_started',
|
||||
check: () => {
|
||||
return backendSrv.get('/api/plugins', { embedded: 0, core: 0 }).then((plugins: any[]) => {
|
||||
return plugins.length > 0;
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
const checkedSteps = await Promise.all(checkedStepsPromises);
|
||||
|
||||
componentDidMount() {
|
||||
this.stepIndex = -1;
|
||||
return this.nextStep().then((res: any) => {
|
||||
this.setState({ checksDone: true });
|
||||
this.setState({
|
||||
currentStep: !checkedSteps[0].done ? 0 : 1,
|
||||
steps: checkedSteps,
|
||||
checksDone: true,
|
||||
});
|
||||
}
|
||||
|
||||
nextStep(): any {
|
||||
if (this.stepIndex === this.steps.length - 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
onForwardClick = () => {
|
||||
this.setState(prevState => ({
|
||||
currentStep: prevState.currentStep + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
this.stepIndex += 1;
|
||||
const currentStep = this.steps[this.stepIndex];
|
||||
return currentStep.check().then(passed => {
|
||||
if (passed) {
|
||||
currentStep.done = true;
|
||||
return this.nextStep();
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
onPreviousClick = () => {
|
||||
this.setState(prevState => ({
|
||||
currentStep: prevState.currentStep - 1,
|
||||
}));
|
||||
};
|
||||
|
||||
dismiss = () => {
|
||||
const { id } = this.props;
|
||||
@ -137,34 +81,130 @@ export class GettingStarted extends PureComponent<PanelProps, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { checksDone } = this.state;
|
||||
if (!checksDone) {
|
||||
return <div>checking...</div>;
|
||||
}
|
||||
const { checksDone, currentStep, steps } = this.state;
|
||||
const styles = getStyles();
|
||||
const step = steps[currentStep];
|
||||
|
||||
return (
|
||||
<div className="progress-tracker-container">
|
||||
<button className="progress-tracker-close-btn" onClick={this.dismiss}>
|
||||
<Icon name="times" />
|
||||
</button>
|
||||
<div className="progress-tracker">
|
||||
{this.steps.map((step, index) => {
|
||||
return (
|
||||
<div key={index} className={step.done ? 'progress-step completed' : 'progress-step active'}>
|
||||
<a className="progress-link" href={step.href} target={step.target} title={step.note}>
|
||||
<span className="progress-marker">
|
||||
<Icon name={step.icon} size="xxl" />
|
||||
</span>
|
||||
<span className="progress-text">{step.title}</span>
|
||||
</a>
|
||||
<a className="btn-small progress-step-cta" href={step.href} target={step.target}>
|
||||
{step.cta}
|
||||
</a>
|
||||
<div className={styles.container}>
|
||||
{!checksDone ? (
|
||||
<div className={styles.loading}>
|
||||
<div className={styles.loadingText}>Checking completed setup steps</div>
|
||||
<Spinner size={24} inline />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.dismiss}>
|
||||
<div onClick={this.dismiss}>Remove this panel</div>
|
||||
</div>
|
||||
{currentStep === steps.length - 1 && (
|
||||
<div className={cx(styles.backForwardButtons, styles.previous)} onClick={this.onPreviousClick}>
|
||||
<Button icon="angle-left" variant="secondary" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
<Step step={step} />
|
||||
</div>
|
||||
{currentStep < steps.length - 1 && (
|
||||
<div className={cx(styles.backForwardButtons, styles.forward)} onClick={this.onForwardClick}>
|
||||
<Button icon="angle-right" variant="secondary" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
const { theme } = config;
|
||||
const backgroundImage = theme.isDark ? imageDark : imageLight;
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: url(${backgroundImage}) no-repeat;
|
||||
background-size: cover;
|
||||
padding: ${theme.spacing.xl} ${theme.spacing.md} 0;
|
||||
`,
|
||||
content: css`
|
||||
label: content;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.xxl}) {
|
||||
margin-left: ${theme.spacing.lg};
|
||||
justify-content: flex-start;
|
||||
}
|
||||
`,
|
||||
header: css`
|
||||
label: header;
|
||||
margin-bottom: ${theme.spacing.lg};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media only screen and (min-width: ${theme.breakpoints.lg}) {
|
||||
flex-direction: row;
|
||||
}
|
||||
`,
|
||||
headerLogo: css`
|
||||
height: 58px;
|
||||
padding-right: ${theme.spacing.md};
|
||||
display: none;
|
||||
|
||||
@media only screen and (min-width: ${theme.breakpoints.md}) {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
heading: css`
|
||||
label: heading;
|
||||
margin-right: ${theme.spacing.lg};
|
||||
margin-bottom: ${theme.spacing.lg};
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
||||
@media only screen and (min-width: ${theme.breakpoints.md}) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
backForwardButtons: css`
|
||||
position: absolute;
|
||||
bottom: 50%;
|
||||
top: 50%;
|
||||
height: 50px;
|
||||
`,
|
||||
previous: css`
|
||||
left: 10px;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.md}) {
|
||||
left: 0;
|
||||
}
|
||||
`,
|
||||
forward: css`
|
||||
right: 10px;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.md}) {
|
||||
right: 0;
|
||||
}
|
||||
`,
|
||||
dismiss: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
margin-right: ${theme.spacing.md};
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
loading: css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
`,
|
||||
loadingText: css`
|
||||
margin-right: ${theme.spacing.sm};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -0,0 +1,61 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Card } from '../types';
|
||||
import { Icon, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
import { cardContent, cardStyle, iconStyle } from './sharedStyles';
|
||||
|
||||
interface Props {
|
||||
card: Card;
|
||||
}
|
||||
|
||||
export const DocsCard: FC<Props> = ({ card }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, card.done);
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<div className={cardContent}>
|
||||
<a href={`${card.href}?utm_source=grafana_gettingstarted`}>
|
||||
<div className={styles.heading}>{card.done ? 'complete' : card.heading}</div>
|
||||
<h4 className={styles.title}>{card.title}</h4>
|
||||
<div>
|
||||
<Icon className={iconStyle(theme, card.done)} name={card.icon} size="xxl" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a href={`${card.learnHref}?utm_source=grafana_gettingstarted`} className={styles.url} target="_blank">
|
||||
Learn how in the docs <Icon name="external-link-alt" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, complete: boolean) => {
|
||||
return {
|
||||
card: css`
|
||||
${cardStyle(theme, complete)}
|
||||
|
||||
min-width: 230px;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.md}) {
|
||||
min-width: 192px;
|
||||
}
|
||||
`,
|
||||
heading: css`
|
||||
text-transform: uppercase;
|
||||
color: ${complete ? theme.palette.blue95 : '#FFB357'};
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
`,
|
||||
title: css`
|
||||
margin-bottom: 48px;
|
||||
`,
|
||||
url: css`
|
||||
border-top: 1px solid ${theme.colors.border1};
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: 8px 16px;
|
||||
width: 100%;
|
||||
`,
|
||||
};
|
||||
});
|
68
public/app/plugins/panel/gettingstarted/components/Step.tsx
Normal file
68
public/app/plugins/panel/gettingstarted/components/Step.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { TutorialCard } from './TutorialCard';
|
||||
import { Card, SetupStep } from '../types';
|
||||
import { DocsCard } from './DocsCard';
|
||||
|
||||
interface Props {
|
||||
step: SetupStep;
|
||||
}
|
||||
|
||||
export const Step: FC<Props> = ({ step }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={styles.setup}>
|
||||
<div className={styles.info}>
|
||||
<h2 className={styles.title}>{step.title}</h2>
|
||||
<p>{step.info}</p>
|
||||
</div>
|
||||
<div className={styles.cards}>
|
||||
{step.cards.map((card: Card, index: number) => {
|
||||
const key = `${card.title}-${index}`;
|
||||
if (card.type === 'tutorial') {
|
||||
return <TutorialCard key={key} card={card} />;
|
||||
}
|
||||
return <DocsCard key={key} card={card} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
setup: css`
|
||||
display: flex;
|
||||
width: 95%;
|
||||
`,
|
||||
info: css`
|
||||
width: 172px;
|
||||
margin-right: 5%;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.xxl}) {
|
||||
margin-right: ${theme.spacing.xl};
|
||||
}
|
||||
@media only screen and (max-width: ${theme.breakpoints.sm}) {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
title: css`
|
||||
color: ${theme.palette.blue95};
|
||||
`,
|
||||
cards: css`
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.xxl}) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
import React, { FC, MouseEvent } from 'react';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Icon, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { css } from 'emotion';
|
||||
import store from 'app/core/store';
|
||||
import { cardContent, cardStyle, iconStyle } from './sharedStyles';
|
||||
import { Card } from '../types';
|
||||
|
||||
interface Props {
|
||||
card: Card;
|
||||
}
|
||||
|
||||
export const TutorialCard: FC<Props> = ({ card }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, card.done);
|
||||
|
||||
return (
|
||||
<a className={styles.card} onClick={(event: MouseEvent<HTMLAnchorElement>) => handleTutorialClick(event, card)}>
|
||||
<div className={cardContent}>
|
||||
<div className={styles.type}>{card.type}</div>
|
||||
<div className={styles.heading}>{card.done ? 'complete' : card.heading}</div>
|
||||
<h4>{card.title}</h4>
|
||||
<div className={styles.info}>{card.info}</div>
|
||||
<Icon className={iconStyle(theme, card.done)} name={card.icon} size="xxl" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const handleTutorialClick = (event: MouseEvent<HTMLAnchorElement>, card: Card) => {
|
||||
event.preventDefault();
|
||||
const isSet = store.get(card.key);
|
||||
if (!isSet) {
|
||||
store.set(card.key, true);
|
||||
}
|
||||
window.open(`${card.href}?utm_source=grafana_gettingstarted`, '_blank');
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, complete: boolean) => {
|
||||
const textColor = `${complete ? theme.palette.blue95 : '#FFB357'}`;
|
||||
return {
|
||||
card: css`
|
||||
${cardStyle(theme, complete)}
|
||||
width: 460px;
|
||||
min-width: 460px;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.xl}) {
|
||||
min-width: 368px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.lg}) {
|
||||
min-width: 272px;
|
||||
}
|
||||
`,
|
||||
type: css`
|
||||
color: ${textColor};
|
||||
text-transform: uppercase;
|
||||
`,
|
||||
heading: css`
|
||||
text-transform: uppercase;
|
||||
color: ${textColor};
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
`,
|
||||
info: css`
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
`,
|
||||
status: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
};
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css } from 'emotion';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
|
||||
export const cardStyle = stylesFactory((theme: GrafanaTheme, complete: boolean) => {
|
||||
const completeGradient = 'linear-gradient(to right, #5182CC 0%, #245BAF 100%)';
|
||||
const darkThemeGradients = complete ? completeGradient : 'linear-gradient(to right, #f05a28 0%, #fbca0a 100%)';
|
||||
const lightThemeGradients = complete ? completeGradient : 'linear-gradient(to right, #FBCA0A 0%, #F05A28 100%)';
|
||||
|
||||
const borderGradient = theme.isDark ? darkThemeGradients : lightThemeGradients;
|
||||
|
||||
return `
|
||||
background-color: ${theme.colors.bg1};
|
||||
margin-right: ${theme.spacing.xl};
|
||||
border: 1px solid ${theme.colors.border1};
|
||||
border-bottom-left-radius: ${theme.border.radius.md};
|
||||
border-bottom-right-radius: ${theme.border.radius.md};
|
||||
position: relative;
|
||||
max-height: 230px;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.xxl}) {
|
||||
margin-right: ${theme.spacing.md};
|
||||
}
|
||||
&::before {
|
||||
display: block;
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
top: 0;
|
||||
background-image: ${borderGradient};
|
||||
}
|
||||
`;
|
||||
});
|
||||
|
||||
export const iconStyle = stylesFactory(
|
||||
(theme: GrafanaTheme, complete: boolean) => css`
|
||||
color: ${complete ? theme.palette.blue95 : theme.colors.textWeak};
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.sm}) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export const cardContent = css`
|
||||
padding: 24px 16px;
|
||||
`;
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 15 KiB |
@ -2,4 +2,4 @@ import { PanelPlugin } from '@grafana/data';
|
||||
import { GettingStarted } from './GettingStarted';
|
||||
|
||||
// Simplest possible panel plugin
|
||||
export const plugin = new PanelPlugin(GettingStarted);
|
||||
export const plugin = new PanelPlugin(GettingStarted).setNoPadding();
|
||||
|
107
public/app/plugins/panel/gettingstarted/steps.ts
Normal file
107
public/app/plugins/panel/gettingstarted/steps.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import store from 'app/core/store';
|
||||
import { SetupStep } from './types';
|
||||
|
||||
const step1TutorialTitle = 'Grafana fundamentals';
|
||||
const step2TutorialTitle = 'Create users and teams';
|
||||
const keyPrefix = 'getting.started.';
|
||||
const step1Key = `${keyPrefix}${step1TutorialTitle
|
||||
.replace(' ', '-')
|
||||
.trim()
|
||||
.toLowerCase()}`;
|
||||
const step2Key = `${keyPrefix}${step2TutorialTitle
|
||||
.replace(' ', '-')
|
||||
.trim()
|
||||
.toLowerCase()}`;
|
||||
|
||||
export const getSteps = (): SetupStep[] => [
|
||||
{
|
||||
heading: 'Welcome to Grafana',
|
||||
subheading: 'The steps below will guide you to quickly finish setting up your Grafana installation.',
|
||||
title: 'Basic',
|
||||
info: 'The steps below will guide you to quickly finish setting up your Grafana installation.',
|
||||
done: false,
|
||||
cards: [
|
||||
{
|
||||
type: 'tutorial',
|
||||
heading: 'Data source and dashboards',
|
||||
title: step1TutorialTitle,
|
||||
info:
|
||||
'Set up and understand Grafana if you have no prior experience. This tutorial guides you through the entire process and covers the “Data source” and “Dashboards” steps to the right.',
|
||||
href: 'https://grafana.com/tutorials/grafana-fundamentals',
|
||||
icon: 'grafana',
|
||||
check: () => Promise.resolve(store.get(step1Key)),
|
||||
key: step1Key,
|
||||
done: false,
|
||||
},
|
||||
{
|
||||
type: 'docs',
|
||||
title: 'Add your first data source',
|
||||
heading: 'data sources',
|
||||
icon: 'database',
|
||||
learnHref: 'https://grafana.com/docs/grafana/latest/features/datasources/add-a-data-source',
|
||||
href: 'datasources/new',
|
||||
check: () => {
|
||||
return new Promise(resolve => {
|
||||
resolve(
|
||||
getDatasourceSrv()
|
||||
.getMetricSources()
|
||||
.filter(item => {
|
||||
return item.meta.builtIn !== true;
|
||||
}).length > 0
|
||||
);
|
||||
});
|
||||
},
|
||||
done: false,
|
||||
},
|
||||
{
|
||||
type: 'docs',
|
||||
heading: 'dashboards',
|
||||
title: 'Create your first dashboard',
|
||||
icon: 'apps',
|
||||
href: 'dashboard/new',
|
||||
learnHref: 'https://grafana.com/docs/grafana/latest/guides/getting_started/#create-a-dashboard',
|
||||
check: async () => {
|
||||
const result = await getBackendSrv().search({ limit: 1 });
|
||||
return result.length > 0;
|
||||
},
|
||||
done: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Setup complete!',
|
||||
subheading:
|
||||
'All necessary steps to use Grafana are done. Now tackle advanced steps or make the best use of this home dashboard – it is, after all, a fully customizable dashboard – and remove this panel.',
|
||||
title: 'Advanced',
|
||||
info: ' Manage your users and teams and add plugins. These steps are optional',
|
||||
done: false,
|
||||
cards: [
|
||||
{
|
||||
type: 'tutorial',
|
||||
heading: 'Users',
|
||||
title: 'Create users and teams',
|
||||
info: 'Learn to organize your users in teams and manage resource access and roles.',
|
||||
href: 'https://grafana.com/tutorials/create-users-and-teams',
|
||||
icon: 'users-alt',
|
||||
key: step2Key,
|
||||
check: () => Promise.resolve(store.get(step2Key)),
|
||||
done: false,
|
||||
},
|
||||
{
|
||||
type: 'docs',
|
||||
heading: 'plugins',
|
||||
title: 'Find and install plugins',
|
||||
learnHref: 'https://grafana.com/docs/grafana/latest/plugins/installation',
|
||||
href: 'plugins',
|
||||
icon: 'plug',
|
||||
check: async () => {
|
||||
const plugins = await getBackendSrv().get('/api/plugins', { embedded: 0, core: 0 });
|
||||
return Promise.resolve(plugins.length > 0);
|
||||
},
|
||||
done: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
26
public/app/plugins/panel/gettingstarted/types.ts
Normal file
26
public/app/plugins/panel/gettingstarted/types.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { IconName } from '@grafana/ui';
|
||||
|
||||
export type CardType = 'tutorial' | 'docs' | 'other';
|
||||
|
||||
export interface Card {
|
||||
title: string;
|
||||
type: CardType;
|
||||
icon: IconName;
|
||||
href: string;
|
||||
check: () => Promise<boolean>;
|
||||
done: boolean;
|
||||
heading: string;
|
||||
info?: string;
|
||||
// For local storage
|
||||
key?: string;
|
||||
learnHref?: string;
|
||||
}
|
||||
|
||||
export interface SetupStep {
|
||||
heading: string;
|
||||
subheading: string;
|
||||
title: string;
|
||||
info: string;
|
||||
cards: Card[];
|
||||
done: boolean;
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { ButtonSelect, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { stylesFactory, useTheme } from '@grafana/ui';
|
||||
import lightBackground from './img/background_light.svg';
|
||||
|
||||
const helpOptions = [
|
||||
{ value: 0, label: 'Documentation', href: 'https://grafana.com/docs/grafana/latest/' },
|
||||
{ value: 1, label: 'Tutorials', href: 'https://grafana.com/tutorials/' },
|
||||
{ value: 2, label: 'Community', href: 'https://community.grafana.com/' },
|
||||
{ value: 3, label: 'Public Slack', href: '' },
|
||||
{ value: 0, label: 'Documentation', href: 'https://grafana.com/docs/grafana/latest' },
|
||||
{ value: 1, label: 'Tutorials', href: 'https://grafana.com/tutorials' },
|
||||
{ value: 2, label: 'Community', href: 'https://community.grafana.com' },
|
||||
{ value: 3, label: 'Public Slack', href: 'http://slack.grafana.com' },
|
||||
];
|
||||
|
||||
export const WelcomeBanner: FC = () => {
|
||||
@ -18,19 +19,14 @@ export const WelcomeBanner: FC = () => {
|
||||
<h1 className={styles.title}>Welcome to Grafana</h1>
|
||||
<div className={styles.help}>
|
||||
<h3 className={styles.helpText}>Need help?</h3>
|
||||
<div className={styles.smallScreenHelp}>
|
||||
<ButtonSelect
|
||||
defaultValue={helpOptions[0]}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onChange={onHelpLinkClick}
|
||||
options={helpOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.helpLinks}>
|
||||
{helpOptions.map((option, index) => {
|
||||
return (
|
||||
<a key={`${option.label}-${index}`} className={styles.helpLink} href={option.href}>
|
||||
<a
|
||||
key={`${option.label}-${index}`}
|
||||
className={styles.helpLink}
|
||||
href={`${option.href}?utm_source=grafana_gettingstarted`}
|
||||
>
|
||||
{option.label}
|
||||
</a>
|
||||
);
|
||||
@ -41,14 +37,8 @@ export const WelcomeBanner: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const onHelpLinkClick = (option: { label: string; href: string }) => {
|
||||
window.open(option.href, '_blank');
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const backgroundImage = theme.isDark
|
||||
? 'public/img/login_background_dark.svg'
|
||||
: 'public/img/login_background_light.svg';
|
||||
const backgroundImage = theme.isDark ? 'public/img/login_background_dark.svg' : lightBackground;
|
||||
|
||||
return {
|
||||
container: css`
|
||||
@ -59,31 +49,28 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
justify-content: space-between;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.xl}) {
|
||||
padding: 0 30px 0 100px;
|
||||
}
|
||||
padding: 0 ${theme.spacing.lg};
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.lg}) {
|
||||
padding: 0 24px 0 44px;
|
||||
background-position: 0px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.sm}) {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding: ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.md} 48px;
|
||||
padding: 0 ${theme.spacing.sm};
|
||||
}
|
||||
`,
|
||||
title: css`
|
||||
margin-bottom: 0;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.lg}) {
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.md}) {
|
||||
font-size: ${theme.typography.heading.h2};
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
}
|
||||
@media only screen and (max-width: ${theme.breakpoints.sm}) {
|
||||
font-size: ${theme.typography.heading.h3};
|
||||
@ -105,14 +92,18 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
helpLinks: css``,
|
||||
helpLinks: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`,
|
||||
helpLink: css`
|
||||
margin-right: 8px;
|
||||
margin-right: ${theme.spacing.md};
|
||||
text-decoration: underline;
|
||||
text-wrap: no-wrap;
|
||||
`,
|
||||
smallScreenHelp: css`
|
||||
display: none;
|
||||
|
||||
@media only screen and (max-width: ${theme.breakpoints.sm}) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
78
public/app/plugins/panel/welcome/img/background_light.svg
Normal file
78
public/app/plugins/panel/welcome/img/background_light.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 26 KiB |
@ -11,7 +11,7 @@
|
||||
},
|
||||
"id": 1,
|
||||
"title": "",
|
||||
"transparent": true,
|
||||
"transparent": false,
|
||||
"type": "welcome"
|
||||
},
|
||||
{
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 85 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 44 KiB |
Loading…
Reference in New Issue
Block a user