EditVariable: Add base form to edit variables (#80172)

This commit is contained in:
Ivan Ortega Alba 2024-01-12 11:01:11 +01:00 committed by GitHub
parent e69feef314
commit e9f1b41d23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 856 additions and 76 deletions

View File

@ -2466,6 +2466,12 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"]
],
"public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/dashboard-scene/settings/variables/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/dashboard-scene/utils/DashboardModelCompatibilityWrapper.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
@ -4559,16 +4565,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "9"],
[0, 0, 0, "Styles should be written using objects.", "10"]
],
"public/app/features/variables/editor/VariableSelectField.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/features/variables/editor/VariableTextAreaField.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/variables/editor/VariableValuesPreview.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/variables/editor/getVariableQueryEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]

View File

@ -141,7 +141,7 @@ export const Pages = {
selectionOptionsIncludeAllSwitch: 'Variable editor Form IncludeAll switch',
selectionOptionsCustomAllInput: 'Variable editor Form IncludeAll field',
selectionOptionsCustomAllInputV2: 'data-testid Variable editor Form IncludeAll field',
previewOfValuesOption: 'Variable editor Preview of Values option',
previewOfValuesOption: 'data-testid Variable editor Preview of Values option',
submitButton: 'Variable editor Submit button',
applyButton: 'data-testid Variable editor Apply button',
},

View File

@ -1,9 +1,12 @@
import { SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes';
import { DashboardLinksEditView, DashboardLinksEditViewState } from './DashboardLinksEditView';
import { VariablesEditView, VariablesEditViewState } from './VariablesEditView';
type EditListViewUrlSync = DashboardLinksEditView | VariablesEditView;
type EditListViewState = DashboardLinksEditViewState | VariablesEditViewState;
export class EditListViewSceneUrlSync implements SceneObjectUrlSyncHandler {
constructor(private _scene: DashboardLinksEditView) {}
constructor(private _scene: EditListViewUrlSync) {}
getKeys(): string[] {
return ['editIndex'];
@ -17,7 +20,7 @@ export class EditListViewSceneUrlSync implements SceneObjectUrlSyncHandler {
}
updateFromUrl(values: SceneObjectUrlValues): void {
let update: Partial<DashboardLinksEditViewState> = {};
let update: Partial<EditListViewState> = {};
if (typeof values.editIndex === 'string') {
update = { editIndex: Number(values.editIndex) };
} else {

View File

@ -1,10 +1,17 @@
import { SceneVariableSet, CustomVariable, SceneGridItem, SceneGridLayout } from '@grafana/scenes';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils } from '@grafana/runtime';
import { SceneVariableSet, CustomVariable, SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils';
import { VariablesEditView } from './VariablesEditView';
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: (id: string) => undefined,
});
describe('VariablesEditView', () => {
describe('Dashboard Variables state', () => {
let dashboard: DashboardScene;
@ -35,7 +42,7 @@ describe('VariablesEditView', () => {
{
type: 'custom',
name: 'customVar2',
query: 'test3, test4',
query: 'test3, test4, $customVar',
value: 'test3',
},
];
@ -99,6 +106,114 @@ describe('VariablesEditView', () => {
errorSpy.mockRestore();
});
it('should change the variable type creating a new variable object', () => {
const previousVariable = variableView.getVariables()[1] as CustomVariable;
variableView.onEdit('customVar2');
variableView.onTypeChange('constant');
expect(variableView.getVariables()).toHaveLength(2);
const variable = variableView.getVariables()[1];
expect(variable).not.toBe(previousVariable);
expect(variable.state.type).toBe('constant');
// Values to be kept between the old and new variable
expect(variable.state.name).toEqual(previousVariable.state.name);
expect(variable.state.label).toEqual(previousVariable.state.label);
});
it('should reset editing variable when going back', () => {
variableView.onEdit('customVar2');
expect(variableView.state.editIndex).toBe(1);
variableView.onGoBack();
expect(variableView.state.editIndex).toBeUndefined();
});
it('should reset editing variable when discarding changes', () => {
variableView.onEdit('customVar2');
const editIndex = variableView.state.editIndex!;
const variable = variableView.getVariables()[editIndex];
const originalState = { ...variable.state };
variable.setState({ name: 'newName' });
variableView.onDiscardChanges();
const newVariable = variableView.getVariables()[editIndex];
expect(newVariable.state).toEqual(originalState);
});
it('should reset editing variable when discarding changes after the type being changed', () => {
variableView.onEdit('customVar2');
const editIndex = variableView.state.editIndex!;
const variable = variableView.getVariables()[editIndex];
const originalState = { ...variable.state };
variableView.onTypeChange('constant');
variableView.onDiscardChanges();
const newVariable = variableView.getVariables()[editIndex];
expect(newVariable.state).toEqual(originalState);
});
it('should go back when discarding changes', () => {
variableView.onEdit('customVar2');
const editIndex = variableView.state.editIndex!;
expect(editIndex).toBeDefined();
variableView.onDiscardChanges();
expect(variableView.state.editIndex).toBeUndefined();
});
});
describe('Dashboard Variables dependencies', () => {
let variableView: VariablesEditView;
let dashboard: DashboardScene;
beforeEach(async () => {
const result = await buildTestScene();
variableView = result.variableView;
dashboard = result.dashboard;
});
// FIXME: This is not working because the variable is replaced or it is not resolved yet
it.skip('should keep dependencies between variables the type is changed so the variable is replaced', () => {
// Uses function to avoid store reference to previous existing variables
const getSourceVariable = () => variableView.getVariables()[0] as CustomVariable;
const getDependantVariable = () => variableView.getVariables()[1] as CustomVariable;
expect(getSourceVariable().getValue()).toBe('test');
// Using getOptionsForSelect to get the interpolated values
expect(getDependantVariable().getOptionsForSelect()[2].label).toBe('test');
variableView.onEdit(getSourceVariable().state.name);
// Simulating changing the type and update the value
variableView.onTypeChange('constant');
getSourceVariable().setState({ value: 'newValue' });
expect(getSourceVariable().getValue()).toBe('newValue');
expect(getDependantVariable().getOptionsForSelect()[2].label).toBe('newValue');
});
it('should keep dependencies with panels when the type is changed so the variable is replaced', async () => {
// Uses function to avoid store reference to previous existing variables
const getSourceVariable = () => variableView.getVariables()[0] as CustomVariable;
const getDependantPanel = () =>
((dashboard.state.body as SceneGridLayout).state.children[0] as SceneGridItem).state.body as VizPanel;
expect(getSourceVariable().getValue()).toBe('test');
// Using description to get the interpolated value
expect(getDependantPanel().getDescription()).toContain('Panel A depends on customVar with current value test');
variableView.onEdit(getSourceVariable().state.name);
// Simulating changing the type and update the value
variableView.onTypeChange('constant');
getSourceVariable().setState({ value: 'newValue' });
expect(getSourceVariable().getValue()).toBe('newValue');
expect(getDependantPanel().getDescription()).toContain('newValue');
});
});
});
@ -115,10 +230,14 @@ async function buildTestScene() {
new CustomVariable({
name: 'customVar',
query: 'test, test2',
value: 'test',
text: 'test',
}),
new CustomVariable({
name: 'customVar2',
query: 'test3, test4',
query: 'test3, test4, $customVar',
value: '$customVar',
text: '$customVar',
}),
],
}),
@ -127,10 +246,12 @@ async function buildTestScene() {
new SceneGridItem({
key: 'griditem-1',
x: 0,
y: 0,
width: 10,
height: 12,
body: undefined,
body: new VizPanel({
title: 'Panel A',
description: 'Panel A depends on customVar with current value $customVar',
key: 'panel-1',
pluginId: 'table',
}),
}),
],
}),

