mirror of https://github.com/grafana/grafana
QueryVariable: Add static options input (#107514)
* static options for query variable * add toggle * fix and add tests * run the hack codegen thing * more test fixes * make betterer happier * also make typecheck happy * make betterer happier * fix i18n key * tranalte static variables sort label * gen translations * update snapshotpull/106511/head
parent
baa89f3eac
commit
8eef17cb37
@ -0,0 +1,106 @@ |
||||
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,93 @@ |
||||
import { useState } from 'react'; |
||||
|
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
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'; |
||||
|
||||
export type StaticOptionsType = QueryVariable['state']['staticOptions']; |
||||
export type StaticOptionsOrderType = QueryVariable['state']['staticOptionsOrder']; |
||||
|
||||
interface QueryVariableStaticOptionsProps { |
||||
staticOptions: StaticOptionsType; |
||||
staticOptionsOrder: StaticOptionsOrderType; |
||||
onStaticOptionsChange: (staticOptions: StaticOptionsType) => void; |
||||
onStaticOptionsOrderChange: (staticOptionsOrder: StaticOptionsOrderType) => void; |
||||
} |
||||
|
||||
const SORT_OPTIONS = [ |
||||
{ label: 'Before query values', value: 'before' }, |
||||
{ label: 'After query values', value: 'after' }, |
||||
{ label: 'Sorted with query values', value: 'sorted' }, |
||||
]; |
||||
|
||||
export function QueryVariableStaticOptions(props: QueryVariableStaticOptionsProps) { |
||||
const { staticOptions, onStaticOptionsChange, staticOptionsOrder, onStaticOptionsOrderChange } = props; |
||||
|
||||
const value = SORT_OPTIONS.find((o) => o.value === staticOptionsOrder) ?? SORT_OPTIONS[0]; |
||||
|
||||
const [areStaticOptionsEnabled, setAreStaticOptionsEnabled] = useState(!!staticOptions?.length); |
||||
|
||||
return ( |
||||
<> |
||||
<VariableLegend> |
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.static-options-legend">Static options</Trans> |
||||
</VariableLegend> |
||||
<Stack direction="column" gap={2}> |
||||
<Field |
||||
noMargin |
||||
label={t('dashboard-scene.query-variable-editor-form.label-use-static-options', 'Use static options')} |
||||
description={t( |
||||
'variables.query-variable-static-options.description', |
||||
'Add custom options in addition to query results' |
||||
)} |
||||
> |
||||
<> |
||||
<Stack direction="column" gap={2}> |
||||
<Switch |
||||
data-testid={ |
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle |
||||
} |
||||
value={areStaticOptionsEnabled} |
||||
onChange={(e) => { |
||||
if (e.currentTarget.checked) { |
||||
setAreStaticOptionsEnabled(true); |
||||
} else { |
||||
setAreStaticOptionsEnabled(false); |
||||
if (!!staticOptions?.length) { |
||||
onStaticOptionsChange(undefined); |
||||
} |
||||
} |
||||
}} |
||||
/> |
||||
|
||||
{areStaticOptionsEnabled && ( |
||||
<VariableOptionsInput width={60} options={staticOptions ?? []} onChange={onStaticOptionsChange} /> |
||||
)} |
||||
</Stack> |
||||
</> |
||||
</Field> |
||||
|
||||
{areStaticOptionsEnabled && ( |
||||
<VariableSelectField |
||||
name={t('dashboard-scene.query-variable-editor-form.label-static-options-sort', 'Static options sort')} |
||||
description={t( |
||||
'variables.query-variable-static-options-sort-select.description-values-variable', |
||||
'How to sort static options with query results' |
||||
)} |
||||
value={value} |
||||
options={SORT_OPTIONS} |
||||
onChange={(opt) => onStaticOptionsOrderChange(opt.value)} |
||||
testId={ |
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown |
||||
} |
||||
width={25} |
||||
/> |
||||
)} |
||||
</Stack> |
||||
</> |
||||
); |
||||
} |
Loading…
Reference in new issue