Core code editor/builder components (#52421)

* migrate experimental to core grafana - update refs

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Scott Lepper
2022-07-20 12:50:08 -04:00
committed by GitHub
parent feb9960f96
commit de956fc3d8
157 changed files with 6644 additions and 210 deletions

View File

@@ -0,0 +1,23 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2, stylesFactory } from '../../themes';
import { Button, ButtonProps } from '../Button';
interface AccessoryButtonProps extends ButtonProps {}
export const AccessoryButton: React.FC<AccessoryButtonProps> = ({ className, ...props }) => {
const theme = useTheme2();
const styles = getButtonStyles(theme);
return <Button {...props} className={cx(className, styles.button)} />;
};
const getButtonStyles = stylesFactory((theme: GrafanaTheme2) => ({
button: css({
paddingLeft: theme.spacing(3 / 2),
paddingRight: theme.spacing(3 / 2),
}),
}));

View File

@@ -0,0 +1,79 @@
import { css } from '@emotion/css';
import React, { ComponentProps } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
import { ReactUtils } from '../../utils';
import { Field } from '../Forms/Field';
import { Icon } from '../Icon/Icon';
import { PopoverContent, Tooltip } from '../Tooltip';
import { Space } from './Space';
interface EditorFieldProps extends ComponentProps<typeof Field> {
label: string;
children: React.ReactElement;
width?: number | string;
optional?: boolean;
tooltip?: PopoverContent;
}
export const EditorField: React.FC<EditorFieldProps> = (props) => {
const { label, optional, tooltip, children, width, ...fieldProps } = props;
const theme = useTheme2();
const styles = getStyles(theme, width);
// Null check for backward compatibility
const childInputId = fieldProps?.htmlFor || ReactUtils?.getChildId(children);
const labelEl = (
<>
<label className={styles.label} htmlFor={childInputId}>
{label}
{optional && <span className={styles.optional}> - optional</span>}
{tooltip && (
<Tooltip placement="top" content={tooltip} theme="info">
<Icon name="info-circle" size="sm" className={styles.icon} />
</Tooltip>
)}
</label>
<Space v={0.5} />
</>
);
return (
<div className={styles.root}>
<Field className={styles.field} label={labelEl} {...fieldProps}>
{children}
</Field>
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme2, width?: number | string) => {
return {
root: css({
minWidth: theme.spacing(width ?? 0),
}),
label: css({
fontSize: 12,
fontWeight: theme.typography.fontWeightMedium,
}),
optional: css({
fontStyle: 'italic',
color: theme.colors.text.secondary,
}),
field: css({
marginBottom: 0, // GrafanaUI/Field has a bottom margin which we must remove
}),
icon: css({
color: theme.colors.text.secondary,
marginLeft: theme.spacing(1),
':hover': {
color: theme.colors.text.primary,
},
}),
};
});

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { Stack } from './Stack';
interface EditorFieldGroupProps {}
export const EditorFieldGroup: React.FC<EditorFieldGroupProps> = ({ children }) => {
return <Stack gap={1}>{children}</Stack>;
};

View File

@@ -0,0 +1,25 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
interface EditorHeaderProps {}
export const EditorHeader: React.FC<EditorHeaderProps> = ({ children }) => {
const theme = useTheme2();
const styles = getStyles(theme);
return <div className={styles.root}>{children}</div>;
};
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
root: css({
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: theme.spacing(3),
minHeight: theme.spacing(4),
}),
}));

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Button } from '../Button';
import { Stack } from './Stack';
interface EditorListProps<T> {
items: Array<Partial<T>>;
renderItem: (
item: Partial<T>,
onChangeItem: (item: Partial<T>) => void,
onDeleteItem: () => void
) => React.ReactElement;
onChange: (items: Array<Partial<T>>) => void;
}
export function EditorList<T>({ items, renderItem, onChange }: EditorListProps<T>) {
const onAddItem = () => {
const newItems = [...items, {}];
onChange(newItems);
};
const onChangeItem = (itemIndex: number, newItem: Partial<T>) => {
const newItems = [...items];
newItems[itemIndex] = newItem;
onChange(newItems);
};
const onDeleteItem = (itemIndex: number) => {
const newItems = [...items];
newItems.splice(itemIndex, 1);
onChange(newItems);
};
return (
<Stack>
{items.map((item, index) => (
<div key={index}>
{renderItem(
item,
(newItem) => onChangeItem(index, newItem),
() => onDeleteItem(index)
)}
</div>
))}
<Button onClick={onAddItem} variant="secondary" size="md" icon="plus" aria-label="Add" type="button" />
</Stack>
);
}

