mirror of https://github.com/grafana/grafana.git
Dashboard: Improve static options editors on variables
This commit is contained in:
parent
2c5ccd3283
commit
aa13249b42
|
@ -2194,11 +2194,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/variables/utils.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
|
|
|
@ -559,6 +559,12 @@ export const versionedPages = {
|
|||
customValueInput: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid custom-variable-input',
|
||||
},
|
||||
optionsOpenButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid custom-variable-options-open-button',
|
||||
},
|
||||
closeButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid custom-variable-close-button',
|
||||
},
|
||||
},
|
||||
IntervalVariable: {
|
||||
intervalsValueInput: {
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import { FormEvent } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
import { TextArea } from '@grafana/ui';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { VariableLegend } from '../components/VariableLegend';
|
||||
import { VariableTextAreaField } from '../components/VariableTextAreaField';
|
||||
|
||||
import { SelectionOptionsForm } from './SelectionOptionsForm';
|
||||
import { VariableLegend } from './VariableLegend';
|
||||
import { VariableTextAreaField } from './VariableTextAreaField';
|
||||
|
||||
interface CustomVariableFormProps {
|
||||
query: string;
|
||||
|
@ -71,41 +66,3 @@ export function CustomVariableForm({
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] {
|
||||
if (!(variable instanceof CustomVariable)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: (descriptor) => <ValuesTextField id={descriptor.props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) {
|
||||
const { query } = variable.useState();
|
||||
|
||||
const onBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
|
||||
variable.setState({ query: event.currentTarget.value });
|
||||
await lastValueFrom(variable.validateAndUpdate!());
|
||||
};
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id={id}
|
||||
rows={2}
|
||||
defaultValue={query}
|
||||
onBlur={onBlur}
|
||||
placeholder={t(
|
||||
'dashboard.edit-pane.variable.custom-options.values-placeholder',
|
||||
'1, 10, mykey : myvalue, myvalue, escaped\,value'
|
||||
)}
|
||||
required
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { VariableValueOption } from '@grafana/scenes';
|
||||
import { Button, Input, Stack } from '@grafana/ui';
|
||||
|
||||
interface VariableOptionsFieldProps {
|
||||
options: VariableValueOption[];
|
||||
onChange: (options: VariableValueOption[]) => void;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function VariableOptionsInput({ options, onChange, width }: VariableOptionsFieldProps) {
|
||||
const [optionsLocal, setOptionsLocal] = useState(options.length ? options : [{ value: '', label: '' }]);
|
||||
|
||||
const updateOptions = (newOptions: VariableValueOption[]) => {
|
||||
setOptionsLocal(newOptions);
|
||||
onChange(
|
||||
newOptions
|
||||
.map((option) => ({
|
||||
label: option.label.trim(),
|
||||
value: String(option.value).trim(),
|
||||
}))
|
||||
.filter((option) => !!option.label)
|
||||
);
|
||||
};
|
||||
|
||||
const handleValueChange = (index: number, value: string) => {
|
||||
if (optionsLocal[index].value !== value) {
|
||||
const newOptions = [...optionsLocal];
|
||||
newOptions[index] = { ...newOptions[index], value };
|
||||
updateOptions(newOptions);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLabelChange = (index: number, label: string) => {
|
||||
if (optionsLocal[index].label !== label) {
|
||||
const newOptions = [...optionsLocal];
|
||||
newOptions[index] = { ...newOptions[index], label };
|
||||
updateOptions(newOptions);
|
||||
}
|
||||
};
|
||||
|
||||
const addOption = () => {
|
||||
const newOption: VariableValueOption = { value: '', label: '' };
|
||||
const newOptions = [...optionsLocal, newOption];
|
||||
updateOptions(newOptions);
|
||||
};
|
||||
|
||||
const removeOption = (index: number) => {
|
||||
const newOptions = optionsLocal.filter((_, i) => i !== index);
|
||||
updateOptions(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2} width={width}>
|
||||
{optionsLocal.map((option, index) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
key={index}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow}
|
||||
>
|
||||
<Input
|
||||
value={option.label}
|
||||
placeholder={t('variables.query-variable-static-options.label-placeholder', 'display label')}
|
||||
onChange={(e) => handleLabelChange(index, e.currentTarget.value)}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={String(option.value)}
|
||||
placeholder={t('variables.query-variable-static-options.value-placeholder', 'value, default empty string')}
|
||||
onChange={(e) => handleValueChange(index, e.currentTarget.value)}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="times"
|
||||
variant="secondary"
|
||||
aria-label={t('variables.query-variable-static-options.remove-option-button-label', 'Remove option')}
|
||||
onClick={() => removeOption(index)}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsDeleteButton
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
<div>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
onClick={addOption}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton
|
||||
}
|
||||
aria-label={t('variables.query-variable-static-options.add-option-button-label', 'Add option')}
|
||||
>
|
||||
<Trans i18nKey="variables.query-variable-static-options.add-option-button-label">Add option</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
import { useEffect, useState, useRef } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { VariableValueOption } from '@grafana/scenes';
|
||||
import { Button, Stack } from '@grafana/ui';
|
||||
|
||||
import { VariableStaticOptionsFormItem } from './VariableStaticOptionsFormItemEditor';
|
||||
import { VariableStaticOptionsFormItems } from './VariableStaticOptionsFormItems';
|
||||
|
||||
interface VariableStaticOptionsFormProps {
|
||||
options: VariableValueOption[];
|
||||
onChange: (options: VariableValueOption[]) => void;
|
||||
|
||||
allowEmptyValue?: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function VariableStaticOptionsForm({
|
||||
options,
|
||||
onChange,
|
||||
allowEmptyValue,
|
||||
width,
|
||||
}: VariableStaticOptionsFormProps) {
|
||||
// Whenever the form is updated, we want to ignore the next update from the parent component.
|
||||
// This is because the parent component will update the options, and we don't want to update the items again.
|
||||
// This is a hack to prevent the form from updating twice and losing items and IDs.
|
||||
// Alternatively, we could maintain a list of emitted items and compare the new options to it, but this is less performant.
|
||||
const ignoreNextUpdate = useRef<boolean>(false);
|
||||
|
||||
const createEmptyItem: () => VariableStaticOptionsFormItem = () => {
|
||||
return {
|
||||
label: '',
|
||||
value: '',
|
||||
id: uuidv4(),
|
||||
};
|
||||
};
|
||||
|
||||
const [items, setItems] = useState<VariableStaticOptionsFormItem[]>(
|
||||
options.length
|
||||
? options.map((option) => ({
|
||||
label: option.label,
|
||||
value: String(option.value),
|
||||
id: uuidv4(),
|
||||
}))
|
||||
: [createEmptyItem()]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ignoreNextUpdate.current) {
|
||||
setItems(
|
||||
options.length
|
||||
? options.map((option) => ({
|
||||
label: option.label,
|
||||
value: String(option.value),
|
||||
id: uuidv4(),
|
||||
}))
|
||||
: [createEmptyItem()]
|
||||
);
|
||||
}
|
||||
|
||||
ignoreNextUpdate.current = false;
|
||||
}, [options]);
|
||||
|
||||
const updateItems = (items: VariableStaticOptionsFormItem[]) => {
|
||||
setItems(items);
|
||||
ignoreNextUpdate.current = true;
|
||||
onChange(
|
||||
items.reduce<VariableValueOption[]>((acc, item) => {
|
||||
const value = item.value.trim();
|
||||
|
||||
if (!allowEmptyValue && !value) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const label = item.label.trim();
|
||||
|
||||
if (!label && !value) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
label: label ? label : value,
|
||||
value,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
};
|
||||
|
||||
const handleAdd = () => setItems([...items, createEmptyItem()]);
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2} width={width}>
|
||||
<VariableStaticOptionsFormItems items={items} onChange={updateItems} width={width} />
|
||||
<div>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
onClick={handleAdd}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton
|
||||
}
|
||||
aria-label={t('variables.query-variable-static-options.add-option-button-label', 'Add option')}
|
||||
>
|
||||
<Trans i18nKey="variables.query-variable-static-options.add-option-button-label">Add option</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import { ChangeEventHandler } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Button, Icon, Input, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface VariableStaticOptionsFormItem {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface VariableStaticOptionsFormItemEditorProps {
|
||||
item: VariableStaticOptionsFormItem;
|
||||
index: number;
|
||||
onChange: (item: VariableStaticOptionsFormItem) => void;
|
||||
onRemove: (item: VariableStaticOptionsFormItem) => void;
|
||||
}
|
||||
|
||||
export function VariableStaticOptionsFormItemEditor({
|
||||
item,
|
||||
index,
|
||||
onChange,
|
||||
onRemove,
|
||||
}: VariableStaticOptionsFormItemEditorProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const handleValueChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
if (item.value !== evt.currentTarget.value) {
|
||||
onChange({ ...item, value: evt.currentTarget.value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleLabelChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
if (item.label !== evt.currentTarget.value) {
|
||||
onChange({ ...item, label: evt.currentTarget.value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = () => onRemove(item);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={item.id} index={index}>
|
||||
{(draggableProvided) => (
|
||||
<Stack
|
||||
ref={draggableProvided.innerRef}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow}
|
||||
{...draggableProvided.draggableProps}
|
||||
>
|
||||
<div {...draggableProvided.dragHandleProps}>
|
||||
<Icon
|
||||
title={t('variables.query-variable-static-options.drag-and-drop', 'Drag and drop to reorder')}
|
||||
name="draggabledots"
|
||||
size="lg"
|
||||
className={styles.dragIcon}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
value={item.value}
|
||||
placeholder={t('variables.query-variable-static-options.value-placeholder', 'Value')}
|
||||
onChange={handleValueChange}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={item.label}
|
||||
placeholder={t('variables.query-variable-static-options.label-placeholder', 'Label, defaults to value')}
|
||||
onChange={handleLabelChange}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="times"
|
||||
variant="secondary"
|
||||
aria-label={t('variables.query-variable-static-options.remove-option-button-label', 'Remove option')}
|
||||
onClick={handleRemove}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsDeleteButton
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
dragIcon: css({
|
||||
cursor: 'grab',
|
||||
color: theme.colors.text.disabled,
|
||||
|
||||
'&:hover': {
|
||||
color: theme.colors.text.primary,
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
import { Stack } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
VariableStaticOptionsFormItem,
|
||||
VariableStaticOptionsFormItemEditor,
|
||||
} from './VariableStaticOptionsFormItemEditor';
|
||||
|
||||
interface VariableStaticOptionsFormProps {
|
||||
items: VariableStaticOptionsFormItem[];
|
||||
onChange: (items: VariableStaticOptionsFormItem[]) => void;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function VariableStaticOptionsFormItems({ items, onChange, width }: VariableStaticOptionsFormProps) {
|
||||
const handleReorder = (result: DropResult) => {
|
||||
if (!result || !result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIdx = result.source.index;
|
||||
const endIdx = result.destination.index;
|
||||
|
||||
if (startIdx === endIdx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItems = [...items];
|
||||
const [removedItem] = newItems.splice(startIdx, 1);
|
||||
newItems.splice(endIdx, 0, removedItem);
|
||||
onChange(newItems);
|
||||
};
|
||||
|
||||
const handleChange = (item: VariableStaticOptionsFormItem) => {
|
||||
const idx = items.findIndex((currentItem) => currentItem.id === item.id);
|
||||
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOptions = [...items];
|
||||
newOptions[idx] = item;
|
||||
onChange(newOptions);
|
||||
};
|
||||
|
||||
const handleRemove = (item: VariableStaticOptionsFormItem) => {
|
||||
const newOptions = items.filter((currentItem) => currentItem.id !== item.id);
|
||||
onChange(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleReorder}>
|
||||
<Droppable droppableId="static-options-list" direction="vertical">
|
||||
{(droppableProvided) => (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={2}
|
||||
width={width}
|
||||
ref={droppableProvided.innerRef}
|
||||
{...droppableProvided.droppableProps}
|
||||
>
|
||||
{items.map((item, idx) => (
|
||||
<VariableStaticOptionsFormItemEditor
|
||||
item={item}
|
||||
index={idx}
|
||||
onChange={handleChange}
|
||||
onRemove={handleRemove}
|
||||
key={item.id}
|
||||
/>
|
||||
))}
|
||||
{droppableProvided.placeholder}
|
||||
</Stack>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
import { FormEvent } from 'react';
|
||||
import { FormEvent, useMemo, useState } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
import { TextArea } from '@grafana/ui';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable, VariableValueOption } from '@grafana/scenes';
|
||||
import { Box, Button, Modal, RadioButtonGroup, Stack, TextArea } from '@grafana/ui';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { CustomVariableForm } from '../components/CustomVariableForm';
|
||||
import { VariableStaticOptionsForm } from '../components/VariableStaticOptionsForm';
|
||||
import { VariableValuesPreview } from '../components/VariableValuesPreview';
|
||||
import { hasVariableOptions } from '../utils';
|
||||
|
||||
interface CustomVariableEditorProps {
|
||||
variable: CustomVariable;
|
||||
|
@ -59,11 +63,123 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
|
|||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: ({ props }) => <ValuesTextField id={props.id} variable={variable} />,
|
||||
render: ({ props }) => <ModalEditor id={props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export function ModalEditor({ variable, id }: { variable: CustomVariable; id?: string }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" direction="column" paddingBottom={1}>
|
||||
<Button
|
||||
tooltip={t(
|
||||
'dashboard.edit-pane.variable.open-editor-tooltip',
|
||||
'For more variable options open variable editor'
|
||||
)}
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="sm"
|
||||
fullWidth
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.optionsOpenButton}
|
||||
>
|
||||
<Trans i18nKey="dashboard.edit-pane.variable.open-editor">Open variable editor</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
<Modal
|
||||
title={t('dashboard.edit-pane.variable.custom-options.modal-title', 'Custom Variable')}
|
||||
isOpen={isOpen}
|
||||
onDismiss={() => setIsOpen(false)}
|
||||
>
|
||||
<Editor variable={variable} id={id} />
|
||||
<Modal.ButtonRow>
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="outline"
|
||||
onClick={() => setIsOpen(false)}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.closeButton}
|
||||
>
|
||||
<Trans i18nKey="dashboard.edit-pane.variable.custom-options.close">Close</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Editor({ variable, id }: { variable: CustomVariable; id?: string }) {
|
||||
// Workaround to toggle a component refresh when values change so that the preview is updated
|
||||
variable.useState();
|
||||
|
||||
const [editorType, setEditorType] = useState<'builder' | 'static'>('builder');
|
||||
|
||||
const editorTypeOptions: Array<SelectableValue<'builder' | 'static'>> = useMemo(
|
||||
() => [
|
||||
{ label: t('dashboard.edit-pane.variable.custom-options.editor-type.builder', 'Builder'), value: 'builder' },
|
||||
{ label: t('dashboard.edit-pane.variable.custom-options.editor-type.static', 'Static'), value: 'static' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const isHasVariableOptions = hasVariableOptions(variable);
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2}>
|
||||
<RadioButtonGroup
|
||||
value={editorType}
|
||||
options={editorTypeOptions}
|
||||
fullWidth
|
||||
onChange={(value) => setEditorType(value)}
|
||||
/>
|
||||
{editorType === 'builder' ? (
|
||||
<ValuesBuilder variable={variable} />
|
||||
) : (
|
||||
<ValuesTextField variable={variable} id={id} />
|
||||
)}
|
||||
{isHasVariableOptions && <VariableValuesPreview options={variable.getOptionsForSelect(false)} />}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function ValuesBuilder({ variable }: { variable: CustomVariable }) {
|
||||
const { query } = variable.useState();
|
||||
const match = useMemo(() => query.match(/(?:\\,|[^,])+/g) ?? [], [query]);
|
||||
const options = useMemo<VariableValueOption[]>(
|
||||
() =>
|
||||
match.map((text) => {
|
||||
text = text.replace(/\\,/g, ',');
|
||||
const textMatch = /^\s*(.+)\s:\s(.+)$/g.exec(text) ?? [];
|
||||
|
||||
if (textMatch.length === 3) {
|
||||
const [, label, value] = textMatch;
|
||||
return { label: label.trim(), value: value.trim() };
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
return { label: '', value: text };
|
||||
}),
|
||||
[match]
|
||||
);
|
||||
|
||||
const handleOptionsChange = async (options: VariableValueOption[]) => {
|
||||
variable.setState({
|
||||
query: options
|
||||
.map((option) => {
|
||||
if (!option.label || option.label === option.value) {
|
||||
return String(option.value).replaceAll(',', '\\,');
|
||||
}
|
||||
|
||||
return `${option.label.replaceAll(',', '\\,')} : ${String(option.value).replaceAll(',', '\\,')}`;
|
||||
})
|
||||
.join(', '),
|
||||
});
|
||||
await lastValueFrom(variable.validateAndUpdate!());
|
||||
};
|
||||
|
||||
return <VariableStaticOptionsForm options={options} onChange={handleOptionsChange} />;
|
||||
}
|
||||
|
||||
function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) {
|
||||
const { query } = variable.useState();
|
||||
|
||||
|
|
|
@ -14,7 +14,11 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat
|
|||
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
|
||||
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
||||
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
||||
import { StaticOptionsOrderType, StaticOptionsType } from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||
import {
|
||||
QueryVariableStaticOptions,
|
||||
StaticOptionsOrderType,
|
||||
StaticOptionsType,
|
||||
} from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||
|
||||
import { QueryVariableEditorForm } from '../components/QueryVariableForm';
|
||||
import { VariableTextAreaField } from '../components/VariableTextAreaField';
|
||||
|
@ -186,7 +190,15 @@ export function ModalEditor({ variable }: { variable: QueryVariable }) {
|
|||
}
|
||||
|
||||
export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
const { datasource: datasourceRef, sort, refresh, query, regex } = variable.useState();
|
||||
const {
|
||||
datasource: datasourceRef,
|
||||
sort,
|
||||
refresh,
|
||||
query,
|
||||
regex,
|
||||
staticOptions,
|
||||
staticOptionsOrder,
|
||||
} = variable.useState();
|
||||
const { value: timeRange } = sceneGraph.getTimeRange(variable).useState();
|
||||
const { value: dsConfig } = useAsync(async () => {
|
||||
const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
|
||||
|
@ -229,6 +241,12 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
|||
const onRefreshChange = (refresh: VariableRefresh) => {
|
||||
variable.setState({ refresh: refresh });
|
||||
};
|
||||
const onStaticOptionsChange = (staticOptions: StaticOptionsType) => {
|
||||
variable.setState({ staticOptions });
|
||||
};
|
||||
const onStaticOptionsOrderChange = (staticOptionsOrder: StaticOptionsOrderType) => {
|
||||
variable.setState({ staticOptionsOrder });
|
||||
};
|
||||
|
||||
const isHasVariableOptions = hasVariableOptions(variable);
|
||||
|
||||
|
@ -237,6 +255,7 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
|||
<Field
|
||||
label={t('dashboard-scene.query-variable-editor-form.label-target-data-source', 'Target data source')}
|
||||
htmlFor="data-source-picker"
|
||||
noMargin
|
||||
>
|
||||
<DataSourcePicker current={selectedDatasource} onChange={onDataSourceChange} variables={true} width={30} />
|
||||
</Field>
|
||||
|
@ -292,6 +311,15 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
|||
refresh={refresh}
|
||||
/>
|
||||
|
||||
{onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||
<QueryVariableStaticOptions
|
||||
staticOptions={staticOptions}
|
||||
staticOptionsOrder={staticOptionsOrder}
|
||||
onStaticOptionsChange={onStaticOptionsChange}
|
||||
onStaticOptionsOrderChange={onStaticOptionsOrderChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHasVariableOptions && <VariableValuesPreview options={variable.getOptionsForSelect(false)} />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,8 +5,8 @@ import { t, Trans } from '@grafana/i18n';
|
|||
import { QueryVariable } from '@grafana/scenes';
|
||||
import { Field, Stack, Switch } from '@grafana/ui';
|
||||
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
|
||||
import { VariableOptionsInput } from 'app/features/dashboard-scene/settings/variables/components/VariableOptionsInput';
|
||||
import { VariableSelectField } from 'app/features/dashboard-scene/settings/variables/components/VariableSelectField';
|
||||
import { VariableStaticOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/VariableStaticOptionsForm';
|
||||
|
||||
export type StaticOptionsType = QueryVariable['state']['staticOptions'];
|
||||
export type StaticOptionsOrderType = QueryVariable['state']['staticOptionsOrder'];
|
||||
|
@ -65,7 +65,12 @@ export function QueryVariableStaticOptions(props: QueryVariableStaticOptionsProp
|
|||
/>
|
||||
|
||||
{areStaticOptionsEnabled && (
|
||||
<VariableOptionsInput width={60} options={staticOptions ?? []} onChange={onStaticOptionsChange} />
|
||||
<VariableStaticOptionsForm
|
||||
allowEmptyValue
|
||||
width={60}
|
||||
options={staticOptions ?? []}
|
||||
onChange={onStaticOptionsChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
|
|
|
@ -4727,6 +4727,12 @@
|
|||
},
|
||||
"variable": {
|
||||
"custom-options": {
|
||||
"close": "Close",
|
||||
"editor-type": {
|
||||
"builder": "Builder",
|
||||
"static": "Static"
|
||||
},
|
||||
"modal-title": "Custom Variable",
|
||||
"values": "Values separated by comma",
|
||||
"values-placeholder": "1, 10, mykey : myvalue, myvalue, escaped,value"
|
||||
},
|
||||
|
@ -14100,9 +14106,10 @@
|
|||
"query-variable-static-options": {
|
||||
"add-option-button-label": "Add option",
|
||||
"description": "Add custom options in addition to query results",
|
||||
"label-placeholder": "display label",
|
||||
"drag-and-drop": "Drag and drop to reorder",
|
||||
"label-placeholder": "Label, defaults to value",
|
||||
"remove-option-button-label": "Remove option",
|
||||
"value-placeholder": "value, default empty string"
|
||||
"value-placeholder": "Value"
|
||||
},
|
||||
"query-variable-static-options-sort-select": {
|
||||
"description-values-variable": "How to sort static options with query results"
|
||||
|
|
Loading…
Reference in New Issue