mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GroupBy variable core integration (#82185)
* Bump scenes * Make GroupByVariableModel a VariableWithOptions * Serialise/deserialise group by variable * WIP: Group by variable editor * WIP tests * Group by variable tests * add feature toggle and gate variable creation behind it * Fix types * Do not resolve DS variable * Do not show the message if no DS is selected * Now groupby has options and current * Update public/app/features/dashboard-scene/settings/variables/components/GroupByVariableForm.test.tsx Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com> * don't allow creating groupby if toggle is off + update tests * add unit tests * remove groupByKeys --------- Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
This commit is contained in:
parent
269fa400f0
commit
f016f95298
@ -181,4 +181,5 @@ export interface FeatureToggles {
|
||||
groupToNestedTableTransformation?: boolean;
|
||||
newPDFRendering?: boolean;
|
||||
kubernetesAggregator?: boolean;
|
||||
groupByVariable?: boolean;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { LoadingState } from './data';
|
||||
import { MetricFindValue } from './datasource';
|
||||
import { DataSourceRef } from './query';
|
||||
|
||||
export type VariableType = TypedVariableModel['type'];
|
||||
@ -66,11 +65,9 @@ export interface AdHocVariableModel extends BaseVariableModel {
|
||||
baseFilters?: AdHocVariableFilter[];
|
||||
}
|
||||
|
||||
export interface GroupByVariableModel extends BaseVariableModel {
|
||||
export interface GroupByVariableModel extends VariableWithOptions {
|
||||
type: 'groupby';
|
||||
datasource: DataSourceRef | null;
|
||||
groupByKeys: string[];
|
||||
defaultOptions?: MetricFindValue[];
|
||||
multi: true;
|
||||
}
|
||||
|
||||
|
@ -182,6 +182,11 @@ export const Pages = {
|
||||
stepCountIntervalSelect: 'data-testid interval variable step count input',
|
||||
minIntervalInput: 'data-testid interval variable mininum interval input',
|
||||
},
|
||||
GroupByVariable: {
|
||||
dataSourceSelect: Components.DataSourcePicker.inputV2,
|
||||
infoText: 'data-testid group by variable info text',
|
||||
modeToggle: 'data-testid group by variable mode toggle',
|
||||
},
|
||||
AdHocFiltersVariable: {
|
||||
datasourceSelect: Components.DataSourcePicker.inputV2,
|
||||
infoText: 'data-testid ad-hoc filters variable info text',
|
||||
|
@ -1212,6 +1212,15 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "groupByVariable",
|
||||
Description: "Enable groupBy variable support in scenes dashboards",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaDashboardsSquad,
|
||||
AllowSelfServe: false,
|
||||
HideFromDocs: true,
|
||||
HideFromAdminPage: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -162,3 +162,4 @@ nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,fals
|
||||
groupToNestedTableTransformation,preview,@grafana/dataviz-squad,false,false,true
|
||||
newPDFRendering,experimental,@grafana/sharing-squad,false,false,false
|
||||
kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
groupByVariable,experimental,@grafana/dashboards-squad,false,false,false
|
||||
|
|
@ -658,4 +658,8 @@ const (
|
||||
// FlagKubernetesAggregator
|
||||
// Enable grafana aggregator
|
||||
FlagKubernetesAggregator = "kubernetesAggregator"
|
||||
|
||||
// FlagGroupByVariable
|
||||
// Enable groupBy variable support in scenes dashboards
|
||||
FlagGroupByVariable = "groupByVariable"
|
||||
)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,12 +11,13 @@ import {
|
||||
toDataFrame,
|
||||
VariableSupportType,
|
||||
} from '@grafana/data';
|
||||
import { setRunRequest } from '@grafana/runtime';
|
||||
import { config, setRunRequest } from '@grafana/runtime';
|
||||
import {
|
||||
AdHocFiltersVariable,
|
||||
ConstantVariable,
|
||||
CustomVariable,
|
||||
DataSourceVariable,
|
||||
GroupByVariable,
|
||||
QueryVariable,
|
||||
SceneVariableSet,
|
||||
TextBoxVariable,
|
||||
@ -401,4 +402,92 @@ describe('sceneVariablesSetToVariables', () => {
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe('when the groupByVariable feature toggle is enabled', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.groupByVariable = true;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.groupByVariable = false;
|
||||
});
|
||||
|
||||
it('should handle GroupByVariable', () => {
|
||||
const variable = new GroupByVariable({
|
||||
name: 'test',
|
||||
label: 'test-label',
|
||||
description: 'test-desc',
|
||||
datasource: { uid: 'fake-std', type: 'fake-std' },
|
||||
defaultOptions: [
|
||||
{
|
||||
text: 'Foo',
|
||||
value: 'foo',
|
||||
},
|
||||
{
|
||||
text: 'Bar',
|
||||
value: 'bar',
|
||||
},
|
||||
],
|
||||
});
|
||||
const set = new SceneVariableSet({
|
||||
variables: [variable],
|
||||
});
|
||||
|
||||
const result = sceneVariablesSetToVariables(set);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"current": {
|
||||
"text": [],
|
||||
"value": [],
|
||||
},
|
||||
"datasource": {
|
||||
"type": "fake-std",
|
||||
"uid": "fake-std",
|
||||
},
|
||||
"description": "test-desc",
|
||||
"label": "test-label",
|
||||
"name": "test",
|
||||
"options": [
|
||||
{
|
||||
"text": "Foo",
|
||||
"value": "foo",
|
||||
},
|
||||
{
|
||||
"text": "Bar",
|
||||
"value": "bar",
|
||||
},
|
||||
],
|
||||
"type": "groupby",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the groupByVariable feature toggle is disabled', () => {
|
||||
it('should not handle GroupByVariable and throw an error', () => {
|
||||
const variable = new GroupByVariable({
|
||||
name: 'test',
|
||||
label: 'test-label',
|
||||
description: 'test-desc',
|
||||
datasource: { uid: 'fake-std', type: 'fake-std' },
|
||||
defaultOptions: [
|
||||
{
|
||||
text: 'Foo',
|
||||
value: 'foo',
|
||||
},
|
||||
{
|
||||
text: 'Bar',
|
||||
value: 'bar',
|
||||
},
|
||||
],
|
||||
});
|
||||
const set = new SceneVariableSet({
|
||||
variables: [variable],
|
||||
});
|
||||
|
||||
expect(() => sceneVariablesSetToVariables(set)).toThrow('Unsupported variable type');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneVariables, sceneUtils } from '@grafana/scenes';
|
||||
import { VariableHide, VariableModel, VariableRefresh, VariableSort } from '@grafana/schema';
|
||||
|
||||
@ -104,6 +105,22 @@ export function sceneVariablesSetToVariables(set: SceneVariables) {
|
||||
},
|
||||
query: variable.state.value,
|
||||
});
|
||||
} else if (sceneUtils.isGroupByVariable(variable) && config.featureToggles.groupByVariable) {
|
||||
variables.push({
|
||||
...commonProperties,
|
||||
datasource: variable.state.datasource,
|
||||
// Only persist the statically defined options
|
||||
options: variable.state.defaultOptions?.map((option) => ({
|
||||
text: option.text,
|
||||
value: String(option.value),
|
||||
})),
|
||||
current: {
|
||||
// @ts-expect-error
|
||||
text: variable.state.text,
|
||||
// @ts-expect-error
|
||||
value: variable.state.value,
|
||||
},
|
||||
});
|
||||
} else if (sceneUtils.isAdHocVariable(variable)) {
|
||||
variables.push({
|
||||
...commonProperties,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
IntervalVariableModel,
|
||||
TypedVariableModel,
|
||||
TextBoxVariableModel,
|
||||
GroupByVariableModel,
|
||||
} from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { config } from '@grafana/runtime';
|
||||
@ -16,6 +17,7 @@ import {
|
||||
ConstantVariable,
|
||||
CustomVariable,
|
||||
DataSourceVariable,
|
||||
GroupByVariable,
|
||||
QueryVariable,
|
||||
SceneDataLayerControls,
|
||||
SceneDataLayers,
|
||||
@ -844,6 +846,116 @@ describe('transformSaveModelToScene', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when groupByVariable feature toggle is enabled', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.groupByVariable = true;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.groupByVariable = false;
|
||||
});
|
||||
|
||||
it('should migrate groupby variable', () => {
|
||||
const variable: GroupByVariableModel = {
|
||||
id: 'groupby',
|
||||
global: false,
|
||||
index: 0,
|
||||
state: LoadingState.Done,
|
||||
error: null,
|
||||
name: 'groupby',
|
||||
label: 'GroupBy Label',
|
||||
description: 'GroupBy Description',
|
||||
type: 'groupby',
|
||||
rootStateKey: 'N4XLmH5Vz',
|
||||
datasource: {
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'prometheus',
|
||||
},
|
||||
multi: true,
|
||||
options: [
|
||||
{
|
||||
selected: false,
|
||||
text: 'Foo',
|
||||
value: 'foo',
|
||||
},
|
||||
{
|
||||
selected: false,
|
||||
text: 'Bar',
|
||||
value: 'bar',
|
||||
},
|
||||
],
|
||||
current: {},
|
||||
query: '',
|
||||
hide: 0,
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
const migrated = createSceneVariableFromVariableModel(variable) as GroupByVariable;
|
||||
const groupbyVarState = migrated.state;
|
||||
|
||||
expect(migrated).toBeInstanceOf(GroupByVariable);
|
||||
expect(groupbyVarState).toEqual({
|
||||
key: expect.any(String),
|
||||
description: 'GroupBy Description',
|
||||
hide: 0,
|
||||
defaultOptions: [
|
||||
{
|
||||
selected: false,
|
||||
text: 'Foo',
|
||||
value: 'foo',
|
||||
},
|
||||
{
|
||||
selected: false,
|
||||
text: 'Bar',
|
||||
value: 'bar',
|
||||
},
|
||||
],
|
||||
isMulti: true,
|
||||
layout: 'horizontal',
|
||||
noValueOnClear: true,
|
||||
label: 'GroupBy Label',
|
||||
name: 'groupby',
|
||||
skipUrlSync: false,
|
||||
type: 'groupby',
|
||||
baseFilters: [],
|
||||
options: [],
|
||||
text: [],
|
||||
value: [],
|
||||
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
|
||||
applyMode: 'auto',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when groupByVariable feature toggle is disabled', () => {
|
||||
it('should not migrate groupby variable and throw an error instead', () => {
|
||||
const variable: GroupByVariableModel = {
|
||||
id: 'groupby',
|
||||
global: false,
|
||||
index: 0,
|
||||
state: LoadingState.Done,
|
||||
error: null,
|
||||
name: 'groupby',
|
||||
label: 'GroupBy Label',
|
||||
description: 'GroupBy Description',
|
||||
type: 'groupby',
|
||||
rootStateKey: 'N4XLmH5Vz',
|
||||
datasource: {
|
||||
uid: 'gdev-prometheus',
|
||||
type: 'prometheus',
|
||||
},
|
||||
multi: true,
|
||||
options: [],
|
||||
current: {},
|
||||
query: '',
|
||||
hide: 0,
|
||||
skipUrlSync: false,
|
||||
};
|
||||
|
||||
expect(() => createSceneVariableFromVariableModel(variable)).toThrow('Scenes: Unsupported variable type');
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['system'])('should throw for unsupported (yet) variables', (type) => {
|
||||
const variable = {
|
||||
name: 'query0',
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
SceneDataLayerControls,
|
||||
TextBoxVariable,
|
||||
UserActionEvent,
|
||||
GroupByVariable,
|
||||
AdHocFiltersVariable,
|
||||
} from '@grafana/scenes';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
@ -291,6 +292,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
||||
const commonProperties = {
|
||||
name: variable.name,
|
||||
label: variable.label,
|
||||
description: variable.description,
|
||||
};
|
||||
if (variable.type === 'adhoc') {
|
||||
return new AdHocFiltersVariable({
|
||||
@ -309,7 +311,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
||||
...commonProperties,
|
||||
value: variable.current?.value ?? '',
|
||||
text: variable.current?.text ?? '',
|
||||
description: variable.description,
|
||||
|
||||
query: variable.query,
|
||||
isMulti: variable.multi,
|
||||
allValue: variable.allValue || undefined,
|
||||
@ -323,7 +325,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
||||
...commonProperties,
|
||||
value: variable.current?.value ?? '',
|
||||
text: variable.current?.text ?? '',
|
||||
description: variable.description,
|
||||
|
||||
query: variable.query,
|
||||
datasource: variable.datasource,
|
||||
sort: variable.sort,
|
||||
@ -342,7 +344,6 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
||||
...commonProperties,
|
||||
value: variable.current?.value ?? '',
|
||||
text: variable.current?.text ?? '',
|
||||
description: variable.description,
|
||||
regex: variable.regex,
|
||||
pluginId: variable.query,
|
||||
allValue: variable.allValue || undefined,
|
||||
@ -358,7 +359,6 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
||||
return new IntervalVariable({
|
||||
...commonProperties,
|
||||
value: currentInterval,
|
||||
description: variable.description,
|
||||
intervals: intervals,
|
||||
autoEnabled: variable.auto,
|
||||
autoStepCount: variable.auto_count,
|
||||
@ -370,7 +370,6 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
||||
} else if (variable.type === 'constant') {
|
||||
return new ConstantVariable({
|
||||
...commonProperties,
|
||||
description: variable.description,
|
||||
value: variable.query,
|
||||
skipUrlSync: variable.skipUrlSync,
|
||||
hide: variable.hide,
|
||||
@ -378,11 +377,21 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
||||
} else if (variable.type === 'textbox') {
|
||||
return new TextBoxVariable({
|
||||
...commonProperties,
|
||||
description: variable.description,
|
||||
value: variable.query,
|
||||
skipUrlSync: variable.skipUrlSync,
|
||||
hide: variable.hide,
|
||||
});
|
||||
} else if (config.featureToggles.groupByVariable && variable.type === 'groupby') {
|
||||
return new GroupByVariable({
|
||||
...commonProperties,
|
||||
datasource: variable.datasource,
|
||||
value: variable.current?.value || [],
|
||||
text: variable.current?.text || [],
|
||||
skipUrlSync: variable.skipUrlSync,
|
||||
hide: variable.hide,
|
||||
// @ts-expect-error
|
||||
defaultOptions: variable.options,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Scenes: Unsupported variable type ${variable.type}`);
|
||||
}
|
||||
|
@ -0,0 +1,114 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { byTestId } from 'testing-library-selector';
|
||||
|
||||
import { VariableSupportType } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
|
||||
|
||||
import { GroupByVariableForm, GroupByVariableFormProps } from './GroupByVariableForm';
|
||||
|
||||
const defaultDatasource = mockDataSource({
|
||||
name: 'Default Test Data Source',
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
const promDatasource = mockDataSource({
|
||||
name: 'Prometheus',
|
||||
type: 'prometheus',
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({
|
||||
...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'),
|
||||
getDataSourceSrv: () => ({
|
||||
get: async () => ({
|
||||
...defaultDatasource,
|
||||
variables: {
|
||||
getType: () => VariableSupportType.Custom,
|
||||
query: jest.fn(),
|
||||
editor: jest.fn().mockImplementation(LegacyVariableQueryEditor),
|
||||
},
|
||||
}),
|
||||
getList: () => [defaultDatasource, promDatasource],
|
||||
getInstanceSettings: () => ({ ...defaultDatasource }),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('GroupByVariableForm', () => {
|
||||
const onDataSourceChangeMock = jest.fn();
|
||||
const onDefaultOptionsChangeMock = jest.fn();
|
||||
|
||||
const defaultProps: GroupByVariableFormProps = {
|
||||
onDataSourceChange: onDataSourceChangeMock,
|
||||
onDefaultOptionsChange: onDefaultOptionsChangeMock,
|
||||
};
|
||||
|
||||
function setup(props?: Partial<GroupByVariableFormProps>) {
|
||||
return {
|
||||
renderer: render(<GroupByVariableForm {...defaultProps} {...props} />),
|
||||
user: userEvent.setup(),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call onDataSourceChange when changing the datasource', async () => {
|
||||
const {
|
||||
renderer: { getByTestId },
|
||||
} = setup();
|
||||
const dataSourcePicker = getByTestId(selectors.components.DataSourcePicker.inputV2);
|
||||
await userEvent.click(dataSourcePicker);
|
||||
await userEvent.click(screen.getByText(/prometheus/i));
|
||||
|
||||
expect(onDataSourceChangeMock).toHaveBeenCalledTimes(1);
|
||||
expect(onDataSourceChangeMock).toHaveBeenCalledWith(promDatasource, undefined);
|
||||
});
|
||||
|
||||
it('should not render code editor when no default options provided', async () => {
|
||||
const {
|
||||
renderer: { queryByTestId },
|
||||
} = setup();
|
||||
const codeEditor = queryByTestId(selectors.components.CodeEditor.container);
|
||||
|
||||
expect(codeEditor).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render code editor when default options provided', async () => {
|
||||
const {
|
||||
renderer: { getByTestId },
|
||||
} = setup({ defaultOptions: [{ text: 'test', value: 'test' }] });
|
||||
const codeEditor = getByTestId(selectors.components.CodeEditor.container);
|
||||
|
||||
await byTestId(selectors.components.CodeEditor.container).find();
|
||||
|
||||
expect(codeEditor).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onDefaultOptionsChange when providing static options', async () => {
|
||||
const {
|
||||
renderer: { getByTestId },
|
||||
} = setup();
|
||||
|
||||
const toggle = getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.modeToggle);
|
||||
|
||||
await userEvent.click(toggle);
|
||||
expect(onDefaultOptionsChangeMock).toHaveBeenCalledTimes(1);
|
||||
expect(onDefaultOptionsChangeMock).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should call onDefaultOptionsChange when toggling off static options', async () => {
|
||||
const {
|
||||
renderer: { getByTestId },
|
||||
} = setup({ defaultOptions: [{ text: 'test', value: 'test' }] });
|
||||
|
||||
const toggle = getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.modeToggle);
|
||||
|
||||
await userEvent.click(toggle);
|
||||
expect(onDefaultOptionsChangeMock).toHaveBeenCalledTimes(1);
|
||||
expect(onDefaultOptionsChangeMock).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { Alert, CodeEditor, Field, Switch } from '@grafana/ui';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
|
||||
import { VariableLegend } from './VariableLegend';
|
||||
|
||||
export interface GroupByVariableFormProps {
|
||||
datasource?: DataSourceRef;
|
||||
onDataSourceChange: (dsSettings: DataSourceInstanceSettings) => void;
|
||||
onDefaultOptionsChange: (options?: MetricFindValue[]) => void;
|
||||
infoText?: string;
|
||||
defaultOptions?: MetricFindValue[];
|
||||
}
|
||||
|
||||
export function GroupByVariableForm({
|
||||
datasource,
|
||||
defaultOptions,
|
||||
infoText,
|
||||
onDataSourceChange,
|
||||
onDefaultOptionsChange,
|
||||
}: GroupByVariableFormProps) {
|
||||
const updateDefaultOptions = useCallback(
|
||||
(csvContent: string) => {
|
||||
const df = readCSV('key,value\n' + csvContent)[0];
|
||||
const options = [];
|
||||
for (let i = 0; i < df.length; i++) {
|
||||
options.push({ text: df.fields[0].values[i], value: df.fields[1].values[i] });
|
||||
}
|
||||
|
||||
onDefaultOptionsChange(options);
|
||||
},
|
||||
[onDefaultOptionsChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VariableLegend>Group by options</VariableLegend>
|
||||
<Field label="Data source" htmlFor="data-source-picker">
|
||||
<DataSourcePicker current={datasource} onChange={onDataSourceChange} width={30} variables={true} noDefault />
|
||||
</Field>
|
||||
|
||||
{infoText ? (
|
||||
<Alert
|
||||
title={infoText}
|
||||
severity="info"
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.infoText}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Field
|
||||
label="Use static Group By dimensions"
|
||||
description="Provide dimensions as CSV: dimensionId, dimensionName "
|
||||
>
|
||||
<Switch
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.modeToggle}
|
||||
value={defaultOptions !== undefined}
|
||||
onChange={(e) => {
|
||||
if (defaultOptions === undefined) {
|
||||
onDefaultOptionsChange([]);
|
||||
} else {
|
||||
onDefaultOptionsChange(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{defaultOptions !== undefined && (
|
||||
<CodeEditor
|
||||
height={300}
|
||||
language="csv"
|
||||
value={defaultOptions.map((o) => `${o.text},${o.value}`).join('\n')}
|
||||
onBlur={updateDefaultOptions}
|
||||
onSave={updateDefaultOptions}
|
||||
showMiniMap={false}
|
||||
showLineNumbers={true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
import { act, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { MetricFindValue, VariableSupportType } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { GroupByVariable } from '@grafana/scenes';
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor';
|
||||
|
||||
import { GroupByVariableEditor } from './GroupByVariableEditor';
|
||||
|
||||
const defaultDatasource = mockDataSource({
|
||||
name: 'Default Test Data Source',
|
||||
uid: 'test-ds',
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
const promDatasource = mockDataSource({
|
||||
name: 'Prometheus',
|
||||
uid: 'prometheus',
|
||||
type: 'prometheus',
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({
|
||||
...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'),
|
||||
getDataSourceSrv: () => ({
|
||||
get: async () => ({
|
||||
...defaultDatasource,
|
||||
variables: {
|
||||
getType: () => VariableSupportType.Custom,
|
||||
query: jest.fn(),
|
||||
editor: jest.fn().mockImplementation(LegacyVariableQueryEditor),
|
||||
},
|
||||
}),
|
||||
getList: () => [defaultDatasource, promDatasource],
|
||||
getInstanceSettings: () => ({ ...defaultDatasource }),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('GroupByVariableEditor', () => {
|
||||
it('renders AdHocVariableForm with correct props', async () => {
|
||||
const { renderer } = await setup();
|
||||
const dataSourcePicker = renderer.getByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.dataSourceSelect
|
||||
);
|
||||
const infoText = renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.infoText);
|
||||
|
||||
expect(dataSourcePicker).toBeInTheDocument();
|
||||
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
|
||||
expect(infoText).toBeInTheDocument();
|
||||
expect(infoText).toHaveTextContent('This data source does not support group by variable yet.');
|
||||
});
|
||||
|
||||
it('should update the variable data source when data source picker is changed', async () => {
|
||||
const { renderer, variable, user } = await setup();
|
||||
|
||||
// Simulate changing the data source
|
||||
await user.click(renderer.getByTestId(selectors.components.DataSourcePicker.inputV2));
|
||||
await user.click(renderer.getByText(/prom/i));
|
||||
|
||||
expect(variable.state.datasource).toEqual({ uid: 'prometheus', type: 'prometheus' });
|
||||
});
|
||||
|
||||
it('should update the variable default options when static options are enabled', async () => {
|
||||
const { renderer, variable, user } = await setup();
|
||||
|
||||
// Simulate toggling static options on
|
||||
await user.click(
|
||||
renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.modeToggle)
|
||||
);
|
||||
|
||||
expect(variable.state.defaultOptions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should update the variable default options when static options are disabled', async () => {
|
||||
const { renderer, variable, user } = await setup([{ text: 'A', value: 'A' }]);
|
||||
|
||||
// Simulate toggling static options off
|
||||
await user.click(
|
||||
renderer.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.modeToggle)
|
||||
);
|
||||
|
||||
expect(variable.state.defaultOptions).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
async function setup(defaultOptions?: MetricFindValue[]) {
|
||||
const onRunQuery = jest.fn();
|
||||
const variable = new GroupByVariable({
|
||||
name: 'groupByVariable',
|
||||
type: 'groupby',
|
||||
label: 'Group By',
|
||||
datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type },
|
||||
defaultOptions,
|
||||
});
|
||||
return {
|
||||
renderer: await act(() => render(<GroupByVariableEditor variable={variable} onRunQuery={onRunQuery} />)),
|
||||
variable,
|
||||
user: userEvent.setup(),
|
||||
mocks: { onRunQuery },
|
||||
};
|
||||
}
|
@ -1,12 +1,51 @@
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceRef, MetricFindValue } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { GroupByVariable } from '@grafana/scenes';
|
||||
|
||||
import { GroupByVariableForm } from '../components/GroupByVariableForm';
|
||||
|
||||
interface GroupByVariableEditorProps {
|
||||
variable: GroupByVariable;
|
||||
onChange: (variable: GroupByVariable) => void;
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
export function GroupByVariableEditor(props: GroupByVariableEditorProps) {
|
||||
return <div>GroupByVariableEditor</div>;
|
||||
const { variable, onRunQuery } = props;
|
||||
const { datasource: datasourceRef, defaultOptions } = variable.useState();
|
||||
|
||||
const { value: datasource } = useAsync(async () => {
|
||||
return await getDataSourceSrv().get(datasourceRef);
|
||||
}, [variable.state]);
|
||||
|
||||
const message = datasource?.getTagKeys
|
||||
? 'Group by dimensions are applied automatically to all queries that target this data source'
|
||||
: 'This data source does not support group by variable yet.';
|
||||
|
||||
const onDataSourceChange = async (ds: DataSourceInstanceSettings) => {
|
||||
const dsRef: DataSourceRef = {
|
||||
uid: ds.uid,
|
||||
type: ds.type,
|
||||
};
|
||||
|
||||
variable.setState({ datasource: dsRef });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
const onDefaultOptionsChange = async (defaultOptions?: MetricFindValue[]) => {
|
||||
variable.setState({ defaultOptions });
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
return (
|
||||
<GroupByVariableForm
|
||||
defaultOptions={defaultOptions}
|
||||
datasource={datasourceRef ?? undefined}
|
||||
infoText={datasourceRef ? message : undefined}
|
||||
onDataSourceChange={onDataSourceChange}
|
||||
onDefaultOptionsChange={onDefaultOptionsChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { setTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import { config, setTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import {
|
||||
CustomVariable,
|
||||
ConstantVariable,
|
||||
@ -99,26 +99,61 @@ describe('isEditableVariableType', () => {
|
||||
});
|
||||
|
||||
describe('getVariableTypeSelectOptions', () => {
|
||||
it('should contain all editable variable types', () => {
|
||||
const options = getVariableTypeSelectOptions();
|
||||
expect(options).toHaveLength(Object.keys(EDITABLE_VARIABLES).length);
|
||||
describe('when groupByVariable is enabled', () => {
|
||||
beforeAll(() => {
|
||||
config.featureToggles.groupByVariable = true;
|
||||
});
|
||||
|
||||
EDITABLE_VARIABLES_SELECT_ORDER.forEach((type) => {
|
||||
expect(EDITABLE_VARIABLES).toHaveProperty(type);
|
||||
afterAll(() => {
|
||||
config.featureToggles.groupByVariable = false;
|
||||
});
|
||||
|
||||
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(8);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an array of selectable values for editable variable types', () => {
|
||||
const options = getVariableTypeSelectOptions();
|
||||
expect(options).toHaveLength(8);
|
||||
describe('when groupByVariable is disabled', () => {
|
||||
it('should contain all editable variable types except groupby', () => {
|
||||
const options = getVariableTypeSelectOptions();
|
||||
expect(options).toHaveLength(Object.keys(EDITABLE_VARIABLES).length - 1);
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const editableType = EDITABLE_VARIABLES_SELECT_ORDER[index];
|
||||
const variableTypeConfig = EDITABLE_VARIABLES[editableType];
|
||||
EDITABLE_VARIABLES_SELECT_ORDER.forEach((type) => {
|
||||
expect(EDITABLE_VARIABLES).toHaveProperty(type);
|
||||
});
|
||||
});
|
||||
|
||||
expect(option.value).toBe(editableType);
|
||||
expect(option.label).toBe(variableTypeConfig.name);
|
||||
expect(option.description).toBe(variableTypeConfig.description);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { chain } from 'lodash';
|
||||
|
||||
import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import {
|
||||
ConstantVariable,
|
||||
CustomVariable,
|
||||
@ -95,11 +95,18 @@ export const EDITABLE_VARIABLES_SELECT_ORDER: EditableVariableType[] = [
|
||||
];
|
||||
|
||||
export function getVariableTypeSelectOptions(): Array<SelectableValue<EditableVariableType>> {
|
||||
return EDITABLE_VARIABLES_SELECT_ORDER.map((variableType) => ({
|
||||
const results = EDITABLE_VARIABLES_SELECT_ORDER.map((variableType) => ({
|
||||
label: EDITABLE_VARIABLES[variableType].name,
|
||||
value: variableType,
|
||||
description: EDITABLE_VARIABLES[variableType].description,
|
||||
}));
|
||||
|
||||
if (!config.featureToggles.groupByVariable) {
|
||||
// Remove group by variable type if feature toggle is off
|
||||
return results.filter((option) => option.value !== 'groupby');
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function getVariableEditor(type: EditableVariableType) {
|
||||
|
@ -165,7 +165,7 @@ describe('type guards', () => {
|
||||
const variableFactsObj: Record<VariableType | ExtraVariableTypes, VariableFacts> = {
|
||||
query: { variable: createQueryVariable(), isMulti: true, hasOptions: true, hasCurrent: true },
|
||||
adhoc: { variable: createAdhocVariable(), isMulti: false, hasOptions: false, hasCurrent: false },
|
||||
groupby: { variable: createGroupByVariable(), isMulti: true, hasOptions: false, hasCurrent: false },
|
||||
groupby: { variable: createGroupByVariable(), isMulti: true, hasOptions: true, hasCurrent: true },
|
||||
constant: { variable: createConstantVariable(), isMulti: false, hasOptions: true, hasCurrent: true },
|
||||
datasource: { variable: createDatasourceVariable(), isMulti: true, hasOptions: true, hasCurrent: true },
|
||||
interval: { variable: createIntervalVariable(), isMulti: false, hasOptions: true, hasCurrent: true },
|
||||
|
@ -82,12 +82,14 @@ export function createAdhocVariable(input?: Partial<AdHocVariableModel>): AdHocV
|
||||
export function createGroupByVariable(input?: Partial<GroupByVariableModel>): GroupByVariableModel {
|
||||
return {
|
||||
...createBaseVariableModel('groupby'),
|
||||
query: '',
|
||||
datasource: {
|
||||
uid: 'abc-123',
|
||||
type: 'prometheus',
|
||||
},
|
||||
groupByKeys: [],
|
||||
multi: true,
|
||||
current: createVariableOption('job'),
|
||||
options: [createVariableOption('job'), createVariableOption('instance')],
|
||||
...input,
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user