View File

@ -1,16 +1,30 @@
import React from 'react';
import { PageLayoutType } from '@grafana/data';
import { SceneComponentProps, SceneObjectBase, SceneVariables, sceneGraph } from '@grafana/scenes';
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
import {
SceneComponentProps,
SceneObjectBase,
SceneVariable,
SceneVariableState,
SceneVariables,
sceneGraph,
AdHocFilterSet,
} from '@grafana/scenes';
import { Page } from 'app/core/components/Page/Page';
import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils';
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
import { VariableEditorForm } from './variables/VariableEditorForm';
import { VariableEditorList } from './variables/VariableEditorList';
export interface VariablesEditViewState extends DashboardEditViewState {}
import { EditableVariableType, getVariableScene, isEditableVariableType } from './variables/utils';
export interface VariablesEditViewState extends DashboardEditViewState {
editIndex?: number | undefined;
originalVariableState?: SceneVariableState;
}
export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> implements DashboardEditView {
public static Component = VariableEditorSettingsListView;
@ -19,6 +33,8 @@ export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> i
return 'variables';
}
protected _urlSync = new EditListViewSceneUrlSync(this);
public getDashboard(): DashboardScene {
return getDashboardSceneFor(this);
}
@ -32,6 +48,32 @@ export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> i
return variables.findIndex((variable) => variable.state.name === identifier);
};
private replaceEditVariable = (newVariable: SceneVariable | AdHocFilterSet) => {
// Find the index of the variable to be deleted
const variableIndex = this.state.editIndex ?? -1;
const { variables } = this.getVariableSet().state;
const variable = variables[variableIndex];
if (!variable) {
// Handle the case where the variable is not found
console.error('Variable not found');
return;
}
if (newVariable instanceof AdHocFilterSet) {
// TODO: Update controls in adding this fiter set to the dashboard
} else {
const updatedVariables = [
...variables.slice(0, variableIndex),
newVariable,
...variables.slice(variableIndex + 1),
];
// Update the state or the variables array
this.getVariableSet().setState({ variables: updatedVariables });
}
};
public onDelete = (identifier: string) => {
// Find the index of the variable to be deleted
const variableIndex = this.getVariableIndex(identifier);
@ -106,7 +148,56 @@ export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> i
};
public onEdit = (identifier: string) => {
return 'not implemented';
const variableIndex = this.getVariableIndex(identifier);
if (variableIndex === -1) {
console.error('Variable not found');
return;
}
this.setState({ editIndex: variableIndex, originalVariableState: { ...this.getVariables()[variableIndex].state } });
};
public onTypeChange = (type: EditableVariableType) => {
// Find the index of the variable to be deleted
const variableIndex = this.state.editIndex ?? -1;
const { variables } = this.getVariableSet().state;
const variable = variables[variableIndex];
if (!variable) {
// Handle the case where the variable is not found
console.error('Variable not found');
return;
}
const { name, label } = variable.state;
const newVariable = getVariableScene(type, { name, label });
this.replaceEditVariable(newVariable);
};
public onGoBack = () => {
this.setState({ editIndex: undefined });
};
public onDiscardChanges: () => void = () => {
const variables = this.getVariableSet().state.variables;
const { editIndex, originalVariableState } = this.state;
if (editIndex === undefined || !originalVariableState) {
return;
}
const variable = variables[editIndex];
if (!variable) {
return;
}
if (isEditableVariableType(originalVariableState.type)) {
const newVariable = getVariableScene(originalVariableState.type, originalVariableState);
if (newVariable instanceof AdHocFilterSet) {
// TODO: Update controls in adding this fiter set to the dashboard
} else {
const updatedVariables = [...variables.slice(0, editIndex), newVariable, ...variables.slice(editIndex + 1)];
this.getVariableSet().setState({ variables: updatedVariables });
}
}
this.setState({ editIndex: undefined, originalVariableState: undefined });
};
}
@ -114,8 +205,26 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps<Variables
const dashboard = model.getDashboard();
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
// get variables from dashboard state
const { onDelete, onDuplicated, onOrderChanged, onEdit } = model;
const { onDelete, onDuplicated, onOrderChanged, onEdit, onTypeChange, onGoBack, onDiscardChanges } = model;
const { variables } = model.getVariableSet().useState();
const { editIndex } = model.useState();
if (editIndex !== undefined && variables[editIndex]) {
const variable = variables[editIndex];
if (variable) {
return (
<VariableEditorSettingsView
variable={variable}
onTypeChange={onTypeChange}
onGoBack={onGoBack}
onDiscardChanges={onDiscardChanges}
pageNav={pageNav}
navModel={navModel}
dashboard={dashboard}
/>
);
}
}
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
@ -131,3 +240,43 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps<Variables
</Page>
);
}
interface VariableEditorSettingsEditViewProps {
variable: SceneVariable;
pageNav: NavModelItem;
navModel: NavModel;
dashboard: DashboardScene;
onTypeChange: (variableType: EditableVariableType) => void;
onGoBack: () => void;
onDiscardChanges: () => void;
}
function VariableEditorSettingsView({
variable,
pageNav,
navModel,
dashboard,
onTypeChange,
onGoBack,
onDiscardChanges,
}: VariableEditorSettingsEditViewProps) {
const parentTab = pageNav.children!.find((p) => p.active)!;
parentTab.parentItem = pageNav;
const { name } = variable.useState();
const editVariablePageNav = {
text: name,
parentItem: parentTab,
};
return (
<Page navModel={navModel} pageNav={editVariablePageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<VariableEditorForm
variable={variable}
onTypeChange={onTypeChange}
onGoBack={onGoBack}
onDiscardChanges={onDiscardChanges}
/>
</Page>
);
}

View File

@ -0,0 +1,128 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { SceneVariable } from '@grafana/scenes';
import { VariableHide, defaultVariableModel } from '@grafana/schema';
import { HorizontalGroup, Button } 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';
import { VariableTextField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextField';
import { VariableValuesPreview } from 'app/features/dashboard-scene/settings/variables/components/VariableValuesPreview';
import { ConfirmDeleteModal } from 'app/features/variables/editor/ConfirmDeleteModal';
import { VariableNameConstraints } from 'app/features/variables/editor/types';
import { VariableTypeSelect } from './components/VariableTypeSelect';
import { EditableVariableType, getVariableEditor, hasVariableOptions, isEditableVariableType } from './utils';
interface VariableEditorFormProps {
variable: SceneVariable;
onTypeChange: (type: EditableVariableType) => void;
onGoBack: () => void;
onDiscardChanges: () => void;
}
export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDiscardChanges }: VariableEditorFormProps) {
const { name: initialName, type, label: initialLabel, description: initialDescription, hide } = variable.useState();
const EditorToRender = isEditableVariableType(type) ? getVariableEditor(type) : undefined;
const [name, setName] = React.useState(initialName ?? '');
const [label, setLabel] = React.useState(initialLabel ?? '');
const [description, setDescription] = React.useState(initialDescription ?? '');
const onVariableTypeChange = (option: SelectableValue<EditableVariableType>) => {
if (option.value) {
onTypeChange(option.value);
}
};
const onNameChange = (e: React.FormEvent<HTMLInputElement>) => setName(e.currentTarget.value);
const onLabelChange = (e: React.FormEvent<HTMLInputElement>) => setLabel(e.currentTarget.value);
const onDescriptionChange = (e: React.FormEvent<HTMLTextAreaElement>) => setDescription(e.currentTarget.value);
const onNameBlur = () => variable.setState({ name });
const onLabelBlur = () => variable.setState({ label });
const onDescriptionBlur = () => variable.setState({ description });
const onHideChange = (hide: VariableHide) => variable.setState({ hide });
return (
<>
<form aria-label="Variable editor Form">
<VariableTypeSelect onChange={onVariableTypeChange} type={type} />
<VariableLegend>General</VariableLegend>
<VariableTextField
value={name}
onBlur={onNameBlur}
onChange={onNameChange}
name="Name"
placeholder="Variable name"
description="The name of the template variable. (Max. 50 characters)"
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2}
maxLength={VariableNameConstraints.MaxSize}
required
/>
<VariableTextField
name="Label"
description="Optional display name"
value={label}
onChange={onLabelChange}
placeholder="Label name"
onBlur={onLabelBlur}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2}
/>
<VariableTextAreaField
name="Description"
value={description}
onChange={onDescriptionChange}
placeholder="Descriptive text"
onBlur={onDescriptionBlur}
width={52}
/>
<VariableHideSelect onChange={onHideChange} hide={hide || defaultVariableModel.hide!} type={type} />
{EditorToRender && <EditorToRender variable={variable} />}
{hasVariableOptions(variable) && <VariableValuesPreview options={variable.options} />}
<div style={{ marginTop: '16px' }}>
<HorizontalGroup spacing="md" height="inherit">
{/* <Button variant="destructive" fill="outline" onClick={onModalOpen}>
Delete
</Button> */}
{/* <Button
type="submit"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton}
disabled={loading}
variant="secondary"
>
Run query
{loading && <Icon className="spin-clockwise" name="sync" size="sm" style={{ marginLeft: '2px' }} />}
</Button> */}
<Button
variant="secondary"
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton}
onClick={onGoBack}
>
Back to list
</Button>
<Button
variant="destructive"
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton}
onClick={onDiscardChanges}
>
Discard changes
</Button>
</HorizontalGroup>
</div>
</form>
<ConfirmDeleteModal
isOpen={false}
varName={variable.state.name}
onConfirm={() => console.log('needs implementation')}
onDismiss={() => console.log('needs implementation')}
/>
</>
);
}

