feat: WiP
This commit is contained in:
@@ -113,7 +113,7 @@ DashboardLink: {
|
||||
placement?: DashboardLinkPlacement
|
||||
}
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
DashboardLinkPlacement: "inControlsMenu"
|
||||
|
||||
@@ -376,7 +376,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -386,7 +386,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
@@ -903,6 +903,7 @@ CustomVariableSpec: {
|
||||
skipUrlSync: bool | *false
|
||||
description?: string
|
||||
allowCustomValue: bool | *true
|
||||
valuesFormat?: string
|
||||
}
|
||||
|
||||
// Custom variable kind
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface IntervalVariableModel extends VariableWithOptions {
|
||||
|
||||
export interface CustomVariableModel extends VariableWithMultiSupport {
|
||||
type: 'custom';
|
||||
valuesFormat?: 'csv' | 'json';
|
||||
}
|
||||
|
||||
export interface DataSourceVariableModel extends VariableWithMultiSupport {
|
||||
|
||||
@@ -111,6 +111,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
|
||||
allValue: variable.state.allValue,
|
||||
includeAll: variable.state.includeAll,
|
||||
allowCustomValue: variable.state.allowCustomValue,
|
||||
valuesFormat: variable.state.valuesFormat,
|
||||
});
|
||||
} else if (sceneUtils.isDataSourceVariable(variable)) {
|
||||
variables.push({
|
||||
@@ -393,6 +394,7 @@ export function sceneVariablesSetToSchemaV2Variables(
|
||||
allValue: variable.state.allValue,
|
||||
includeAll: variable.state.includeAll ?? false,
|
||||
allowCustomValue: variable.state.allowCustomValue ?? true,
|
||||
valuesFormat: variable.state.valuesFormat,
|
||||
},
|
||||
};
|
||||
variables.push(customVariable);
|
||||
|
||||
+5
-1
@@ -321,12 +321,12 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
|
||||
supportsMultiValueOperators: Boolean(getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators),
|
||||
});
|
||||
}
|
||||
|
||||
if (variable.kind === defaultCustomVariableKind().kind) {
|
||||
return new CustomVariable({
|
||||
...commonProperties,
|
||||
value: variable.spec.current?.value ?? '',
|
||||
text: variable.spec.current?.text ?? '',
|
||||
|
||||
query: variable.spec.query,
|
||||
isMulti: variable.spec.multi,
|
||||
allValue: variable.spec.allValue || undefined,
|
||||
@@ -334,6 +334,10 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
|
||||
defaultToAll: Boolean(variable.spec.includeAll),
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
valuesFormat:
|
||||
variable.spec.valuesFormat === 'csv' || variable.spec.valuesFormat === 'json'
|
||||
? variable.spec.valuesFormat
|
||||
: undefined,
|
||||
});
|
||||
} else if (variable.kind === defaultQueryVariableKind().kind) {
|
||||
return new QueryVariable({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { SceneVariable } from '@grafana/scenes';
|
||||
import { VariableHide, defaultVariableModel } from '@grafana/schema';
|
||||
import { Button, LoadingPlaceholder, ConfirmModal, ModalsController, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Button, ConfirmModal, LoadingPlaceholder, ModalsController, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { VariableHideSelect } from 'app/features/dashboard-scene/settings/variables/components/VariableHideSelect';
|
||||
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
|
||||
import { VariableTextAreaField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextAreaField';
|
||||
@@ -123,7 +123,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
|
||||
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
|
||||
|
||||
{isHasVariableOptions && <VariableValuesPreview options={variable.getOptionsForSelect(false)} />}
|
||||
{isHasVariableOptions && <VariableValuesPreview variable={variable} />}
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Stack gap={2}>
|
||||
|
||||
+114
-5
@@ -1,7 +1,10 @@
|
||||
import { FormEvent } from 'react';
|
||||
import { isObject } from 'lodash';
|
||||
import { FormEvent, useState } from 'react';
|
||||
|
||||
import { CustomVariableModel, shallowCompare } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { FieldValidationMessage, Icon, RadioButtonGroup, Stack, TextLink, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { SelectionOptionsForm } from './SelectionOptionsForm';
|
||||
import { VariableLegend } from './VariableLegend';
|
||||
@@ -9,6 +12,7 @@ import { VariableTextAreaField } from './VariableTextAreaField';
|
||||
|
||||
interface CustomVariableFormProps {
|
||||
query: string;
|
||||
valuesFormat?: CustomVariableModel['valuesFormat'];
|
||||
multi: boolean;
|
||||
allValue?: string | null;
|
||||
includeAll: boolean;
|
||||
@@ -20,10 +24,12 @@ interface CustomVariableFormProps {
|
||||
onQueryBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||
onAllValueBlur?: (event: FormEvent<HTMLInputElement>) => void;
|
||||
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
|
||||
onValuesFormatChange?: (format: CustomVariableModel['valuesFormat']) => void;
|
||||
}
|
||||
|
||||
export function CustomVariableForm({
|
||||
query,
|
||||
valuesFormat,
|
||||
multi,
|
||||
allValue,
|
||||
includeAll,
|
||||
@@ -33,23 +39,70 @@ export function CustomVariableForm({
|
||||
onIncludeAllChange,
|
||||
onAllValueChange,
|
||||
onAllowCustomValueChange,
|
||||
onValuesFormatChange,
|
||||
}: CustomVariableFormProps) {
|
||||
const [validationError, setValidationError] = useState<Error>();
|
||||
|
||||
const onChangeFormat = (newFormat: CustomVariableModel['valuesFormat']) => {
|
||||
onValuesFormatChange?.(newFormat);
|
||||
setValidationError(undefined);
|
||||
};
|
||||
|
||||
const onQueryBlur = (e: FormEvent<HTMLTextAreaElement>) => {
|
||||
if (valuesFormat === 'json') {
|
||||
setValidationError(validateJsonQuery(e.currentTarget.value));
|
||||
}
|
||||
onQueryChange(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<VariableLegend>
|
||||
<Trans i18nKey="dashboard-scene.custom-variable-form.custom-options">Custom options</Trans>
|
||||
</VariableLegend>
|
||||
|
||||
<Stack direction="row" gap={1}>
|
||||
<RadioButtonGroup
|
||||
value={valuesFormat}
|
||||
onChange={onChangeFormat}
|
||||
options={[
|
||||
{
|
||||
value: 'csv',
|
||||
label: t('dashboard-scene.custom-variable-form.name-values-separated-comma', 'Values separated by comma'),
|
||||
},
|
||||
{
|
||||
value: 'json',
|
||||
// TODO: add translation
|
||||
label: t('dashboard-scene.custom-variable-form.name-json-values', 'Object values in a JSON array'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{valuesFormat === 'json' && (
|
||||
<Tooltip content={TooltipJsonFormat} placement="top" interactive>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<VariableTextAreaField
|
||||
name={t('dashboard-scene.custom-variable-form.name-values-separated-comma', 'Values separated by comma')}
|
||||
// we don't use a controlled component so we make sure the textarea content is cleared when changing format by providing a key
|
||||
key={valuesFormat}
|
||||
name=""
|
||||
placeholder={
|
||||
valuesFormat === 'json'
|
||||
? // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
'[{ "text": "text1", "propA": "a1", "propB": "b1" },\n{ "text": "text2", "propA": "a2", "propB": "b2" }]'
|
||||
: // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
'1, 10, mykey : myvalue, myvalue, escaped\,value'
|
||||
}
|
||||
defaultValue={query}
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
placeholder="1, 10, mykey : myvalue, myvalue, escaped\,value"
|
||||
onBlur={onQueryChange}
|
||||
onBlur={onQueryBlur}
|
||||
required
|
||||
width={52}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
||||
/>
|
||||
{validationError && <FieldValidationMessage>{validationError.message}</FieldValidationMessage>}
|
||||
|
||||
<VariableLegend>
|
||||
<Trans i18nKey="dashboard-scene.custom-variable-form.selection-options">Selection options</Trans>
|
||||
</VariableLegend>
|
||||
@@ -58,6 +111,8 @@ export function CustomVariableForm({
|
||||
includeAll={includeAll}
|
||||
allValue={allValue}
|
||||
allowCustomValue={allowCustomValue}
|
||||
disableAllowCustomValue={valuesFormat === 'json'}
|
||||
disableCustomAllValue={valuesFormat === 'json'}
|
||||
onMultiChange={onMultiChange}
|
||||
onIncludeAllChange={onIncludeAllChange}
|
||||
onAllValueChange={onAllValueChange}
|
||||
@@ -66,3 +121,57 @@ export function CustomVariableForm({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipJsonFormat() {
|
||||
return (
|
||||
// TODO: add translation
|
||||
<Trans i18nKey="">
|
||||
Provide a JSON representing an array of objects, where each object can have any number of properties.
|
||||
<br />
|
||||
Check{' '}
|
||||
<TextLink href="https://grafana.com/docs/grafana/latest/variables/xxx" external>
|
||||
our docs
|
||||
</TextLink>{' '}
|
||||
for more information.
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
|
||||
const validateJsonQuery = (rawQuwey: string): Error | undefined => {
|
||||
const query = rawQuwey.trim();
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const options = JSON.parse(query);
|
||||
|
||||
if (!Array.isArray(options)) {
|
||||
throw new Error('Invalid JSON array!');
|
||||
}
|
||||
|
||||
if (!options.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Object.keys(options[0]);
|
||||
if (!keys.length || !keys.some((k) => k === 'value')) {
|
||||
throw new Error('The objects in the array must have at least a "value" key!');
|
||||
}
|
||||
|
||||
for (let i = 0; i < options.length; i += 1) {
|
||||
if (!isObject(options[i])) {
|
||||
throw new Error(`All items in the array must be objects. Item at index=${i} is incorrect!`);
|
||||
}
|
||||
|
||||
if (!shallowCompare(keys, Object.keys(options[i]))) {
|
||||
throw new Error(`All objects in the array must have the same keys. Object at index=${i} is incorrect!`);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return error as Error;
|
||||
}
|
||||
};
|
||||
|
||||
+73
-39
@@ -1,4 +1,4 @@
|
||||
import { FormEvent } from 'react';
|
||||
import { FormEvent, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data';
|
||||
@@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { QueryVariable } from '@grafana/scenes';
|
||||
import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema';
|
||||
import { Field, TextLink } from '@grafana/ui';
|
||||
import { Box, Field, Switch, TextLink } from '@grafana/ui';
|
||||
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
|
||||
import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
@@ -15,9 +15,9 @@ import { getVariableQueryEditor } from 'app/features/variables/editor/getVariabl
|
||||
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
||||
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
||||
import {
|
||||
QueryVariableStaticOptions,
|
||||
StaticOptionsOrderType,
|
||||
StaticOptionsType,
|
||||
QueryVariableStaticOptions,
|
||||
} from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||
|
||||
import { VariableLegend } from './VariableLegend';
|
||||
@@ -94,6 +94,16 @@ export function QueryVariableEditorForm({
|
||||
|
||||
const { datasource, VariableQueryEditor } = dsConfig ?? {};
|
||||
|
||||
// TODO: remove me after finished testing - each DS can/should implement their own UI
|
||||
const [returnsMultiProps, setReturnsMultiProps] = useState(false);
|
||||
const onChangeReturnsMultipleProps = (e: FormEvent<HTMLInputElement>) => {
|
||||
setReturnsMultiProps(e.currentTarget.checked);
|
||||
onAllowCustomValueChange?.({ currentTarget: { checked: false } });
|
||||
onAllValueChange({ currentTarget: { value: '' } });
|
||||
onRegExChange({ currentTarget: { value: '' } });
|
||||
onStaticOptionsChange?.([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<VariableLegend>
|
||||
@@ -102,48 +112,70 @@ export function QueryVariableEditorForm({
|
||||
<Field
|
||||
label={t('dashboard-scene.query-variable-editor-form.label-data-source', 'Data source')}
|
||||
htmlFor="data-source-picker"
|
||||
noMargin
|
||||
>
|
||||
<DataSourcePicker current={datasourceRef} onChange={onDataSourceChange} variables={true} width={30} />
|
||||
</Field>
|
||||
|
||||
{datasource && VariableQueryEditor && (
|
||||
<QueryEditor
|
||||
onQueryChange={onQueryChange}
|
||||
onLegacyQueryChange={onLegacyQueryChange}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
VariableQueryEditor={VariableQueryEditor}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<Box marginBottom={2}>
|
||||
<QueryEditor
|
||||
onQueryChange={onQueryChange}
|
||||
onLegacyQueryChange={onLegacyQueryChange}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
VariableQueryEditor={VariableQueryEditor}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<Field
|
||||
// TODO: add translation
|
||||
label="Enable access to all the fields of the query results"
|
||||
description={
|
||||
// TODO: add translation
|
||||
<Trans i18nKey="">
|
||||
Check{' '}
|
||||
<TextLink href="https://grafana.com/docs/grafana/latest/variables/xxx" external>
|
||||
our docs
|
||||
</TextLink>{' '}
|
||||
for more information.
|
||||
</Trans>
|
||||
}
|
||||
noMargin
|
||||
>
|
||||
<Switch onChange={onChangeReturnsMultipleProps} />
|
||||
</Field>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<VariableTextAreaField
|
||||
defaultValue={regex ?? ''}
|
||||
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
|
||||
description={
|
||||
<div>
|
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
|
||||
Optional, if you want to extract part of a series name or metric node segment.
|
||||
</Trans>
|
||||
<br />
|
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
|
||||
Named capture groups can be used to separate the display text and value (
|
||||
<TextLink
|
||||
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
||||
external
|
||||
>
|
||||
see examples
|
||||
</TextLink>
|
||||
).
|
||||
</Trans>
|
||||
</div>
|
||||
}
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
||||
onBlur={onRegExChange}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
||||
width={52}
|
||||
/>
|
||||
{!returnsMultiProps && (
|
||||
<VariableTextAreaField
|
||||
defaultValue={regex ?? ''}
|
||||
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
|
||||
description={
|
||||
<div>
|
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
|
||||
Optional, if you want to extract part of a series name or metric node segment.
|
||||
</Trans>
|
||||
<br />
|
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
|
||||
Named capture groups can be used to separate the display text and value (
|
||||
<TextLink
|
||||
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
||||
external
|
||||
>
|
||||
see examples
|
||||
</TextLink>
|
||||
).
|
||||
</Trans>
|
||||
</div>
|
||||
}
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
||||
onBlur={onRegExChange}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
||||
width={52}
|
||||
/>
|
||||
)}
|
||||
|
||||
<QueryVariableSortSelect
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
|
||||
@@ -157,7 +189,7 @@ export function QueryVariableEditorForm({
|
||||
refresh={refresh}
|
||||
/>
|
||||
|
||||
{onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||
{!returnsMultiProps && onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||
<QueryVariableStaticOptions
|
||||
staticOptions={staticOptions}
|
||||
staticOptionsOrder={staticOptionsOrder}
|
||||
@@ -173,6 +205,8 @@ export function QueryVariableEditorForm({
|
||||
multi={!!isMulti}
|
||||
includeAll={!!includeAll}
|
||||
allowCustomValue={allowCustomValue}
|
||||
disableAllowCustomValue={returnsMultiProps}
|
||||
disableCustomAllValue={returnsMultiProps}
|
||||
allValue={allValue}
|
||||
onMultiChange={onMultiChange}
|
||||
onIncludeAllChange={onIncludeAllChange}
|
||||
|
||||
+18
-13
@@ -10,7 +10,9 @@ interface SelectionOptionsFormProps {
|
||||
multi: boolean;
|
||||
includeAll: boolean;
|
||||
allowCustomValue?: boolean;
|
||||
disableAllowCustomValue?: boolean;
|
||||
allValue?: string | null;
|
||||
disableCustomAllValue?: boolean;
|
||||
onMultiChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onAllowCustomValueChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onIncludeAllChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
@@ -20,8 +22,10 @@ interface SelectionOptionsFormProps {
|
||||
export function SelectionOptionsForm({
|
||||
multi,
|
||||
allowCustomValue,
|
||||
disableAllowCustomValue,
|
||||
includeAll,
|
||||
allValue,
|
||||
disableCustomAllValue,
|
||||
onMultiChange,
|
||||
onAllowCustomValueChange,
|
||||
onIncludeAllChange,
|
||||
@@ -39,18 +43,19 @@ export function SelectionOptionsForm({
|
||||
onChange={onMultiChange}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}
|
||||
/>
|
||||
{onAllowCustomValueChange && ( // backwards compat with old arch, remove on cleanup
|
||||
<VariableCheckboxField
|
||||
value={allowCustomValue ?? true}
|
||||
name={t('dashboard-scene.selection-options-form.name-allow-custom-values', 'Allow custom values')}
|
||||
description={t(
|
||||
'dashboard-scene.selection-options-form.description-enables-users-custom-values',
|
||||
'Enables users to add custom values to the list'
|
||||
)}
|
||||
onChange={onAllowCustomValueChange}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch}
|
||||
/>
|
||||
)}
|
||||
{!disableAllowCustomValue &&
|
||||
onAllowCustomValueChange && ( // backwards compat with old arch, remove on cleanup
|
||||
<VariableCheckboxField
|
||||
value={allowCustomValue ?? true}
|
||||
name={t('dashboard-scene.selection-options-form.name-allow-custom-values', 'Allow custom values')}
|
||||
description={t(
|
||||
'dashboard-scene.selection-options-form.description-enables-users-custom-values',
|
||||
'Enables users to add custom values to the list'
|
||||
)}
|
||||
onChange={onAllowCustomValueChange}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch}
|
||||
/>
|
||||
)}
|
||||
<VariableCheckboxField
|
||||
value={includeAll}
|
||||
name={t('dashboard-scene.selection-options-form.name-include-all-option', 'Include All option')}
|
||||
@@ -61,7 +66,7 @@ export function SelectionOptionsForm({
|
||||
onChange={onIncludeAllChange}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}
|
||||
/>
|
||||
{includeAll && (
|
||||
{!disableCustomAllValue && includeAll && (
|
||||
<VariableTextField
|
||||
defaultValue={allValue ?? ''}
|
||||
onBlur={onAllValueChange}
|
||||
|
||||
+39
-14
@@ -4,14 +4,44 @@ import { MouseEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { VariableValueOption } from '@grafana/scenes';
|
||||
import { Button, InlineFieldRow, InlineLabel, useStyles2, Text } from '@grafana/ui';
|
||||
import { MultiValueVariable, VariableValueOption } from '@grafana/scenes';
|
||||
import { Button, InlineFieldRow, InlineLabel, InteractiveTable, Text, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface VariableValuesPreviewProps {
|
||||
options: VariableValueOption[];
|
||||
export interface Props {
|
||||
variable: MultiValueVariable;
|
||||
}
|
||||
|
||||
export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) => {
|
||||
export const VariableValuesPreview = ({ variable }: Props) => {
|
||||
const options = variable.getOptionsForSelect(false);
|
||||
if (!options.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('valuesFormat' in variable.state && variable.state.valuesFormat === 'json') {
|
||||
return <VariableValuesWithPropsPreview options={options} />;
|
||||
}
|
||||
|
||||
return <VariableValuesWithoutPropsPreview options={options} />;
|
||||
};
|
||||
VariableValuesPreview.displayName = 'VariableValuesPreview';
|
||||
|
||||
function VariableValuesWithPropsPreview({ options }: { options: VariableValueOption[] }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const data = options.map((o) => ({ label: String(o.label), value: String(o.value), ...o.properties }));
|
||||
const columns = Object.keys(data[0]).map((id) => ({ id, header: id, sortType: 'alphanumeric' as const }));
|
||||
|
||||
return (
|
||||
<div className={styles.previewContainer} style={{ gap: '8px' }}>
|
||||
<Text variant="bodySmall" weight="medium">
|
||||
<Trans i18nKey="dashboard-scene.variable-values-preview.preview-of-values">Preview of values</Trans>
|
||||
</Text>
|
||||
<InteractiveTable columns={columns} data={data} getRowId={(r) => String(r.value)} pageSize={2} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VariableValuesWithoutPropsPreview({ options }: { options: VariableValueOption[] }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [previewLimit, setPreviewLimit] = useState(20);
|
||||
const [previewOptions, setPreviewOptions] = useState<VariableValueOption[]>([]);
|
||||
const showMoreOptions = useCallback(
|
||||
@@ -21,15 +51,10 @@ export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) =
|
||||
},
|
||||
[previewLimit, setPreviewLimit]
|
||||
);
|
||||
const styles = useStyles2(getStyles);
|
||||
useEffect(() => setPreviewOptions(options.slice(0, previewLimit)), [previewLimit, options]);
|
||||
|
||||
if (!previewOptions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: '16px' }}>
|
||||
<div className={styles.previewContainer}>
|
||||
<Text variant="bodySmall" weight="medium">
|
||||
<Trans i18nKey="dashboard-scene.variable-values-preview.preview-of-values">Preview of values</Trans>
|
||||
</Text>
|
||||
@@ -51,12 +76,12 @@ export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) =
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
VariableValuesPreview.displayName = 'VariableValuesPreview';
|
||||
}
|
||||
VariableValuesWithoutPropsPreview.displayName = 'VariableValuesWithoutPropsPreview';
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
wrapper: css({
|
||||
previewContainer: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: theme.spacing(2),
|
||||
|
||||
+21
-2
@@ -1,5 +1,6 @@
|
||||
import { FormEvent, useCallback } from 'react';
|
||||
import { FormEvent, useCallback, useState } from 'react';
|
||||
|
||||
import { CustomVariableModel } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
|
||||
@@ -14,7 +15,23 @@ interface CustomVariableEditorProps {
|
||||
}
|
||||
|
||||
export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) {
|
||||
const { query, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
|
||||
const { query, valuesFormat, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
|
||||
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const onValuesFormatChange = useCallback(
|
||||
(format: CustomVariableModel['valuesFormat']) => {
|
||||
variable.setState({ valuesFormat: format });
|
||||
variable.setState({ allowCustomValue: false });
|
||||
variable.setState({ allValue: undefined });
|
||||
|
||||
variable.setState({ query: prevQuery });
|
||||
if (query !== prevQuery) {
|
||||
setPrevQuery(query);
|
||||
}
|
||||
onRunQuery();
|
||||
},
|
||||
[onRunQuery, prevQuery, query, variable]
|
||||
);
|
||||
|
||||
const onMultiChange = useCallback(
|
||||
(event: FormEvent<HTMLInputElement>) => {
|
||||
@@ -55,6 +72,7 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
|
||||
return (
|
||||
<CustomVariableForm
|
||||
query={query ?? ''}
|
||||
valuesFormat={valuesFormat ?? 'csv'}
|
||||
multi={!!isMulti}
|
||||
allValue={allValue ?? ''}
|
||||
includeAll={!!includeAll}
|
||||
@@ -64,6 +82,7 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
|
||||
onQueryChange={onQueryChange}
|
||||
onAllValueChange={onAllValueChange}
|
||||
onAllowCustomValueChange={onAllowCustomValueChange}
|
||||
onValuesFormatChange={onValuesFormatChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export function getLocalVariableValueSet(
|
||||
name: variable.state.name,
|
||||
value,
|
||||
text,
|
||||
properties: variable.state.options.find((o) => o.value === value)?.properties,
|
||||
isMulti: variable.state.isMulti,
|
||||
includeAll: variable.state.includeAll,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user