QueryGroup & DataSourceSrv & DataSourcePicker changes simplify usage, error handling and reduce duplication, support for uid (#29542)

* Starting moving more stuff into data source picker

* WIP progress

* Progress on datasource picker rethink

* Things are working now some details to figure out

* Removed commented part

* Complex work on getting data source lists

* Fixed variable support showing correct data sources

* Tried fixing dashboard import but failed

* Fixes

* Fixed import dashboard

* Fixed unit test

* Fixed explore test

* Fixed test

* Fix

* fixed more tests

* fixed more tests

* fixed showing which option is default in picker

* Changed query variable to use data source picker, updated tests and e2e

* Fixed more tests

* Updated snapshots, had wrong typescript version
This commit is contained in:
Torkel Ödegaard 2020-12-04 14:24:55 +01:00 committed by GitHub
parent c62a0aa4d0
commit 3d6380a0aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 707 additions and 807 deletions

View File

@ -46,10 +46,9 @@ describe('Variables - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.should('be.visible')
.within(select => {
e2e.components.Select.singleValue().should('have.text', '');
e2e.components.Select.singleValue().should('have.text', 'gdev-testdata');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect()
.should('be.visible')
.within(select => {

View File

@ -567,6 +567,7 @@ export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataS
username?: string;
password?: string; // when access is direct, for some legacy datasources
database?: string;
isDefault?: boolean;
/**
* This is the full Authorization header if basic auth is enabled.
@ -582,7 +583,6 @@ export interface DataSourceSelectItem {
name: string;
value: string | null;
meta: DataSourcePluginMeta;
sort: string;
}
/**

View File

@ -1,5 +1,3 @@
import { Pages } from './pages';
export const Components = {
DataSource: {
TestData: {
@ -57,8 +55,8 @@ export const Components = {
},
OptionsPane: {
content: 'Panel editor option pane content',
close: Pages.Dashboard.Toolbar.toolbarItems('Close options pane'),
open: Pages.Dashboard.Toolbar.toolbarItems('Open options pane'),
close: 'Dashboard navigation bar button Close options pane',
open: 'Dashboard navigation bar button Open options pane',
select: 'Panel editor option pane select',
tab: (title: string) => `Panel editor option pane tab ${title}`,
},

View File

@ -1,3 +1,5 @@
import { Components } from './components';
export const Pages = {
Login: {
url: '/login',
@ -87,7 +89,7 @@ export const Pages = {
submitButton: 'Variable editor Submit button',
},
QueryVariable: {
queryOptionsDataSourceSelect: 'Variable editor Form Query DataSource select',
queryOptionsDataSourceSelect: Components.DataSourcePicker.container,
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
queryOptionsSortSelect: 'Variable editor Form Query Sort select',

View File

@ -16,14 +16,9 @@ export interface DataSourceSrv {
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>;
/**
* Get all data sources
* Get a list of data sources
*/
getAll(): DataSourceInstanceSettings[];
/**
* Get all data sources except for internal ones that usually should not be listed like mixed data source.
*/
getExternal(): DataSourceInstanceSettings[];
getList(filters?: GetDataSourceListFilters): DataSourceInstanceSettings[];
/**
* Get settings and plugin metadata by name or uid
@ -31,6 +26,17 @@ export interface DataSourceSrv {
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined;
}
/** @public */
export interface GetDataSourceListFilters {
mixed?: boolean;
metrics?: boolean;
tracing?: boolean;
annotations?: boolean;
dashboard?: boolean;
variables?: boolean;
pluginId?: string;
}
let singletonInstance: DataSourceSrv;
/**

View File

@ -117,9 +117,11 @@ const getStyles = stylesFactory(
? 0
: `-${finalSpacing}`;
const label = orientation === Orientation.Vertical ? 'vertical-group' : 'horizontal-group';
return {
layout: css`
label: HorizontalGroup;
label: ${label};
display: flex;
flex-direction: ${orientation === Orientation.Vertical ? 'column' : 'row'};
flex-wrap: ${wrap ? 'wrap' : 'nowrap'};

View File

@ -59,14 +59,7 @@ export const SingleValue = (props: Props) => {
return (
<components.SingleValue {...props}>
<div
className={cx(
styles.singleValue,
css`
overflow: hidden;
`
)}
>
<div className={cx(styles.singleValue)}>
{data.imgUrl ? (
<FadeWithImage loading={loading} imgUrl={data.imgUrl} />
) : (

View File

@ -146,7 +146,6 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
if isDefault, _ := dsM["isDefault"].(bool); isDefault {
defaultDS = n
}
delete(dsM, "isDefault")
meta := dsM["meta"].(*plugins.DataSourcePlugin)
if meta.Preload {

View File

@ -3,72 +3,119 @@ import React, { PureComponent } from 'react';
// Components
import { HorizontalGroup, Select } from '@grafana/ui';
import { SelectableValue, DataSourceSelectItem } from '@grafana/data';
import { SelectableValue, DataSourceInstanceSettings } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { isUnsignedPluginSignature, PluginSignatureBadge } from '../../../features/plugins/PluginSignatureBadge';
import { getDataSourceSrv } from '@grafana/runtime';
export interface Props {
onChange: (ds: DataSourceSelectItem) => void;
datasources: DataSourceSelectItem[];
current?: DataSourceSelectItem | null;
onChange: (ds: DataSourceInstanceSettings) => void;
current: string | null;
hideTextValue?: boolean;
onBlur?: () => void;
autoFocus?: boolean;
openMenuOnFocus?: boolean;
showLoading?: boolean;
placeholder?: string;
invalid?: boolean;
tracing?: boolean;
mixed?: boolean;
dashboard?: boolean;
metrics?: boolean;
annotations?: boolean;
variables?: boolean;
pluginId?: string;
noDefault?: boolean;
}
export class DataSourcePicker extends PureComponent<Props> {
export interface State {
error?: string;
}
export class DataSourcePicker extends PureComponent<Props, State> {
dataSourceSrv = getDataSourceSrv();
static defaultProps: Partial<Props> = {
autoFocus: false,
openMenuOnFocus: false,
placeholder: 'Select datasource',
};
searchInput: HTMLElement;
state: State = {};
constructor(props: Props) {
super(props);
}
onChange = (item: SelectableValue<string>) => {
const ds = this.props.datasources.find(ds => ds.name === item.value);
componentDidMount() {
const { current } = this.props;
const dsSettings = this.dataSourceSrv.getInstanceSettings(current);
if (!dsSettings) {
this.setState({ error: 'Could not find data source ' + current });
}
}
if (ds) {
this.props.onChange(ds);
onChange = (item: SelectableValue<string>) => {
const dsSettings = this.dataSourceSrv.getInstanceSettings(item.value);
if (dsSettings) {
this.props.onChange(dsSettings);
this.setState({ error: undefined });
}
};
render() {
const {
datasources,
current,
autoFocus,
hideTextValue,
onBlur,
openMenuOnFocus,
showLoading,
placeholder,
invalid,
} = this.props;
private getCurrentValue() {
const { current, hideTextValue, noDefault } = this.props;
const options = datasources.map(ds => ({
value: ds.name,
label: ds.name,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
}));
if (!current && noDefault) {
return null;
}
const value = current && {
label: current.name.substr(0, 37),
value: current.name,
imgUrl: current.meta.info.logos.small,
loading: showLoading,
const ds = this.dataSourceSrv.getInstanceSettings(current);
if (ds) {
return {
label: ds.name.substr(0, 37),
value: ds.name,
imgUrl: ds.meta.info.logos.small,
hideText: hideTextValue,
meta: ds.meta,
};
}
return {
label: (current ?? 'no name') + ' - not found',
value: current,
imgUrl: '',
hideText: hideTextValue,
meta: current.meta,
};
}
getDataSourceOptions() {
const { tracing, metrics, mixed, dashboard, variables, annotations, pluginId } = this.props;
const options = this.dataSourceSrv
.getList({
tracing,
metrics,
dashboard,
mixed,
variables,
annotations,
pluginId,
})
.map(ds => ({
value: ds.name,
label: `${ds.name}${ds.isDefault ? ' (default)' : ''}`,
imgUrl: ds.meta.info.logos.small,
meta: ds.meta,
}));
return options;
}
render() {
const { autoFocus, onBlur, openMenuOnFocus, placeholder } = this.props;
const { error } = this.state;
const options = this.getDataSourceOptions();
const value = this.getCurrentValue();
return (
<div aria-label={selectors.components.DataSourcePicker.container}>
@ -87,9 +134,9 @@ export class DataSourcePicker extends PureComponent<Props> {
placeholder={placeholder}
noOptionsMessage="No datasources found"
value={value}
invalid={invalid}
invalid={!!error}
getOptionLabel={o => {
if (isUnsignedPluginSignature(o.meta.signature) && o !== value) {
if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) {
return (
<HorizontalGroup align="center" justify="space-between">
<span>{o.label}</span> <PluginSignatureBadge status={o.meta.signature} />
@ -103,5 +150,3 @@ export class DataSourcePicker extends PureComponent<Props> {
);
}
}
export default DataSourcePicker;

View File

@ -5,11 +5,11 @@ import _ from 'lodash';
import { DataQuery, DataSourceApi, dateTimeFormat, AppEvents, urlUtil, ExploreUrlState } from '@grafana/data';
import appEvents from 'app/core/app_events';
import store from 'app/core/store';
import { getExploreDatasources } from '../../features/explore/state/selectors';
// Types
import { RichHistoryQuery } from 'app/types/explore';
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { getDataSourceSrv } from '@grafana/runtime';
const RICH_HISTORY_KEY = 'grafana.explore.richHistory';
@ -275,22 +275,21 @@ export function mapQueriesToHeadings(query: RichHistoryQuery[], sortOrder: SortO
* exploreDatasources add generic datasource image and add property isRemoved = true.
*/
export function createDatasourcesList(queriesDatasources: string[]) {
const exploreDatasources = getExploreDatasources();
const datasources: Array<{ label: string; value: string; imgUrl: string; isRemoved: boolean }> = [];
queriesDatasources.forEach(queryDsName => {
const index = exploreDatasources.findIndex(exploreDs => exploreDs.name === queryDsName);
if (index !== -1) {
queriesDatasources.forEach(dsName => {
const dsSettings = getDataSourceSrv().getInstanceSettings(dsName);
if (dsSettings) {
datasources.push({
label: queryDsName,
value: queryDsName,
imgUrl: exploreDatasources[index].meta.info.logos.small,
label: dsSettings.name,
value: dsSettings.name,
imgUrl: dsSettings.meta.info.logos.small,
isRemoved: false,
});
} else {
datasources.push({
label: queryDsName,
value: queryDsName,
label: dsName,
value: dsName,
imgUrl: 'public/img/icn-datasource.svg',
isRemoved: true,
});

View File

@ -22,13 +22,10 @@ describe('getAlertingValidationMessage', () => {
const getMock = jest.fn().mockResolvedValue(datasource);
const datasourceSrv: DataSourceSrv = {
get: getMock,
getExternal(): DataSourceInstanceSettings[] {
getList(): DataSourceInstanceSettings[] {
return [];
},
getInstanceSettings: (() => {}) as any,
getAll(): DataSourceInstanceSettings[] {
return [];
},
};
const targets: ElasticsearchQuery[] = [
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
@ -66,10 +63,7 @@ describe('getAlertingValidationMessage', () => {
return Promise.resolve(alertingDatasource);
},
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
getList(): DataSourceInstanceSettings[] {
return [];
},
};
@ -96,10 +90,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = {
get: getMock,
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
getList(): DataSourceInstanceSettings[] {
return [];
},
};
@ -128,10 +119,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = {
get: getMock,
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
getList(): DataSourceInstanceSettings[] {
return [];
},
};
@ -160,10 +148,7 @@ describe('getAlertingValidationMessage', () => {
const datasourceSrv: DataSourceSrv = {
get: getMock,
getInstanceSettings: (() => {}) as any,
getExternal(): DataSourceInstanceSettings[] {
return [];
},
getAll(): DataSourceInstanceSettings[] {
getList(): DataSourceInstanceSettings[] {
return [];
},
};

View File

@ -2,7 +2,6 @@ import React, { PureComponent } from 'react';
import { QueryGroup } from 'app/features/query/components/QueryGroup';
import { QueryGroupOptions } from 'app/features/query/components/QueryGroupOptions';
import { PanelModel } from '../../state';
import { DataQuery, DataSourceSelectItem } from '@grafana/data';
import { getLocationSrv } from '@grafana/runtime';
interface Props {
@ -22,6 +21,10 @@ export class PanelEditorQueries extends PureComponent<Props, State> {
buildQueryOptions({ panel }: Props): QueryGroupOptions {
return {
dataSource: {
name: panel.datasource,
},
queries: panel.targets,
maxDataPoints: panel.maxDataPoints,
minInterval: panel.interval,
timeRange: {
@ -32,29 +35,10 @@ export class PanelEditorQueries extends PureComponent<Props, State> {
};
}
onDataSourceChange = (ds: DataSourceSelectItem, queries: DataQuery[]) => {
const { panel } = this.props;
panel.datasource = ds.value;
panel.targets = queries;
panel.refresh();
this.forceUpdate();
};
onRunQueries = () => {
this.props.panel.refresh();
};
onQueriesChange = (queries: DataQuery[]) => {
const { panel } = this.props;
panel.targets = queries;
panel.refresh();
this.forceUpdate();
};
onOpenQueryInspector = () => {
getLocationSrv().update({
query: { inspect: this.props.panel.id, inspectTab: 'query' },
@ -62,9 +46,11 @@ export class PanelEditorQueries extends PureComponent<Props, State> {
});
};
onQueryOptionsChange = (options: QueryGroupOptions) => {
onOptionsChange = (options: QueryGroupOptions) => {
const { panel } = this.props;
panel.datasource = options.dataSource.default ? null : options.dataSource.name!;
panel.targets = options.queries;
panel.timeFrom = options.timeRange?.from;
panel.timeShift = options.timeRange?.shift;
panel.hideTimeOverride = options.timeRange?.hide;
@ -81,15 +67,11 @@ export class PanelEditorQueries extends PureComponent<Props, State> {
return (
<QueryGroup
dataSourceName={panel.datasource}
options={options}
queryRunner={panel.getQueryRunner()}
queries={panel.targets}
onQueriesChange={this.onQueriesChange}
onDataSourceChange={this.onDataSourceChange}
onRunQueries={this.onRunQueries}
onOpenQueryInspector={this.onOpenQueryInspector}
onOptionsChange={this.onQueryOptionsChange}
onOptionsChange={this.onOptionsChange}
/>
);
}

View File

@ -7,7 +7,7 @@ import { css } from 'emotion';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { Icon, IconButton, LegacyForms, SetInterval, Tooltip } from '@grafana/ui';
import { DataQuery, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
import { DataQuery, DataSourceInstanceSettings, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { StoreState } from 'app/types/store';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
@ -24,7 +24,6 @@ import { LiveTailButton } from './LiveTailButton';
import { ResponsiveButton } from './ResponsiveButton';
import { RunButton } from './RunButton';
import { LiveTailControls } from './useLiveTailControls';
import { getExploreDatasources } from './state/selectors';
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
import { cancelQueries, clearQueries, runQueries } from './state/query';
@ -81,8 +80,8 @@ interface DispatchProps {
type Props = StateProps & DispatchProps & OwnProps;
export class UnConnectedExploreToolbar extends PureComponent<Props> {
onChangeDatasource = async (option: { value: any }) => {
this.props.changeDatasource(this.props.exploreId, option.value, { importQueries: true });
onChangeDatasource = async (dsSettings: DataSourceInstanceSettings) => {
this.props.changeDatasource(this.props.exploreId, dsSettings.name, { importQueries: true });
};
onClearAll = () => {
@ -141,12 +140,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
});
}
getSelectedDatasource = () => {
const { datasourceName } = this.props;
const exploreDatasources = getExploreDatasources();
return datasourceName ? exploreDatasources.find(datasource => datasource.name === datasourceName) : undefined;
};
render() {
const {
datasourceMissing,
@ -214,8 +207,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
>
<DataSourcePicker
onChange={this.onChangeDatasource}
datasources={getExploreDatasources()}
current={this.getSelectedDatasource()}
current={this.props.datasourceName}
hideTextValue={showSmallDataSourcePicker}
/>
</div>

View File

@ -13,14 +13,8 @@ describe('createSpanLinkFactory', () => {
it('returns undefined if there is no loki data source', () => {
setDataSourceSrv({
getExternal() {
return [
{
meta: {
id: 'not loki',
},
} as DataSourceInstanceSettings,
];
getList() {
return [];
},
} as any);
const splitOpenFn = jest.fn();
@ -30,7 +24,7 @@ describe('createSpanLinkFactory', () => {
it('creates correct link', () => {
setDataSourceSrv({
getExternal() {
getList() {
return [
{
name: 'loki1',

View File

@ -15,9 +15,7 @@ export function createSpanLinkFactory(splitOpenFn: (options: { datasourceUid: st
}
// Right now just hardcoded for first loki DS we can find
const lokiDs = getDataSourceSrv()
.getExternal()
.find(ds => ds.meta.id === 'loki');
const lokiDs = getDataSourceSrv().getList({ pluginId: 'loki' })[0];
if (!lokiDs) {
return undefined;

View File

@ -220,10 +220,12 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
const dsSettings = options?.datasources || defaultDatasources;
setDataSourceSrv({
getExternal(): DataSourceInstanceSettings[] {
getList(): DataSourceInstanceSettings[] {
return dsSettings.map(d => d.settings);
},
getInstanceSettings(name: string) {
return dsSettings.map(d => d.settings).find(x => x.name === name);
},
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
return Promise.resolve((name ? dsSettings.find(d => d.api.name === name) : dsSettings[0])!.api);
},

View File

@ -10,26 +10,9 @@ import {
refreshExplore,
} from './explorePane';
import { setQueriesAction } from './query';
import * as DatasourceSrv from 'app/features/plugins/datasource_srv';
import { makeExplorePaneState, makeInitialUpdateState } from './utils';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
jest.mock('app/features/plugins/datasource_srv');
const getDatasourceSrvMock = (DatasourceSrv.getDatasourceSrv as any) as jest.Mock<DatasourceSrv.DatasourceSrv>;
beforeEach(() => {
getDatasourceSrvMock.mockClear();
getDatasourceSrvMock.mockImplementation(
() =>
({
getExternal: jest.fn().mockReturnValue([]),
get: jest.fn().mockReturnValue({
testDatasource: jest.fn(),
init: jest.fn(),
}),
} as any)
);
});
import { setDataSourceSrv } from '@grafana/runtime';
jest.mock('../../dashboard/services/TimeSrv', () => ({
getTimeSrv: jest.fn().mockReturnValue({
@ -47,6 +30,21 @@ const testRange = {
},
};
setDataSourceSrv({
getList() {
return [];
},
getInstanceSettings(name: string) {
return { name: 'hello' };
},
get() {
return Promise.resolve({
testDatasource: jest.fn(),
init: jest.fn(),
});
},
} as any);
const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
const exploreId = ExploreId.left;
const containerWidth = 1920;

View File

@ -32,7 +32,7 @@ import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { runQueries, setQueriesAction } from './query';
import { updateTime } from './time';
import { toRawTimeRange } from '../utils/time';
import { getExploreDatasources } from './selectors';
import { getDataSourceSrv } from '@grafana/runtime';
//
// Actions and Payloads
@ -131,7 +131,7 @@ export function initializeExplore(
originPanelId?: number | null
): ThunkResult<void> {
return async (dispatch, getState) => {
const exploreDatasources = getExploreDatasources();
const exploreDatasources = getDataSourceSrv().getList();
let instance = undefined;
let history: HistoryItem[] = [];

View File

@ -1,8 +1,6 @@
import { createSelector } from 'reselect';
import { ExploreItemState } from 'app/types';
import { filterLogLevels, dedupLogRows } from 'app/core/logs_model';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
import { DataSourceSelectItem } from '@grafana/data';
const logsRowsSelector = (state: ExploreItemState) => state.logsResult && state.logsResult.rows;
const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels;
@ -19,16 +17,3 @@ export const deduplicatedRowsSelector = createSelector(
return dedupLogRows(filteredRows, dedupStrategy);
}
);
export const getExploreDatasources = (): DataSourceSelectItem[] => {
return getDatasourceSrv()
.getExternal()
.map(
(ds: any) =>
({
value: ds.name,
name: ds.name,
meta: ds.meta,
} as DataSourceSelectItem)
);
};

View File

@ -11,11 +11,11 @@ import {
Legend,
} from '@grafana/ui';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import DataSourcePicker from 'app/core/components/Select/DataSourcePicker';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
import { validateTitle, validateUid } from '../utils/validation';
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState' | 'watch'> {
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState'> {
uidReset: boolean;
inputs: DashboardInputs;
initialFolderId: number;
@ -36,8 +36,10 @@ export const ImportDashboardForm: FC<Props> = ({
onUidReset,
onCancel,
onSubmit,
watch,
}) => {
const [isSubmitted, setSubmitted] = useState(false);
const watchDataSources = watch('dataSources');
/*
This useEffect is needed for overwriting a dashboard. It
@ -96,6 +98,7 @@ export const ImportDashboardForm: FC<Props> = ({
{inputs.dataSources &&
inputs.dataSources.map((input: DataSourceInput, index: number) => {
const dataSourceOption = `dataSources[${index}]`;
const current = watchDataSources ?? [];
return (
<Field
label={input.label}
@ -105,8 +108,10 @@ export const ImportDashboardForm: FC<Props> = ({
>
<InputControl
as={DataSourcePicker}
noDefault={true}
pluginId={input.pluginId}
name={`${dataSourceOption}`}
datasources={input.options}
current={current[index]?.name}
control={control}
placeholder={input.info}
rules={{ required: true }}

View File

@ -87,7 +87,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
validateFieldsOnMount={['title', 'uid']}
validateOn="onChange"
>
{({ register, errors, control, getValues }) => (
{({ register, errors, control, watch, getValues }) => (
<ImportDashboardForm
register={register}
errors={errors}
@ -98,6 +98,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
onCancel={this.onCancel}
onUidReset={this.onUidReset}
onSubmit={this.onSubmit}
watch={watch}
initialFolderId={folder.id}
/>
)}

View File

@ -1,6 +1,5 @@
import { AppEvents, DataSourceInstanceSettings, DataSourceSelectItem, locationUtil } from '@grafana/data';
import { AppEvents, DataSourceInstanceSettings, locationUtil } from '@grafana/data';
import { getBackendSrv } from 'app/core/services/backend_srv';
import config from 'app/core/config';
import {
clearDashboard,
setInputs,
@ -13,6 +12,7 @@ import { updateLocation } from 'app/core/actions';
import { ThunkResult, FolderInfo, DashboardDTO, DashboardDataDTO } from 'app/types';
import { appEvents } from '../../../core/core';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { getDataSourceSrv } from '@grafana/runtime';
export function fetchGcomDashboard(id: string): ThunkResult<void> {
return async dispatch => {
@ -73,13 +73,13 @@ export function importDashboard(importDashboardForm: ImportDashboardDTO): ThunkR
const inputs = getState().importDashboard.inputs;
let inputsToPersist = [] as any[];
importDashboardForm.dataSources?.forEach((dataSource: DataSourceSelectItem, index: number) => {
importDashboardForm.dataSources?.forEach((dataSource: DataSourceInstanceSettings, index: number) => {
const input = inputs.dataSources[index];
inputsToPersist.push({
name: input.name,
type: input.type,
pluginId: input.pluginId,
value: dataSource.value,
value: dataSource.name,
});
});
@ -105,19 +105,13 @@ export function importDashboard(importDashboardForm: ImportDashboardDTO): ThunkR
}
const getDataSourceOptions = (input: { pluginId: string; pluginName: string }, inputModel: any) => {
const sources = Object.values(config.datasources).filter(
(val: DataSourceInstanceSettings) => val.type === input.pluginId
);
const sources = getDataSourceSrv().getList({ pluginId: input.pluginId });
if (sources.length === 0) {
inputModel.info = 'No data sources of type ' + input.pluginName + ' found';
} else if (!inputModel.info) {
inputModel.info = 'Select a ' + input.pluginName + ' data source';
}
inputModel.options = sources.map(val => {
return { name: val.name, value: val.name, meta: val.meta };
});
};
export function moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {

View File

@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { DataSourceSelectItem } from '@grafana/data';
import { DataSourceInstanceSettings } from '@grafana/data';
export enum DashboardSource {
Gcom = 0,
@ -11,7 +11,7 @@ export interface ImportDashboardDTO {
uid: string;
gnetId: string;
constants: string[];
dataSources: DataSourceSelectItem[];
dataSources: DataSourceInstanceSettings[];
folder: { id: number; title?: string };
}
@ -30,7 +30,6 @@ export interface DashboardInput {
export interface DataSourceInput extends DashboardInput {
pluginId: string;
options: DataSourceSelectItem[];
}
export interface DashboardInputs {

View File

@ -1,9 +1,9 @@
// Libraries
import sortBy from 'lodash/sortBy';
import coreModule from 'app/core/core_module';
// Services & Utils
import { importDataSourcePlugin } from './plugin_loader';
import {
GetDataSourceListFilters,
DataSourceSrv as DataSourceService,
getDataSourceSrv as getDataSourceService,
TemplateSrv,
@ -15,6 +15,7 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
// Pretend Datasource
import { expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
import { DataSourceVariableModel } from '../variables/types';
import { cloneDeep } from 'lodash';
export class DatasourceSrv implements DataSourceService {
private datasources: Record<string, DataSourceApi> = {};
@ -49,6 +50,20 @@ export class DatasourceSrv implements DataSourceService {
return this.settingsMapByName[this.defaultName];
}
// Complex logic to support template variable data source names
// For this we just pick the current or first data source in the variable
if (nameOrUid[0] === '$') {
const interpolatedName = this.templateSrv.replace(nameOrUid, {}, variableInterpolation);
const dsSettings = this.settingsMapByUid[interpolatedName] ?? this.settingsMapByName[interpolatedName];
if (!dsSettings) {
return undefined;
}
// The return name or uid needs preservet string containing the variable
const clone = cloneDeep(dsSettings);
clone.name = nameOrUid;
return clone;
}
return this.settingsMapByUid[nameOrUid] ?? this.settingsMapByName[nameOrUid];
}
@ -69,12 +84,7 @@ export class DatasourceSrv implements DataSourceService {
}
// Interpolation here is to support template variable in data source selection
nameOrUid = this.templateSrv.replace(nameOrUid, scopedVars, (value: any[]) => {
if (Array.isArray(value)) {
return value[0];
}
return value;
});
nameOrUid = this.templateSrv.replace(nameOrUid, scopedVars, variableInterpolation);
if (nameOrUid === 'default') {
return this.get(this.defaultName);
@ -130,88 +140,109 @@ export class DatasourceSrv implements DataSourceService {
return Object.values(this.settingsMapByName);
}
getExternal(): DataSourceInstanceSettings[] {
const datasources = this.getAll().filter(ds => !ds.meta.builtIn);
return sortBy(datasources, ['name']);
}
getAnnotationSources() {
const sources: any[] = [];
this.addDataSourceVariables(sources);
Object.values(this.settingsMapByName).forEach(value => {
if (value.meta?.annotations) {
sources.push(value);
getList(filters: GetDataSourceListFilters = {}): DataSourceInstanceSettings[] {
const base = Object.values(this.settingsMapByName).filter(x => {
if (x.meta.id === 'grafana' || x.meta.id === 'mixed' || x.meta.id === 'dashboard') {
return false;
}
if (filters.metrics && !x.meta.metrics) {
return false;
}
if (filters.tracing && !x.meta.tracing) {
return false;
}
if (filters.annotations && !x.meta.annotations) {
return false;
}
if (filters.pluginId && x.meta.id !== filters.pluginId) {
return false;
}
return true;
});
return sources;
}
if (filters.variables) {
for (const variable of this.templateSrv.getVariables().filter(variable => variable.type === 'datasource')) {
const dsVar = variable as DataSourceVariableModel;
const first = dsVar.current.value === 'default' ? this.defaultName : dsVar.current.value;
const dsName = (first as unknown) as string;
const dsSettings = this.settingsMapByName[dsName];
getMetricSources(options?: { skipVariables?: boolean }) {
const metricSources: DataSourceSelectItem[] = [];
Object.entries(this.settingsMapByName).forEach(([key, value]) => {
if (value.meta?.metrics) {
let metricSource: DataSourceSelectItem = { value: key, name: key, meta: value.meta, sort: key };
//Make sure grafana and mixed are sorted at the bottom
if (value.meta.id === 'grafana') {
metricSource.sort = String.fromCharCode(253);
} else if (value.meta.id === 'dashboard') {
metricSource.sort = String.fromCharCode(254);
} else if (value.meta.id === 'mixed') {
metricSource.sort = String.fromCharCode(255);
}
metricSources.push(metricSource);
if (key === this.defaultName) {
metricSource = { value: null, name: 'default', meta: value.meta, sort: key };
metricSources.push(metricSource);
if (dsSettings) {
const key = `$\{${variable.name}\}`;
base.push({
...dsSettings,
name: key,
});
}
}
});
if (!options || !options.skipVariables) {
this.addDataSourceVariables(metricSources);
}
metricSources.sort((a, b) => {
if (a.sort.toLowerCase() > b.sort.toLowerCase()) {
const sorted = base.sort((a, b) => {
if (a.name.toLowerCase() > b.name.toLowerCase()) {
return 1;
}
if (a.sort.toLowerCase() < b.sort.toLowerCase()) {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
}
return 0;
});
return metricSources;
if (!filters.pluginId) {
if (filters.mixed) {
base.push(this.getInstanceSettings('-- Mixed --')!);
}
if (filters.dashboard) {
base.push(this.getInstanceSettings('-- Dashboard --')!);
}
if (!filters.tracing) {
base.push(this.getInstanceSettings('-- Grafana --')!);
}
}
return sorted;
}
addDataSourceVariables(list: any[]) {
// look for data source variables
this.templateSrv
.getVariables()
.filter(variable => variable.type === 'datasource')
.forEach((variable: DataSourceVariableModel) => {
const first = variable.current.value === 'default' ? this.defaultName : variable.current.value;
const index = (first as unknown) as string;
const ds = this.settingsMapByName[index];
if (ds) {
const key = `$${variable.name}`;
list.push({
name: key,
value: key,
meta: ds.meta,
sort: key,
});
}
});
/**
* @deprecated use getList
* */
getExternal(): DataSourceInstanceSettings[] {
return this.getList();
}
/**
* @deprecated use getList
* */
getAnnotationSources() {
return this.getList({ annotations: true, variables: true }).map(x => {
return {
name: x.name,
value: x.isDefault ? null : x.name,
meta: x.meta,
};
});
}
/**
* @deprecated use getList
* */
getMetricSources(options?: { skipVariables?: boolean }): DataSourceSelectItem[] {
return this.getList({ metrics: true, variables: !options?.skipVariables }).map(x => {
return {
name: x.name,
value: x.isDefault ? null : x.name,
meta: x.meta,
};
});
}
}
export function variableInterpolation(value: any[]) {
if (Array.isArray(value)) {
return value[0];
}
return value;
}
export const getDatasourceSrv = (): DatasourceSrv => {

View File

@ -1,6 +1,6 @@
import 'app/features/plugins/datasource_srv';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DataSourceInstanceSettings, DataSourcePlugin, DataSourcePluginMeta, PluginMeta } from '@grafana/data';
import { DataSourceInstanceSettings, DataSourcePlugin } from '@grafana/data';
// Datasource variable $datasource with current value 'BBB'
const templateSrv: any = {
@ -13,7 +13,9 @@ const templateSrv: any = {
},
},
],
replace: (v: string) => v,
replace: (v: string) => {
return v.replace('${datasource}', 'BBB');
},
};
class TestDataSource {
@ -27,120 +29,184 @@ jest.mock('../plugin_loader', () => ({
}));
describe('datasource_srv', () => {
const _datasourceSrv = new DatasourceSrv({} as any, {} as any, templateSrv);
const datasources = {
buildIn: {
id: 1,
uid: '1',
type: 'b',
name: 'buildIn',
meta: { builtIn: true } as DataSourcePluginMeta,
jsonData: {},
const dataSourceSrv = new DatasourceSrv({} as any, {} as any, templateSrv);
const dataSourceInit = {
mmm: {
type: 'test-db',
name: 'mmm',
uid: 'uid-code-mmm',
meta: { metrics: true, annotations: true } as any,
},
external1: {
id: 2,
uid: '2',
type: 'e',
name: 'external1',
meta: { builtIn: false } as DataSourcePluginMeta,
jsonData: {},
'-- Grafana --': {
type: 'grafana',
name: '-- Grafana --',
meta: { builtIn: true, metrics: true, id: 'grafana' },
},
external2: {
id: 3,
uid: '3',
type: 'e2',
name: 'external2',
meta: {} as PluginMeta,
jsonData: {},
'-- Dashboard --': {
type: 'dashboard',
name: '-- Dashboard --',
meta: { builtIn: true, metrics: true, id: 'dashboard' },
},
'-- Mixed --': {
type: 'test-db',
name: '-- Mixed --',
meta: { builtIn: true, metrics: true, id: 'mixed' },
},
ZZZ: {
type: 'test-db',
name: 'ZZZ',
uid: 'uid-code-ZZZ',
meta: { metrics: true },
},
aaa: {
type: 'test-db',
name: 'aaa',
uid: 'uid-code-aaa',
meta: { metrics: true },
},
BBB: {
type: 'test-db',
name: 'BBB',
uid: 'uid-code-BBB',
meta: { metrics: true },
},
Jaeger: {
type: 'jaeger-db',
name: 'Jaeger',
uid: 'uid-code-Jaeger',
meta: { tracing: true, id: 'jaeger' },
},
};
beforeEach(() => {
_datasourceSrv.init(datasources, 'external1');
});
describe('when getting data source class instance', () => {
it('should load plugin and create instance and set meta', async () => {
const ds = (await _datasourceSrv.get('external1')) as any;
expect(ds.meta).toBe(datasources.external1.meta);
expect(ds.instanceSettings).toBe(datasources.external1);
// validate that it caches instance
const ds2 = await _datasourceSrv.get('external1');
expect(ds).toBe(ds2);
});
it('should be able to load data source using uid as well', async () => {
const dsByUid = await _datasourceSrv.get('2');
const dsByName = await _datasourceSrv.get('external1');
expect(dsByUid.meta).toBe(datasources.external1.meta);
expect(dsByUid).toBe(dsByName);
});
});
describe('when getting external metric sources', () => {
it('should return list of explore sources', () => {
const externalSources = _datasourceSrv.getExternal();
expect(externalSources.length).toBe(2);
expect(externalSources[0].name).toBe('external1');
expect(externalSources[1].name).toBe('external2');
});
});
describe('when loading metric sources', () => {
let metricSources: any;
describe('Given a list of data sources', () => {
beforeEach(() => {
_datasourceSrv.init(
{
mmm: {
type: 'test-db',
meta: { metrics: true } as any,
},
'--Grafana--': {
type: 'grafana',
meta: { builtIn: true, metrics: true, id: 'grafana' },
},
'--Mixed--': {
type: 'test-db',
meta: { builtIn: true, metrics: true, id: 'mixed' },
},
ZZZ: {
type: 'test-db',
meta: { metrics: true },
},
aaa: {
type: 'test-db',
meta: { metrics: true },
},
BBB: {
type: 'test-db',
meta: { metrics: true },
},
} as any,
'BBB'
);
metricSources = _datasourceSrv.getMetricSources({});
dataSourceSrv.init(dataSourceInit as any, 'BBB');
});
it('should return a list of sources sorted case insensitively with builtin sources last', () => {
expect(metricSources[1].name).toBe('aaa');
expect(metricSources[2].name).toBe('BBB');
expect(metricSources[3].name).toBe('default');
expect(metricSources[4].name).toBe('mmm');
expect(metricSources[5].name).toBe('ZZZ');
expect(metricSources[6].name).toBe('--Grafana--');
expect(metricSources[7].name).toBe('--Mixed--');
describe('when getting data source class instance', () => {
it('should load plugin and create instance and set meta', async () => {
const ds = (await dataSourceSrv.get('mmm')) as any;
expect(ds.meta).toBe(dataSourceInit.mmm.meta);
expect(ds.instanceSettings).toBe(dataSourceInit.mmm);
// validate that it caches instance
const ds2 = await dataSourceSrv.get('mmm');
expect(ds).toBe(ds2);
});
it('should be able to load data source using uid as well', async () => {
const dsByUid = await dataSourceSrv.get('uid-code-mmm');
const dsByName = await dataSourceSrv.get('mmm');
expect(dsByUid.meta).toBe(dsByName.meta);
expect(dsByUid).toBe(dsByName);
});
});
it('should set default data source', () => {
expect(metricSources[3].name).toBe('default');
expect(metricSources[3].sort).toBe('BBB');
describe('when getting instance settings', () => {
it('should work by name or uid', () => {
expect(dataSourceSrv.getInstanceSettings('mmm')).toBe(dataSourceSrv.getInstanceSettings('uid-code-mmm'));
});
it('should work with variable', () => {
const ds = dataSourceSrv.getInstanceSettings('${datasource}');
expect(ds?.name).toBe('${datasource}');
expect(ds?.uid).toBe('uid-code-BBB');
});
});
it('should set default inject the variable datasources', () => {
expect(metricSources[0].name).toBe('$datasource');
expect(metricSources[0].sort).toBe('$datasource');
describe('when getting external metric sources', () => {
it('should return list of explore sources', () => {
const externalSources = dataSourceSrv.getExternal();
expect(externalSources.length).toBe(6);
});
});
it('Can get list of data sources with variables: true', () => {
const list = dataSourceSrv.getList({ metrics: true, variables: true });
expect(list[0].name).toBe('${datasource}');
});
it('Can get list of data sources with tracing: true', () => {
const list = dataSourceSrv.getList({ tracing: true });
expect(list[0].name).toBe('Jaeger');
});
it('Can get list of data sources with annotation: true', () => {
const list = dataSourceSrv.getList({ annotations: true });
expect(list[0].name).toBe('mmm');
});
it('Can get get list and filter by pluginId', () => {
const list = dataSourceSrv.getList({ pluginId: 'jaeger' });
expect(list[0].name).toBe('Jaeger');
expect(list.length).toBe(1);
});
it('Can get list of data sources with metrics: true, builtIn: true, mixed: true', () => {
expect(dataSourceSrv.getList({ metrics: true, dashboard: true, mixed: true })).toMatchInlineSnapshot(`
Array [
Object {
"meta": Object {
"metrics": true,
},
"name": "aaa",
"type": "test-db",
"uid": "uid-code-aaa",
},
Object {
"meta": Object {
"metrics": true,
},
"name": "BBB",
"type": "test-db",
"uid": "uid-code-BBB",
},
Object {
"meta": Object {
"annotations": true,
"metrics": true,
},
"name": "mmm",
"type": "test-db",
"uid": "uid-code-mmm",
},
Object {
"meta": Object {
"metrics": true,
},
"name": "ZZZ",
"type": "test-db",
"uid": "uid-code-ZZZ",
},
Object {
"meta": Object {
"builtIn": true,
"id": "mixed",
"metrics": true,
},
"name": "-- Mixed --",
"type": "test-db",
},
Object {
"meta": Object {
"builtIn": true,
"id": "dashboard",
"metrics": true,
},
"name": "-- Dashboard --",
"type": "dashboard",
},
Object {
"meta": Object {
"builtIn": true,
"id": "grafana",
"metrics": true,
},
"name": "-- Grafana --",
"type": "grafana",
},
]
`);
});
});
});

View File

@ -16,6 +16,7 @@ import {
TimeRange,
toLegacyResponseData,
EventBusExtended,
DataSourceInstanceSettings,
} from '@grafana/data';
import { QueryEditorRowTitle } from './QueryEditorRowTitle';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
@ -27,8 +28,7 @@ import { PanelModel } from 'app/features/dashboard/state';
interface Props {
data: PanelData;
query: DataQuery;
dataSourceValue: string | null;
inMixedMode?: boolean;
dsSettings: DataSourceInstanceSettings;
id: string;
index: number;
onAddQuery: (query?: DataQuery) => void;
@ -38,7 +38,7 @@ interface Props {
}
interface State {
loadedDataSourceValue: string | null | undefined;
loadedDataSourceIdentifier?: string | null;
datasource: DataSourceApi | null;
hasTextEditMode: boolean;
data?: PanelData;
@ -52,7 +52,6 @@ export class QueryEditorRow extends PureComponent<Props, State> {
state: State = {
datasource: null,
loadedDataSourceValue: undefined,
hasTextEditMode: false,
data: undefined,
isOpen: true,
@ -89,27 +88,31 @@ export class QueryEditorRow extends PureComponent<Props, State> {
};
}
getQueryDataSourceIdentifier(): string | null | undefined {
const { query, dsSettings } = this.props;
return dsSettings.meta.mixed ? query.datasource : dsSettings.uid;
}
async loadDatasource() {
const { query, dataSourceValue } = this.props;
const dataSourceSrv = getDatasourceSrv();
let datasource;
let datasource: DataSourceApi;
const dataSourceIdentifier = this.getQueryDataSourceIdentifier();
try {
const datasourceName = dataSourceValue || query.datasource;
datasource = await dataSourceSrv.get(datasourceName);
datasource = await dataSourceSrv.get(dataSourceIdentifier);
} catch (error) {
datasource = await dataSourceSrv.get();
}
this.setState({
datasource,
loadedDataSourceValue: this.props.dataSourceValue,
loadedDataSourceIdentifier: dataSourceIdentifier,
hasTextEditMode: _.has(datasource, 'components.QueryCtrl.prototype.toggleEditorMode'),
});
}
componentDidUpdate(prevProps: Props) {
const { loadedDataSourceValue } = this.state;
const { datasource, loadedDataSourceIdentifier } = this.state;
const { data, query } = this.props;
if (data !== prevProps.data) {
@ -125,7 +128,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
}
// check if we need to load another datasource
if (loadedDataSourceValue !== this.props.dataSourceValue) {
if (datasource && loadedDataSourceIdentifier !== this.getQueryDataSourceIdentifier()) {
if (this.angularQueryEditor) {
this.angularQueryEditor.destroy();
this.angularQueryEditor = null;
@ -137,6 +140,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
if (!this.element || this.angularQueryEditor) {
return;
}
this.renderAngularQueryEditor();
}
@ -259,14 +263,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
};
renderTitle = (props: { isOpen: boolean; openRow: () => void }) => {
const { query, inMixedMode } = this.props;
const { query, dsSettings } = this.props;
const { datasource } = this.state;
const isDisabled = query.hide;
return (
<QueryEditorRowTitle
query={query}
inMixedMode={inMixedMode}
inMixedMode={dsSettings.meta.mixed}
datasource={datasource!}
disabled={isDisabled}
onClick={e => this.onToggleEditMode(e, props)}

View File

@ -2,14 +2,14 @@
import React, { PureComponent } from 'react';
// Types
import { DataQuery, PanelData, DataSourceSelectItem } from '@grafana/data';
import { DataQuery, DataSourceInstanceSettings, PanelData } from '@grafana/data';
import { QueryEditorRow } from './QueryEditorRow';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
interface Props {
// The query configuration
queries: DataQuery[];
datasource: DataSourceSelectItem;
dsSettings: DataSourceInstanceSettings;
// Query editing
onQueriesChange: (queries: DataQuery[]) => void;
@ -67,7 +67,7 @@ export class QueryEditorRows extends PureComponent<Props> {
};
render() {
const { props } = this;
const { dsSettings, data, queries } = this.props;
return (
<DragDropContext onDragEnd={this.onDragEnd}>
@ -75,19 +75,18 @@ export class QueryEditorRows extends PureComponent<Props> {
{provided => {
return (
<div ref={provided.innerRef} {...provided.droppableProps}>
{props.queries.map((query, index) => (
{queries.map((query, index) => (
<QueryEditorRow
dataSourceValue={query.datasource || props.datasource.value}
dsSettings={dsSettings}
id={query.refId}
index={index}
key={query.refId}
data={props.data}
data={data}
query={query}
onChange={query => this.onChangeQuery(query, index)}
onRemoveQuery={this.onRemoveQuery}
onAddQuery={this.props.onAddQuery}
onRunQuery={this.props.onRunQueries}
inMixedMode={props.datasource.meta.mixed}
/>
))}
{provided.placeholder}

View File

@ -2,21 +2,20 @@
import React, { PureComponent } from 'react';
// Components
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { Button, CustomScrollbar, HorizontalGroup, Modal, stylesFactory, Field } from '@grafana/ui';
import { Button, CustomScrollbar, HorizontalGroup, Modal, stylesFactory } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime';
import { QueryEditorRows } from './QueryEditorRows';
// Services
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { backendSrv } from 'app/core/services/backend_srv';
import config from 'app/core/config';
// Types
import {
DataQuery,
DataSourceSelectItem,
DefaultTimeRange,
LoadingState,
PanelData,
DataSourceApi,
DataSourceInstanceSettings,
} from '@grafana/data';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { addQuery } from 'app/core/utils/query';
@ -30,20 +29,15 @@ import { css } from 'emotion';
interface Props {
queryRunner: PanelQueryRunner;
queries: DataQuery[];
dataSourceName: string | null;
options: QueryGroupOptions;
onOpenQueryInspector?: () => void;
onRunQueries: () => void;
onQueriesChange: (queries: DataQuery[]) => void;
onDataSourceChange: (ds: DataSourceSelectItem, queries: DataQuery[]) => void;
onOptionsChange: (options: QueryGroupOptions) => void;
}
interface State {
dataSource?: DataSourceApi;
dataSourceItem: DataSourceSelectItem;
dataSourceError?: string;
dsSettings?: DataSourceInstanceSettings;
helpContent: React.ReactNode;
isLoadingHelp: boolean;
isPickerOpen: boolean;
@ -54,13 +48,12 @@ interface State {
}
export class QueryGroup extends PureComponent<Props, State> {
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
backendSrv = backendSrv;
dataSourceSrv = getDataSourceSrv();
querySubscription: Unsubscribable | null;
state: State = {
isLoadingHelp: false,
dataSourceItem: this.findCurrentDataSource(this.props.dataSourceName),
helpContent: null,
isPickerOpen: false,
isAddingMixed: false,
@ -74,19 +67,18 @@ export class QueryGroup extends PureComponent<Props, State> {
};
async componentDidMount() {
const { queryRunner, dataSourceName: datasourceName } = this.props;
const { queryRunner, options } = this.props;
this.querySubscription = queryRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({
next: (data: PanelData) => this.onPanelDataUpdate(data),
});
try {
const ds = await getDataSourceSrv().get(datasourceName);
this.setState({ dataSource: ds });
const ds = await this.dataSourceSrv.get(options.dataSource.name);
const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource.name);
this.setState({ dataSource: ds, dsSettings });
} catch (error) {
const ds = await getDataSourceSrv().get();
const dataSourceItem = this.findCurrentDataSource(ds.name);
this.setState({ dataSource: ds, dataSourceError: error?.message, dataSourceItem });
console.log('failed to load data source', error);
}
}
@ -101,62 +93,73 @@ export class QueryGroup extends PureComponent<Props, State> {
this.setState({ data });
}
findCurrentDataSource(dataSourceName: string | null): DataSourceSelectItem {
return this.datasources.find(datasource => datasource.value === dataSourceName) || this.datasources[0];
}
onChangeDataSource = async (newDsItem: DataSourceSelectItem) => {
let { queries } = this.props;
const { dataSourceItem } = this.state;
onChangeDataSource = async (newSettings: DataSourceInstanceSettings) => {
let { queries } = this.props.options;
const { dsSettings } = this.state;
// switching to mixed
if (newDsItem.meta.mixed) {
if (newSettings.meta.mixed) {
for (const query of queries) {
if (query.datasource !== ExpressionDatasourceID) {
query.datasource = query.datasource;
query.datasource = dsSettings?.name;
if (!query.datasource) {
query.datasource = config.defaultDatasource;
}
}
}
} else if (dataSourceItem) {
} else if (dsSettings) {
// if switching from mixed
if (dataSourceItem.meta.mixed) {
if (dsSettings.meta.mixed) {
// Remove the explicit datasource
for (const query of queries) {
if (query.datasource !== ExpressionDatasourceID) {
delete query.datasource;
}
}
} else if (dataSourceItem.meta.id !== newDsItem.meta.id) {
} else if (dsSettings.meta.id !== newSettings.meta.id) {
// we are changing data source type, clear queries
queries = [{ refId: 'A' }];
}
}
const dataSource = await getDataSourceSrv().get(newDsItem.value);
const dataSource = await this.dataSourceSrv.get(newSettings.name);
this.props.onDataSourceChange(newDsItem, queries);
this.onChange({
queries,
dataSource: {
name: newSettings.name,
uid: newSettings.uid,
default: newSettings.isDefault,
},
});
this.setState({
dataSourceItem: newDsItem,
dataSource: dataSource,
dataSourceError: undefined,
dsSettings: newSettings,
});
};
onAddQueryClick = () => {
if (this.state.dataSourceItem.meta.mixed) {
if (this.state.dsSettings?.meta.mixed) {
this.setState({ isAddingMixed: true });
return;
}
this.props.onQueriesChange(addQuery(this.props.queries));
this.onChange({ queries: addQuery(this.props.options.queries) });
this.onScrollBottom();
};
onChange(changedProps: Partial<QueryGroupOptions>) {
this.props.onOptionsChange({
...this.props.options,
...changedProps,
});
}
onAddExpressionClick = () => {
this.props.onQueriesChange(addQuery(this.props.queries, expressionDatasource.newQuery()));
this.onChange({
queries: addQuery(this.props.options.queries, expressionDatasource.newQuery()),
});
this.onScrollBottom();
};
@ -166,45 +169,51 @@ export class QueryGroup extends PureComponent<Props, State> {
renderTopSection(styles: QueriesTabStyls) {
const { onOpenQueryInspector, options, onOptionsChange } = this.props;
const { dataSourceItem, dataSource, dataSourceError, data } = this.state;
if (!dataSource) {
return null;
}
const { dataSource, data } = this.state;
return (
<div>
<div className={styles.dataSourceRow}>
<div className={styles.dataSourceRowItem}>
<Field invalid={!!dataSourceError} error={dataSourceError}>
<DataSourcePicker
datasources={this.datasources}
onChange={this.onChangeDataSource}
current={dataSourceItem}
/>
</Field>
</div>
<div className={styles.dataSourceRowItem}>
<Button
variant="secondary"
icon="question-circle"
title="Open data source help"
onClick={this.onOpenHelp}
<DataSourcePicker
onChange={this.onChangeDataSource}
current={options.dataSource.name}
metrics={true}
mixed={true}
dashboard={true}
variables={true}
/>
</div>
<div className={styles.dataSourceRowItemOptions}>
<QueryGroupOptionsEditor options={options} dataSource={dataSource} data={data} onChange={onOptionsChange} />
</div>
{onOpenQueryInspector && (
<div className={styles.dataSourceRowItem}>
<Button
variant="secondary"
onClick={onOpenQueryInspector}
aria-label={selectors.components.QueryTab.queryInspectorButton}
>
Query inspector
</Button>
</div>
{dataSource && (
<>
<div className={styles.dataSourceRowItem}>
<Button
variant="secondary"
icon="question-circle"
title="Open data source help"
onClick={this.onOpenHelp}
/>
</div>
<div className={styles.dataSourceRowItemOptions}>
<QueryGroupOptionsEditor
options={options}
dataSource={dataSource}
data={data}
onChange={onOptionsChange}
/>
</div>
{onOpenQueryInspector && (
<div className={styles.dataSourceRowItem}>
<Button
variant="secondary"
onClick={onOpenQueryInspector}
aria-label={selectors.components.QueryTab.queryInspectorButton}
>
Query inspector
</Button>
</div>
)}
</>
)}
</div>
</div>
@ -220,13 +229,9 @@ export class QueryGroup extends PureComponent<Props, State> {
};
renderMixedPicker = () => {
// We cannot filter on mixed flag as some mixed data sources like external plugin
// meta queries data source is mixed but also supports it's own queries
const filteredDsList = this.datasources.filter(ds => ds.meta.id !== 'mixed');
return (
<DataSourcePicker
datasources={filteredDsList}
mixed={false}
onChange={this.onAddMixedQuery}
current={null}
autoFocus={true}
@ -246,8 +251,8 @@ export class QueryGroup extends PureComponent<Props, State> {
};
onAddQuery = (query: Partial<DataQuery>) => {
const { queries, onQueriesChange } = this.props;
onQueriesChange(addQuery(queries, query));
const { queries } = this.props.options;
this.onChange({ queries: addQuery(queries, query) });
this.onScrollBottom();
};
@ -256,20 +261,24 @@ export class QueryGroup extends PureComponent<Props, State> {
this.setState({ scrollTop: target.scrollTop });
};
renderQueries() {
const { onQueriesChange, queries, onRunQueries } = this.props;
const { dataSourceItem, data } = this.state;
onQueriesChange = (queries: DataQuery[]) => {
this.onChange({ queries });
};
if (isSharedDashboardQuery(dataSourceItem.name)) {
return <DashboardQueryEditor queries={queries} panelData={data} onChange={onQueriesChange} />;
renderQueries(dsSettings: DataSourceInstanceSettings) {
const { options, onRunQueries } = this.props;
const { data } = this.state;
if (isSharedDashboardQuery(dsSettings.name)) {
return <DashboardQueryEditor queries={options.queries} panelData={data} onChange={this.onQueriesChange} />;
}
return (
<div aria-label={selectors.components.QueryTab.content}>
<QueryEditorRows
queries={queries}
datasource={dataSourceItem}
onQueriesChange={onQueriesChange}
queries={options.queries}
dsSettings={dsSettings}
onQueriesChange={this.onQueriesChange}
onAddQuery={this.onAddQuery}
onRunQueries={onRunQueries}
data={data}
@ -278,9 +287,9 @@ export class QueryGroup extends PureComponent<Props, State> {
);
}
renderAddQueryRow() {
const { dataSourceItem, isAddingMixed } = this.state;
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(dataSourceItem.name));
renderAddQueryRow(dsSettings: DataSourceInstanceSettings) {
const { isAddingMixed } = this.state;
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(dsSettings.name));
return (
<HorizontalGroup spacing="md" align="flex-start">
@ -305,7 +314,7 @@ export class QueryGroup extends PureComponent<Props, State> {
}
render() {
const { scrollTop, isHelpOpen } = this.state;
const { scrollTop, isHelpOpen, dsSettings } = this.state;
const styles = getStyles();
return (
@ -318,13 +327,16 @@ export class QueryGroup extends PureComponent<Props, State> {
>
<div className={styles.innerWrapper}>
{this.renderTopSection(styles)}
<div className={styles.queriesWrapper}>{this.renderQueries()}</div>
{this.renderAddQueryRow()}
{isHelpOpen && (
<Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}>
<PluginHelp plugin={this.state.dataSourceItem.meta} type="query_help" />
</Modal>
{dsSettings && (
<>
<div className={styles.queriesWrapper}>{this.renderQueries(dsSettings)}</div>
{this.renderAddQueryRow(dsSettings)}
{isHelpOpen && (
<Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}>
<PluginHelp plugin={dsSettings.meta} type="query_help" />
</Modal>
)}
</>
)}
</div>
</CustomScrollbar>

View File

@ -2,7 +2,7 @@
import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
// Utils
import { rangeUtil, PanelData, DataSourceApi } from '@grafana/data';
import { rangeUtil, PanelData, DataSourceApi, DataQuery } from '@grafana/data';
// Components
import { Switch, Input, InlineField, InlineFormLabel, stylesFactory } from '@grafana/ui';
@ -13,6 +13,8 @@ import { config } from 'app/core/config';
import { css } from 'emotion';
export interface QueryGroupOptions {
queries: DataQuery[];
dataSource: QueryGroupDataSource;
maxDataPoints?: number | null;
minInterval?: string | null;
cacheTimeout?: string | null;
@ -23,6 +25,12 @@ export interface QueryGroupOptions {
};
}
interface QueryGroupDataSource {
name?: string | null;
uid?: string;
default?: boolean;
}
interface Props {
options: QueryGroupOptions;
dataSource: DataSourceApi;

View File

@ -1,12 +1,4 @@
import {
ApplyFieldOverrideOptions,
DataQuery,
DataSourceSelectItem,
DataTransformerConfig,
dateMath,
FieldColorModeId,
PanelData,
} from '@grafana/data';
import { ApplyFieldOverrideOptions, DataTransformerConfig, dateMath, FieldColorModeId, PanelData } from '@grafana/data';
import { GraphNG, Table } from '@grafana/ui';
import { config } from 'app/core/config';
import React, { FC, useMemo, useState } from 'react';
@ -16,43 +8,29 @@ import { QueryGroupOptions } from '../query/components/QueryGroupOptions';
import { PanelQueryRunner } from '../query/state/PanelQueryRunner';
interface State {
queries: DataQuery[];
queryRunner: PanelQueryRunner;
dataSourceName: string | null;
queryOptions: QueryGroupOptions;
data?: PanelData;
}
export const TestStuffPage: FC = () => {
const [state, setState] = useState<State>(getDefaultState());
const { queryOptions, queryRunner, queries, dataSourceName } = state;
const onDataSourceChange = (ds: DataSourceSelectItem, queries: DataQuery[]) => {
setState({
...state,
dataSourceName: ds.value,
queries: queries,
});
};
const { queryOptions, queryRunner } = state;
const onRunQueries = () => {
const timeRange = { from: 'now-1h', to: 'now' };
queryRunner.run({
queries,
queries: queryOptions.queries,
datasource: queryOptions.dataSource.name!,
timezone: 'browser',
datasource: dataSourceName,
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
maxDataPoints: queryOptions.maxDataPoints ?? 100,
minInterval: queryOptions.minInterval,
});
};
const onQueriesChange = (queries: DataQuery[]) => {
setState({ ...state, queries: queries });
};
const onQueryOptionsChange = (queryOptions: QueryGroupOptions) => {
const onOptionsChange = (queryOptions: QueryGroupOptions) => {
setState({ ...state, queryOptions });
};
@ -68,13 +46,9 @@ export const TestStuffPage: FC = () => {
<div>
<QueryGroup
options={queryOptions}
dataSourceName={dataSourceName}
queryRunner={queryRunner}
queries={queries}
onDataSourceChange={onDataSourceChange}
onRunQueries={onRunQueries}
onQueriesChange={onQueriesChange}
onOptionsChange={onQueryOptionsChange}
onOptionsChange={onOptionsChange}
/>
</div>
@ -109,10 +83,12 @@ export function getDefaultState(): State {
};
return {
queries: [],
dataSourceName: 'gdev-testdata',
queryRunner: new PanelQueryRunner(dataConfig),
queryOptions: {
queries: [],
dataSource: {
name: 'gdev-testdata',
},
maxDataPoints: 100,
},
};

View File

@ -478,6 +478,5 @@ function createDatasource(name: string, selectable = true): DataSourceSelectItem
meta: {
mixed: !selectable,
} as DataSourcePluginMeta,
sort: '',
};
}

View File

@ -27,13 +27,11 @@ describe('data source actions', () => {
name: 'first-name',
value: 'first-value',
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
sort: '',
},
{
name: 'second-name',
value: 'second-value',
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
sort: '',
},
];
@ -80,13 +78,11 @@ describe('data source actions', () => {
name: 'first-name',
value: 'first-value',
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
sort: '',
},
{
name: 'second-name',
value: 'second-value',
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
sort: '',
},
];
@ -134,13 +130,11 @@ describe('data source actions', () => {
name: 'first-name',
value: 'first-value',
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
sort: '',
},
{
name: 'second-name',
value: 'second-value',
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
sort: '',
},
{
name: 'mixed-name',
@ -150,7 +144,6 @@ describe('data source actions', () => {
id: 'mixed-data-id',
mixed: true,
} as unknown) as DataSourcePluginMeta),
sort: '',
},
];

View File

@ -1,5 +1,5 @@
import React, { FormEvent, PropsWithChildren, ReactElement, useCallback } from 'react';
import { HorizontalGroup, InlineField, TextArea, useStyles } from '@grafana/ui';
import { InlineField, TextArea, useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
@ -38,16 +38,7 @@ export function VariableTextAreaField({
}, []);
return (
<HorizontalGroup spacing="none">
<InlineField
label={name}
labelWidth={labelWidth ?? 12}
grow={false}
tooltip={tooltip}
className={styles.inlineFieldOverride}
>
<span hidden />
</InlineField>
<InlineField label={name} labelWidth={labelWidth ?? 12} tooltip={tooltip}>
<TextArea
rows={getLineCount(value)}
value={value}
@ -59,15 +50,12 @@ export function VariableTextAreaField({
cols={width}
className={styles.textarea}
/>
</HorizontalGroup>
</InlineField>
);
}
function getStyles(theme: GrafanaTheme) {
return {
inlineFieldOverride: css`
margin: 0;
`,
textarea: css`
white-space: pre-wrap;
min-height: 32px;

View File

@ -1,28 +0,0 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { DataSourceSelectItem, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
interface Props {
onChange: (option: SelectableValue<string>) => void;
datasource: string | null;
dataSources?: DataSourceSelectItem[];
}
export function QueryVariableDatasourceSelect({ onChange, datasource, dataSources }: PropsWithChildren<Props>) {
const options = useMemo(() => {
return dataSources ? dataSources.map(ds => ({ label: ds.name, value: ds.value ?? '' })) : [];
}, [dataSources]);
const value = useMemo(() => options.find(o => o.value === datasource) ?? options[0], [options, datasource]);
return (
<VariableSelectField
name="Data source"
value={value}
options={options}
onChange={onChange}
labelWidth={10}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect}
/>
);
}

View File

@ -9,6 +9,7 @@ import { initialVariableEditorState } from '../editor/reducer';
import { describe, expect } from '../../../../test/lib/common';
import { NEW_VARIABLE_ID } from '../state/types';
import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
import { setDataSourceSrv } from '@grafana/runtime';
const setupTestContext = (options: Partial<Props>) => {
const defaults: Props = {
@ -21,7 +22,6 @@ const setupTestContext = (options: Partial<Props>) => {
...initialVariableEditorState,
extended: {
VariableQueryEditor: LegacyVariableQueryEditor,
dataSources: [],
dataSource: ({} as unknown) as DataSourceApi,
},
},
@ -34,6 +34,11 @@ const setupTestContext = (options: Partial<Props>) => {
return { rerender, props };
};
setDataSourceSrv({
getInstanceSettings: () => null,
getList: () => [],
} as any);
describe('QueryVariableEditor', () => {
describe('when the component is mounted', () => {
it('then it should call initQueryVariableEditor', () => {

View File

@ -1,9 +1,9 @@
import React, { ChangeEvent, PureComponent } from 'react';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { InlineField, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { getTemplateSrv } from '@grafana/runtime';
import { LoadingState, SelectableValue } from '@grafana/data';
import { DataSourceInstanceSettings, LoadingState, SelectableValue } from '@grafana/data';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
@ -20,9 +20,9 @@ import { isLegacyQueryEditor, isQueryEditor } from '../guard';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableSwitchField } from '../editor/VariableSwitchField';
import { QueryVariableDatasourceSelect } from './QueryVariableDatasourceSelect';
import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
import { QueryVariableSortSelect } from './QueryVariableSortSelect';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
export interface OwnProps extends VariableEditorProps<QueryVariableModel> {}
@ -65,9 +65,12 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
}
}
onDataSourceChange = (option: SelectableValue<string>) => {
onDataSourceChange = (dsSettings: DataSourceInstanceSettings) => {
this.props.onPropChange({ propName: 'query', propValue: '' });
this.props.onPropChange({ propName: 'datasource', propValue: option.value });
this.props.onPropChange({
propName: 'datasource',
propValue: dsSettings.isDefault ? null : dsSettings.name,
});
};
onLegacyQueryChange = async (query: any, definition: string) => {
@ -182,19 +185,19 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Query Options" />
<VerticalGroup spacing="md">
<VerticalGroup spacing="lg">
<VerticalGroup spacing="none">
<VerticalGroup spacing="xs">
<InlineFieldRow>
<QueryVariableDatasourceSelect
<InlineFieldRow>
<InlineField label="Data source" labelWidth={20}>
<DataSourcePicker
current={this.props.variable.datasource}
onChange={this.onDataSourceChange}
datasource={this.props.variable.datasource}
dataSources={this.props.editor.extended?.dataSources}
variables={true}
/>
<QueryVariableRefreshSelect onChange={this.onRefreshChange} refresh={this.props.variable.refresh} />
</InlineFieldRow>
<div style={{ flexDirection: 'column' }}>{this.renderQueryEditor()}</div>
</VerticalGroup>
</InlineField>
<QueryVariableRefreshSelect onChange={this.onRefreshChange} refresh={this.props.variable.refresh} />
</InlineFieldRow>
<div style={{ flexDirection: 'column' }}>{this.renderQueryEditor()}</div>
<VariableTextField
value={this.state.regex ?? this.props.variable.regex}
name="Regex"

View File

@ -37,25 +37,22 @@ import { notifyApp } from '../../../core/reducers/appNotification';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
import { getTimeSrv, setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
import { setVariableQueryRunner, VariableQueryRunner } from './VariableQueryRunner';
import { setDataSourceSrv } from '@grafana/runtime';
const mocks: Record<string, any> = {
datasource: {
metricFindQuery: jest.fn().mockResolvedValue([]),
},
datasourceSrv: {
getMetricSources: jest.fn().mockReturnValue([]),
dataSourceSrv: {
get: (name: string) => Promise.resolve(mocks[name]),
getList: jest.fn().mockReturnValue([]),
},
pluginLoader: {
importDataSourcePlugin: jest.fn().mockResolvedValue({ components: {} }),
},
};
jest.mock('../../plugins/datasource_srv', () => ({
getDatasourceSrv: jest.fn(() => ({
get: jest.fn((name: string) => mocks[name]),
getMetricSources: () => mocks.datasourceSrv.getMetricSources(),
})),
}));
setDataSourceSrv(mocks.dataSourceSrv as any);
jest.mock('../../plugins/plugin_loader', () => ({
importDataSourcePlugin: () => mocks.pluginLoader.importDataSourcePlugin(),
@ -272,11 +269,10 @@ describe('query actions', () => {
describe('when initQueryVariableEditor is dispatched', () => {
it('then correct actions are dispatched', async () => {
const variable = createVariable({ includeAll: true, useTags: false });
const defaultMetricSource = { name: '', value: '', meta: {}, sort: '' };
const testMetricSource = { name: 'test', value: 'test', meta: {}, sort: '' };
const testMetricSource = { name: 'test', value: 'test', meta: {} };
const editor = {};
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([testMetricSource]);
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([testMetricSource]);
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
});
@ -287,12 +283,9 @@ describe('query actions', () => {
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [updateDatasources, setDatasource, setEditor] = actions;
const expectedNumberOfActions = 3;
const [setDatasource, setEditor] = actions;
const expectedNumberOfActions = 2;
expect(updateDatasources).toEqual(
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultMetricSource, testMetricSource] })
);
expect(setDatasource).toEqual(
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
);
@ -305,11 +298,10 @@ describe('query actions', () => {
describe('when initQueryVariableEditor is dispatched and metricsource without value is available', () => {
it('then correct actions are dispatched', async () => {
const variable = createVariable({ includeAll: true, useTags: false });
const defaultMetricSource = { name: '', value: '', meta: {}, sort: '' };
const testMetricSource = { name: 'test', value: (null as unknown) as string, meta: {}, sort: '' };
const testMetricSource = { name: 'test', value: (null as unknown) as string, meta: {} };
const editor = {};
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([testMetricSource]);
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([testMetricSource]);
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
});
@ -320,12 +312,9 @@ describe('query actions', () => {
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [updateDatasources, setDatasource, setEditor] = actions;
const expectedNumberOfActions = 3;
const [setDatasource, setEditor] = actions;
const expectedNumberOfActions = 2;
expect(updateDatasources).toEqual(
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultMetricSource] })
);
expect(setDatasource).toEqual(
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
);
@ -338,10 +327,9 @@ describe('query actions', () => {
describe('when initQueryVariableEditor is dispatched and no metric sources was found', () => {
it('then correct actions are dispatched', async () => {
const variable = createVariable({ includeAll: true, useTags: false });
const defaultDatasource = { name: '', value: '', meta: {}, sort: '' };
const editor = {};
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([]);
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([]);
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
});
@ -352,12 +340,9 @@ describe('query actions', () => {
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [updateDatasources, setDatasource, setEditor] = actions;
const expectedNumberOfActions = 3;
const [setDatasource, setEditor] = actions;
const expectedNumberOfActions = 2;
expect(updateDatasources).toEqual(
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultDatasource] })
);
expect(setDatasource).toEqual(
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
);
@ -370,7 +355,6 @@ describe('query actions', () => {
describe('when initQueryVariableEditor is dispatched and variable dont have datasource', () => {
it('then correct actions are dispatched', async () => {
const variable = createVariable({ datasource: undefined });
const ds = { name: '', value: '', meta: {}, sort: '' };
const tester = await reduxTester<{ templating: TemplatingState }>()
.givenRootReducer(getRootReducer())
@ -378,10 +362,10 @@ describe('query actions', () => {
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
const [updateDatasources] = actions;
const [setDatasource] = actions;
const expectedNumberOfActions = 1;
expect(updateDatasources).toEqual(changeVariableEditorExtended({ propName: 'dataSources', propValue: [ds] }));
expect(setDatasource).toEqual(changeVariableEditorExtended({ propName: 'dataSource', propValue: undefined }));
return actions.length === expectedNumberOfActions;
});
});

View File

@ -1,10 +1,8 @@
import { DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data';
import { toDataQueryError } from '@grafana/runtime';
import { updateOptions } from '../state/actions';
import { QueryVariableModel } from '../types';
import { ThunkResult } from '../../../types';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
import { getDataSourceSrv } from '@grafana/runtime';
import { getVariable } from '../state/selectors';
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
import { changeVariableProp } from '../state/sharedReducer';
@ -24,7 +22,7 @@ export const updateQueryVariableOptions = (
if (getState().templating.editor.id === variableInState.id) {
dispatch(removeVariableEditorError({ errorProp: 'update' }));
}
const datasource = await getDatasourceSrv().get(variableInState.datasource ?? '');
const datasource = await getDataSourceSrv().get(variableInState.datasource ?? '');
// we need to await the result from variableQueryRunner before moving on otherwise variables dependent on this
// variable will have the wrong current value as input
@ -53,18 +51,7 @@ export const initQueryVariableEditor = (identifier: VariableIdentifier): ThunkRe
dispatch,
getState
) => {
const dataSources: DataSourceSelectItem[] = getDatasourceSrv()
.getMetricSources()
.filter(ds => !ds.meta.mixed && ds.value !== null);
const defaultDatasource: DataSourceSelectItem = { name: '', value: '', meta: {} as DataSourcePluginMeta, sort: '' };
const allDataSources = [defaultDatasource].concat(dataSources);
dispatch(changeVariableEditorExtended({ propName: 'dataSources', propValue: allDataSources }));
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
if (!variable.datasource) {
return;
}
await dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
};
@ -74,7 +61,7 @@ export const changeQueryVariableDataSource = (
): ThunkResult<void> => {
return async (dispatch, getState) => {
try {
const dataSource = await getDatasourceSrv().get(name ?? '');
const dataSource = await getDataSourceSrv().get(name ?? '');
dispatch(changeVariableEditorExtended({ propName: 'dataSource', propValue: dataSource }));
const VariableQueryEditor = await getVariableQueryEditor(dataSource);

View File

@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import _ from 'lodash';
import { DataSourceApi, DataSourceSelectItem, MetricFindValue, stringToJsRegex } from '@grafana/data';
import { DataSourceApi, MetricFindValue, stringToJsRegex } from '@grafana/data';
import {
initialVariableModelState,
@ -29,7 +29,6 @@ interface VariableOptionsUpdate {
export interface QueryVariableEditorState {
VariableQueryEditor: VariableQueryEditorType;
dataSources: DataSourceSelectItem[];
dataSource: DataSourceApi | null;
}

View File

@ -61,6 +61,7 @@ import { expect } from '../../../../test/lib/common';
import { ConstantVariableModel, VariableRefresh } from '../types';
import { updateVariableOptions } from '../query/reducer';
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
import { setDataSourceSrv } from '@grafana/runtime';
variableAdapters.setInit(() => [
createQueryVariableAdapter(),
@ -82,12 +83,10 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => ({
}),
}));
jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: jest.fn(() => ({
get: getDatasource,
getMetricSources,
})),
}));
setDataSourceSrv({
get: getDatasource,
getList: getMetricSources,
} as any);
describe('shared actions', () => {
describe('when initDashboardTemplating is dispatched', () => {

View File

@ -14,6 +14,7 @@ import { updateVariableOptions } from '../query/reducer';
import { customBuilder, queryBuilder } from '../shared/testing/builders';
import { variablesInitTransaction } from './transactionReducer';
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
import { setDataSourceSrv } from '@grafana/runtime';
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
getTimeSrv: jest.fn().mockReturnValue({
@ -28,39 +29,37 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => ({
}),
}));
jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: () => ({
get: jest.fn().mockResolvedValue({
metricFindQuery: jest.fn().mockImplementation((query, options) => {
if (query === '$custom.*') {
return Promise.resolve([
{ value: 'AA', text: 'AA' },
{ value: 'AB', text: 'AB' },
{ value: 'AC', text: 'AC' },
]);
}
setDataSourceSrv({
get: jest.fn().mockResolvedValue({
metricFindQuery: jest.fn().mockImplementation((query, options) => {
if (query === '$custom.*') {
return Promise.resolve([
{ value: 'AA', text: 'AA' },
{ value: 'AB', text: 'AB' },
{ value: 'AC', text: 'AC' },
]);
}
if (query === '$custom.$queryDependsOnCustom.*') {
return Promise.resolve([
{ value: 'AAA', text: 'AAA' },
{ value: 'AAB', text: 'AAB' },
{ value: 'AAC', text: 'AAC' },
]);
}
if (query === '$custom.$queryDependsOnCustom.*') {
return Promise.resolve([
{ value: 'AAA', text: 'AAA' },
{ value: 'AAB', text: 'AAB' },
{ value: 'AAC', text: 'AAC' },
]);
}
if (query === '*') {
return Promise.resolve([
{ value: 'A', text: 'A' },
{ value: 'B', text: 'B' },
{ value: 'C', text: 'C' },
]);
}
if (query === '*') {
return Promise.resolve([
{ value: 'A', text: 'A' },
{ value: 'B', text: 'B' },
{ value: 'C', text: 'C' },
]);
}
return Promise.resolve([]);
}),
return Promise.resolve([]);
}),
}),
}));
} as any);
variableAdapters.setInit(() => [createCustomVariableAdapter(), createQueryVariableAdapter()]);

View File

@ -1,12 +1,11 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { css } from 'emotion';
import { DataSourceSelectItem, VariableSuggestion } from '@grafana/data';
import { VariableSuggestion } from '@grafana/data';
import { Button, LegacyForms, DataLinkInput, stylesFactory } from '@grafana/ui';
const { FormField, Switch } = LegacyForms;
import { DataLinkConfig } from '../types';
import { usePrevious } from 'react-use';
import { getDatasourceSrv } from '../../../../features/plugins/datasource_srv';
import DataSourcePicker from '../../../../core/components/Select/DataSourcePicker';
import { DataSourcePicker } from '../../../../core/components/Select/DataSourcePicker';
const getStyles = stylesFactory(() => ({
firstRow: css`
@ -107,14 +106,16 @@ export const DataLink = (props: Props) => {
/>
{showInternalLink && (
<DataSourceSection
onChange={datasourceUid => {
<DataSourcePicker
tracing={true}
// Uid and value should be always set in the db and so in the items.
onChange={ds => {
onChange({
...value,
datasourceUid,
datasourceUid: ds.uid,
});
}}
datasourceUid={value.datasourceUid}
current={value.datasourceUid}
/>
)}
</div>
@ -122,37 +123,6 @@ export const DataLink = (props: Props) => {
);
};
type DataSourceSectionProps = {
datasourceUid?: string;
onChange: (uid: string) => void;
};
const DataSourceSection = (props: DataSourceSectionProps) => {
const { datasourceUid, onChange } = props;
const datasources: DataSourceSelectItem[] = getDatasourceSrv()
.getExternal()
// At this moment only Jaeger and Zipkin datasource is supported as the link target.
.filter(ds => ds.meta.tracing)
.map(
ds =>
({
value: ds.uid,
name: ds.name,
meta: ds.meta,
} as DataSourceSelectItem)
);
let selectedDatasource = datasourceUid && datasources.find(d => d.value === datasourceUid);
return (
<DataSourcePicker
// Uid and value should be always set in the db and so in the items.
onChange={ds => onChange(ds.value!)}
datasources={datasources}
current={selectedDatasource || undefined}
/>
);
};
function useInternalLink(datasourceUid?: string): [boolean, Dispatch<SetStateAction<boolean>>] {
const [showInternalLink, setShowInternalLink] = useState<boolean>(!!datasourceUid);
const previousUid = usePrevious(datasourceUid);

View File

@ -1,7 +1,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { DerivedField } from './DerivedField';
import DataSourcePicker from '../../../../core/components/Select/DataSourcePicker';
import { DataSourcePicker } from '../../../../core/components/Select/DataSourcePicker';
import { DataSourceInstanceSettings } from '@grafana/data';
jest.mock('app/features/plugins/datasource_srv', () => ({
@ -41,12 +41,7 @@ describe('DerivedField', () => {
};
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
expect(
wrapper
.find('DataSourceSection')
.dive()
.find(DataSourcePicker).length
).toBe(1);
expect(wrapper.find(DataSourcePicker).length).toBe(1);
});
it('shows url link if uid is not set', () => {
@ -56,7 +51,7 @@ describe('DerivedField', () => {
url: 'test',
};
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
expect(wrapper.find('DataSourceSection').length).toBe(0);
expect(wrapper.find(DataSourcePicker).length).toBe(0);
});
it('shows only tracing datasources for internal link', () => {
@ -66,13 +61,6 @@ describe('DerivedField', () => {
datasourceUid: 'test',
};
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
const dsSection = wrapper.find('DataSourceSection').dive();
expect(dsSection.find(DataSourcePicker).props().datasources).toEqual([
{
meta: { tracing: true },
name: 'tracing_ds',
value: 'tracing',
},
]);
expect(wrapper.find(DataSourcePicker).props().tracing).toEqual(true);
});
});

View File

@ -1,15 +1,13 @@
import React, { useEffect, useState } from 'react';
import { css } from 'emotion';
import { Button, DataLinkInput, stylesFactory, LegacyForms } from '@grafana/ui';
const { Switch, FormField } = LegacyForms;
import { VariableSuggestion } from '@grafana/data';
import { DataSourceSelectItem } from '@grafana/data';
import { DerivedFieldConfig } from '../types';
import DataSourcePicker from 'app/core/components/Select/DataSourcePicker';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { usePrevious } from 'react-use';
const { Switch, FormField } = LegacyForms;
const getStyles = stylesFactory(() => ({
row: css`
display: flex;
@ -128,48 +126,18 @@ export const DerivedField = (props: Props) => {
/>
{showInternalLink && (
<DataSourceSection
onChange={datasourceUid => {
<DataSourcePicker
tracing={true}
onChange={ds =>
onChange({
...value,
datasourceUid,
});
}}
datasourceUid={value.datasourceUid}
datasourceUid: ds.uid,
})
}
current={value.datasourceUid}
/>
)}
</div>
</div>
);
};
type DataSourceSectionProps = {
datasourceUid?: string;
onChange: (uid: string) => void;
};
const DataSourceSection = (props: DataSourceSectionProps) => {
const { datasourceUid, onChange } = props;
const datasources: DataSourceSelectItem[] = getDatasourceSrv()
.getExternal()
// At this moment only Jaeger and Zipkin datasource is supported as the link target.
.filter(ds => ds.meta.tracing)
.map(
ds =>
({
value: ds.uid,
name: ds.name,
meta: ds.meta,
} as DataSourceSelectItem)
);
let selectedDatasource = datasourceUid && datasources.find(d => d.value === datasourceUid);
return (
<DataSourcePicker
// Uid and value should be always set in the db and so in the items.
onChange={ds => onChange(ds.value!)}
datasources={datasources}
current={selectedDatasource || undefined}
/>
);
};

View File

@ -16592,17 +16592,6 @@ jest-util@^24.0.0, jest-util@^24.9.0:
slash "^2.0.0"
source-map "^0.6.0"
jest-util@^25.5.0:
version "25.5.0"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.5.0.tgz#31c63b5d6e901274d264a4fec849230aa3fa35b0"
integrity sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==
dependencies:
"@jest/types" "^25.5.0"
chalk "^3.0.0"
graceful-fs "^4.2.4"
is-ci "^2.0.0"
make-dir "^3.0.0"
jest-util@^26.1.0, jest-util@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1"
@ -23862,7 +23851,7 @@ source-map-support@^0.3.2:
dependencies:
source-map "0.1.32"
source-map-support@^0.5.16, source-map-support@~0.5.19:
source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@~0.5.19:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@ -25203,23 +25192,6 @@ ts-jest@26.4.4:
semver "7.x"
yargs-parser "20.x"
ts-jest@26.4.4:
version "26.4.4"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49"
integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg==
dependencies:
"@types/jest" "26.x"
bs-logger "0.x"
buffer-from "1.x"
fast-json-stable-stringify "2.x"
jest-util "^26.1.0"
json5 "2.x"
lodash.memoize "4.x"
make-error "1.x"
mkdirp "1.x"
semver "7.x"
yargs-parser "20.x"
ts-loader@6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.1.tgz#67939d5772e8a8c6bdaf6277ca023a4812da02ef"
@ -25263,6 +25235,11 @@ tslib@1.10.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
tslib@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
tslib@2.0.3, tslib@^2.0.0, tslib@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
@ -26660,11 +26637,6 @@ yargs-parser@20.x, yargs-parser@^20.2.2:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
yargs-parser@20.x:
version "20.2.4"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
yargs-parser@^11.1.1:
version "11.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"