diff --git a/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts b/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts index 24ca250d97b..b636a0f2029 100644 --- a/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts +++ b/packages/grafana-data/src/types/OptionsUIRegistryBuilder.ts @@ -75,6 +75,10 @@ export interface OptionsUIRegistryBuilderAPI< config: OptionEditorConfig ): this; + addStringArray?( + config: OptionEditorConfig + ): this; + addSelect?>( config: OptionEditorConfig ): this; diff --git a/packages/grafana-data/src/utils/OptionsUIBuilders.ts b/packages/grafana-data/src/utils/OptionsUIBuilders.ts index a9f75559399..5b828c89c18 100644 --- a/packages/grafana-data/src/utils/OptionsUIBuilders.ts +++ b/packages/grafana-data/src/utils/OptionsUIBuilders.ts @@ -143,6 +143,16 @@ export class PanelOptionsEditorBuilder extends OptionsUIRegistryBuilde }); } + addStringArray( + config: PanelOptionsEditorConfig + ) { + return this.addCustomEditor({ + ...config, + id: config.path, + editor: standardEditorsRegistry.get('strings').editor as any, + }); + } + addSelect>( config: PanelOptionsEditorConfig ) { diff --git a/packages/grafana-ui/src/components/OptionsUI/strings.tsx b/packages/grafana-ui/src/components/OptionsUI/strings.tsx new file mode 100644 index 00000000000..501da51deb9 --- /dev/null +++ b/packages/grafana-ui/src/components/OptionsUI/strings.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { FieldConfigEditorProps, StringFieldConfigSettings, GrafanaTheme } from '@grafana/data'; +import { Input } from '../Input/Input'; +import { Icon } from '../Icon/Icon'; +import { stylesFactory, getTheme } from '../../themes'; +import { css } from 'emotion'; +import { Button } from '../Button'; + +type Props = FieldConfigEditorProps; +interface State { + showAdd: boolean; +} + +export class StringArrayEditor extends React.PureComponent { + state = { + showAdd: false, + }; + + onRemoveString = (index: number) => { + const { value, onChange } = this.props; + const copy = [...value]; + copy.splice(index, 1); + onChange(copy); + }; + + onValueChange = (e: React.SyntheticEvent, idx: number) => { + const evt = e as React.KeyboardEvent; + if (e.hasOwnProperty('key')) { + if (evt.key !== 'Enter') { + return; + } + } + const { value, onChange } = this.props; + + // Form event, or Enter + const v = evt.currentTarget.value.trim(); + if (idx < 0) { + if (v) { + evt.currentTarget.value = ''; // reset last value + onChange([...value, v]); + } + this.setState({ showAdd: false }); + return; + } + + if (!v) { + return this.onRemoveString(idx); + } + + const copy = [...value]; + copy[idx] = v; + onChange(copy); + }; + + render() { + const { value, item } = this.props; + const { showAdd } = this.state; + const styles = getStyles(getTheme()); + const placeholder = item.settings?.placeholder || 'Add text'; + return ( +
+ {value.map((v, index) => { + return ( + this.onValueChange(e, index)} + onKeyDown={e => this.onValueChange(e, index)} + suffix={ this.onRemoveString(index)} />} + /> + ); + })} + + {showAdd ? ( + this.onValueChange(e, -1)} + onKeyDown={e => this.onValueChange(e, -1)} + suffix={} + /> + ) : ( + + )} +
+ ); + } +} + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + textInput: css` + margin-bottom: 5px; + &:hover { + border: 1px solid ${theme.colors.formInputBorderHover}; + } + `, + trashIcon: css` + color: ${theme.colors.textWeak}; + cursor: pointer; + + &:hover { + color: ${theme.colors.text}; + } + `, + }; +}); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 8eafccedf19..b7faf9bc9b2 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -118,6 +118,7 @@ export { Slider } from './Slider/Slider'; // TODO: namespace!! export { StringValueEditor } from './OptionsUI/string'; +export { StringArrayEditor } from './OptionsUI/strings'; export { NumberValueEditor } from './OptionsUI/number'; export { SelectValueEditor } from './OptionsUI/select'; export { FieldConfigItemHeaderTitle } from './FieldConfigs/FieldConfigItemHeaderTitle'; diff --git a/packages/grafana-ui/src/utils/standardEditors.tsx b/packages/grafana-ui/src/utils/standardEditors.tsx index 7d9e808e819..2e1f727fcbe 100644 --- a/packages/grafana-ui/src/utils/standardEditors.tsx +++ b/packages/grafana-ui/src/utils/standardEditors.tsx @@ -20,7 +20,13 @@ import { } from '@grafana/data'; import { Switch } from '../components/Switch/Switch'; -import { NumberValueEditor, RadioButtonGroup, StringValueEditor, SelectValueEditor } from '../components'; +import { + NumberValueEditor, + RadioButtonGroup, + StringValueEditor, + StringArrayEditor, + SelectValueEditor, +} from '../components'; import { ValueMappingsValueEditor } from '../components/OptionsUI/mappings'; import { ThresholdsValueEditor } from '../components/OptionsUI/thresholds'; import { UnitValueEditor } from '../components/OptionsUI/units'; @@ -227,6 +233,13 @@ export const getStandardOptionEditors = () => { editor: StringValueEditor as any, }; + const strings: StandardEditorsRegistryItem = { + id: 'strings', + name: 'String array', + description: 'An array of strings', + editor: StringArrayEditor as any, + }; + const boolean: StandardEditorsRegistryItem = { id: 'boolean', name: 'Boolean', @@ -290,5 +303,5 @@ export const getStandardOptionEditors = () => { description: '', }; - return [text, number, boolean, radio, select, unit, mappings, thresholds, links, color, statsPicker]; + return [text, number, boolean, radio, select, unit, mappings, thresholds, links, color, statsPicker, strings]; }; diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 24f1c93df1a..d60e3b09ec3 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -19,6 +19,15 @@ export const plugin = new PanelPlugin(TextPanel) }, defaultValue: 'markdown', }) + .addStringArray({ + path: 'strings', + name: 'String Array', + description: 'list of strings', + settings: { + placeholder: 'Add a string value (text2 demo)', + }, + defaultValue: ['hello', 'world'], + }) .addTextInput({ path: 'content', name: 'Content',