feat: Update dynamic dashboards editors
This commit is contained in:
+42
-31
@@ -48,27 +48,7 @@ export function CustomVariableForm({
|
||||
<Trans i18nKey="dashboard-scene.custom-variable-form.custom-options">Custom options</Trans>
|
||||
</VariableLegend>
|
||||
|
||||
<Stack direction="row" gap={1}>
|
||||
<RadioButtonGroup
|
||||
value={valuesFormat}
|
||||
onChange={onValuesFormatChange}
|
||||
options={[
|
||||
{
|
||||
value: 'csv',
|
||||
label: t('dashboard-scene.custom-variable-form.name-values-separated-comma', 'Values separated by comma'),
|
||||
},
|
||||
{
|
||||
value: 'json',
|
||||
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>
|
||||
<ValuesFormatSelector valuesFormat={valuesFormat} onValuesFormatChange={onValuesFormatChange} />
|
||||
|
||||
<VariableTextAreaField
|
||||
// we don't use a controlled component so we make sure the textarea content is cleared when changing format by providing a key
|
||||
@@ -108,16 +88,47 @@ export function CustomVariableForm({
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipJsonFormat() {
|
||||
interface ValuesFormatSelectorProps {
|
||||
valuesFormat?: CustomVariableModel['valuesFormat'];
|
||||
onValuesFormatChange?: (format: CustomVariableModel['valuesFormat']) => void;
|
||||
}
|
||||
|
||||
export function ValuesFormatSelector({ valuesFormat, onValuesFormatChange }: ValuesFormatSelectorProps) {
|
||||
return (
|
||||
<Trans i18nKey="dashboard-scene.custom-variable-form.json-values-tooltip">
|
||||
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>
|
||||
<Stack direction="row" gap={1}>
|
||||
<RadioButtonGroup
|
||||
value={valuesFormat}
|
||||
onChange={onValuesFormatChange}
|
||||
options={[
|
||||
{
|
||||
value: 'csv',
|
||||
label: t('dashboard-scene.custom-variable-form.name-values-separated-comma', 'Values separated by comma'),
|
||||
},
|
||||
{
|
||||
value: 'json',
|
||||
label: t('dashboard-scene.custom-variable-form.name-json-values', 'Object values in a JSON array'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{valuesFormat === 'json' && (
|
||||
<Tooltip
|
||||
content={
|
||||
<Trans i18nKey="dashboard-scene.custom-variable-form.json-values-tooltip">
|
||||
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>
|
||||
}
|
||||
placement="top"
|
||||
interactive
|
||||
>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
+12
-1
@@ -36,7 +36,13 @@ function VariableValuesWithPropsPreview({ options }: { options: VariableValueOpt
|
||||
<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={10} />
|
||||
<InteractiveTable
|
||||
className={styles.table}
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={(r) => String(r.value)}
|
||||
pageSize={8}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -97,5 +103,10 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '50vw',
|
||||
}),
|
||||
table: css({
|
||||
td: css({
|
||||
padding: theme.spacing(0.5, 1),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+5
-2
@@ -23,6 +23,7 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
|
||||
const onValuesFormatChange = useCallback(
|
||||
(format: CustomVariableModel['valuesFormat']) => {
|
||||
variable.setState({ query: prevQuery });
|
||||
variable.setState({ value: isMulti ? [] : undefined });
|
||||
variable.setState({ valuesFormat: format });
|
||||
variable.setState({ allowCustomValue: false });
|
||||
variable.setState({ allValue: undefined });
|
||||
@@ -33,7 +34,7 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
|
||||
setPrevQuery(query);
|
||||
}
|
||||
},
|
||||
[onRunQuery, prevQuery, query, variable]
|
||||
[isMulti, onRunQuery, prevQuery, query, variable]
|
||||
);
|
||||
|
||||
const onMultiChange = useCallback(
|
||||
@@ -52,6 +53,8 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
(event: FormEvent<HTMLTextAreaElement>) => {
|
||||
setPrevQuery('');
|
||||
|
||||
if (valuesFormat === 'json') {
|
||||
const validationError = validateJsonQuery(event.currentTarget.value);
|
||||
setQueryValidationError(validationError);
|
||||
@@ -113,7 +116,7 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
|
||||
];
|
||||
}
|
||||
|
||||
const validateJsonQuery = (rawQuery: string): Error | undefined => {
|
||||
export const validateJsonQuery = (rawQuery: string): Error | undefined => {
|
||||
const query = rawQuery.trim();
|
||||
if (!query) {
|
||||
return;
|
||||
|
||||
+73
-11
@@ -1,15 +1,16 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { FormEvent, useCallback, useState } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { CustomVariableModel } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { CustomVariable } from '@grafana/scenes';
|
||||
import { Button, Modal, Stack } from '@grafana/ui';
|
||||
import { Button, FieldValidationMessage, Modal, Stack, TextArea } from '@grafana/ui';
|
||||
|
||||
import { VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm';
|
||||
import { VariableStaticOptionsFormAddButton } from '../../components/VariableStaticOptionsFormAddButton';
|
||||
import { ValuesFormatSelector } from '../../components/CustomVariableForm';
|
||||
import { VariableValuesPreview } from '../../components/VariableValuesPreview';
|
||||
|
||||
import { ValuesBuilder } from './ValuesBuilder';
|
||||
import { ValuesPreview } from './ValuesPreview';
|
||||
import { validateJsonQuery } from './CustomVariableEditor';
|
||||
|
||||
interface ModalEditorProps {
|
||||
variable: CustomVariable;
|
||||
@@ -18,9 +19,49 @@ interface ModalEditorProps {
|
||||
}
|
||||
|
||||
export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) {
|
||||
const formRef = useRef<VariableStaticOptionsFormRef | null>(null);
|
||||
const { query, valuesFormat, isMulti } = variable.useState();
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const [queryValidationError, setQueryValidationError] = useState<Error>();
|
||||
|
||||
const handleOnAdd = useCallback(() => formRef.current?.addItem(), []);
|
||||
const onValuesFormatChange = useCallback(
|
||||
async (format: CustomVariableModel['valuesFormat']) => {
|
||||
variable.setState({ query: prevQuery });
|
||||
variable.setState({ value: isMulti ? [] : undefined });
|
||||
variable.setState({ valuesFormat: format });
|
||||
variable.setState({ allowCustomValue: false });
|
||||
variable.setState({ allValue: undefined });
|
||||
|
||||
await lastValueFrom(variable.validateAndUpdate());
|
||||
|
||||
setQueryValidationError(undefined);
|
||||
if (query !== prevQuery) {
|
||||
setPrevQuery(query);
|
||||
}
|
||||
},
|
||||
[isMulti, prevQuery, query, variable]
|
||||
);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
async (event: FormEvent<HTMLTextAreaElement>) => {
|
||||
setPrevQuery('');
|
||||
|
||||
if (valuesFormat === 'json') {
|
||||
const validationError = validateJsonQuery(event.currentTarget.value);
|
||||
setQueryValidationError(validationError);
|
||||
if (validationError) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
variable.setState({ query: event.currentTarget.value });
|
||||
await lastValueFrom(variable.validateAndUpdate());
|
||||
},
|
||||
[valuesFormat, variable]
|
||||
);
|
||||
|
||||
const optionsForSelect = variable.getOptionsForSelect(false);
|
||||
const hasJsonValuesFormat = variable.state.valuesFormat === 'json';
|
||||
const hasMultiProps = hasJsonValuesFormat || optionsForSelect.every((o) => Boolean(o.properties));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -29,10 +70,31 @@ export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) {
|
||||
onDismiss={onClose}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
<ValuesBuilder variable={variable} ref={formRef} />
|
||||
<ValuesPreview variable={variable} />
|
||||
<ValuesFormatSelector valuesFormat={valuesFormat} onValuesFormatChange={onValuesFormatChange} />
|
||||
<div>
|
||||
<TextArea
|
||||
id={valuesFormat}
|
||||
key={valuesFormat}
|
||||
rows={4}
|
||||
defaultValue={query}
|
||||
onBlur={onQueryChange}
|
||||
placeholder={
|
||||
valuesFormat === 'json'
|
||||
? // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
'[{ "text":"text1", "value":"val1", "propA":"a1", "propB":"b1" },\n{ "text":"text2", "value":"val2", "propA":"a2", "propB":"b2" }]'
|
||||
: // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
'1, 10, mykey : myvalue, myvalue, escaped\,value'
|
||||
}
|
||||
required
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
||||
/>
|
||||
{queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>}
|
||||
</div>
|
||||
<div>
|
||||
<VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />
|
||||
</div>
|
||||
</Stack>
|
||||
<Modal.ButtonRow leftItems={<VariableStaticOptionsFormAddButton onAdd={handleOnAdd} />}>
|
||||
<Modal.ButtonRow>
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="outline"
|
||||
|
||||
-2
@@ -1,4 +1,3 @@
|
||||
import { t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
|
||||
import { OptionsPaneItemDescriptor } from '../../../../../dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
@@ -12,7 +11,6 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: ({ props }) => <PaneItem id={props.id} variable={variable} />,
|
||||
}),
|
||||
|
||||
+72
-38
@@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { QueryVariable, sceneGraph, SceneVariable } from '@grafana/scenes';
|
||||
import { VariableRefresh, VariableSort } from '@grafana/schema';
|
||||
import { Box, Button, Field, Modal, TextLink } from '@grafana/ui';
|
||||
import { Box, Button, Field, Modal, Switch, TextLink } from '@grafana/ui';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
@@ -250,6 +250,19 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
|
||||
const isHasVariableOptions = hasVariableOptions(variable);
|
||||
|
||||
// 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);
|
||||
variable.setState({ allowCustomValue: false });
|
||||
variable.setState({ allValue: '' });
|
||||
variable.setState({ regex: '' });
|
||||
onStaticOptionsChange?.([]);
|
||||
};
|
||||
|
||||
const optionsForSelect = variable.getOptionsForSelect(false);
|
||||
const hasMultiProps = returnsMultiProps || optionsForSelect.every((o) => Boolean(o.properties));
|
||||
|
||||
return (
|
||||
<div data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.editor}>
|
||||
<Field
|
||||
@@ -261,43 +274,64 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
</Field>
|
||||
|
||||
{selectedDatasource && VariableQueryEditor && (
|
||||
<QueryEditor
|
||||
onQueryChange={onQueryChange}
|
||||
onLegacyQueryChange={onQueryChange}
|
||||
datasource={selectedDatasource}
|
||||
query={query}
|
||||
VariableQueryEditor={VariableQueryEditor}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<Box marginBottom={2}>
|
||||
<QueryEditor
|
||||
onQueryChange={onQueryChange}
|
||||
onLegacyQueryChange={onQueryChange}
|
||||
datasource={selectedDatasource}
|
||||
query={query}
|
||||
VariableQueryEditor={VariableQueryEditor}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
{/* TODO: remove me after finished testing - each DS can/should implement their own UI */}
|
||||
<Field
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
label="Enable access to all the fields of the query results"
|
||||
description={
|
||||
<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}
|
||||
@@ -311,7 +345,7 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
refresh={refresh}
|
||||
/>
|
||||
|
||||
{onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||
{!returnsMultiProps && onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||
<QueryVariableStaticOptions
|
||||
staticOptions={staticOptions}
|
||||
staticOptionsOrder={staticOptionsOrder}
|
||||
@@ -320,7 +354,7 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHasVariableOptions && <VariableValuesPreview options={variable.getOptionsForSelect(false)} />}
|
||||
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-1
@@ -45,7 +45,9 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
|
||||
'A wildcard regex or other value to represent All'
|
||||
),
|
||||
useShowIf: () => {
|
||||
return variable.useState().includeAll ?? false;
|
||||
const state = variable.useState();
|
||||
const hasJsonValuesFormat = 'valuesFormat' in state && state.valuesFormat === 'json';
|
||||
return hasJsonValuesFormat ? false : (state.includeAll ?? false);
|
||||
},
|
||||
render: (descriptor) => <CustomAllValueInput id={descriptor.props.id} variable={variable} />,
|
||||
})
|
||||
@@ -58,6 +60,11 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
|
||||
'dashboard.edit-pane.variable.selection-options.allow-custom-values-description',
|
||||
'Enables users to enter values'
|
||||
),
|
||||
useShowIf: () => {
|
||||
const state = variable.useState();
|
||||
const hasJsonValuesFormat = 'valuesFormat' in state && state.valuesFormat === 'json';
|
||||
return !hasJsonValuesFormat;
|
||||
},
|
||||
render: (descriptor) => <AllowCustomSwitch id={descriptor.props.id} variable={variable} />,
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user