View File

@@ -0,0 +1,30 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { Stack } from './Stack';
interface EditorRowProps {}
export const EditorRow: React.FC<EditorRowProps> = ({ children }) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.root}>
<Stack gap={2}>{children}</Stack>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
root: css({
padding: theme.spacing(1),
backgroundColor: theme.colors.background.secondary,
borderRadius: theme.shape.borderRadius(1),
}),
};
};

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { Stack } from './Stack';
interface EditorRowsProps {}
export const EditorRows: React.FC<EditorRowsProps> = ({ children }) => {
return (
<Stack gap={0.5} direction="column">
{children}
</Stack>
);
};

View File

@@ -0,0 +1,25 @@
import { css } from '@emotion/css';
import React, { ComponentProps } from 'react';
import { Switch } from '../Switch/Switch';
// Wrapper component around <Switch /> that properly aligns it in <EditorField />
export const EditorSwitch: React.FC<ComponentProps<typeof Switch>> = (props) => {
const styles = getStyles();
return (
<div className={styles.switch}>
<Switch {...props} />
</div>
);
};
const getStyles = () => {
return {
switch: css({
display: 'flex',
alignItems: 'center',
minHeight: 30,
}),
};
};

View File

@@ -0,0 +1,10 @@
import React from 'react';
interface FlexItemProps {
grow?: number;
shrink?: number;
}
export const FlexItem: React.FC<FlexItemProps> = ({ grow, shrink }) => {
return <div style={{ display: 'block', flexGrow: grow, flexShrink: shrink }} />;
};

View File

@@ -0,0 +1,89 @@
import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { GroupBase } from 'react-select';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
import { Select } from '../Select/Select';
import { SelectContainerProps, SelectContainer as BaseSelectContainer } from '../Select/SelectContainer';
import { SelectCommonProps } from '../Select/types';
interface InlineSelectProps<T> extends SelectCommonProps<T> {
label?: string;
}
export function InlineSelect<T>({ label: labelProp, ...props }: InlineSelectProps<T>) {
const theme = useTheme2();
const [id] = useState(() => Math.random().toString(16).slice(2));
const styles = getSelectStyles(theme);
const components = {
SelectContainer,
ValueContainer,
SingleValue: ValueContainer,
};
return (
<div className={styles.root}>
{labelProp && (
<label className={styles.label} htmlFor={id}>
{labelProp}
{':'}&nbsp;
</label>
)}
{/* @ts-ignore */}
<Select openMenuOnFocus inputId={id} {...props} components={components} />
</div>
);
}
const SelectContainer = <Option, isMulti extends boolean, Group extends GroupBase<Option>>(
props: SelectContainerProps<Option, isMulti, Group>
) => {
const { children } = props;
const theme = useTheme2();
const styles = getSelectStyles(theme);
return (
<BaseSelectContainer {...props} className={cx(props.className, styles.container)}>
{children}
</BaseSelectContainer>
);
};
const ValueContainer = <Option, isMulti extends boolean, Group extends GroupBase<Option>>(
props: SelectContainerProps<Option, isMulti, Group>
) => {
const { className, children } = props;
const theme = useTheme2();
const styles = getSelectStyles(theme);
return <div className={cx(className, styles.valueContainer)}>{children}</div>;
};
const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => ({
root: css({
display: 'flex',
fontSize: 12,
alignItems: 'center',
}),
label: css({
color: theme.colors.text.secondary,
whiteSpace: 'nowrap',
}),
container: css({
background: 'none',
borderColor: 'transparent',
}),
valueContainer: css({
display: 'flex',
alignItems: 'center',
flex: 'initial',
color: theme.colors.text.secondary,
fontSize: 12,
}),
}));

