mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
FieldOverrides: UI updates (#23630)
* UI improvements for field overrides * Update tests * Fix missing key * Fix e2e
This commit is contained in:
parent
a9e408fecf
commit
416111c5f6
@ -4,8 +4,7 @@ import { css } from 'emotion';
|
||||
import { Button } from '../../Button/Button';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { Modal } from '../../Modal/Modal';
|
||||
import { FullWidthButtonContainer } from '../../Button/FullWidthButtonContainer';
|
||||
import { selectThemeVariant, stylesFactory, useTheme } from '../../../themes';
|
||||
import { stylesFactory, useTheme } from '../../../themes';
|
||||
import { DataLinksListItem } from './DataLinksListItem';
|
||||
import { DataLinkEditorModalContent } from './DataLinkEditorModalContent';
|
||||
|
||||
@ -99,42 +98,17 @@ export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ li
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<FullWidthButtonContainer>
|
||||
<Button size="sm" icon="plus" onClick={onDataLinkAdd} variant="secondary">
|
||||
Add link
|
||||
</Button>
|
||||
</FullWidthButtonContainer>
|
||||
<Button size="sm" icon="plus" onClick={onDataLinkAdd} variant="secondary">
|
||||
Add data link
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getDataLinksInlineEditorStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const borderColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.gray85,
|
||||
dark: theme.palette.dark9,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
const shadow = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.gray85,
|
||||
dark: theme.palette.black,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
border: 1px dashed ${borderColor};
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
transition: box-shadow 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px ${shadow};
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { DataFrame, DataLink, GrafanaTheme, VariableSuggestion } from '@grafana/data';
|
||||
import { selectThemeVariant, stylesFactory, useTheme } from '../../../themes';
|
||||
import { HorizontalGroup } from '../../Layout/Layout';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
import { stylesFactory, useTheme } from '../../../themes';
|
||||
import { HorizontalGroup, VerticalGroup } from '../../Layout/Layout';
|
||||
import { IconButton } from '../../IconButton/IconButton';
|
||||
|
||||
interface DataLinksListItemProps {
|
||||
index: number;
|
||||
@ -16,16 +16,7 @@ interface DataLinksListItemProps {
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export const DataLinksListItem: FC<DataLinksListItemProps> = ({
|
||||
index,
|
||||
link,
|
||||
data,
|
||||
onChange,
|
||||
suggestions,
|
||||
isEditing,
|
||||
onEdit,
|
||||
onRemove,
|
||||
}) => {
|
||||
export const DataLinksListItem: FC<DataLinksListItemProps> = ({ link, onEdit, onRemove }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getDataLinkListItemStyles(theme);
|
||||
|
||||
@ -34,65 +25,48 @@ export const DataLinksListItem: FC<DataLinksListItemProps> = ({
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div>
|
||||
<div className={cx(!hasTitle && styles.notConfigured)}>{hasTitle ? link.title : 'No data link provided'}</div>
|
||||
<div className={cx(!hasUrl && styles.notConfigured, styles.url)}>{hasUrl ? link.url : 'No url provided'}</div>
|
||||
</div>
|
||||
|
||||
<HorizontalGroup>
|
||||
<div onClick={onEdit} className={styles.action}>
|
||||
<Icon name="pen" />
|
||||
</div>
|
||||
<div onClick={onRemove} className={cx(styles.action, styles.remove)}>
|
||||
<Icon name="trash-alt" />
|
||||
<VerticalGroup spacing="xs">
|
||||
<HorizontalGroup justify="space-between" align="flex-start" width="100%">
|
||||
<div className={cx(styles.title, !hasTitle && styles.notConfigured)}>
|
||||
{hasTitle ? link.title : 'Data link title not provided'}
|
||||
</div>
|
||||
<HorizontalGroup>
|
||||
<IconButton name="pen" onClick={onEdit} />
|
||||
<IconButton name="times" onClick={onRemove} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<div className={cx(styles.url, !hasUrl && styles.notConfigured)} title={link.url}>
|
||||
{hasUrl ? link.url : 'Data link url not provided'}
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getDataLinkListItemStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const borderColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.gray85,
|
||||
dark: theme.palette.dark9,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
const bg = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.white,
|
||||
dark: theme.palette.dark1,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
border-bottom: 1px dashed ${borderColor};
|
||||
padding: ${theme.spacing.sm};
|
||||
transition: background 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
width: 100%;
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: ${bg};
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
action: css`
|
||||
cursor: pointer;
|
||||
`,
|
||||
|
||||
notConfigured: css`
|
||||
font-style: italic;
|
||||
`,
|
||||
url: css`
|
||||
title: css`
|
||||
color: ${theme.colors.formLabel};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
`,
|
||||
remove: css`
|
||||
color: ${theme.palette.red88};
|
||||
url: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 90%;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -9,7 +9,7 @@ export interface FieldProps {
|
||||
/** Form input element, i.e Input or Switch */
|
||||
children: React.ReactElement;
|
||||
/** Label for the field */
|
||||
label?: string | JSX.Element;
|
||||
label?: React.ReactNode;
|
||||
/** Description of the field */
|
||||
description?: string;
|
||||
/** Indicates if field is in invalid state */
|
||||
|
@ -6,14 +6,15 @@ import { Icon } from '../Icon/Icon';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
||||
children: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
category?: string[];
|
||||
}
|
||||
|
||||
export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
label: css`
|
||||
label: Label;
|
||||
font-size: ${theme.typography.size.sm};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
line-height: 1.25;
|
||||
@ -22,7 +23,12 @@ export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
color: ${theme.colors.formLabel};
|
||||
max-width: 480px;
|
||||
`,
|
||||
labelContent: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
description: css`
|
||||
label: Label-description;
|
||||
color: ${theme.colors.formDescription};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
font-weight: ${theme.typography.weight.regular};
|
||||
@ -30,6 +36,7 @@ export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
display: block;
|
||||
`,
|
||||
categories: css`
|
||||
label: Label-categories;
|
||||
color: ${theme.isLight
|
||||
? tinycolor(theme.colors.formLabel)
|
||||
.lighten(10)
|
||||
@ -49,9 +56,9 @@ export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
export const Label: React.FC<LabelProps> = ({ children, description, className, category, ...labelProps }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getLabelStyles(theme);
|
||||
const categories = category?.map(c => {
|
||||
const categories = category?.map((c, i) => {
|
||||
return (
|
||||
<span className={styles.categories}>
|
||||
<span className={styles.categories} key={`${c}/${i}`}>
|
||||
<span>{c}</span>
|
||||
<Icon name="angle-right" className={styles.chevron} />
|
||||
</span>
|
||||
@ -61,8 +68,10 @@ export const Label: React.FC<LabelProps> = ({ children, description, className,
|
||||
return (
|
||||
<div className={cx(styles.label, className)}>
|
||||
<label {...labelProps}>
|
||||
{categories}
|
||||
{children}
|
||||
<div className={styles.labelContent}>
|
||||
{categories}
|
||||
{children}
|
||||
</div>
|
||||
{description && <span className={styles.description}>{description}</span>}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -17,6 +17,7 @@ export interface LayoutProps {
|
||||
spacing?: Spacing;
|
||||
justify?: Justify;
|
||||
align?: Align;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
export interface ContainerProps {
|
||||
@ -30,14 +31,15 @@ export const Layout: React.FC<LayoutProps> = ({
|
||||
spacing = 'sm',
|
||||
justify = 'flex-start',
|
||||
align = 'normal',
|
||||
width = 'auto',
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, orientation, spacing, justify, align);
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
<div className={styles.layout} style={{ width }}>
|
||||
{React.Children.map(children, (child, index) => {
|
||||
return (
|
||||
<div className={styles.buttonWrapper} key={index}>
|
||||
<div className={styles.childWrapper} key={index}>
|
||||
{child}
|
||||
</div>
|
||||
);
|
||||
@ -51,13 +53,14 @@ export const HorizontalGroup: React.FC<Omit<LayoutProps, 'orientation'>> = ({
|
||||
spacing,
|
||||
justify,
|
||||
align = 'center',
|
||||
width,
|
||||
}) => (
|
||||
<Layout spacing={spacing} justify={justify} orientation={Orientation.Horizontal} align={align}>
|
||||
<Layout spacing={spacing} justify={justify} orientation={Orientation.Horizontal} align={align} width={width}>
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation'>> = ({ children, spacing, justify }) => (
|
||||
<Layout spacing={spacing} justify={justify} orientation={Orientation.Vertical}>
|
||||
export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation'>> = ({ children, spacing, justify, width }) => (
|
||||
<Layout spacing={spacing} justify={justify} orientation={Orientation.Vertical} width={width}>
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
@ -77,8 +80,9 @@ const getStyles = stylesFactory(
|
||||
justify-content: ${justify};
|
||||
align-items: ${align};
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
`,
|
||||
buttonWrapper: css`
|
||||
childWrapper: css`
|
||||
margin-bottom: ${orientation === Orientation.Horizontal ? 0 : theme.spacing[spacing]};
|
||||
margin-right: ${orientation === Orientation.Horizontal ? theme.spacing[spacing] : 0};
|
||||
display: flex;
|
||||
|
@ -15,9 +15,9 @@ import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
import { stylesFactory } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup';
|
||||
import { Field } from '../Forms/Field';
|
||||
import { Button } from '../Button';
|
||||
import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer';
|
||||
import { Label } from '../Forms/Label';
|
||||
|
||||
const modes: Array<SelectableValue<ThresholdsMode>> = [
|
||||
{ value: ThresholdsMode.Absolute, label: 'Absolute', description: 'Pick thresholds based on the absolute values' },
|
||||
@ -217,11 +217,12 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Field label="Threshold mode" description="Percentage means thresholds relative to min & max">
|
||||
<div>
|
||||
<Label description="Percentage means thresholds relative to min & max">Thresholds mode</Label>
|
||||
<FullWidthButtonContainer>
|
||||
<RadioButtonGroup size="sm" options={modes} onChange={this.onModeChanged} value={thresholds.mode} />
|
||||
</FullWidthButtonContainer>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@ -278,7 +279,7 @@ const getStyles = stylesFactory(
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: -${theme.spacing.formSpacingBase * 2}px;
|
||||
// margin-bottom: -${theme.spacing.formSpacingBase * 2}px;
|
||||
`,
|
||||
thresholds: css`
|
||||
display: flex;
|
||||
|
@ -1,12 +1,9 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { HorizontalGroup } from '../Layout/Layout';
|
||||
import { Select } from '../index';
|
||||
import { FullWidthButtonContainer, IconButton, Label, RadioButtonGroup } from '../index';
|
||||
import { Field } from '../Forms/Field';
|
||||
import { Input } from '../Input/Input';
|
||||
import { MappingType, RangeMap, ValueMap, ValueMapping } from '@grafana/data';
|
||||
import * as styleMixins from '../../themes/mixins';
|
||||
import { useTheme } from '../../themes';
|
||||
import { FieldConfigItemHeaderTitle } from '../FieldConfigs/FieldConfigItemHeaderTitle';
|
||||
import { MappingType, RangeMap, SelectableValue, ValueMap, ValueMapping } from '@grafana/data';
|
||||
|
||||
export interface Props {
|
||||
valueMapping: ValueMapping;
|
||||
@ -14,13 +11,12 @@ export interface Props {
|
||||
removeValueMapping: () => void;
|
||||
}
|
||||
|
||||
const MAPPING_OPTIONS = [
|
||||
const MAPPING_OPTIONS: Array<SelectableValue<MappingType>> = [
|
||||
{ value: MappingType.ValueToText, label: 'Value' },
|
||||
{ value: MappingType.RangeToText, label: 'Range' },
|
||||
];
|
||||
|
||||
export const MappingRow: React.FC<Props> = ({ valueMapping, updateValueMapping, removeValueMapping }) => {
|
||||
const theme = useTheme();
|
||||
const { type } = valueMapping;
|
||||
|
||||
const onMappingValueChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
@ -76,22 +72,26 @@ export const MappingRow: React.FC<Props> = ({ valueMapping, updateValueMapping,
|
||||
);
|
||||
};
|
||||
|
||||
const styles = styleMixins.panelEditorNestedListStyles(theme);
|
||||
|
||||
const label = (
|
||||
<HorizontalGroup justify="space-between" align="center">
|
||||
<Label>Mapping type</Label>
|
||||
<IconButton name="times" onClick={removeValueMapping} aria-label="ValueMappingsEditor remove button" />
|
||||
</HorizontalGroup>
|
||||
);
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<FieldConfigItemHeaderTitle title="Mapping type" onRemove={removeValueMapping}>
|
||||
<div className={styles.itemContent}>
|
||||
<Select
|
||||
placeholder="Choose type"
|
||||
isSearchable={false}
|
||||
<div>
|
||||
<Field label={label}>
|
||||
<FullWidthButtonContainer>
|
||||
<RadioButtonGroup
|
||||
options={MAPPING_OPTIONS}
|
||||
value={MAPPING_OPTIONS.find(o => o.value === type)}
|
||||
onChange={type => onMappingTypeChange(type.value!)}
|
||||
value={type}
|
||||
onChange={type => {
|
||||
onMappingTypeChange(type!);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FieldConfigItemHeaderTitle>
|
||||
<div className={styles.content}>{renderRow()}</div>
|
||||
</FullWidthButtonContainer>
|
||||
</Field>
|
||||
{renderRow()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -32,8 +32,9 @@ describe('On remove mapping', () => {
|
||||
it('Should remove mapping at index 0', () => {
|
||||
const onChangeSpy = jest.fn();
|
||||
const wrapper = setup(onChangeSpy);
|
||||
const remove = wrapper.find('*[aria-label="FieldConfigItemHeaderTitle remove button"]');
|
||||
const remove = wrapper.find('button[aria-label="ValueMappingsEditor remove button"]');
|
||||
remove.at(0).simulate('click');
|
||||
|
||||
expect(onChangeSpy).toBeCalledWith([
|
||||
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
|
||||
]);
|
||||
@ -43,7 +44,7 @@ describe('On remove mapping', () => {
|
||||
const onChangeSpy = jest.fn();
|
||||
const wrapper = setup(onChangeSpy);
|
||||
|
||||
const remove = wrapper.find('*[aria-label="FieldConfigItemHeaderTitle remove button"]');
|
||||
const remove = wrapper.find('button[aria-label="ValueMappingsEditor remove button"]');
|
||||
remove.at(1).simulate('click');
|
||||
|
||||
expect(onChangeSpy).toBeCalledWith([
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { MappingType, ValueMapping } from '@grafana/data';
|
||||
import { Button } from '../Button/Button';
|
||||
import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer';
|
||||
import { MappingRow } from './MappingRow';
|
||||
|
||||
export interface Props {
|
||||
@ -65,17 +64,15 @@ export const ValueMappingsEditor: React.FC<Props> = ({ valueMappings, onChange,
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<FullWidthButtonContainer>
|
||||
<Button
|
||||
size="sm"
|
||||
icon="plus"
|
||||
onClick={onAdd}
|
||||
aria-label="ValueMappingsEditor add mapping button"
|
||||
variant="secondary"
|
||||
>
|
||||
Add mapping
|
||||
</Button>
|
||||
</FullWidthButtonContainer>
|
||||
<Button
|
||||
size="sm"
|
||||
icon="plus"
|
||||
onClick={onAdd}
|
||||
aria-label="ValueMappingsEditor add mapping button"
|
||||
variant="secondary"
|
||||
>
|
||||
Add value mapping
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { selectThemeVariant } from './selectThemeVariant';
|
||||
import { css } from 'emotion';
|
||||
import { stylesFactory } from './stylesFactory';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export function cardChrome(theme: GrafanaTheme): string {
|
||||
@ -36,53 +33,3 @@ export function listItemSelected(theme: GrafanaTheme): string {
|
||||
color: ${theme.colors.textStrong};
|
||||
`;
|
||||
}
|
||||
|
||||
export const panelEditorNestedListStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const borderColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.gray85,
|
||||
dark: theme.palette.dark9,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
const shadow = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.gray85,
|
||||
dark: theme.palette.black,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
const headerBg = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.white,
|
||||
dark: theme.palette.dark1,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
border: 1px dashed ${borderColor};
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
transition: box-shadow 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
box-shadow: none;
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px ${shadow};
|
||||
}
|
||||
`,
|
||||
headerWrapper: css`
|
||||
background: ${headerBg};
|
||||
padding: ${theme.spacing.xs} 0;
|
||||
`,
|
||||
|
||||
content: css`
|
||||
padding: ${theme.spacing.xs} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm};
|
||||
border-top: 1px dashed ${borderColor};
|
||||
> *:last-child {
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
itemContent: css`
|
||||
padding: ${theme.spacing.xs} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { DynamicConfigValue, FieldConfigOptionsRegistry, FieldOverrideContext, GrafanaTheme } from '@grafana/data';
|
||||
import { FieldConfigItemHeaderTitle, selectThemeVariant, stylesFactory, useTheme } from '@grafana/ui';
|
||||
|
||||
import { css } from 'emotion';
|
||||
import React from 'react';
|
||||
import { Field, HorizontalGroup, IconButton, IconName, Label, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
import { OptionsGroup } from './OptionsGroup';
|
||||
interface DynamicConfigValueEditorProps {
|
||||
property: DynamicConfigValue;
|
||||
registry: FieldConfigOptionsRegistry;
|
||||
onChange: (value: DynamicConfigValue) => void;
|
||||
context: FieldOverrideContext;
|
||||
onRemove: () => void;
|
||||
isCollapsible?: boolean;
|
||||
}
|
||||
|
||||
export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> = ({
|
||||
@ -17,6 +18,7 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
|
||||
registry,
|
||||
onChange,
|
||||
onRemove,
|
||||
isCollapsible,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
@ -25,11 +27,41 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
let editor;
|
||||
const renderLabel = (iconName: IconName, includeDescription = true) => () => (
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Label description={includeDescription ? item.description : undefined}>{item.name}</Label>
|
||||
<div>
|
||||
<IconButton name={iconName} onClick={onRemove} />
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<FieldConfigItemHeaderTitle onRemove={onRemove} title={item.name} description={item.description} transparent>
|
||||
<div className={styles.property}>
|
||||
if (isCollapsible) {
|
||||
editor = (
|
||||
<OptionsGroup
|
||||
renderTitle={renderLabel('trash-alt', false)}
|
||||
className={css`
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
`}
|
||||
nested
|
||||
defaultToClosed={property.value !== undefined}
|
||||
>
|
||||
<item.override
|
||||
value={property.value}
|
||||
onChange={value => {
|
||||
onChange(value);
|
||||
}}
|
||||
item={item}
|
||||
context={context}
|
||||
/>
|
||||
</OptionsGroup>
|
||||
);
|
||||
} else {
|
||||
editor = (
|
||||
<div>
|
||||
<Field label={renderLabel('times')()} description={item.description}>
|
||||
<item.override
|
||||
value={property.value}
|
||||
onChange={value => {
|
||||
@ -38,51 +70,30 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
|
||||
item={item}
|
||||
context={context}
|
||||
/>
|
||||
</div>
|
||||
</FieldConfigItemHeaderTitle>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
isCollapsible && styles.collapsibleOverrideEditor,
|
||||
!isCollapsible && 'dynamicConfigValueEditor--nonCollapsible'
|
||||
)}
|
||||
>
|
||||
{editor}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const borderColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.gray85,
|
||||
dark: theme.palette.dark9,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
const highlightColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.blue95,
|
||||
dark: theme.palette.blue77,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
border-top: 1px dashed ${borderColor};
|
||||
position: relative;
|
||||
&:hover {
|
||||
&:before {
|
||||
background: ${highlightColor};
|
||||
}
|
||||
collapsibleOverrideEditor: css`
|
||||
label: collapsibleOverrideEditor;
|
||||
& + .dynamicConfigValueEditor--nonCollapsible {
|
||||
margin-top: ${theme.spacing.formSpacingBase}px;
|
||||
}
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
left: -1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
transition: background 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
`,
|
||||
property: css`
|
||||
padding: ${theme.spacing.xs} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -8,10 +8,9 @@ import {
|
||||
PanelPlugin,
|
||||
SelectableValue,
|
||||
} from '@grafana/data';
|
||||
import { fieldMatchersUI, ValuePicker, useTheme, Label, Field } from '@grafana/ui';
|
||||
import { fieldMatchersUI, ValuePicker, Label, Field, Container } from '@grafana/ui';
|
||||
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
|
||||
import { OverrideEditor } from './OverrideEditor';
|
||||
import { css } from 'emotion';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { OptionsGroup } from './OptionsGroup';
|
||||
|
||||
@ -27,8 +26,6 @@ interface Props {
|
||||
* Expects the container div to have size set and will fill it 100%
|
||||
*/
|
||||
export const OverrideFieldConfigEditor: React.FC<Props> = props => {
|
||||
const theme = useTheme();
|
||||
|
||||
const onOverrideChange = (index: number, override: any) => {
|
||||
const { config } = props;
|
||||
let overrides = cloneDeep(config.overrides);
|
||||
@ -73,6 +70,7 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
|
||||
// TODO: apply matcher to retrieve fields
|
||||
return (
|
||||
<OverrideEditor
|
||||
name={`Override ${i + 1}`}
|
||||
key={`${o.matcher.id}/${i}`}
|
||||
data={data}
|
||||
override={o}
|
||||
@ -88,24 +86,23 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
|
||||
|
||||
const renderAddOverride = () => {
|
||||
return (
|
||||
<ValuePicker
|
||||
icon="plus"
|
||||
label="Add override"
|
||||
variant="secondary"
|
||||
options={fieldMatchersUI
|
||||
.list()
|
||||
.map<SelectableValue<string>>(i => ({ label: i.name, value: i.id, description: i.description }))}
|
||||
onChange={value => onOverrideAdd(value)}
|
||||
/>
|
||||
<Container padding="md">
|
||||
<ValuePicker
|
||||
icon="plus"
|
||||
label="Add override"
|
||||
size="md"
|
||||
options={fieldMatchersUI
|
||||
.list()
|
||||
.map<SelectableValue<string>>(i => ({ label: i.name, value: i.id, description: i.description }))}
|
||||
onChange={value => onOverrideAdd(value)}
|
||||
isFullWidth={false}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
padding: ${theme.spacing.md};
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
{renderOverrides()}
|
||||
{renderAddOverride()}
|
||||
</div>
|
||||
|
@ -4,54 +4,96 @@ import { GrafanaTheme } from '@grafana/data';
|
||||
import { useTheme, Icon, stylesFactory } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
title?: React.ReactNode;
|
||||
renderTitle?: (isExpanded: boolean) => React.ReactNode;
|
||||
defaultToClosed?: boolean;
|
||||
className?: string;
|
||||
nested?: boolean;
|
||||
}
|
||||
|
||||
export const OptionsGroup: FC<Props> = ({ title, children, defaultToClosed }) => {
|
||||
export const OptionsGroup: FC<Props> = ({
|
||||
title,
|
||||
children,
|
||||
defaultToClosed,
|
||||
renderTitle,
|
||||
className,
|
||||
nested = false,
|
||||
}) => {
|
||||
const [isExpanded, toggleExpand] = useState(defaultToClosed ? false : true);
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, isExpanded);
|
||||
const styles = getStyles(theme, isExpanded, nested);
|
||||
|
||||
return (
|
||||
<div className={styles.box}>
|
||||
<div className={cx(styles.box, className, 'options-group')}>
|
||||
<div className={styles.header} onClick={() => toggleExpand(!isExpanded)}>
|
||||
<div className={cx(styles.toggle, 'editor-options-group-toggle')}>
|
||||
<Icon name={isExpanded ? 'angle-down' : 'angle-right'} />
|
||||
</div>
|
||||
{title}
|
||||
<div style={{ width: '100%' }}>{renderTitle ? renderTitle(isExpanded) : title}</div>
|
||||
</div>
|
||||
{isExpanded && <div className={styles.body}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean) => {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean, isNested: boolean) => {
|
||||
return {
|
||||
box: css`
|
||||
border-bottom: 1px solid ${theme.colors.pageHeaderBorder};
|
||||
`,
|
||||
box: cx(
|
||||
!isNested &&
|
||||
css`
|
||||
border-bottom: 1px solid ${theme.colors.pageHeaderBorder};
|
||||
`,
|
||||
isNested &&
|
||||
isExpanded &&
|
||||
css`
|
||||
margin-bottom: ${theme.spacing.formSpacingBase * 2}px;
|
||||
`
|
||||
),
|
||||
toggle: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
font-size: ${theme.typography.size.lg};
|
||||
margin-right: ${theme.spacing.sm};
|
||||
`,
|
||||
header: css`
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: baseline;
|
||||
padding: ${theme.spacing.sm} ${theme.spacing.md} ${theme.spacing.sm} ${theme.spacing.sm};
|
||||
color: ${isExpanded ? theme.colors.text : theme.colors.formLabel};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
header: cx(
|
||||
css`
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: baseline;
|
||||
padding: ${theme.spacing.sm} ${theme.spacing.md} ${theme.spacing.sm} ${theme.spacing.sm};
|
||||
color: ${isExpanded ? theme.colors.text : theme.colors.formLabel};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
|
||||
&:hover {
|
||||
.editor-options-group-toggle {
|
||||
color: ${theme.colors.text};
|
||||
&:hover {
|
||||
.editor-options-group-toggle {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
body: css`
|
||||
padding: 0 ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.xl};
|
||||
`,
|
||||
`,
|
||||
isNested &&
|
||||
css`
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
`
|
||||
),
|
||||
body: cx(
|
||||
css`
|
||||
padding: 0 ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.xl};
|
||||
`,
|
||||
isNested &&
|
||||
css`
|
||||
position: relative;
|
||||
padding-right: 0;
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 8px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: ${theme.colors.pageHeaderBorder};
|
||||
}
|
||||
`
|
||||
),
|
||||
};
|
||||
});
|
||||
|
@ -197,17 +197,6 @@ export const TabsBarContent: React.FC<{
|
||||
<div className="flex-grow-1" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/*
|
||||
<div className={styles.tabsButton}>
|
||||
<DashNavButton
|
||||
icon="search"
|
||||
iconSize="md"
|
||||
tooltip="Search all options"
|
||||
classSuffix="search-options"
|
||||
onClick={() => setSearchMode(true)}
|
||||
/>
|
||||
</div> */}
|
||||
<div className={styles.tabsButton}>
|
||||
<DashNavButton
|
||||
icon="angle-right"
|
||||
|
@ -4,17 +4,29 @@ import {
|
||||
DataFrame,
|
||||
DynamicConfigValue,
|
||||
FieldConfigOptionsRegistry,
|
||||
VariableSuggestionsScope,
|
||||
FieldConfigProperty,
|
||||
GrafanaTheme,
|
||||
VariableSuggestionsScope,
|
||||
} from '@grafana/data';
|
||||
import { fieldMatchersUI, stylesFactory, useTheme, ValuePicker, selectThemeVariant } from '@grafana/ui';
|
||||
import {
|
||||
Field,
|
||||
fieldMatchersUI,
|
||||
HorizontalGroup,
|
||||
Icon,
|
||||
IconButton,
|
||||
Label,
|
||||
stylesFactory,
|
||||
useTheme,
|
||||
ValuePicker,
|
||||
} from '@grafana/ui';
|
||||
import { DynamicConfigValueEditor } from './DynamicConfigValueEditor';
|
||||
|
||||
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
|
||||
import { css } from 'emotion';
|
||||
import { FieldConfigItemHeaderTitle } from '@grafana/ui/src/components/FieldConfigs/FieldConfigItemHeaderTitle';
|
||||
import { OptionsGroup } from './OptionsGroup';
|
||||
|
||||
interface OverrideEditorProps {
|
||||
name: string;
|
||||
data: DataFrame[];
|
||||
override: ConfigOverrideRule;
|
||||
onChange: (config: ConfigOverrideRule) => void;
|
||||
@ -22,8 +34,28 @@ interface OverrideEditorProps {
|
||||
registry: FieldConfigOptionsRegistry;
|
||||
}
|
||||
|
||||
export const OverrideEditor: React.FC<OverrideEditorProps> = ({ data, override, onChange, onRemove, registry }) => {
|
||||
const COLLECTION_STANDARD_PROPERTIES = [
|
||||
FieldConfigProperty.Thresholds,
|
||||
FieldConfigProperty.Links,
|
||||
FieldConfigProperty.Mappings,
|
||||
];
|
||||
|
||||
export const OverrideEditor: React.FC<OverrideEditorProps> = ({
|
||||
name,
|
||||
data,
|
||||
override,
|
||||
onChange,
|
||||
onRemove,
|
||||
registry,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const matcherUi = fieldMatchersUI.get(override.matcher.id);
|
||||
const styles = getStyles(theme);
|
||||
const matcherLabel = (
|
||||
<Label category={['Matcher']} description={matcherUi.description}>
|
||||
{matcherUi.name}
|
||||
</Label>
|
||||
);
|
||||
const onMatcherConfigChange = useCallback(
|
||||
(matcherConfig: any) => {
|
||||
override.matcher.options = matcherConfig;
|
||||
@ -71,20 +103,34 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({ data, override,
|
||||
};
|
||||
});
|
||||
|
||||
const matcherUi = fieldMatchersUI.get(override.matcher.id);
|
||||
const styles = getStyles(theme);
|
||||
const renderOverrideTitle = (isExpanded: boolean) => {
|
||||
return (
|
||||
<div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div>{name}</div>
|
||||
<IconButton name="trash-alt" onClick={onRemove} />
|
||||
</HorizontalGroup>
|
||||
{!isExpanded && (
|
||||
<div className={styles.overrideDetails}>
|
||||
Matcher <Icon name="angle-right" /> {matcherUi.name} <br />
|
||||
{override.properties.length === 0 ? 'No' : override.properties.length} properties overriden
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<FieldConfigItemHeaderTitle onRemove={onRemove} title={matcherUi.name} description={matcherUi.description}>
|
||||
<div className={styles.matcherUi}>
|
||||
<matcherUi.component
|
||||
matcher={matcherUi.matcher}
|
||||
data={data}
|
||||
options={override.matcher.options}
|
||||
onChange={option => onMatcherConfigChange(option)}
|
||||
/>
|
||||
</div>
|
||||
</FieldConfigItemHeaderTitle>
|
||||
<OptionsGroup renderTitle={renderOverrideTitle}>
|
||||
<Field label={matcherLabel} description={matcherUi.description}>
|
||||
<matcherUi.component
|
||||
matcher={matcherUi.matcher}
|
||||
data={data}
|
||||
options={override.matcher.options}
|
||||
onChange={option => onMatcherConfigChange(option)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div>
|
||||
{override.properties.map((p, j) => {
|
||||
const item = registry.getIfExists(p.id);
|
||||
@ -92,70 +138,56 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({ data, override,
|
||||
if (!item) {
|
||||
return <div>Unknown property: {p.id}</div>;
|
||||
}
|
||||
const isCollapsible =
|
||||
Array.isArray(p.value) || COLLECTION_STANDARD_PROPERTIES.includes(p.id as FieldConfigProperty);
|
||||
|
||||
return (
|
||||
<div key={`${p.id}/${j}`}>
|
||||
<DynamicConfigValueEditor
|
||||
onChange={value => onDynamicConfigValueChange(j, value)}
|
||||
onRemove={() => onDynamicConfigValueRemove(j)}
|
||||
property={p}
|
||||
registry={registry}
|
||||
context={{
|
||||
data,
|
||||
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DynamicConfigValueEditor
|
||||
key={`${p.id}/${j}`}
|
||||
isCollapsible={isCollapsible}
|
||||
onChange={value => onDynamicConfigValueChange(j, value)}
|
||||
onRemove={() => onDynamicConfigValueRemove(j)}
|
||||
property={p}
|
||||
registry={registry}
|
||||
context={{
|
||||
data,
|
||||
getSuggestions: (scope?: VariableSuggestionsScope) => getDataLinksVariableSuggestions(data, scope),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className={styles.propertyPickerWrapper}>
|
||||
<ValuePicker
|
||||
label="Set config property"
|
||||
icon="plus"
|
||||
options={configPropertiesOptions}
|
||||
variant={'link'}
|
||||
onChange={o => {
|
||||
onDynamicConfigValueAdd(o.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{override.matcher.options && (
|
||||
<div className={styles.propertyPickerWrapper}>
|
||||
<ValuePicker
|
||||
label="Add override property"
|
||||
variant="secondary"
|
||||
size="md"
|
||||
icon="plus"
|
||||
options={configPropertiesOptions}
|
||||
onChange={o => {
|
||||
onDynamicConfigValueAdd(o.value);
|
||||
}}
|
||||
isFullWidth={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</OptionsGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const borderColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.gray85,
|
||||
dark: theme.palette.dark9,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
const shadow = selectThemeVariant(
|
||||
{
|
||||
light: theme.palette.gray85,
|
||||
dark: theme.palette.black,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
border: 1px dashed ${borderColor};
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
transition: box-shadow 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
box-shadow: none;
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px ${shadow};
|
||||
}
|
||||
`,
|
||||
matcherUi: css`
|
||||
padding: ${theme.spacing.sm};
|
||||
`,
|
||||
propertyPickerWrapper: css`
|
||||
border-top: 1px solid ${borderColor};
|
||||
margin-top: ${theme.spacing.formSpacingBase * 2}px;
|
||||
`,
|
||||
overrideDetails: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
color: ${theme.colors.textWeak};
|
||||
font-weight: ${theme.typography.weight.regular};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user