mirror of https://github.com/grafana/grafana
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>pull/82465/head
parent
269fa400f0
commit
f016f95298
|
File diff suppressed because it is too large
Load Diff
@ -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} |
||||
/> |
||||
); |
||||
} |
||||
|
Loading…
Reference in new issue