View File

@@ -0,0 +1,54 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
interface InputGroupProps {}
export const InputGroup: React.FC<InputGroupProps> = ({ children }) => {
const theme = useTheme2();
const styles = useStyles(theme);
return <div className={styles.root}>{children}</div>;
};
const useStyles = stylesFactory((theme: GrafanaTheme2) => ({
root: css({
display: 'flex',
// Style the direct children of the component
'> *': {
'&:not(:first-child)': {
// Negative margin hides the double-border on adjacent selects
marginLeft: -1,
},
'&:first-child': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
'&:last-child': {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
'&:not(:first-child):not(:last-child)': {
borderRadius: 0,
},
//
position: 'relative',
zIndex: 1,
'&:hover': {
zIndex: 2,
},
'&:focus-within': {
zIndex: 2,
},
},
}),
}));

View File

@@ -0,0 +1,40 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
export interface SpaceProps {
v?: number;
h?: number;
layout?: 'block' | 'inline';
}
export const Space = (props: SpaceProps) => {
const theme = useTheme2();
const styles = getStyles(theme, props);
return <span className={cx(styles.wrapper)} />;
};
Space.defaultProps = {
v: 0,
h: 0,
layout: 'block',
};
const getStyles = stylesFactory((theme: GrafanaTheme2, props: SpaceProps) => ({
wrapper: css([
{
paddingRight: theme.spacing(props.h ?? 0),
paddingBottom: theme.spacing(props.v ?? 0),
},
props.layout === 'inline' && {
display: 'inline-block',
},
props.layout === 'block' && {
display: 'block',
},
]),
}));

View File

@@ -0,0 +1,30 @@
import { css } from '@emotion/css';
import React, { CSSProperties } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
interface StackProps {
direction?: CSSProperties['flexDirection'];
alignItems?: CSSProperties['alignItems'];
wrap?: boolean;
gap?: number;
}
export const Stack: React.FC<StackProps> = ({ children, ...props }) => {
const theme = useTheme2();
const styles = useStyles(theme, props);
return <div className={styles.root}>{children}</div>;
};
const useStyles = stylesFactory((theme: GrafanaTheme2, props: StackProps) => ({
root: css({
display: 'flex',
flexDirection: props.direction ?? 'row',
flexWrap: props.wrap ?? true ? 'wrap' : undefined,
alignItems: props.alignItems,
gap: theme.spacing(props.gap ?? 2),
}),
}));

View File

@@ -0,0 +1,13 @@
export { AccessoryButton } from './AccessoryButton';
export { EditorFieldGroup } from './EditorFieldGroup';
export { EditorHeader } from './EditorHeader';
export { EditorField } from './EditorField';
export { EditorRow } from './EditorRow';
export { EditorList } from './EditorList';
export { EditorRows } from './EditorRows';
export { EditorSwitch } from './EditorSwitch';
export { FlexItem } from './FlexItem';
export { Stack } from './Stack';
export { InlineSelect } from './InlineSelect';
export { InputGroup } from './InputGroup';
export { Space } from './Space';

View File

@@ -265,3 +265,4 @@ export * from './PanelChrome/types';
export { EmotionPerfTest } from './ThemeDemos/EmotionPerfTest';
export { Label as BrowserLabel } from './BrowserLabel/Label';
export { PanelContainer } from './PanelContainer/PanelContainer';
export * from './QueryEditor';