Dashboard Scene: Chore - Add missing unit test to PanelDataQueryTab (#87139)
* Create basic structure PanelDataQueriesTab unit test * Add more test cases to the PanelDataQueriesTab test * Update public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx
This commit is contained in:
@@ -286,6 +286,8 @@ export const Components = {
|
|||||||
queryInspectorButton: 'Query inspector button',
|
queryInspectorButton: 'Query inspector button',
|
||||||
queryHistoryButton: 'data-testid query-history-button',
|
queryHistoryButton: 'data-testid query-history-button',
|
||||||
addQuery: 'data-testid query-tab-add-query',
|
addQuery: 'data-testid query-tab-add-query',
|
||||||
|
queryGroupTopSection: 'data-testid query group top section',
|
||||||
|
addExpression: 'data-testid query-tab-add-expression',
|
||||||
},
|
},
|
||||||
QueryHistory: {
|
QueryHistory: {
|
||||||
queryText: 'Query text',
|
queryText: 'Query text',
|
||||||
|
|||||||
+346
@@ -0,0 +1,346 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
import { of, map } from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DataQuery,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataSourceApi,
|
||||||
|
DataSourceJsonData,
|
||||||
|
DataSourceRef,
|
||||||
|
FieldType,
|
||||||
|
LoadingState,
|
||||||
|
PanelData,
|
||||||
|
TimeRange,
|
||||||
|
toDataFrame,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||||
|
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||||
|
import { DashboardDataDTO } from 'app/types';
|
||||||
|
|
||||||
|
import { transformSaveModelToScene } from '../../serialization/transformSaveModelToScene';
|
||||||
|
import { DashboardModelCompatibilityWrapper } from '../../utils/DashboardModelCompatibilityWrapper';
|
||||||
|
import { findVizPanelByKey } from '../../utils/utils';
|
||||||
|
import { VizPanelManager } from '../VizPanelManager';
|
||||||
|
import { testDashboard } from '../testfiles/testDashboard';
|
||||||
|
|
||||||
|
import { PanelDataQueriesTab, PanelDataQueriesTabRendered } from './PanelDataQueriesTab';
|
||||||
|
|
||||||
|
async function createModelMock() {
|
||||||
|
const panelManager = setupVizPanelManger('panel-1');
|
||||||
|
panelManager.activate();
|
||||||
|
await Promise.resolve();
|
||||||
|
const queryTabModel = new PanelDataQueriesTab(panelManager);
|
||||||
|
|
||||||
|
// mock queryRunner data state
|
||||||
|
jest.spyOn(queryTabModel.queryRunner, 'state', 'get').mockReturnValue({
|
||||||
|
...queryTabModel.queryRunner.state,
|
||||||
|
data: {
|
||||||
|
state: LoadingState.Done,
|
||||||
|
series: [
|
||||||
|
toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'values', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
timeRange: {} as TimeRange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return queryTabModel;
|
||||||
|
}
|
||||||
|
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
|
const result: PanelData = {
|
||||||
|
state: LoadingState.Loading,
|
||||||
|
series: [],
|
||||||
|
timeRange: request.range,
|
||||||
|
};
|
||||||
|
|
||||||
|
return of([]).pipe(
|
||||||
|
map(() => {
|
||||||
|
result.state = LoadingState.Done;
|
||||||
|
result.series = [
|
||||||
|
toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [100, 200, 300] },
|
||||||
|
{ name: 'values', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ds1Mock: DataSourceApi = {
|
||||||
|
meta: {
|
||||||
|
id: 'grafana-testdata-datasource',
|
||||||
|
},
|
||||||
|
name: 'grafana-testdata-datasource',
|
||||||
|
type: 'grafana-testdata-datasource',
|
||||||
|
uid: 'gdev-testdata',
|
||||||
|
getRef: () => {
|
||||||
|
return { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' };
|
||||||
|
},
|
||||||
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||||
|
|
||||||
|
const ds2Mock: DataSourceApi = {
|
||||||
|
meta: {
|
||||||
|
id: 'grafana-prometheus-datasource',
|
||||||
|
},
|
||||||
|
name: 'grafana-prometheus-datasource',
|
||||||
|
type: 'grafana-prometheus-datasource',
|
||||||
|
uid: 'gdev-prometheus',
|
||||||
|
getRef: () => {
|
||||||
|
return { type: 'grafana-prometheus-datasource', uid: 'gdev-prometheus' };
|
||||||
|
},
|
||||||
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||||
|
|
||||||
|
const ds3Mock: DataSourceApi = {
|
||||||
|
meta: {
|
||||||
|
id: DASHBOARD_DATASOURCE_PLUGIN_ID,
|
||||||
|
},
|
||||||
|
name: SHARED_DASHBOARD_QUERY,
|
||||||
|
type: SHARED_DASHBOARD_QUERY,
|
||||||
|
uid: SHARED_DASHBOARD_QUERY,
|
||||||
|
getRef: () => {
|
||||||
|
return { type: SHARED_DASHBOARD_QUERY, uid: SHARED_DASHBOARD_QUERY };
|
||||||
|
},
|
||||||
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||||
|
|
||||||
|
const defaultDsMock: DataSourceApi = {
|
||||||
|
meta: {
|
||||||
|
id: 'grafana-testdata-datasource',
|
||||||
|
},
|
||||||
|
name: 'grafana-testdata-datasource',
|
||||||
|
type: 'grafana-testdata-datasource',
|
||||||
|
uid: 'gdev-testdata',
|
||||||
|
getRef: () => {
|
||||||
|
return { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' };
|
||||||
|
},
|
||||||
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||||
|
|
||||||
|
const instance1SettingsMock = {
|
||||||
|
id: 1,
|
||||||
|
uid: 'gdev-testdata',
|
||||||
|
name: 'testDs1',
|
||||||
|
type: 'grafana-testdata-datasource',
|
||||||
|
meta: {
|
||||||
|
id: 'grafana-testdata-datasource',
|
||||||
|
info: {
|
||||||
|
logos: {
|
||||||
|
small: 'test-logo.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance2SettingsMock = {
|
||||||
|
id: 1,
|
||||||
|
uid: 'gdev-prometheus',
|
||||||
|
name: 'testDs2',
|
||||||
|
type: 'grafana-prometheus-datasource',
|
||||||
|
meta: {
|
||||||
|
id: 'grafana-prometheus-datasource',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||||
|
const grafanaDs = {
|
||||||
|
id: 1,
|
||||||
|
uid: '-- Grafana --',
|
||||||
|
name: 'grafana',
|
||||||
|
type: 'grafana',
|
||||||
|
meta: {
|
||||||
|
id: 'grafana',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||||
|
const MixedDs = {
|
||||||
|
id: 5,
|
||||||
|
uid: '-- Mixed --',
|
||||||
|
name: 'Mixed',
|
||||||
|
type: 'datasource',
|
||||||
|
meta: {
|
||||||
|
id: 'grafana',
|
||||||
|
mixed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MixedDsSettingsMock = {
|
||||||
|
id: 5,
|
||||||
|
uid: '-- Mixed --',
|
||||||
|
name: 'Mixed',
|
||||||
|
type: 'datasource',
|
||||||
|
meta: {
|
||||||
|
id: 'grafana',
|
||||||
|
mixed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
|
return runRequestMock(ds, request);
|
||||||
|
},
|
||||||
|
getDataSourceSrv: () => ({
|
||||||
|
get: async (ref: DataSourceRef) => {
|
||||||
|
// Mocking the build in Grafana data source to avoid annotations data layer errors.
|
||||||
|
if (ref.uid === '-- Grafana --') {
|
||||||
|
return grafanaDs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.uid === 'gdev-testdata') {
|
||||||
|
return ds1Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.uid === 'gdev-prometheus') {
|
||||||
|
return ds2Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.uid === '-- Mixed --') {
|
||||||
|
return MixedDs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.uid === SHARED_DASHBOARD_QUERY) {
|
||||||
|
return ds3Mock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if datasource is not found, return default datasource
|
||||||
|
return defaultDsMock;
|
||||||
|
},
|
||||||
|
getInstanceSettings: (ref: DataSourceRef) => {
|
||||||
|
if (ref.uid === 'gdev-testdata') {
|
||||||
|
return instance1SettingsMock;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.uid === 'gdev-prometheus') {
|
||||||
|
return instance2SettingsMock;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref.uid === '-- Mixed --') {
|
||||||
|
return MixedDsSettingsMock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if datasource is not found, return default instance settings
|
||||||
|
return instance1SettingsMock;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
locationService: {
|
||||||
|
partial: jest.fn(),
|
||||||
|
getSearchObject: jest.fn().mockReturnValue({
|
||||||
|
firstPanel: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
...jest.requireActual('@grafana/runtime').config,
|
||||||
|
defaultDatasource: 'gdev-testdata',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
describe('PanelDataQueriesModel', () => {
|
||||||
|
it('can add a new query', async () => {
|
||||||
|
const vizPanelManager = setupVizPanelManger('panel-1');
|
||||||
|
vizPanelManager.activate();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
const model = new PanelDataQueriesTab(vizPanelManager);
|
||||||
|
model.addQueryClick();
|
||||||
|
expect(model.queryRunner.state.queries).toHaveLength(2);
|
||||||
|
expect(model.queryRunner.state.queries[1].refId).toBe('B');
|
||||||
|
expect(model.queryRunner.state.queries[1].hide).toBe(false);
|
||||||
|
expect(model.queryRunner.state.queries[1].datasource).toEqual({
|
||||||
|
type: 'grafana-testdata-datasource',
|
||||||
|
uid: 'gdev-testdata',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add a new query when datasource is mixed', async () => {
|
||||||
|
const vizPanelManager = setupVizPanelManger('panel-7');
|
||||||
|
vizPanelManager.activate();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
const model = new PanelDataQueriesTab(vizPanelManager);
|
||||||
|
expect(vizPanelManager.state.datasource?.uid).toBe('-- Mixed --');
|
||||||
|
expect(model.queryRunner.state.datasource?.uid).toBe('-- Mixed --');
|
||||||
|
model.addQueryClick();
|
||||||
|
|
||||||
|
expect(model.queryRunner.state.queries).toHaveLength(2);
|
||||||
|
expect(model.queryRunner.state.queries[1].refId).toBe('B');
|
||||||
|
expect(model.queryRunner.state.queries[1].hide).toBe(false);
|
||||||
|
expect(model.queryRunner.state.queries[1].datasource?.uid).toBe('gdev-testdata');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PanelDataQueriesTab', () => {
|
||||||
|
it('renders query group top section', async () => {
|
||||||
|
const modelMock = await createModelMock();
|
||||||
|
|
||||||
|
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
||||||
|
await screen.findByTestId(selectors.components.QueryTab.queryGroupTopSection);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders queries rows when queries are set', async () => {
|
||||||
|
const modelMock = await createModelMock();
|
||||||
|
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
||||||
|
|
||||||
|
await screen.findByTestId('query-editor-rows');
|
||||||
|
expect(screen.getAllByTestId('query-editor-row')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow to add a new query when user clicks on add new', async () => {
|
||||||
|
const modelMock = await createModelMock();
|
||||||
|
jest.spyOn(modelMock, 'addQueryClick');
|
||||||
|
jest.spyOn(modelMock, 'onQueriesChange');
|
||||||
|
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
||||||
|
|
||||||
|
await screen.findByTestId(selectors.components.QueryTab.addQuery);
|
||||||
|
await userEvent.click(screen.getByTestId(selectors.components.QueryTab.addQuery));
|
||||||
|
|
||||||
|
const expectedQueries = [
|
||||||
|
{
|
||||||
|
datasource: { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' },
|
||||||
|
refId: 'A',
|
||||||
|
scenarioId: 'random_walk',
|
||||||
|
seriesCount: 1,
|
||||||
|
},
|
||||||
|
{ datasource: { type: 'grafana-testdata-datasource', uid: 'gdev-testdata' }, hide: false, refId: 'B' },
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(modelMock.addQueryClick).toHaveBeenCalled();
|
||||||
|
expect(modelMock.onQueriesChange).toHaveBeenCalledWith(expectedQueries);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow to remove a query when user clicks on remove', async () => {
|
||||||
|
const modelMock = await createModelMock();
|
||||||
|
jest.spyOn(modelMock, 'addQueryClick');
|
||||||
|
jest.spyOn(modelMock, 'onQueriesChange');
|
||||||
|
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
|
||||||
|
|
||||||
|
await screen.findByTestId('data-testid Remove query');
|
||||||
|
await userEvent.click(screen.getByTestId('data-testid Remove query'));
|
||||||
|
|
||||||
|
expect(modelMock.onQueriesChange).toHaveBeenCalledWith([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupVizPanelManger = (panelId: string) => {
|
||||||
|
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||||
|
const panel = findVizPanelByKey(scene, panelId)!;
|
||||||
|
|
||||||
|
const vizPanelManager = VizPanelManager.createFor(panel);
|
||||||
|
|
||||||
|
// The following happens on DahsboardScene activation. For the needs of this test this activation aint needed hence we hand-call it
|
||||||
|
// @ts-expect-error
|
||||||
|
getDashboardSrv().setCurrent(new DashboardModelCompatibilityWrapper(scene));
|
||||||
|
|
||||||
|
return vizPanelManager;
|
||||||
|
};
|
||||||
+2
-2
@@ -185,7 +185,7 @@ export class PanelDataQueriesTab extends SceneObjectBase<PanelDataQueriesTabStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQueriesTab>) {
|
export function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQueriesTab>) {
|
||||||
const { datasource, dsSettings } = model.panelManager.useState();
|
const { datasource, dsSettings } = model.panelManager.useState();
|
||||||
const { data, queries } = model.panelManager.queryRunner.useState();
|
const { data, queries } = model.panelManager.queryRunner.useState();
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ function PanelDataQueriesTabRendered({ model }: SceneComponentProps<PanelDataQue
|
|||||||
icon="plus"
|
icon="plus"
|
||||||
onClick={model.onAddExpressionClick}
|
onClick={model.onAddExpressionClick}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
data-testid="query-tab-add-expression"
|
data-testid={selectors.components.QueryTab.addExpression}
|
||||||
>
|
>
|
||||||
<span>Expression </span>
|
<span>Expression </span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -498,6 +498,99 @@ export const panelWithDataSourceNotFound = {
|
|||||||
type: 'timeseries',
|
type: 'timeseries',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const panelWithQueriesAndMixedDatasource = {
|
||||||
|
datasource: {
|
||||||
|
type: 'datasource',
|
||||||
|
uid: '-- Mixed --',
|
||||||
|
},
|
||||||
|
fieldConfig: {
|
||||||
|
defaults: {
|
||||||
|
color: {
|
||||||
|
mode: 'palette-classic',
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
axisBorderShow: false,
|
||||||
|
axisCenteredZero: false,
|
||||||
|
axisColorMode: 'text',
|
||||||
|
axisLabel: '',
|
||||||
|
axisPlacement: 'auto',
|
||||||
|
barAlignment: 0,
|
||||||
|
drawStyle: 'line',
|
||||||
|
fillOpacity: 0,
|
||||||
|
gradientMode: 'none',
|
||||||
|
hideFrom: {
|
||||||
|
legend: false,
|
||||||
|
tooltip: false,
|
||||||
|
viz: false,
|
||||||
|
},
|
||||||
|
insertNulls: false,
|
||||||
|
lineInterpolation: 'linear',
|
||||||
|
lineWidth: 1,
|
||||||
|
pointSize: 5,
|
||||||
|
scaleDistribution: {
|
||||||
|
type: 'linear',
|
||||||
|
},
|
||||||
|
showPoints: 'auto',
|
||||||
|
spanNulls: false,
|
||||||
|
stacking: {
|
||||||
|
group: 'A',
|
||||||
|
mode: 'none',
|
||||||
|
},
|
||||||
|
thresholdsStyle: {
|
||||||
|
mode: 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mappings: [],
|
||||||
|
thresholds: {
|
||||||
|
mode: 'absolute',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
color: 'green',
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'red',
|
||||||
|
value: 80,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: [],
|
||||||
|
},
|
||||||
|
gridPos: {
|
||||||
|
h: 8,
|
||||||
|
w: 12,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
id: 7,
|
||||||
|
options: {
|
||||||
|
legend: {
|
||||||
|
calcs: [],
|
||||||
|
displayMode: 'list',
|
||||||
|
placement: 'bottom',
|
||||||
|
showLegend: true,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'single',
|
||||||
|
sort: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
datasource: {
|
||||||
|
type: 'grafana-testdata-datasource',
|
||||||
|
uid: 'gdev-testdata',
|
||||||
|
},
|
||||||
|
refId: 'A',
|
||||||
|
scenarioId: 'random_walk',
|
||||||
|
seriesCount: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: 'Panel with just queries',
|
||||||
|
type: 'timeseries',
|
||||||
|
};
|
||||||
|
|
||||||
export const testDashboard = {
|
export const testDashboard = {
|
||||||
annotations: {
|
annotations: {
|
||||||
list: [
|
list: [
|
||||||
@@ -528,6 +621,7 @@ export const testDashboard = {
|
|||||||
panelWithDashboardQueryAndTransformations,
|
panelWithDashboardQueryAndTransformations,
|
||||||
panelWithNoDataSource,
|
panelWithNoDataSource,
|
||||||
panelWithDataSourceNotFound,
|
panelWithDataSourceNotFound,
|
||||||
|
panelWithQueriesAndMixedDatasource,
|
||||||
],
|
],
|
||||||
refresh: '',
|
refresh: '',
|
||||||
schemaVersion: 39,
|
schemaVersion: 39,
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ export function QueryGroupTopSection({
|
|||||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div data-testid={selectors.components.QueryTab.queryGroupTopSection}>
|
||||||
<div className={styles.dataSourceRow}>
|
<div className={styles.dataSourceRow}>
|
||||||
<InlineFormLabel htmlFor="data-source-picker" width={'auto'}>
|
<InlineFormLabel htmlFor="data-source-picker" width={'auto'}>
|
||||||
Data source
|
Data source
|
||||||
|
|||||||
Reference in New Issue
Block a user