Dashboard: Improve static options editors on variables (#110831)
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { Locator } from '@playwright/test';
|
||||
|
||||
import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e';
|
||||
|
||||
import { flows } from './utils';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: true,
|
||||
dashboardNewLayouts: true,
|
||||
dashboardUndoRedo: true,
|
||||
groupByVariable: true,
|
||||
},
|
||||
});
|
||||
|
||||
test.use({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
});
|
||||
|
||||
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
|
||||
const DASHBOARD_NAME = 'Test variable output';
|
||||
|
||||
test.describe(
|
||||
'Dashboard edit - Custom variable',
|
||||
{
|
||||
tag: ['@dashboards'],
|
||||
},
|
||||
() => {
|
||||
let addButton: Locator | undefined;
|
||||
let rows: Locator | undefined;
|
||||
let valueInputs: Locator | undefined;
|
||||
let labelInputs: Locator | undefined;
|
||||
let deleteButtons: Locator | undefined;
|
||||
|
||||
const getAddButton = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => {
|
||||
addButton = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.addButton
|
||||
);
|
||||
await expect(addButton).toBeVisible();
|
||||
};
|
||||
|
||||
const refetchItems = (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => {
|
||||
rows = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.row
|
||||
);
|
||||
|
||||
valueInputs = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.valueInput
|
||||
);
|
||||
|
||||
labelInputs = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.labelInput
|
||||
);
|
||||
|
||||
deleteButtons = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.deleteButton
|
||||
);
|
||||
};
|
||||
|
||||
const checkRows = async (length: number) => {
|
||||
expect(await rows!.all()).toHaveLength(length);
|
||||
};
|
||||
|
||||
const fillValue = async (text: string, index: number) => {
|
||||
await valueInputs!.nth(index).fill(text);
|
||||
};
|
||||
|
||||
const fillLabel = async (text: string, index: number) => {
|
||||
await labelInputs!.nth(index).fill(text);
|
||||
};
|
||||
|
||||
const fillLabelValue = async (value: string, label: string, index: number) => {
|
||||
await fillValue(value, index);
|
||||
await fillLabel(label, index);
|
||||
};
|
||||
|
||||
const openModal = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => {
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.optionsOpenButton)
|
||||
.click();
|
||||
|
||||
await getAddButton(dashboardPage, selectors);
|
||||
|
||||
refetchItems(dashboardPage, selectors);
|
||||
};
|
||||
|
||||
const closeModal = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => {
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.closeButton)
|
||||
.click();
|
||||
};
|
||||
|
||||
const checkItems = async (items: Array<[string, string?]>) => {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const [value, label] = items[i];
|
||||
await expect(valueInputs!.nth(i)).toHaveValue(value);
|
||||
await expect(labelInputs!.nth(i)).toHaveValue(label ?? '');
|
||||
}
|
||||
};
|
||||
|
||||
const checkPreview = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, labels: string[]) => {
|
||||
const previewOptions = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption
|
||||
);
|
||||
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
expect(await previewOptions.nth(i).textContent()).toBe(labels[i]);
|
||||
}
|
||||
};
|
||||
|
||||
const addItem = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, value = '', label = '') => {
|
||||
await addButton!.click();
|
||||
refetchItems(dashboardPage, selectors);
|
||||
await fillLabelValue(value ?? '', label ?? '', (await rows!.all()).length - 1);
|
||||
};
|
||||
|
||||
const removeItem = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, index: number) => {
|
||||
await deleteButtons!.nth(index).click();
|
||||
refetchItems(dashboardPage, selectors);
|
||||
};
|
||||
|
||||
test.beforeEach(() => {
|
||||
valueInputs = undefined;
|
||||
labelInputs = undefined;
|
||||
deleteButtons = undefined;
|
||||
});
|
||||
|
||||
test('can add a new custom variable', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
|
||||
|
||||
// common steps to add a new variable
|
||||
await flows.newEditPaneVariableClick(dashboardPage, selectors);
|
||||
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, {
|
||||
type: 'custom',
|
||||
name: 'foo',
|
||||
label: 'Foo',
|
||||
value: '',
|
||||
});
|
||||
|
||||
await openModal(dashboardPage, selectors);
|
||||
await checkRows(1);
|
||||
await addItem(dashboardPage, selectors);
|
||||
await checkRows(2);
|
||||
await fillValue('first value', 0);
|
||||
await fillLabelValue('second value', 'second label', 1);
|
||||
await addItem(dashboardPage, selectors, 'third value', 'third label');
|
||||
await addItem(dashboardPage, selectors, 'fourth value', 'fourth value');
|
||||
await removeItem(dashboardPage, selectors, 2);
|
||||
await checkRows(3);
|
||||
await checkPreview(dashboardPage, selectors, ['first value', 'second label', 'fourth value']);
|
||||
await closeModal(dashboardPage, selectors);
|
||||
|
||||
// assert variable is visible and has the correct values
|
||||
const variableLabel = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.SubMenu.submenuItemLabels('Foo')
|
||||
);
|
||||
await expect(variableLabel).toBeVisible();
|
||||
await expect(variableLabel).toContainText('Foo');
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('first value')
|
||||
)
|
||||
).toBeVisible();
|
||||
|
||||
// check that variable deletion works
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
|
||||
await expect(variableLabel).toBeHidden();
|
||||
});
|
||||
|
||||
test('can edit a custom variable', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({
|
||||
uid: PAGE_UNDER_TEST,
|
||||
queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }),
|
||||
});
|
||||
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
|
||||
|
||||
// Create a custom variable in the dashboard settings page
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click();
|
||||
const typeSelect = dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2)
|
||||
.locator('input');
|
||||
await typeSelect.fill('Custom');
|
||||
await typeSelect.press('Enter');
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2)
|
||||
.fill('foo');
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2)
|
||||
.fill('Foo');
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput)
|
||||
.fill('first value, second label : second value, fourth value : fourth value');
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton)
|
||||
.click();
|
||||
await dashboardPage
|
||||
.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton)
|
||||
.click();
|
||||
|
||||
// Open the modal editor in the side pane
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.node('Variables')).click();
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('foo')).click();
|
||||
await openModal(dashboardPage, selectors);
|
||||
|
||||
// Check the items
|
||||
await checkItems([['first value'], ['second value', 'second label'], ['fourth value']]);
|
||||
await checkPreview(dashboardPage, selectors, ['first value', 'second label', 'fourth value']);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -20,46 +20,6 @@ test.describe(
|
||||
tag: ['@dashboards'],
|
||||
},
|
||||
() => {
|
||||
test('can add a new custom variable', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
|
||||
|
||||
const variable: Variable = {
|
||||
type: 'custom',
|
||||
name: 'foo',
|
||||
label: 'Foo',
|
||||
value: 'one,two,three',
|
||||
};
|
||||
|
||||
// common steps to add a new variable
|
||||
await flows.newEditPaneVariableClick(dashboardPage, selectors);
|
||||
await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable);
|
||||
|
||||
// set the custom variable value
|
||||
const customValueInput = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
|
||||
);
|
||||
await customValueInput.fill(variable.value);
|
||||
await customValueInput.blur();
|
||||
|
||||
// assert the dropdown for the variable is visible and has the correct values
|
||||
const variableLabel = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!)
|
||||
);
|
||||
await expect(variableLabel).toBeVisible();
|
||||
await expect(variableLabel).toContainText(variable.label!);
|
||||
|
||||
const values = variable.value.split(',');
|
||||
const firstValueLink = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(values[0])
|
||||
);
|
||||
await expect(firstValueLink).toBeVisible();
|
||||
|
||||
// check that variable deletion works
|
||||
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click();
|
||||
await expect(variableLabel).toBeHidden();
|
||||
});
|
||||
|
||||
test('can add a new constant variable', async ({ gotoDashboardPage, selectors, page }) => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST });
|
||||
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
|
||||
|
||||
@@ -2189,19 +2189,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/variables/utils.ts": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
},
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/sharing/ShareButton/share-externally/EmailShare/ConfigEmailSharing/ConfigEmailSharing.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
|
||||
+2
-2
@@ -296,8 +296,8 @@
|
||||
"@grafana/plugin-ui": "^0.10.10",
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "6.39.3",
|
||||
"@grafana/scenes-react": "6.39.3",
|
||||
"@grafana/scenes": "6.39.4",
|
||||
"@grafana/scenes-react": "6.39.4",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
|
||||
@@ -500,24 +500,9 @@ export const versionedPages = {
|
||||
queryOptionsQueryInput: {
|
||||
'10.4.0': 'data-testid Variable editor Form Default Variable Query Editor textarea',
|
||||
},
|
||||
queryOptionsStaticOptionsRow: {
|
||||
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options row',
|
||||
},
|
||||
queryOptionsStaticOptionsToggle: {
|
||||
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options toggle',
|
||||
},
|
||||
queryOptionsStaticOptionsLabelInput: {
|
||||
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Label input',
|
||||
},
|
||||
queryOptionsStaticOptionsValueInput: {
|
||||
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Value input',
|
||||
},
|
||||
queryOptionsStaticOptionsDeleteButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Delete button',
|
||||
},
|
||||
queryOptionsStaticOptionsAddButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Add button',
|
||||
},
|
||||
queryOptionsStaticOptionsOrderDropdown: {
|
||||
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Order dropdown',
|
||||
},
|
||||
@@ -559,6 +544,12 @@ export const versionedPages = {
|
||||
customValueInput: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid custom-variable-input',
|
||||
},
|
||||
optionsOpenButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid custom-variable-options-open-button',
|
||||
},
|
||||
closeButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid custom-variable-close-button',
|
||||
},
|
||||
},
|
||||
IntervalVariable: {
|
||||
intervalsValueInput: {
|
||||
@@ -607,6 +598,26 @@ export const versionedPages = {
|
||||
['12.3.0']: 'data-testid switch variable disabled value input',
|
||||
},
|
||||
},
|
||||
StaticOptionsEditor: {
|
||||
addButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Add button',
|
||||
},
|
||||
labelInput: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Label input',
|
||||
},
|
||||
valueInput: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Value input',
|
||||
},
|
||||
moveButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Move button',
|
||||
},
|
||||
deleteButton: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Delete button',
|
||||
},
|
||||
row: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Static Options Row',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+2
-45
@@ -1,16 +1,11 @@
|
||||
import { FormEvent } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
import { TextArea } from '@grafana/ui';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { VariableLegend } from '../components/VariableLegend';
|
||||
import { VariableTextAreaField } from '../components/VariableTextAreaField';
|
||||
|
||||
import { SelectionOptionsForm } from './SelectionOptionsForm';
|
||||
import { VariableLegend } from './VariableLegend';
|
||||
import { VariableTextAreaField } from './VariableTextAreaField';
|
||||
|
||||
interface CustomVariableFormProps {
|
||||
query: string;
|
||||
@@ -71,41 +66,3 @@ export function CustomVariableForm({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] {
|
||||
if (!(variable instanceof CustomVariable)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: (descriptor) => <ValuesTextField id={descriptor.props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) {
|
||||
const { query } = variable.useState();
|
||||
|
||||
const onBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
|
||||
variable.setState({ query: event.currentTarget.value });
|
||||
await lastValueFrom(variable.validateAndUpdate!());
|
||||
};
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id={id}
|
||||
rows={2}
|
||||
defaultValue={query}
|
||||
onBlur={onBlur}
|
||||
placeholder={t(
|
||||
'dashboard.edit-pane.variable.custom-options.values-placeholder',
|
||||
'1, 10, mykey : myvalue, myvalue, escaped\,value'
|
||||
)}
|
||||
required
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-12
@@ -337,17 +337,15 @@ describe('QueryVariableEditorForm', () => {
|
||||
);
|
||||
await userEvent.click(staticOptionsToggle);
|
||||
|
||||
const addButton = getByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton
|
||||
);
|
||||
const addButton = getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.addButton);
|
||||
await userEvent.click(addButton);
|
||||
|
||||
// Now enter label and value for the new option
|
||||
const labelInputs = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.labelInput
|
||||
);
|
||||
const valueInputs = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.valueInput
|
||||
);
|
||||
|
||||
// Enter label for the new option (second input)
|
||||
@@ -372,7 +370,7 @@ describe('QueryVariableEditorForm', () => {
|
||||
});
|
||||
|
||||
const deleteButtons = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsDeleteButton
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.deleteButton
|
||||
);
|
||||
|
||||
// Remove the first option
|
||||
@@ -392,7 +390,7 @@ describe('QueryVariableEditorForm', () => {
|
||||
});
|
||||
|
||||
const labelInputs = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.labelInput
|
||||
);
|
||||
|
||||
await userEvent.clear(labelInputs[0]);
|
||||
@@ -411,7 +409,7 @@ describe('QueryVariableEditorForm', () => {
|
||||
});
|
||||
|
||||
const valueInputs = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.valueInput
|
||||
);
|
||||
|
||||
await userEvent.clear(valueInputs[0]);
|
||||
@@ -443,9 +441,7 @@ describe('QueryVariableEditorForm', () => {
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Option rows should be visible
|
||||
expect(
|
||||
getAllByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow)
|
||||
).toHaveLength(2);
|
||||
expect(getAllByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.row)).toHaveLength(2);
|
||||
|
||||
// Uncheck the static options switch
|
||||
const staticOptionsToggle = getByTestId(
|
||||
@@ -464,7 +460,7 @@ describe('QueryVariableEditorForm', () => {
|
||||
)
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
queryByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow)
|
||||
queryByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.row)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { VariableValueOption } from '@grafana/scenes';
|
||||
import { Button, Input, Stack } from '@grafana/ui';
|
||||
|
||||
interface VariableOptionsFieldProps {
|
||||
options: VariableValueOption[];
|
||||
onChange: (options: VariableValueOption[]) => void;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function VariableOptionsInput({ options, onChange, width }: VariableOptionsFieldProps) {
|
||||
const [optionsLocal, setOptionsLocal] = useState(options.length ? options : [{ value: '', label: '' }]);
|
||||
|
||||
const updateOptions = (newOptions: VariableValueOption[]) => {
|
||||
setOptionsLocal(newOptions);
|
||||
onChange(
|
||||
newOptions
|
||||
.map((option) => ({
|
||||
label: option.label.trim(),
|
||||
value: String(option.value).trim(),
|
||||
}))
|
||||
.filter((option) => !!option.label)
|
||||
);
|
||||
};
|
||||
|
||||
const handleValueChange = (index: number, value: string) => {
|
||||
if (optionsLocal[index].value !== value) {
|
||||
const newOptions = [...optionsLocal];
|
||||
newOptions[index] = { ...newOptions[index], value };
|
||||
updateOptions(newOptions);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLabelChange = (index: number, label: string) => {
|
||||
if (optionsLocal[index].label !== label) {
|
||||
const newOptions = [...optionsLocal];
|
||||
newOptions[index] = { ...newOptions[index], label };
|
||||
updateOptions(newOptions);
|
||||
}
|
||||
};
|
||||
|
||||
const addOption = () => {
|
||||
const newOption: VariableValueOption = { value: '', label: '' };
|
||||
const newOptions = [...optionsLocal, newOption];
|
||||
updateOptions(newOptions);
|
||||
};
|
||||
|
||||
const removeOption = (index: number) => {
|
||||
const newOptions = optionsLocal.filter((_, i) => i !== index);
|
||||
updateOptions(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2} width={width}>
|
||||
{optionsLocal.map((option, index) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
key={index}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow}
|
||||
>
|
||||
<Input
|
||||
value={option.label}
|
||||
placeholder={t('variables.query-variable-static-options.label-placeholder', 'display label')}
|
||||
onChange={(e) => handleLabelChange(index, e.currentTarget.value)}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={String(option.value)}
|
||||
placeholder={t('variables.query-variable-static-options.value-placeholder', 'value, default empty string')}
|
||||
onChange={(e) => handleValueChange(index, e.currentTarget.value)}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="times"
|
||||
variant="secondary"
|
||||
aria-label={t('variables.query-variable-static-options.remove-option-button-label', 'Remove option')}
|
||||
onClick={() => removeOption(index)}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsDeleteButton
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
<div>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
onClick={addOption}
|
||||
data-testid={
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton
|
||||
}
|
||||
aria-label={t('variables.query-variable-static-options.add-option-button-label', 'Add option')}
|
||||
>
|
||||
<Trans i18nKey="variables.query-variable-static-options.add-option-button-label">Add option</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect, useState, useRef, useCallback, useImperativeHandle, forwardRef } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { VariableValueOption } from '@grafana/scenes';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { VariableStaticOptionsFormAddButton } from './VariableStaticOptionsFormAddButton';
|
||||
import { VariableStaticOptionsFormItem } from './VariableStaticOptionsFormItemEditor';
|
||||
import { VariableStaticOptionsFormItems } from './VariableStaticOptionsFormItems';
|
||||
|
||||
interface VariableStaticOptionsFormProps {
|
||||
options: VariableValueOption[];
|
||||
onChange: (options: VariableValueOption[]) => void;
|
||||
|
||||
allowEmptyValue?: boolean;
|
||||
isInModal?: boolean;
|
||||
}
|
||||
|
||||
export interface VariableStaticOptionsFormRef {
|
||||
addItem: () => void;
|
||||
}
|
||||
|
||||
export const VariableStaticOptionsForm = forwardRef<VariableStaticOptionsFormRef, VariableStaticOptionsFormProps>(
|
||||
function ({ options, onChange, allowEmptyValue, isInModal = false }: VariableStaticOptionsFormProps, ref) {
|
||||
const styles = useStyles2(getStyles, isInModal);
|
||||
|
||||
// Whenever the form is updated, we want to ignore the next update from the parent component.
|
||||
// This is because the parent component will update the options, and we don't want to update the items again.
|
||||
// This is a hack to prevent the form from updating twice and losing items and IDs.
|
||||
// Alternatively, we could maintain a list of emitted items and compare the new options to it, but this is less performant.
|
||||
const ignoreNextUpdate = useRef<boolean>(false);
|
||||
|
||||
const mapOption = useCallback(
|
||||
(option: VariableValueOption) => ({
|
||||
label: option.label,
|
||||
value: String(option.value),
|
||||
id: uuidv4(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const [items, setItems] = useState<VariableStaticOptionsFormItem[]>(
|
||||
options.length ? options.map(mapOption) : [createEmptyItem()]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ignoreNextUpdate.current) {
|
||||
setItems(
|
||||
options.length
|
||||
? options.map((option) => ({
|
||||
label: option.label,
|
||||
value: String(option.value),
|
||||
id: uuidv4(),
|
||||
}))
|
||||
: [createEmptyItem()]
|
||||
);
|
||||
}
|
||||
|
||||
ignoreNextUpdate.current = false;
|
||||
}, [options]);
|
||||
|
||||
const updateItems = useCallback(
|
||||
(items: VariableStaticOptionsFormItem[]) => {
|
||||
setItems(items);
|
||||
ignoreNextUpdate.current = true;
|
||||
onChange(
|
||||
items.reduce<VariableValueOption[]>((acc, item) => {
|
||||
const value = item.value.trim();
|
||||
|
||||
if (!allowEmptyValue && !value) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const label = item.label.trim();
|
||||
|
||||
if (!label && !value) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
label: label ? label : value,
|
||||
value,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
},
|
||||
[allowEmptyValue, onChange]
|
||||
);
|
||||
|
||||
const handleAdd = useCallback(() => setItems([...items, createEmptyItem()]), [items]);
|
||||
|
||||
useImperativeHandle(ref, () => ({ addItem: handleAdd }), [handleAdd]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<VariableStaticOptionsFormItems items={items} onChange={updateItems} />
|
||||
{!isInModal && (
|
||||
<div>
|
||||
<VariableStaticOptionsFormAddButton onAdd={handleAdd} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
VariableStaticOptionsForm.displayName = 'VariableStaticOptionsForm';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, isInModal: boolean) => ({
|
||||
container: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
width: '100%',
|
||||
maxWidth: theme.spacing(60),
|
||||
...(isInModal
|
||||
? {
|
||||
maxWidth: '100%',
|
||||
|
||||
// Simulate sticky modal buttons
|
||||
maxHeight: 'calc(80vh - 170px)',
|
||||
overflow: 'auto',
|
||||
minHeight: theme.spacing(5),
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
});
|
||||
|
||||
function createEmptyItem(): VariableStaticOptionsFormItem {
|
||||
return {
|
||||
label: '',
|
||||
value: '',
|
||||
id: uuidv4(),
|
||||
};
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { Button } from '@grafana/ui';
|
||||
|
||||
interface VariableStaticOptionsFormAddButtonProps {
|
||||
onAdd: () => void;
|
||||
}
|
||||
|
||||
export const VariableStaticOptionsFormAddButton = ({ onAdd }: VariableStaticOptionsFormAddButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
onClick={onAdd}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.addButton}
|
||||
aria-label={t('variables.static-options.add-option-button-label', 'Add new option')}
|
||||
>
|
||||
<Trans i18nKey="variables.static-options.add-option-button-label">Add new option</Trans>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import { ChangeEventHandler } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Icon, IconButton, Input, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface VariableStaticOptionsFormItem {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface VariableStaticOptionsFormItemEditorProps {
|
||||
item: VariableStaticOptionsFormItem;
|
||||
index: number;
|
||||
onChange: (item: VariableStaticOptionsFormItem) => void;
|
||||
onRemove: (item: VariableStaticOptionsFormItem) => void;
|
||||
}
|
||||
|
||||
export function VariableStaticOptionsFormItemEditor({
|
||||
item,
|
||||
index,
|
||||
onChange,
|
||||
onRemove,
|
||||
}: VariableStaticOptionsFormItemEditorProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const handleValueChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
if (item.value !== evt.currentTarget.value) {
|
||||
onChange({ ...item, value: evt.currentTarget.value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleLabelChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
|
||||
if (item.label !== evt.currentTarget.value) {
|
||||
onChange({ ...item, label: evt.currentTarget.value });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = () => onRemove(item);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={item.id} index={index}>
|
||||
{(draggableProvided) => (
|
||||
<tr
|
||||
ref={draggableProvided.innerRef}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.row}
|
||||
{...draggableProvided.draggableProps}
|
||||
>
|
||||
<td>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.moveButton}
|
||||
{...draggableProvided.dragHandleProps}
|
||||
>
|
||||
<Icon
|
||||
title={t('variables.static-options.drag-and-drop', 'Drag and drop to reorder')}
|
||||
name="draggabledots"
|
||||
size="lg"
|
||||
className={styles.dragIcon}
|
||||
/>
|
||||
</Stack>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
value={item.value}
|
||||
placeholder={t('variables.static-options.value-placeholder', 'Value')}
|
||||
onChange={handleValueChange}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.valueInput}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
value={item.label}
|
||||
placeholder={t('variables.static-options.label-placeholder', 'Defaults to value')}
|
||||
onChange={handleLabelChange}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.labelInput}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
aria-label={t('variables.static-options.remove-option-button-label', 'Remove option')}
|
||||
onClick={handleRemove}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.deleteButton}
|
||||
/>
|
||||
</Stack>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
dragIcon: css({
|
||||
cursor: 'grab',
|
||||
|
||||
// create a focus ring around the whole row when the drag handle is tab-focused
|
||||
// needs position: relative on the drag row to work correctly
|
||||
'&:focus-visible&:after': {
|
||||
bottom: 0,
|
||||
content: '""',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
outline: `2px solid ${theme.colors.primary.main}`,
|
||||
outlineOffset: '-2px',
|
||||
},
|
||||
}),
|
||||
});
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
VariableStaticOptionsFormItem,
|
||||
VariableStaticOptionsFormItemEditor,
|
||||
} from './VariableStaticOptionsFormItemEditor';
|
||||
|
||||
interface VariableStaticOptionsFormProps {
|
||||
items: VariableStaticOptionsFormItem[];
|
||||
onChange: (items: VariableStaticOptionsFormItem[]) => void;
|
||||
}
|
||||
|
||||
export function VariableStaticOptionsFormItems({ items, onChange }: VariableStaticOptionsFormProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const handleReorder = (result: DropResult) => {
|
||||
if (!result || !result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIdx = result.source.index;
|
||||
const endIdx = result.destination.index;
|
||||
|
||||
if (startIdx === endIdx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItems = [...items];
|
||||
const [removedItem] = newItems.splice(startIdx, 1);
|
||||
newItems.splice(endIdx, 0, removedItem);
|
||||
onChange(newItems);
|
||||
};
|
||||
|
||||
const handleChange = (item: VariableStaticOptionsFormItem) => {
|
||||
const idx = items.findIndex((currentItem) => currentItem.id === item.id);
|
||||
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOptions = [...items];
|
||||
newOptions[idx] = item;
|
||||
onChange(newOptions);
|
||||
};
|
||||
|
||||
const handleRemove = (item: VariableStaticOptionsFormItem) => {
|
||||
const newOptions = items.filter((currentItem) => currentItem.id !== item.id);
|
||||
onChange(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.headerIconColumn} />
|
||||
<th className={styles.headerInputColumn}>
|
||||
<Trans i18nKey="variables.static-options.value-header">Value</Trans>
|
||||
</th>
|
||||
<th className={styles.headerInputColumn}>
|
||||
<Trans i18nKey="variables.static-options.label-header">Display text</Trans>
|
||||
</th>
|
||||
<th className={styles.headerIconColumn} />
|
||||
</tr>
|
||||
</thead>
|
||||
<DragDropContext onDragEnd={handleReorder}>
|
||||
<Droppable droppableId="static-options-list" direction="vertical">
|
||||
{(droppableProvided) => (
|
||||
<tbody ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||
{items.map((item, idx) => (
|
||||
<VariableStaticOptionsFormItemEditor
|
||||
item={item}
|
||||
index={idx}
|
||||
onChange={handleChange}
|
||||
onRemove={handleRemove}
|
||||
key={item.id}
|
||||
/>
|
||||
))}
|
||||
{droppableProvided.placeholder}
|
||||
</tbody>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
table: css({
|
||||
'tbody tr': css({
|
||||
position: 'relative',
|
||||
}),
|
||||
|
||||
'tbody tr:hover': css({
|
||||
background: theme.colors.action.hover,
|
||||
}),
|
||||
|
||||
'th, td': {
|
||||
padding: theme.spacing(1),
|
||||
width: '49%',
|
||||
},
|
||||
|
||||
'th:first-child, td:first-child, th:last-child, td:last-child': css({
|
||||
width: '1%',
|
||||
}),
|
||||
}),
|
||||
headerIconColumn: css({
|
||||
width: '1%',
|
||||
}),
|
||||
headerInputColumn: css({
|
||||
width: '49%',
|
||||
}),
|
||||
});
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { FormEvent } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
import { TextArea } from '@grafana/ui';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { CustomVariableForm } from '../components/CustomVariableForm';
|
||||
|
||||
interface CustomVariableEditorProps {
|
||||
variable: CustomVariable;
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) {
|
||||
const { query, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
|
||||
|
||||
const onMultiChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
variable.setState({ isMulti: event.currentTarget.checked });
|
||||
};
|
||||
const onIncludeAllChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
variable.setState({ includeAll: event.currentTarget.checked });
|
||||
};
|
||||
const onQueryChange = (event: FormEvent<HTMLTextAreaElement>) => {
|
||||
variable.setState({ query: event.currentTarget.value });
|
||||
onRunQuery();
|
||||
};
|
||||
const onAllValueChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
variable.setState({ allValue: event.currentTarget.value });
|
||||
};
|
||||
const onAllowCustomValueChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
variable.setState({ allowCustomValue: event.currentTarget.checked });
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomVariableForm
|
||||
query={query ?? ''}
|
||||
multi={!!isMulti}
|
||||
allValue={allValue ?? ''}
|
||||
includeAll={!!includeAll}
|
||||
allowCustomValue={allowCustomValue}
|
||||
onMultiChange={onMultiChange}
|
||||
onIncludeAllChange={onIncludeAllChange}
|
||||
onQueryChange={onQueryChange}
|
||||
onAllValueChange={onAllValueChange}
|
||||
onAllowCustomValueChange={onAllowCustomValueChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] {
|
||||
if (!(variable instanceof CustomVariable)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: ({ props }) => <ValuesTextField id={props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function ValuesTextField({ variable, id }: { variable: CustomVariable; id?: string }) {
|
||||
const { query } = variable.useState();
|
||||
|
||||
const onBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
|
||||
variable.setState({ query: event.currentTarget.value });
|
||||
await lastValueFrom(variable.validateAndUpdate!());
|
||||
};
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id={id}
|
||||
rows={2}
|
||||
defaultValue={query}
|
||||
onBlur={onBlur}
|
||||
placeholder={t(
|
||||
'dashboard.edit-pane.variable.custom-options.values-placeholder',
|
||||
'1, 10, mykey : myvalue, myvalue, escaped\,value'
|
||||
)}
|
||||
required
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { FormEvent, useCallback } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
|
||||
import { OptionsPaneItemDescriptor } from '../../../../../dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
import { CustomVariableForm } from '../../components/CustomVariableForm';
|
||||
|
||||
import { PaneItem } from './PaneItem';
|
||||
|
||||
interface CustomVariableEditorProps {
|
||||
variable: CustomVariable;
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) {
|
||||
const { query, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
|
||||
|
||||
const onMultiChange = useCallback(
|
||||
(event: FormEvent<HTMLInputElement>) => {
|
||||
variable.setState({ isMulti: event.currentTarget.checked });
|
||||
},
|
||||
[variable]
|
||||
);
|
||||
|
||||
const onIncludeAllChange = useCallback(
|
||||
(event: FormEvent<HTMLInputElement>) => {
|
||||
variable.setState({ includeAll: event.currentTarget.checked });
|
||||
},
|
||||
[variable]
|
||||
);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
(event: FormEvent<HTMLTextAreaElement>) => {
|
||||
variable.setState({ query: event.currentTarget.value });
|
||||
onRunQuery();
|
||||
},
|
||||
[variable, onRunQuery]
|
||||
);
|
||||
|
||||
const onAllValueChange = useCallback(
|
||||
(event: FormEvent<HTMLInputElement>) => {
|
||||
variable.setState({ allValue: event.currentTarget.value });
|
||||
},
|
||||
[variable]
|
||||
);
|
||||
|
||||
const onAllowCustomValueChange = useCallback(
|
||||
(event: FormEvent<HTMLInputElement>) => {
|
||||
variable.setState({ allowCustomValue: event.currentTarget.checked });
|
||||
},
|
||||
[variable]
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomVariableForm
|
||||
query={query ?? ''}
|
||||
multi={!!isMulti}
|
||||
allValue={allValue ?? ''}
|
||||
includeAll={!!includeAll}
|
||||
allowCustomValue={allowCustomValue}
|
||||
onMultiChange={onMultiChange}
|
||||
onIncludeAllChange={onIncludeAllChange}
|
||||
onQueryChange={onQueryChange}
|
||||
onAllValueChange={onAllValueChange}
|
||||
onAllowCustomValueChange={onAllowCustomValueChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] {
|
||||
if (!(variable instanceof CustomVariable)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: ({ props }) => <PaneItem id={props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
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 { VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm';
|
||||
import { VariableStaticOptionsFormAddButton } from '../../components/VariableStaticOptionsFormAddButton';
|
||||
|
||||
import { ValuesBuilder } from './ValuesBuilder';
|
||||
import { ValuesPreview } from './ValuesPreview';
|
||||
|
||||
interface ModalEditorProps {
|
||||
variable: CustomVariable;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) {
|
||||
const formRef = useRef<VariableStaticOptionsFormRef | null>(null);
|
||||
|
||||
const handleOnAdd = useCallback(() => formRef.current?.addItem(), []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('dashboard.edit-pane.variable.custom-options.modal-title', 'Custom Variable')}
|
||||
isOpen={isOpen}
|
||||
onDismiss={onClose}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
<ValuesBuilder variable={variable} ref={formRef} />
|
||||
<ValuesPreview variable={variable} />
|
||||
</Stack>
|
||||
<Modal.ButtonRow leftItems={<VariableStaticOptionsFormAddButton onAdd={handleOnAdd} />}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="outline"
|
||||
onClick={onClose}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.closeButton}
|
||||
>
|
||||
<Trans i18nKey="dashboard.edit-pane.variable.custom-options.close">Close</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { CustomVariable } from '@grafana/scenes';
|
||||
import { Box, Button } from '@grafana/ui';
|
||||
|
||||
import { ModalEditor } from './ModalEditor';
|
||||
|
||||
interface PaneItemProps {
|
||||
variable: CustomVariable;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function PaneItem({ variable }: PaneItemProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" direction="column" paddingBottom={1}>
|
||||
<Button
|
||||
tooltip={t(
|
||||
'dashboard.edit-pane.variable.open-editor-tooltip',
|
||||
'For more variable options open variable editor'
|
||||
)}
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="sm"
|
||||
fullWidth
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.optionsOpenButton}
|
||||
>
|
||||
<Trans i18nKey="dashboard.edit-pane.variable.open-editor">Open variable editor</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
<ModalEditor variable={variable} isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { CustomVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes';
|
||||
|
||||
import { VariableStaticOptionsForm, VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm';
|
||||
|
||||
interface ValuesBuilderProps {
|
||||
variable: CustomVariable;
|
||||
}
|
||||
|
||||
export const ValuesBuilder = forwardRef<VariableStaticOptionsFormRef, ValuesBuilderProps>(function (
|
||||
{ variable }: ValuesBuilderProps,
|
||||
ref
|
||||
) {
|
||||
const { query } = variable.useState();
|
||||
|
||||
const options = variable.transformCsvStringToOptions(query, false).map(({ label, value }) => ({
|
||||
value,
|
||||
label: value === label ? '' : label,
|
||||
}));
|
||||
|
||||
const escapeEntities = useCallback((text: VariableValueSingle) => String(text).trim().replaceAll(',', '\\,'), []);
|
||||
|
||||
const formatOption = useCallback(
|
||||
(option: VariableValueOption) => {
|
||||
if (!option.label || option.label === option.value) {
|
||||
return escapeEntities(option.value);
|
||||
}
|
||||
|
||||
return `${escapeEntities(option.label)} : ${escapeEntities(String(option.value))}`;
|
||||
},
|
||||
[escapeEntities]
|
||||
);
|
||||
|
||||
const generateQuery = useCallback(
|
||||
(options: VariableValueOption[]) => options.map(formatOption).join(', '),
|
||||
[formatOption]
|
||||
);
|
||||
|
||||
const handleOptionsChange = useCallback(
|
||||
async (options: VariableValueOption[]) => {
|
||||
variable.setState({ query: generateQuery(options) });
|
||||
await lastValueFrom(variable.validateAndUpdate!());
|
||||
},
|
||||
[variable, generateQuery]
|
||||
);
|
||||
|
||||
return <VariableStaticOptionsForm options={options} onChange={handleOptionsChange} ref={ref} isInModal />;
|
||||
});
|
||||
|
||||
ValuesBuilder.displayName = 'ValuesBuilder';
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { CustomVariable } from '@grafana/scenes';
|
||||
|
||||
import { VariableValuesPreview } from '../../components/VariableValuesPreview';
|
||||
import { hasVariableOptions } from '../../utils';
|
||||
|
||||
export function ValuesPreview({ variable }: { variable: CustomVariable }) {
|
||||
// Workaround to toggle a component refresh when values change so that the preview is updated
|
||||
variable.useState();
|
||||
|
||||
const isHasVariableOptions = hasVariableOptions(variable);
|
||||
|
||||
return isHasVariableOptions ? <VariableValuesPreview options={variable.getOptionsForSelect(false)} /> : null;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
|
||||
import { OptionsPaneItemDescriptor } from '../../../../../dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { PaneItem } from './PaneItem';
|
||||
|
||||
export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] {
|
||||
if (!(variable instanceof CustomVariable)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: ({ props }) => <PaneItem id={props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
+5
-7
@@ -384,17 +384,15 @@ describe('QueryVariableEditor', () => {
|
||||
await userEvent.click(staticOptionsToggle);
|
||||
|
||||
// Add first static option
|
||||
const addButton = getByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton
|
||||
);
|
||||
const addButton = getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.addButton);
|
||||
await user.click(addButton);
|
||||
|
||||
// Enter label and value for first option
|
||||
const labelInputs = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.labelInput
|
||||
);
|
||||
const valueInputs = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.valueInput
|
||||
);
|
||||
|
||||
await user.type(labelInputs[0], 'First Option');
|
||||
@@ -411,10 +409,10 @@ describe('QueryVariableEditor', () => {
|
||||
|
||||
// Get updated inputs (now there should be 2 sets)
|
||||
const updatedLabelInputs = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.labelInput
|
||||
);
|
||||
const updatedValueInputs = getAllByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.StaticOptionsEditor.valueInput
|
||||
);
|
||||
|
||||
// Enter label and value for second option
|
||||
|
||||
+30
-2
@@ -14,7 +14,11 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat
|
||||
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
|
||||
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
||||
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
||||
import { StaticOptionsOrderType, StaticOptionsType } from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||
import {
|
||||
QueryVariableStaticOptions,
|
||||
StaticOptionsOrderType,
|
||||
StaticOptionsType,
|
||||
} from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||
|
||||
import { QueryVariableEditorForm } from '../components/QueryVariableForm';
|
||||
import { VariableTextAreaField } from '../components/VariableTextAreaField';
|
||||
@@ -186,7 +190,15 @@ export function ModalEditor({ variable }: { variable: QueryVariable }) {
|
||||
}
|
||||
|
||||
export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
const { datasource: datasourceRef, sort, refresh, query, regex } = variable.useState();
|
||||
const {
|
||||
datasource: datasourceRef,
|
||||
sort,
|
||||
refresh,
|
||||
query,
|
||||
regex,
|
||||
staticOptions,
|
||||
staticOptionsOrder,
|
||||
} = variable.useState();
|
||||
const { value: timeRange } = sceneGraph.getTimeRange(variable).useState();
|
||||
const { value: dsConfig } = useAsync(async () => {
|
||||
const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
|
||||
@@ -229,6 +241,12 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
const onRefreshChange = (refresh: VariableRefresh) => {
|
||||
variable.setState({ refresh: refresh });
|
||||
};
|
||||
const onStaticOptionsChange = (staticOptions: StaticOptionsType) => {
|
||||
variable.setState({ staticOptions });
|
||||
};
|
||||
const onStaticOptionsOrderChange = (staticOptionsOrder: StaticOptionsOrderType) => {
|
||||
variable.setState({ staticOptionsOrder });
|
||||
};
|
||||
|
||||
const isHasVariableOptions = hasVariableOptions(variable);
|
||||
|
||||
@@ -237,6 +255,7 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
<Field
|
||||
label={t('dashboard-scene.query-variable-editor-form.label-target-data-source', 'Target data source')}
|
||||
htmlFor="data-source-picker"
|
||||
noMargin
|
||||
>
|
||||
<DataSourcePicker current={selectedDatasource} onChange={onDataSourceChange} variables={true} width={30} />
|
||||
</Field>
|
||||
@@ -292,6 +311,15 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
||||
refresh={refresh}
|
||||
/>
|
||||
|
||||
{onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||
<QueryVariableStaticOptions
|
||||
staticOptions={staticOptions}
|
||||
staticOptionsOrder={staticOptionsOrder}
|
||||
onStaticOptionsChange={onStaticOptionsChange}
|
||||
onStaticOptionsOrderChange={onStaticOptionsOrderChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHasVariableOptions && <VariableValuesPreview options={variable.getOptionsForSelect(false)} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { SHARED_DASHBOARD_QUERY, DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plug
|
||||
|
||||
import { AdHocFiltersVariableEditor } from './editors/AdHocFiltersVariableEditor';
|
||||
import { ConstantVariableEditor } from './editors/ConstantVariableEditor';
|
||||
import { CustomVariableEditor } from './editors/CustomVariableEditor';
|
||||
import { CustomVariableEditor } from './editors/CustomVariableEditor/CustomVariableEditor';
|
||||
import { DataSourceVariableEditor } from './editors/DataSourceVariableEditor';
|
||||
import { GroupByVariableEditor } from './editors/GroupByVariableEditor';
|
||||
import { IntervalVariableEditor } from './editors/IntervalVariableEditor';
|
||||
|
||||
@@ -27,7 +27,8 @@ import { getIntervalsQueryFromNewIntervalModel } from '../../utils/utils';
|
||||
|
||||
import { AdHocFiltersVariableEditor, getAdHocFilterOptions } from './editors/AdHocFiltersVariableEditor';
|
||||
import { ConstantVariableEditor, getConstantVariableOptions } from './editors/ConstantVariableEditor';
|
||||
import { CustomVariableEditor, getCustomVariableOptions } from './editors/CustomVariableEditor';
|
||||
import { CustomVariableEditor } from './editors/CustomVariableEditor/CustomVariableEditor';
|
||||
import { getCustomVariableOptions } from './editors/CustomVariableEditor/getCustomVariableOptions';
|
||||
import { DataSourceVariableEditor, getDataSourceVariableOptions } from './editors/DataSourceVariableEditor';
|
||||
import { getGroupByVariableOptions, GroupByVariableEditor } from './editors/GroupByVariableEditor';
|
||||
import { getIntervalVariableOptions, IntervalVariableEditor } from './editors/IntervalVariableEditor';
|
||||
@@ -38,7 +39,7 @@ import { TextBoxVariableEditor, getTextBoxVariableOptions } from './editors/Text
|
||||
interface EditableVariableConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
editor: React.ComponentType<any>;
|
||||
editor: React.ComponentType<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
getOptions?: (variable: SceneVariable) => OptionsPaneItemDescriptor[];
|
||||
}
|
||||
|
||||
@@ -132,6 +133,7 @@ export const getEditableVariables: () => Record<EditableVariableType, EditableVa
|
||||
|
||||
export function getEditableVariableDefinition(type: string): EditableVariableConfig {
|
||||
const editableVariables = getEditableVariables();
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const editableVariable = editableVariables[type as EditableVariableType];
|
||||
if (!editableVariable) {
|
||||
throw new Error(`Variable type ${type} not found`);
|
||||
|
||||
@@ -5,8 +5,8 @@ import { t, Trans } from '@grafana/i18n';
|
||||
import { QueryVariable } from '@grafana/scenes';
|
||||
import { Field, Stack, Switch } from '@grafana/ui';
|
||||
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
|
||||
import { VariableOptionsInput } from 'app/features/dashboard-scene/settings/variables/components/VariableOptionsInput';
|
||||
import { VariableSelectField } from 'app/features/dashboard-scene/settings/variables/components/VariableSelectField';
|
||||
import { VariableStaticOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/VariableStaticOptionsForm';
|
||||
|
||||
export type StaticOptionsType = QueryVariable['state']['staticOptions'];
|
||||
export type StaticOptionsOrderType = QueryVariable['state']['staticOptionsOrder'];
|
||||
@@ -65,7 +65,11 @@ export function QueryVariableStaticOptions(props: QueryVariableStaticOptionsProp
|
||||
/>
|
||||
|
||||
{areStaticOptionsEnabled && (
|
||||
<VariableOptionsInput width={60} options={staticOptions ?? []} onChange={onStaticOptionsChange} />
|
||||
<VariableStaticOptionsForm
|
||||
allowEmptyValue
|
||||
options={staticOptions ?? []}
|
||||
onChange={onStaticOptionsChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
|
||||
@@ -4749,8 +4749,9 @@
|
||||
},
|
||||
"variable": {
|
||||
"custom-options": {
|
||||
"values": "Values separated by comma",
|
||||
"values-placeholder": "1, 10, mykey : myvalue, myvalue, escaped,value"
|
||||
"close": "Close",
|
||||
"modal-title": "Custom Variable",
|
||||
"values": "Values separated by comma"
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Name filter",
|
||||
@@ -14139,15 +14140,20 @@
|
||||
}
|
||||
},
|
||||
"query-variable-static-options": {
|
||||
"add-option-button-label": "Add option",
|
||||
"description": "Add custom options in addition to query results",
|
||||
"label-placeholder": "display label",
|
||||
"remove-option-button-label": "Remove option",
|
||||
"value-placeholder": "value, default empty string"
|
||||
"description": "Add custom options in addition to query results"
|
||||
},
|
||||
"query-variable-static-options-sort-select": {
|
||||
"description-values-variable": "How to sort static options with query results"
|
||||
},
|
||||
"static-options": {
|
||||
"add-option-button-label": "Add new option",
|
||||
"drag-and-drop": "Drag and drop to reorder",
|
||||
"label-header": "Display text",
|
||||
"label-placeholder": "Defaults to value",
|
||||
"remove-option-button-label": "Remove option",
|
||||
"value-header": "Value",
|
||||
"value-placeholder": "Value"
|
||||
},
|
||||
"text-box-variable-editor": {
|
||||
"name-default-value": "Default value",
|
||||
"placeholder-default-value-if-any": "default value, if any",
|
||||
|
||||
@@ -3524,11 +3524,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes-react@npm:6.39.3":
|
||||
version: 6.39.3
|
||||
resolution: "@grafana/scenes-react@npm:6.39.3"
|
||||
"@grafana/scenes-react@npm:6.39.4":
|
||||
version: 6.39.4
|
||||
resolution: "@grafana/scenes-react@npm:6.39.4"
|
||||
dependencies:
|
||||
"@grafana/scenes": "npm:6.39.3"
|
||||
"@grafana/scenes": "npm:6.39.4"
|
||||
lru-cache: "npm:^10.2.2"
|
||||
react-use: "npm:^17.4.0"
|
||||
peerDependencies:
|
||||
@@ -3540,13 +3540,13 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/7e989c0d34ab23add873fcdf8f2ebf2bb94c7cfd427631be13da4a08a0f78832c354868d97e584534a69a4bf4224fcb49a8d49fb16bf43cbed58203bd36c90d4
|
||||
checksum: 10/9acbf6b80af12cb6807f9ba364d6684c967ebe65eb4ab51e8c3f6e461476d71cb89e64b53ba9a9e29a62972fb7876877be630a685aa32c815d4b36cb9ff4d9f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/scenes@npm:6.39.3":
|
||||
version: 6.39.3
|
||||
resolution: "@grafana/scenes@npm:6.39.3"
|
||||
"@grafana/scenes@npm:6.39.4":
|
||||
version: 6.39.4
|
||||
resolution: "@grafana/scenes@npm:6.39.4"
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:^0.26.16"
|
||||
"@leeoniya/ufuzzy": "npm:^1.0.16"
|
||||
@@ -3566,7 +3566,7 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/e5ddaf4f5e9afc7370eb4b6608d26056d1e4072465c2179e87415ac4ad01a4f8693bafaa372d7ed8ec6f295847561116ea85993cb5e5e6c4927fbb82b56f2e96
|
||||
checksum: 10/07126c816f69fa69a06e6f4c77028bed515d4bd4d9cc0e081bbd2e4e4bfa8c9b1b0c41e5b686a646542a205e22a88826bcdd795896240e95857823c82a8a5fe5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -18289,8 +18289,8 @@ __metadata:
|
||||
"@grafana/plugin-ui": "npm:^0.10.10"
|
||||
"@grafana/prometheus": "workspace:*"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": "npm:6.39.3"
|
||||
"@grafana/scenes-react": "npm:6.39.3"
|
||||
"@grafana/scenes": "npm:6.39.4"
|
||||
"@grafana/scenes-react": "npm:6.39.4"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/sql": "workspace:*"
|
||||
"@grafana/test-utils": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user