View File

@ -45,8 +45,8 @@ export function VariableSelectField({
function getStyles(theme: GrafanaTheme2) {
return {
selectContainer: css`
margin-right: ${theme.spacing(0.5)};
`,
selectContainer: css({
marginRight: theme.spacing(0.5),
}),
};
}

View File

@ -9,7 +9,7 @@ interface VariableTextAreaFieldProps {
name: string;
value: string;
placeholder: string;
onChange: (event: FormEvent<HTMLTextAreaElement>) => void;
onChange?: (event: FormEvent<HTMLTextAreaElement>) => void;
width: number;
ariaLabel?: string;
required?: boolean;
@ -54,17 +54,17 @@ export function VariableTextAreaField({
export function getStyles(theme: GrafanaTheme2) {
return {
textarea: css`
white-space: pre-wrap;
min-height: ${theme.spacing(4)};
height: auto;
overflow: auto;
padding: ${theme.spacing(0.75, 1)};
width: inherit;
textarea: css({
whiteSpace: 'pre-wrap',
minHeight: theme.spacing(4),
height: 'auto',
overflow: 'auto',
padding: `${theme.spacing(0.75)} ${theme.spacing(1)}`,
width: 'inherit',
${theme.breakpoints.down('sm')} {
width: 100%;
}
`,
[theme.breakpoints.down('sm')]: {
width: '100%',
},
}),
};
}

View File

@ -7,7 +7,7 @@ interface VariableTextFieldProps {
value: string;
name: string;
placeholder?: string;
onChange: (event: FormEvent<HTMLInputElement>) => void;
onChange?: (event: FormEvent<HTMLInputElement>) => void;
testId?: string;
required?: boolean;
width?: number;

View File

@ -0,0 +1,30 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue, VariableType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from 'app/features/dashboard-scene/settings/variables/components/VariableSelectField';
import { EditableVariableType, getVariableTypeSelectOptions } from '../utils';
interface Props {
onChange: (option: SelectableValue<EditableVariableType>) => void;
type: VariableType;
}
export function VariableTypeSelect({ onChange, type }: PropsWithChildren<Props>) {
const options = useMemo(() => getVariableTypeSelectOptions(), []);
const value = useMemo(
() => options.find((o: SelectableValue<EditableVariableType>) => o.value === type) ?? options[0],
[options, type]
);
return (
<VariableSelectField
name="Select variable type"
value={value}
options={options}
onChange={onChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2}
/>
);
}

View File

@ -3,17 +3,16 @@ import React, { MouseEvent, useCallback, useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableValueOption } from '@grafana/scenes';
import { Button, InlineFieldRow, InlineLabel, useStyles2 } from '@grafana/ui';
import { VariableOption, VariableWithOptions } from '../types';
export interface VariableValuesPreviewProps {
variable: VariableWithOptions;
options: VariableValueOption[];
}
export const VariableValuesPreview = ({ variable: { options } }: VariableValuesPreviewProps) => {
export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) => {
const [previewLimit, setPreviewLimit] = useState(20);
const [previewOptions, setPreviewOptions] = useState<VariableOption[]>([]);
const [previewOptions, setPreviewOptions] = useState<VariableValueOption[]>([]);
const showMoreOptions = useCallback(
(event: MouseEvent) => {
event.preventDefault();
@ -34,8 +33,8 @@ export const VariableValuesPreview = ({ variable: { options } }: VariableValuesP
<InlineFieldRow>
{previewOptions.map((o, index) => (
<InlineFieldRow key={`${o.value}-${index}`} className={styles.optionContainer}>
<InlineLabel aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption}>
<div className={styles.label}>{o.text}</div>
<InlineLabel data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption}>
<div className={styles.label}>{o.label}</div>
</InlineLabel>
</InlineFieldRow>
))}

View File

@ -0,0 +1,12 @@
import React from 'react';
import { AdHocFiltersVariable } from '@grafana/scenes';
interface AdHocFiltersVariableEditorProps {
variable: AdHocFiltersVariable;
onChange: (variable: AdHocFiltersVariable) => void;
}
export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProps) {
return <div>AdHocFiltersVariableEditor</div>;
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import { ConstantVariable } from '@grafana/scenes';
interface ConstantVariableEditorProps {
variable: ConstantVariable;
onChange: (variable: ConstantVariable) => void;
}
export function ConstantVariableEditor(props: ConstantVariableEditorProps) {
return <div>ConstantVariableEditor</div>;
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import { CustomVariable } from '@grafana/scenes';
interface CustomVariableEditorProps {
variable: CustomVariable;
}
export function CustomVariableEditor(props: CustomVariableEditorProps) {
return <div>CustomVariableEditor</div>;
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import { DataSourceVariable } from '@grafana/scenes';
interface DataSourceVariableEditorProps {
variable: DataSourceVariable;
onChange: (variable: DataSourceVariable) => void;
}
export function DataSourceVariableEditor(props: DataSourceVariableEditorProps) {
return <div>DataSourceVariableEditor</div>;
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import { IntervalVariable } from '@grafana/scenes';
interface IntervalVariableEditorProps {
variable: IntervalVariable;
onChange: (variable: IntervalVariable) => void;
}
export function IntervalVariableEditor(props: IntervalVariableEditorProps) {
return <div>IntervalVariableEditor</div>;
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import { QueryVariable } from '@grafana/scenes';
interface QueryVariableEditorProps {
variable: QueryVariable;
onChange: (variable: QueryVariable) => void;
}
export function QueryVariableEditor(props: QueryVariableEditorProps) {
return <div>QueryVariableEditor</div>;
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import { TextBoxVariable } from '@grafana/scenes';
interface TextBoxVariableEditorProps {
variable: TextBoxVariable;
onChange: (variable: TextBoxVariable) => void;
}
export function TextBoxVariableEditor(props: TextBoxVariableEditorProps) {
return <div>TextBoxVariableEditor</div>;
}

View File

@ -0,0 +1,140 @@
import { setTemplateSrv, TemplateSrv } from '@grafana/runtime';
import {
CustomVariable,
ConstantVariable,
IntervalVariable,
QueryVariable,
DataSourceVariable,
AdHocFiltersVariable,
TextBoxVariable,
} from '@grafana/scenes';
import { VariableType } from '@grafana/schema';
import { AdHocFiltersVariableEditor } from './editors/AdHocFiltersVariableEditor';
import { ConstantVariableEditor } from './editors/ConstantVariableEditor';
import { CustomVariableEditor } from './editors/CustomVariableEditor';
import { DataSourceVariableEditor } from './editors/DataSourceVariableEditor';
import { IntervalVariableEditor } from './editors/IntervalVariableEditor';
import { QueryVariableEditor } from './editors/QueryVariableEditor';
import { TextBoxVariableEditor } from './editors/TextBoxVariableEditor';
import {
isEditableVariableType,
EDITABLE_VARIABLES,
EDITABLE_VARIABLES_SELECT_ORDER,
getVariableTypeSelectOptions,
getVariableEditor,
getVariableScene,
hasVariableOptions,
EditableVariableType,
} from './utils';
const templateSrv = {
getAdhocFilters: jest.fn().mockReturnValue([{ key: 'origKey', operator: '=', value: '' }]),
} as unknown as TemplateSrv;
describe('isEditableVariableType', () => {
it('should return true for editable variable types', () => {
const editableTypes: VariableType[] = ['custom', 'query', 'constant', 'interval', 'datasource', 'adhoc', 'textbox'];
editableTypes.forEach((type) => {
expect(isEditableVariableType(type)).toBe(true);
});
});
it('should return false for non-editable variable types', () => {
const nonEditableTypes: VariableType[] = ['system'];
nonEditableTypes.forEach((type) => {
expect(isEditableVariableType(type)).toBe(false);
});
});
});
describe('getVariableTypeSelectOptions', () => {
it('should contain all editable variable types', () => {
const options = getVariableTypeSelectOptions();
expect(options).toHaveLength(Object.keys(EDITABLE_VARIABLES).length);
EDITABLE_VARIABLES_SELECT_ORDER.forEach((type) => {
expect(EDITABLE_VARIABLES).toHaveProperty(type);
});
});
it('should return an array of selectable values for editable variable types', () => {
const options = getVariableTypeSelectOptions();
expect(options).toHaveLength(7);
options.forEach((option, index) => {
const editableType = EDITABLE_VARIABLES_SELECT_ORDER[index];
const variableTypeConfig = EDITABLE_VARIABLES[editableType];
expect(option.value).toBe(editableType);
expect(option.label).toBe(variableTypeConfig.name);
expect(option.description).toBe(variableTypeConfig.description);
});
});
});
describe('getVariableEditor', () => {
it.each(Object.keys(EDITABLE_VARIABLES) as EditableVariableType[])(
'should define an editor for every variable type',
(type) => {
const editor = getVariableEditor(type);
expect(editor).toBeDefined();
}
);
it.each([
['custom', CustomVariableEditor],
['query', QueryVariableEditor],
['constant', ConstantVariableEditor],
['interval', IntervalVariableEditor],
['datasource', DataSourceVariableEditor],
['adhoc', AdHocFiltersVariableEditor],
['textbox', TextBoxVariableEditor],
])('should return the correct editor for each variable type', (type, ExpectedVariableEditor) => {
expect(getVariableEditor(type as EditableVariableType)).toBe(ExpectedVariableEditor);
});
});
describe('getVariableScene', () => {
beforeAll(() => {
setTemplateSrv(templateSrv);
});
it.each(Object.keys(EDITABLE_VARIABLES) as EditableVariableType[])(
'should define a scene object for every variable type',
(type) => {
const variable = getVariableScene(type, { name: 'foo' });
expect(variable).toBeDefined();
}
);
it.each([
['custom', CustomVariable],
['query', QueryVariable],
['constant', ConstantVariable],
['interval', IntervalVariable],
['datasource', DataSourceVariable],
['adhoc', AdHocFiltersVariable],
['textbox', TextBoxVariable],
])('should return the scene variable instance for the given editable variable type', () => {
const initialState = { name: 'MyVariable' };
const sceneVariable = getVariableScene('custom', initialState);
expect(sceneVariable).toBeInstanceOf(CustomVariable);
expect(sceneVariable.state.name).toBe(initialState.name);
});
});
describe('hasVariableOptions', () => {
it('should return true for scene variables with options property', () => {
const variableWithOptions = new CustomVariable({
name: 'MyVariable',
options: [{ value: 'option1', label: 'Option 1' }],
});
expect(hasVariableOptions(variableWithOptions)).toBe(true);
});
it('should return false for scene variables without options property', () => {
const variableWithoutOptions = new ConstantVariable({ name: 'MyVariable' });
expect(hasVariableOptions(variableWithoutOptions)).toBe(false);
});
});

View File

@ -0,0 +1,124 @@
import { SelectableValue } from '@grafana/data';
import {
ConstantVariable,
CustomVariable,
DataSourceVariable,
IntervalVariable,
TextBoxVariable,
QueryVariable,
AdHocFilterSet,
SceneVariable,
VariableValueOption,
} from '@grafana/scenes';
import { VariableType } from '@grafana/schema';
import { AdHocFiltersVariableEditor } from './editors/AdHocFiltersVariableEditor';
import { ConstantVariableEditor } from './editors/ConstantVariableEditor';
import { CustomVariableEditor } from './editors/CustomVariableEditor';
import { DataSourceVariableEditor } from './editors/DataSourceVariableEditor';
import { IntervalVariableEditor } from './editors/IntervalVariableEditor';
import { QueryVariableEditor } from './editors/QueryVariableEditor';
import { TextBoxVariableEditor } from './editors/TextBoxVariableEditor';
interface EditableVariableConfig {
name: string;
description: string;
editor: React.ComponentType<any>;
}
export type EditableVariableType = Exclude<VariableType, 'system'>;
export function isEditableVariableType(type: VariableType): type is EditableVariableType {
return type !== 'system';
}
export const EDITABLE_VARIABLES: Record<EditableVariableType, EditableVariableConfig> = {
custom: {
name: 'Custom',
description: 'Define variable values manually',
editor: CustomVariableEditor,
},
query: {
name: 'Query',
description: 'Variable values are fetched from a datasource query',
editor: QueryVariableEditor,
},
constant: {
name: 'Constant',
description: 'Define a hidden constant variable, useful for metric prefixes in dashboards you want to share',
editor: ConstantVariableEditor,
},
interval: {
name: 'Interval',
description: 'Define a timespan interval (ex 1m, 1h, 1d)',
editor: IntervalVariableEditor,
},
datasource: {
name: 'Data source',
description: 'Enables you to dynamically switch the data source for multiple panels',
editor: DataSourceVariableEditor,
},
adhoc: {
name: 'Ad hoc filters',
description: 'Add key/value filters on the fly',
editor: AdHocFiltersVariableEditor,
},
textbox: {
name: 'Textbox',
description: 'Define a textbox variable, where users can enter any arbitrary string',
editor: TextBoxVariableEditor,
},
};
export const EDITABLE_VARIABLES_SELECT_ORDER: EditableVariableType[] = [
'query',
'custom',
'textbox',
'constant',
'datasource',
'interval',
'adhoc',
];
export function getVariableTypeSelectOptions(): Array<SelectableValue<EditableVariableType>> {
return EDITABLE_VARIABLES_SELECT_ORDER.map((variableType) => ({
label: EDITABLE_VARIABLES[variableType].name,
value: variableType,
description: EDITABLE_VARIABLES[variableType].description,
}));
}
export function getVariableEditor(type: EditableVariableType) {
return EDITABLE_VARIABLES[type].editor;
}
interface CommonVariableProperties {
name: string;
label?: string;
}
export function getVariableScene(type: EditableVariableType, initialState: CommonVariableProperties) {
switch (type) {
case 'custom':
return new CustomVariable(initialState);
case 'query':
return new QueryVariable(initialState);
case 'constant':
return new ConstantVariable(initialState);
case 'interval':
return new IntervalVariable(initialState);
case 'datasource':
return new DataSourceVariable(initialState);
case 'adhoc':
// TODO: Initialize properly AdHocFilterSet with initialState
return new AdHocFilterSet({ name: initialState.name });
case 'textbox':
return new TextBoxVariable(initialState);
}
}
export function hasVariableOptions(
variable: SceneVariable
): variable is SceneVariable & { options: VariableValueOption[] } {
return 'options' in variable.state;
}

View File

@ -6,7 +6,7 @@ import { Alert, Field } from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { StoreState } from 'app/types';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { initialVariableEditorState } from '../editor/reducer';
import { getAdhocVariableEditorState } from '../editor/selectors';
import { VariableEditorProps } from '../editor/types';

View File

@ -2,8 +2,8 @@ import React, { FormEvent, PureComponent } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { VariableTextField } from '../../dashboard-scene/settings/variables/components/VariableTextField';
import { VariableEditorProps } from '../editor/types';
import { ConstantVariableModel } from '../types';

View File

@ -5,9 +5,9 @@ import { selectors } from '@grafana/e2e-selectors';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { StoreState } from 'app/types';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { VariableTextAreaField } from '../../dashboard-scene/settings/variables/components/VariableTextAreaField';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableTextAreaField } from '../editor/VariableTextAreaField';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { changeVariableMultiValue } from '../state/actions';
import { CustomVariableModel, VariableWithMultiSupport } from '../types';

View File

@ -5,10 +5,10 @@ import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { StoreState } from '../../../types';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { VariableSelectField } from '../../dashboard-scene/settings/variables/components/VariableSelectField';
import { VariableTextField } from '../../dashboard-scene/settings/variables/components/VariableTextField';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableTextField } from '../editor/VariableTextField';
import { initialVariableEditorState } from '../editor/reducer';
import { getDatasourceVariableEditorState } from '../editor/selectors';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';

View File

@ -4,10 +4,9 @@ import React, { useCallback, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { TextArea, useStyles2 } from '@grafana/ui';
import { getStyles } from '../../dashboard-scene/settings/variables/components/VariableTextAreaField';
import { VariableQueryEditorProps } from '../types';
import { getStyles } from './VariableTextAreaField';
export const LEGACY_VARIABLE_QUERY_EDITOR_NAME = 'Grafana-LegacyVariableQueryEditor';
export const LegacyVariableQueryEditor = ({ onChange, query }: VariableQueryEditorProps) => {

View File

@ -3,12 +3,12 @@ import React, { ChangeEvent, FormEvent, useCallback } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { VerticalGroup } from '@grafana/ui';
import { VariableCheckboxField } from '../../dashboard-scene/settings/variables/components/VariableCheckboxField';
import { VariableTextField } from '../../dashboard-scene/settings/variables/components/VariableTextField';
import { KeyedVariableIdentifier } from '../state/types';
import { VariableWithMultiSupport } from '../types';
import { toKeyedVariableIdentifier } from '../utils';
import { VariableCheckboxField } from './VariableCheckboxField';
import { VariableTextField } from './VariableTextField';
import { VariableEditorProps } from './types';
export interface SelectionOptionsEditorProps<Model extends VariableWithMultiSupport = VariableWithMultiSupport>

View File

@ -8,6 +8,11 @@ import { locationService } from '@grafana/runtime';
import { Button, HorizontalGroup, Icon } from '@grafana/ui';
import { StoreState, ThunkDispatch } from '../../../types';
import { VariableHideSelect } from '../../dashboard-scene/settings/variables/components/VariableHideSelect';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { VariableTextAreaField } from '../../dashboard-scene/settings/variables/components/VariableTextAreaField';
import { VariableTextField } from '../../dashboard-scene/settings/variables/components/VariableTextField';
import { VariableValuesPreview } from '../../dashboard-scene/settings/variables/components/VariableValuesPreview';
import { variableAdapters } from '../adapters';
import { hasOptions } from '../guard';
import { updateOptions } from '../state/actions';
@ -19,12 +24,7 @@ import { VariableHide } from '../types';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
import { ConfirmDeleteModal } from './ConfirmDeleteModal';
import { VariableHideSelect } from './VariableHideSelect';
import { VariableLegend } from './VariableLegend';
import { VariableTextAreaField } from './VariableTextAreaField';
import { VariableTextField } from './VariableTextField';
import { VariableTypeSelect } from './VariableTypeSelect';
import { VariableValuesPreview } from './VariableValuesPreview';
import { changeVariableName, variableEditorMount, variableEditorUnMount } from './actions';
import { OnPropChangeArguments, VariableNameConstraints } from './types';
@ -138,6 +138,14 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State>
locationService.partial({ editIndex: null });
};
getVariableOptions = () => {
const { variable } = this.props;
if (!hasOptions(variable)) {
return [];
}
return variable.options.map((option) => ({ label: String(option.text), value: String(option.value) }));
};
render() {
const { variable } = this.props;
const EditorToRender = variableAdapters.get(this.props.variable.type).editor;
@ -188,7 +196,7 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State>
{EditorToRender && <EditorToRender variable={this.props.variable} onPropChange={this.onPropChanged} />}
{hasOptions(this.props.variable) ? <VariableValuesPreview variable={this.props.variable} /> : null}
{hasOptions(this.props.variable) ? <VariableValuesPreview options={this.getVariableOptions()} /> : null}
<div style={{ marginTop: '16px' }}>
<HorizontalGroup spacing="md" height="inherit">

View File

@ -3,7 +3,7 @@ import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue, VariableType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableSelectField } from '../../dashboard-scene/settings/variables/components/VariableSelectField';
import { getVariableTypes } from '../utils';
interface Props {

View File

@ -5,10 +5,10 @@ import { GrafanaTheme2, IntervalVariableModel, SelectableValue } from '@grafana/
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '@grafana/ui';
import { VariableCheckboxField } from '../editor/VariableCheckboxField';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableCheckboxField } from '../../dashboard-scene/settings/variables/components/VariableCheckboxField';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { VariableSelectField } from '../../dashboard-scene/settings/variables/components/VariableSelectField';
import { VariableTextField } from '../../dashboard-scene/settings/variables/components/VariableTextField';
import { VariableEditorProps } from '../editor/types';
const STEP_OPTIONS = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500].map((count) => ({

View File

@ -9,9 +9,9 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat
import { StoreState } from '../../../types';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { VariableTextAreaField } from '../../dashboard-scene/settings/variables/components/VariableTextAreaField';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableTextAreaField } from '../editor/VariableTextAreaField';
import { initialVariableEditorState } from '../editor/reducer';
import { getQueryVariableEditorState } from '../editor/selectors';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';

View File

@ -3,7 +3,7 @@ import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableSelectField } from '../../dashboard-scene/settings/variables/components/VariableSelectField';
import { VariableSort } from '../types';
interface Props {

View File

@ -2,8 +2,8 @@ import React, { FormEvent, ReactElement, useCallback } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableLegend } from '../../dashboard-scene/settings/variables/components/VariableLegend';
import { VariableTextField } from '../../dashboard-scene/settings/variables/components/VariableTextField';
import { VariableEditorProps } from '../editor/types';
import { TextBoxVariableModel } from '../types';