Unverified Commit 75b21c76 authored by Dominik Prokop's avatar Dominik Prokop Committed by GitHub
Browse files

Docs: Add styling.md with guide to Emotion at Grafana (#19411)

* Add styling.md with guide to emotion

* Update style_guides/styling.md

* Update style_guides/styling.md

* Update style_guides/styling.md

* Update PR guide

* Add stylesFactory helper function

* Simplify styles creator signature

* Make styles factory deps optional

* Update typing

* First batch of updates

* Remove unused import

* Update tests
parent a3008ffb
......@@ -42,7 +42,7 @@ Whether you are contributing or doing code review, first read and understand htt
### Low-level checks
- [ ] The pull request contains a title that explains it. It follows [PR and commit messages guidelines](#Pull-Requests-titles-and-message).
- [ ] The pull request contains necessary links to issues.
- [ ] The pull request contains necessary links to issues.
- [ ] The pull request contains commits with messages that are small and understandable. It follows [PR and commit messages guidelines](#Pull-Requests-titles-and-message).
- [ ] The pull request does not contain magic strings or numbers that could be replaced with an `Enum` or `const` instead.
......@@ -58,6 +58,8 @@ Whether you are contributing or doing code review, first read and understand htt
- [ ] The pull request does not contain uses of `any` or `{}` without comments describing why.
- [ ] The pull request does not contain large React components that could easily be split into several smaller components.
- [ ] The pull request does not contain back end calls directly from components, use actions and Redux instead.
- [ ] The pull request follows our [styling with Emotion convention](./style_guides/styling.md)
> We still use a lot of SASS, but any new CSS work should be using or migrating existing code to Emotion
#### Redux specific checks (skip if your pull request does not contain Redux changes)
......
......@@ -9,6 +9,7 @@ import { getColorFromHexRgbOrName } from '../../utils';
// Types
import { Themeable } from '../../types';
import { stylesFactory } from '../../themes/stylesFactory';
export interface BigValueSparkline {
data: any[][]; // [[number,number]]
......@@ -31,6 +32,33 @@ export interface Props extends Themeable {
className?: string;
}
const getStyles = stylesFactory(() => {
return {
wrapper: css`
position: 'relative';
display: 'table';
`,
title: css`
line-height: 1;
text-align: 'center';
z-index: 1;
display: 'block';
width: '100%';
position: 'absolute';
`,
value: css`
line-height: 1;
text-align: 'center';
z-index: 1;
display: 'table-cell';
vertical-align: 'middle';
position: 'relative';
font-size: '3em';
font-weight: 500;
`,
};
});
/*
* This visualization is still in POC state, needed more tests & better structure
*/
......@@ -122,46 +150,12 @@ export class BigValue extends PureComponent<Props> {
render() {
const { height, width, value, prefix, suffix, sparkline, backgroundColor, onClick, className } = this.props;
const styles = getStyles();
return (
<div
className={cx(
css({
position: 'relative',
display: 'table',
}),
className
)}
style={{ width, height, backgroundColor }}
onClick={onClick}
>
{value.title && (
<div
className={css({
lineHeight: 1,
textAlign: 'center',
zIndex: 1,
display: 'block',
width: '100%',
position: 'absolute',
})}
>
{value.title}
</div>
)}
<span
className={css({
lineHeight: 1,
textAlign: 'center',
zIndex: 1,
display: 'table-cell',
verticalAlign: 'middle',
position: 'relative',
fontSize: '3em',
fontWeight: 500, // TODO: $font-weight-semi-bold
})}
>
<div className={cx(styles.wrapper, className)} style={{ width, height, backgroundColor }} onClick={onClick}>
{value.title && <div className={styles.title}>{value.title}</div>}
<span className={styles.value}>
{this.renderText(prefix, '0px 2px 0px 0px')}
{this.renderText(value)}
{this.renderText(suffix)}
......
......@@ -3,6 +3,7 @@ import tinycolor from 'tinycolor2';
import { css, cx } from 'emotion';
import { Themeable, GrafanaTheme } from '../../types';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
import { stylesFactory } from '../../themes/stylesFactory';
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent';
......@@ -49,7 +50,13 @@ const buttonVariantStyles = (
}
`;
const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonVariant, withIcon: boolean) => {
interface StyleDeps {
theme: GrafanaTheme;
size: ButtonSize;
variant: ButtonVariant;
withIcon: boolean;
}
const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: StyleDeps) => {
const borderRadius = theme.border.radius.sm;
let padding,
background,
......@@ -155,7 +162,7 @@ const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonV
filter: brightness(100);
`,
};
};
});
export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
renderAs,
......@@ -167,7 +174,7 @@ export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
children,
...otherProps
}) => {
const buttonStyles = getButtonStyles(theme, size, variant, !!icon);
const buttonStyles = getButtonStyles({ theme, size, variant, withIcon: !!icon });
const nonHtmlProps = {
theme,
size,
......
......@@ -2,6 +2,7 @@ import React from 'react';
import { Themeable, GrafanaTheme } from '../../types/theme';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
import { css, cx } from 'emotion';
import { stylesFactory } from '../../themes';
export interface CallToActionCardProps extends Themeable {
message?: string | JSX.Element;
......@@ -10,7 +11,7 @@ export interface CallToActionCardProps extends Themeable {
className?: string;
}
const getCallToActionCardStyles = (theme: GrafanaTheme) => ({
const getCallToActionCardStyles = stylesFactory((theme: GrafanaTheme) => ({
wrapper: css`
label: call-to-action-card;
padding: ${theme.spacing.lg};
......@@ -28,7 +29,7 @@ const getCallToActionCardStyles = (theme: GrafanaTheme) => ({
footer: css`
margin-top: ${theme.spacing.lg};
`,
});
}));
export const CallToActionCard: React.FunctionComponent<CallToActionCardProps> = ({
message,
......
......@@ -3,9 +3,10 @@ import { css, cx } from 'emotion';
import { GrafanaTheme } from '../../types/theme';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
import { ThemeContext } from '../../themes/index';
import { ThemeContext } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes/stylesFactory';
const getStyles = (theme: GrafanaTheme) => ({
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
collapse: css`
label: collapse;
margin-top: ${theme.spacing.sm};
......@@ -79,7 +80,7 @@ const getStyles = (theme: GrafanaTheme) => ({
font-size: ${theme.typography.heading.h6};
box-shadow: ${selectThemeVariant({ light: 'none', dark: '1px 1px 4px rgb(45, 45, 45)' }, theme.type)};
`,
});
}));
interface Props {
isOpen: boolean;
......
......@@ -2,6 +2,7 @@ import React, { useContext, useRef } from 'react';
import { css, cx } from 'emotion';
import useClickAway from 'react-use/lib/useClickAway';
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
import { stylesFactory } from '../../themes/stylesFactory';
import { Portal, List } from '../index';
import { LinkTarget } from '@grafana/data';
......@@ -26,7 +27,7 @@ export interface ContextMenuProps {
renderHeader?: () => JSX.Element;
}
const getContextMenuStyles = (theme: GrafanaTheme) => {
const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => {
const linkColor = selectThemeVariant(
{
light: theme.colors.dark2,
......@@ -146,7 +147,7 @@ const getContextMenuStyles = (theme: GrafanaTheme) => {
top: 4px;
`,
};
};
});
export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClose, items, renderHeader }) => {
const theme = useContext(ThemeContext);
......
......@@ -3,8 +3,9 @@ import { DataLink } from '@grafana/data';
import { FormField, Switch } from '../index';
import { VariableSuggestion } from './DataLinkSuggestions';
import { css } from 'emotion';
import { ThemeContext } from '../../themes/index';
import { ThemeContext, stylesFactory } from '../../themes/index';
import { DataLinkInput } from './DataLinkInput';
import { GrafanaTheme } from '../../types';
interface DataLinkEditorProps {
index: number;
......@@ -15,9 +16,21 @@ interface DataLinkEditorProps {
onRemove: (link: DataLink) => void;
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
listItem: css`
margin-bottom: ${theme.spacing.sm};
`,
infoText: css`
padding-bottom: ${theme.spacing.md};
margin-left: 66px;
color: ${theme.colors.textWeak};
`,
}));
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
({ index, value, onChange, onRemove, suggestions, isLast }) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
const [title, setTitle] = useState(value.title);
const onUrlChange = (url: string, callback?: () => void) => {
......@@ -39,18 +52,8 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
onChange(index, { ...value, targetBlank: !value.targetBlank });
};
const listItemStyle = css`
margin-bottom: ${theme.spacing.sm};
`;
const infoTextStyle = css`
padding-bottom: ${theme.spacing.md};
margin-left: 66px;
color: ${theme.colors.textWeak};
`;
return (
<div className={listItemStyle}>
<div className={styles.listItem}>
<div className="gf-form gf-form--inline">
<FormField
className="gf-form--grow"
......@@ -76,7 +79,7 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
`}
/>
{isLast && (
<div className={infoTextStyle}>
<div className={styles.infoText}>
With data links you can reference data variables like series name, labels and values. Type CMD+Space,
CTRL+Space, or $ to open variable suggestions.
</div>
......
import React, { useState, useMemo, useCallback, useContext, useRef, RefObject } from 'react';
import React, { useState, useMemo, useContext, useRef, RefObject } from 'react';
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
import { ThemeContext, DataLinkBuiltInVars, makeValue } from '../../index';
import { SelectionReference } from './SelectionReference';
......@@ -12,6 +12,8 @@ import { css, cx } from 'emotion';
import { SlatePrism } from '../../slate-plugins';
import { SCHEMA } from '../../utils/slate';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '../../types';
const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
......@@ -28,26 +30,25 @@ const plugins = [
}),
];
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
editor: css`
.token.builtInVariable {
color: ${theme.colors.queryGreen};
}
.token.variable {
color: ${theme.colors.queryKeyword};
}
`,
}));
export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, suggestions }) => {
const editorRef = useRef<Editor>() as RefObject<Editor>;
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
const [showingSuggestions, setShowingSuggestions] = useState(false);
const [suggestionsIndex, setSuggestionsIndex] = useState(0);
const [linkUrl, setLinkUrl] = useState<Value>(makeValue(value));
const getStyles = useCallback(() => {
return {
editor: css`
.token.builtInVariable {
color: ${theme.colors.queryGreen};
}
.token.variable {
color: ${theme.colors.queryKeyword};
}
`,
};
}, [theme]);
// Workaround for https://github.com/ianstormtaylor/slate/issues/2927
const stateRef = useRef({ showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange });
stateRef.current = { showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange };
......@@ -155,7 +156,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
onBlur={onUrlBlur}
onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)}
plugins={plugins}
className={getStyles().editor}
className={styles.editor}
/>
</div>
</div>
......
......@@ -5,6 +5,7 @@ import React, { useRef, useContext, useMemo } from 'react';
import useClickAway from 'react-use/lib/useClickAway';
import { List } from '../index';
import tinycolor from 'tinycolor2';
import { stylesFactory } from '../../themes';
export enum VariableOrigin {
Series = 'series',
......@@ -28,7 +29,7 @@ interface DataLinkSuggestionsProps {
onClose?: () => void;
}
const getStyles = (theme: GrafanaTheme) => {
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const wrapperBg = selectThemeVariant(
{
light: theme.colors.white,
......@@ -129,7 +130,7 @@ const getStyles = (theme: GrafanaTheme) => {
color: ${itemDocsColor};
`,
};
};
});
export const DataLinkSuggestions: React.FC<DataLinkSuggestionsProps> = ({ suggestions, ...otherProps }) => {
const ref = useRef(null);
......
import React, { PureComponent, ReactNode } from 'react';
import { Alert } from '../Alert/Alert';
import { css } from 'emotion';
import { stylesFactory } from '../../themes';
interface ErrorInfo {
componentStack: string;
......@@ -44,12 +45,12 @@ export class ErrorBoundary extends PureComponent<Props, State> {
}
}
function getAlertPageStyle() {
const getStyles = stylesFactory(() => {
return css`
width: 500px;
margin: 64px auto;
`;
}
});
interface WithAlertBoxProps {
title?: string;
......@@ -85,7 +86,7 @@ export class ErrorBoundaryAlert extends PureComponent<WithAlertBoxProps> {
);
} else {
return (
<div className={getAlertPageStyle()}>
<div className={getStyles()}>
<h2>{title}</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{error && error.toString()}
......
......@@ -5,6 +5,8 @@ import { LegendItem } from '../Legend/Legend';
import { SeriesColorChangeHandler } from './GraphWithLegend';
import { LegendStatsList } from '../Legend/LegendStatsList';
import { ThemeContext } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '../../types';
export interface GraphLegendItemProps {
key?: React.Key;
......@@ -56,6 +58,32 @@ export const GraphLegendListItem: React.FunctionComponent<GraphLegendItemProps>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
row: css`
font-size: ${theme.typography.size.sm};
td {
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
white-space: nowrap;
}
`,
label: css`
cursor: pointer;
white-space: nowrap;
`,
itemWrapper: css`
display: flex;
white-space: nowrap;
`,
value: css`
text-align: right;
`,
yAxisLabel: css`
color: ${theme.colors.gray2};
`,
};
});
export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps> = ({
item,
onSeriesColorChange,
......@@ -64,27 +92,11 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
className,
}) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
return (
<tr
className={cx(
css`
font-size: ${theme.typography.size.sm};
td {
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
white-space: nowrap;
}
`,
className
)}
>
<tr className={cx(styles.row, className)}>
<td>
<span
className={css`
display: flex;
white-space: nowrap;
`}
>
<span className={styles.itemWrapper}>
<LegendSeriesIcon
disabled={!!onSeriesColorChange}
color={item.color}
......@@ -102,33 +114,16 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
onLabelClick(item, event);
}
}}
className={css`
cursor: pointer;
white-space: nowrap;
`}
className={styles.label}
>
{item.label}{' '}
{item.yAxis === 2 && (
<span
className={css`
color: ${theme.colors.gray2};
`}
>
(right y-axis)
</span>
)}
{item.label} {item.yAxis === 2 && <span className={styles.yAxisLabel}>(right y-axis)</span>}
</div>
</span>
</td>
{item.displayValues &&
item.displayValues.map((stat, index) => {
return (
<td
className={css`
text-align: right;
`}
key={`${stat.title}-${index}`}
>
<td className={styles.value} key={`${stat.title}-${index}`}>
{stat.text}
</td>
);
......
......@@ -8,6 +8,7 @@ import { Graph, GraphProps } from './Graph';
import { LegendRenderOptions, LegendItem, LegendDisplayMode } from '../Legend/Legend';
import { GraphLegend } from './GraphLegend';
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
import { stylesFactory } from '../../themes';
export type SeriesOptionChangeHandler<TOption> = (label: string, option: TOption) => void;
export type SeriesColorChangeHandler = SeriesOptionChangeHandler<string>;
......@@ -24,7 +25,7 @@ export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions {
onToggleSort: (sortBy: string) => void;
}
const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({
const getGraphWithLegendStyles = stylesFactory(({ placement }: GraphWithLegendProps) => ({
wrapper: css`
display: flex;
flex-direction: ${placement === 'under' ? 'column' : 'row'};
......@@ -38,7 +39,7 @@ const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({
padding: 10px 0;
max-height: ${placement === 'under' ? '35%' : 'none'};
`,
});
}));
const shouldHideLegendItem = (data: GraphSeriesValue[][], hideEmpty = false, hideZero = false) => {
const isZeroOnlySeries = data.reduce((acc, current) => acc + (current[1] || 0), 0) === 0;
......
......@@ -4,6 +4,30 @@ import { InlineList } from '../List/InlineList';
import { List } from '../List/List';
import { css, cx } from 'emotion';
import { ThemeContext } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '../../types';
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
item: css`
padding-left: 10px;
display: flex;
font-size: ${theme.typography.size.sm};
white-space: nowrap;
`,
wrapper: css`
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
`,
section: css`
display: flex;
`,
sectionRight: css`
justify-content: flex-end;
flex-grow: 1;
`,
}));
export const LegendList: React.FunctionComponent<LegendComponentProps> = ({
items,
......@@ -12,45 +36,16 @@ export const LegendList: React.FunctionComponent<LegendComponentProps> = ({
className,
}) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
const renderItem = (item: LegendItem, index: number) => {
return (
<span
className={css`
padding-left: 10px;
display: flex;
font-size: ${theme.typography.size.sm};
white-space: nowrap;
`}
>
{itemRenderer ? itemRenderer(item, index) : item.label}