mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
a3008ffb05
commit
75b21c7603
@ -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>
|
||||
)}
|
||||
<div className={cx(styles.wrapper, className)} style={{ width, height, backgroundColor }} onClick={onClick}>
|
||||
{value.title && <div className={styles.title}>{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
|
||||
})}
|
||||
>
|
||||
<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>
|
||||
|
@ -1,4 +1,4 @@
|
||||
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);
|
||||
|
@ -1,6 +1,7 @@
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
return <span className={styles.item}>{itemRenderer ? itemRenderer(item, index) : item.label}</span>;
|
||||
};
|
||||
|
||||
const getItemKey = (item: LegendItem) => `${item.label}`;
|
||||
|
||||
const styles = {
|
||||
wrapper: cx(
|
||||
css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`,
|
||||
className
|
||||
),
|
||||
section: css`
|
||||
display: flex;
|
||||
`,
|
||||
sectionRight: css`
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
`,
|
||||
};
|
||||
|
||||
return placement === 'under' ? (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={cx(styles.wrapper, className)}>
|
||||
<div className={styles.section}>
|
||||
<InlineList items={items.filter(item => item.yAxis === 1)} renderItem={renderItem} getItemKey={getItemKey} />
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { cx, css } from 'emotion';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
export interface ListProps<T> {
|
||||
items: T[];
|
||||
@ -12,32 +13,27 @@ interface AbstractListProps<T> extends ListProps<T> {
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((inlineList = false) => ({
|
||||
list: css`
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`,
|
||||
|
||||
item: css`
|
||||
display: ${(inlineList && 'inline-block') || 'block'};
|
||||
`,
|
||||
}));
|
||||
|
||||
export class AbstractList<T> extends React.PureComponent<AbstractListProps<T>> {
|
||||
constructor(props: AbstractListProps<T>) {
|
||||
super(props);
|
||||
this.getListStyles = this.getListStyles.bind(this);
|
||||
}
|
||||
|
||||
getListStyles() {
|
||||
const { inline, className } = this.props;
|
||||
return {
|
||||
list: cx([
|
||||
css`
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`,
|
||||
className,
|
||||
]),
|
||||
item: css`
|
||||
display: ${(inline && 'inline-block') || 'block'};
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, renderItem, getItemKey, className } = this.props;
|
||||
const styles = this.getListStyles();
|
||||
const { items, renderItem, getItemKey, className, inline } = this.props;
|
||||
const styles = getStyles(inline);
|
||||
|
||||
return (
|
||||
<ul className={cx(styles.list, className)}>
|
||||
{items.map((item, i) => {
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
exports[`AbstractList allows custom item key 1`] = `
|
||||
<ul
|
||||
className="css-9xf0yn"
|
||||
className="css-1ld8h5b"
|
||||
>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="item1"
|
||||
>
|
||||
<div>
|
||||
@ -18,7 +18,7 @@ exports[`AbstractList allows custom item key 1`] = `
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="item2"
|
||||
>
|
||||
<div>
|
||||
@ -31,7 +31,7 @@ exports[`AbstractList allows custom item key 1`] = `
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="item3"
|
||||
>
|
||||
<div>
|
||||
@ -48,10 +48,10 @@ exports[`AbstractList allows custom item key 1`] = `
|
||||
|
||||
exports[`AbstractList renders items using renderItem prop function 1`] = `
|
||||
<ul
|
||||
className="css-9xf0yn"
|
||||
className="css-1ld8h5b"
|
||||
>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="0"
|
||||
>
|
||||
<div>
|
||||
@ -64,7 +64,7 @@ exports[`AbstractList renders items using renderItem prop function 1`] = `
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="1"
|
||||
>
|
||||
<div>
|
||||
@ -77,7 +77,7 @@ exports[`AbstractList renders items using renderItem prop function 1`] = `
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="css-rwbibe"
|
||||
className="css-8qpjjf"
|
||||
key="2"
|
||||
>
|
||||
<div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ThemeContext, withTheme, useTheme } from './ThemeContext';
|
||||
import { getTheme, mockTheme } from './getTheme';
|
||||
import { selectThemeVariant } from './selectThemeVariant';
|
||||
|
||||
export { stylesFactory } from './stylesFactory';
|
||||
export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme };
|
||||
|
31
packages/grafana-ui/src/themes/stylesFactory.test.ts
Normal file
31
packages/grafana-ui/src/themes/stylesFactory.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { stylesFactory } from './stylesFactory';
|
||||
|
||||
interface FakeProps {
|
||||
theme: {
|
||||
a: string;
|
||||
};
|
||||
}
|
||||
describe('Stylesheet creation', () => {
|
||||
it('memoizes results', () => {
|
||||
const spy = jest.fn();
|
||||
|
||||
const getStyles = stylesFactory(({ theme }: FakeProps) => {
|
||||
spy();
|
||||
return {
|
||||
className: `someClass${theme.a}`,
|
||||
};
|
||||
});
|
||||
|
||||
const props: FakeProps = { theme: { a: '-interpolated' } };
|
||||
const changedProps: FakeProps = { theme: { a: '-interpolatedChanged' } };
|
||||
const styles = getStyles(props);
|
||||
getStyles(props);
|
||||
|
||||
expect(spy).toBeCalledTimes(1);
|
||||
expect(styles.className).toBe('someClass-interpolated');
|
||||
|
||||
const styles2 = getStyles(changedProps);
|
||||
expect(spy).toBeCalledTimes(2);
|
||||
expect(styles2.className).toBe('someClass-interpolatedChanged');
|
||||
});
|
||||
});
|
12
packages/grafana-ui/src/themes/stylesFactory.ts
Normal file
12
packages/grafana-ui/src/themes/stylesFactory.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import memoizeOne from 'memoize-one';
|
||||
// import { KeyValue } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* Creates memoized version of styles creator
|
||||
* @param stylesCreator function accepting dependencies based on which styles are created
|
||||
*/
|
||||
export function stylesFactory<ResultFn extends (this: any, ...newArgs: any[]) => ReturnType<ResultFn>>(
|
||||
stylesCreator: ResultFn
|
||||
) {
|
||||
return memoizeOne(stylesCreator);
|
||||
}
|
84
style_guides/styling.md
Normal file
84
style_guides/styling.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Styling Grafana
|
||||
|
||||
## Emotion
|
||||
|
||||
[Emotion](https://emotion.sh/docs/introduction) is our default-to-be approach to styling React components. It provides a way for styles to be a consequence of properties and state of a component.
|
||||
|
||||
### Usage
|
||||
|
||||
#### Basic styling
|
||||
|
||||
For styling components use Emotion's `css` function
|
||||
|
||||
```tsx
|
||||
import { css } from 'emotion';
|
||||
|
||||
|
||||
const ComponentA = () => {
|
||||
return (
|
||||
<div className={css`background: red;`}>
|
||||
As red as you can ge
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Styling complex components
|
||||
|
||||
In more complex cases, especially when you need to style multiple DOM elements in one component or when your styles that depend on properties and/or state, you should create a helper function that returns an object with desired stylesheet. Let's say you need to style a component that has different background depending on the theme:
|
||||
|
||||
```tsx
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme, useTheme, selectThemeVariant } from '@grafana/ui';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => {
|
||||
const backgroundColor = selectThemeVariant({ light: theme.colors.red, dark: theme.colors.blue }, theme.type);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
background: ${backgroundColor};
|
||||
`,
|
||||
icon: css`font-size:${theme.typography.size.sm}`;
|
||||
};
|
||||
}
|
||||
|
||||
const ComponentA = () => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
As red as you can ge
|
||||
<i className={styles.icon} /\>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For more information about themes at Grafana please see [themes guide](./themes.md)
|
||||
|
||||
#### Composing class names
|
||||
|
||||
For class composition use Emotion's `cx` function
|
||||
|
||||
```tsx
|
||||
import { css, cx } from 'emotion';
|
||||
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ComponentA: React.FC<Props> = ({ className }) => {
|
||||
const finalClassName = cx(
|
||||
className,
|
||||
css`background: red`,
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={finalClassName}>
|
||||
As red as you can ge
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
Loading…
Reference in New Issue
Block a user