From 3d6380a0aa7c97673845ea9050c12dcc8468e07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 4 Dec 2020 14:24:55 +0100 Subject: [PATCH] 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 --- .../specs/variables/new-query-variable.ts | 3 +- packages/grafana-data/src/types/datasource.ts | 2 +- .../src/selectors/components.ts | 6 +- .../src/selectors/pages.ts | 4 +- .../src/services/dataSourceSrv.ts | 20 +- .../src/components/Layout/Layout.tsx | 4 +- .../src/components/Select/SingleValue.tsx | 9 +- pkg/api/frontendsettings.go | 1 - .../components/Select/DataSourcePicker.tsx | 125 +++++--- public/app/core/utils/richHistory.ts | 19 +- .../getAlertingValidationMessage.test.ts | 25 +- .../PanelEditor/PanelEditorQueries.tsx | 34 +-- .../app/features/explore/ExploreToolbar.tsx | 16 +- .../explore/TraceView/createSpanLink.test.ts | 12 +- .../explore/TraceView/createSpanLink.tsx | 4 +- public/app/features/explore/Wrapper.test.tsx | 6 +- .../explore/state/explorePane.test.ts | 34 +-- .../app/features/explore/state/explorePane.ts | 4 +- .../app/features/explore/state/selectors.ts | 15 - .../components/ImportDashboardForm.tsx | 11 +- .../components/ImportDashboardOverview.tsx | 3 +- .../manage-dashboards/state/actions.ts | 16 +- .../manage-dashboards/state/reducers.ts | 5 +- public/app/features/plugins/datasource_srv.ts | 173 ++++++----- .../plugins/specs/datasource_srv.test.ts | 274 +++++++++++------- .../query/components/QueryEditorRow.tsx | 30 +- .../query/components/QueryEditorRows.tsx | 13 +- .../features/query/components/QueryGroup.tsx | 200 +++++++------ .../query/components/QueryGroupOptions.tsx | 10 +- public/app/features/sandbox/TestStuffPage.tsx | 44 +-- .../features/variables/adhoc/actions.test.ts | 1 - .../variables/datasource/actions.test.ts | 7 - .../editor/VariableTextAreaField.tsx | 18 +- .../query/QueryVariableDatasourceSelect.tsx | 28 -- .../query/QueryVariableEditor.test.tsx | 7 +- .../variables/query/QueryVariableEditor.tsx | 33 ++- .../features/variables/query/actions.test.ts | 52 ++-- .../app/features/variables/query/actions.ts | 19 +- .../app/features/variables/query/reducer.ts | 3 +- .../features/variables/state/actions.test.ts | 11 +- .../variables/state/processVariable.test.ts | 55 ++-- .../elasticsearch/configuration/DataLink.tsx | 46 +-- .../loki/configuration/DerivedField.test.tsx | 20 +- .../loki/configuration/DerivedField.tsx | 52 +--- yarn.lock | 40 +-- 45 files changed, 707 insertions(+), 807 deletions(-) delete mode 100644 public/app/features/variables/query/QueryVariableDatasourceSelect.tsx diff --git a/e2e/suite1/specs/variables/new-query-variable.ts b/e2e/suite1/specs/variables/new-query-variable.ts index 3955481b03a..48485efb702 100644 --- a/e2e/suite1/specs/variables/new-query-variable.ts +++ b/e2e/suite1/specs/variables/new-query-variable.ts @@ -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 => { diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index f6cfcdb874f..88ea58cb799 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -567,6 +567,7 @@ export interface DataSourceInstanceSettings `Panel editor option pane tab ${title}`, }, diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 2c753972131..1a238aff590 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -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', diff --git a/packages/grafana-runtime/src/services/dataSourceSrv.ts b/packages/grafana-runtime/src/services/dataSourceSrv.ts index 96758a0a4b2..1edc5b6f3b2 100644 --- a/packages/grafana-runtime/src/services/dataSourceSrv.ts +++ b/packages/grafana-runtime/src/services/dataSourceSrv.ts @@ -16,14 +16,9 @@ export interface DataSourceSrv { get(name?: string | null, scopedVars?: ScopedVars): Promise; /** - * 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; /** diff --git a/packages/grafana-ui/src/components/Layout/Layout.tsx b/packages/grafana-ui/src/components/Layout/Layout.tsx index ec2d6b36ff7..daebb434733 100644 --- a/packages/grafana-ui/src/components/Layout/Layout.tsx +++ b/packages/grafana-ui/src/components/Layout/Layout.tsx @@ -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'}; diff --git a/packages/grafana-ui/src/components/Select/SingleValue.tsx b/packages/grafana-ui/src/components/Select/SingleValue.tsx index 5a237b2456c..bbbb899c1b7 100644 --- a/packages/grafana-ui/src/components/Select/SingleValue.tsx +++ b/packages/grafana-ui/src/components/Select/SingleValue.tsx @@ -59,14 +59,7 @@ export const SingleValue = (props: Props) => { return ( -
+
{data.imgUrl ? ( ) : ( diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 914ac809291..b734f4dd78b 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -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 { diff --git a/public/app/core/components/Select/DataSourcePicker.tsx b/public/app/core/components/Select/DataSourcePicker.tsx index a2e9eeb4dca..090ab2392ee 100644 --- a/public/app/core/components/Select/DataSourcePicker.tsx +++ b/public/app/core/components/Select/DataSourcePicker.tsx @@ -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 { +export interface State { + error?: string; +} + +export class DataSourcePicker extends PureComponent { + dataSourceSrv = getDataSourceSrv(); + static defaultProps: Partial = { autoFocus: false, openMenuOnFocus: false, placeholder: 'Select datasource', }; - searchInput: HTMLElement; + state: State = {}; constructor(props: Props) { super(props); } - onChange = (item: SelectableValue) => { - 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) => { + 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 (
@@ -87,9 +134,9 @@ export class DataSourcePicker extends PureComponent { 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 ( {o.label} @@ -103,5 +150,3 @@ export class DataSourcePicker extends PureComponent { ); } } - -export default DataSourcePicker; diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index 2ad5fb00f98..9ca3e60a699 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -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, }); diff --git a/public/app/features/alerting/getAlertingValidationMessage.test.ts b/public/app/features/alerting/getAlertingValidationMessage.test.ts index e98c5c071a3..ed505319381 100644 --- a/public/app/features/alerting/getAlertingValidationMessage.test.ts +++ b/public/app/features/alerting/getAlertingValidationMessage.test.ts @@ -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 []; }, }; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx index dfe8f45e0d1..f0f059fe786 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx @@ -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 { 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 { }; } - 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 { }); }; - 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 { return ( ); } diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 2250c6de878..7d1726b5596 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -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 { - 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 { }); } - 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 { >
diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 46088b4e198..7ff974bb9b1 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -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', diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index b9ac8dc5f7c..e71976c7e58 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -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; diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx index 104b5c0974d..ba1e8f1cfc3 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/Wrapper.test.tsx @@ -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 { return Promise.resolve((name ? dsSettings.find(d => d.api.name === name) : dsSettings[0])!.api); }, diff --git a/public/app/features/explore/state/explorePane.test.ts b/public/app/features/explore/state/explorePane.test.ts index c3b99c513bf..5856c202898 100644 --- a/public/app/features/explore/state/explorePane.test.ts +++ b/public/app/features/explore/state/explorePane.test.ts @@ -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; - -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) => { const exploreId = ExploreId.left; const containerWidth = 1920; diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 17e2f5dafa0..980d3cfb0c9 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -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 { return async (dispatch, getState) => { - const exploreDatasources = getExploreDatasources(); + const exploreDatasources = getDataSourceSrv().getList(); let instance = undefined; let history: HistoryItem[] = []; diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts index f14c6ad7e7d..32e097b4f62 100644 --- a/public/app/features/explore/state/selectors.ts +++ b/public/app/features/explore/state/selectors.ts @@ -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) - ); -}; diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx index 0262ab2b186..dc93b5effb0 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -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, 'formState' | 'watch'> { +interface Props extends Omit, 'formState'> { uidReset: boolean; inputs: DashboardInputs; initialFolderId: number; @@ -36,8 +36,10 @@ export const ImportDashboardForm: FC = ({ 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 = ({ {inputs.dataSources && inputs.dataSources.map((input: DataSourceInput, index: number) => { const dataSourceOption = `dataSources[${index}]`; + const current = watchDataSources ?? []; return ( = ({ > { validateFieldsOnMount={['title', 'uid']} validateOn="onChange" > - {({ register, errors, control, getValues }) => ( + {({ register, errors, control, watch, getValues }) => ( { onCancel={this.onCancel} onUidReset={this.onUidReset} onSubmit={this.onSubmit} + watch={watch} initialFolderId={folder.id} /> )} diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 1033ea18ffe..fd71b3582d6 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -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 { 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) { diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index ee23a8bf380..724847f00a1 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -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 { diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 76d86c6000d..e82ed4f9313 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -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 = {}; @@ -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 => { diff --git a/public/app/features/plugins/specs/datasource_srv.test.ts b/public/app/features/plugins/specs/datasource_srv.test.ts index ccd3e631394..8817efa358f 100644 --- a/public/app/features/plugins/specs/datasource_srv.test.ts +++ b/public/app/features/plugins/specs/datasource_srv.test.ts @@ -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", + }, + ] + `); }); }); }); diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index ae3423136c4..e0af8a410c5 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -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 { state: State = { datasource: null, - loadedDataSourceValue: undefined, hasTextEditMode: false, data: undefined, isOpen: true, @@ -89,27 +88,31 @@ export class QueryEditorRow extends PureComponent { }; } + 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 { } // 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 { if (!this.element || this.angularQueryEditor) { return; } + this.renderAngularQueryEditor(); } @@ -259,14 +263,14 @@ export class QueryEditorRow extends PureComponent { }; 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 ( this.onToggleEditMode(e, props)} diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index 09d43dcacef..55d9e9805b4 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -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 { }; render() { - const { props } = this; + const { dsSettings, data, queries } = this.props; return ( @@ -75,19 +75,18 @@ export class QueryEditorRows extends PureComponent { {provided => { return (
- {props.queries.map((query, index) => ( + {queries.map((query, index) => ( this.onChangeQuery(query, index)} onRemoveQuery={this.onRemoveQuery} onAddQuery={this.props.onAddQuery} onRunQuery={this.props.onRunQueries} - inMixedMode={props.datasource.meta.mixed} /> ))} {provided.placeholder} diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index 14086ec55a9..8c4c5b65089 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -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 { - 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 { }; 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 { 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) { + 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 { 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 (
- - - -
-
-
-
- -
- {onOpenQueryInspector && ( -
- -
+ {dataSource && ( + <> +
+
+
+ +
+ {onOpenQueryInspector && ( +
+ +
+ )} + )}
@@ -220,13 +229,9 @@ export class QueryGroup extends PureComponent { }; 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 ( { }; onAddQuery = (query: Partial) => { - 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 { 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 ; + renderQueries(dsSettings: DataSourceInstanceSettings) { + const { options, onRunQueries } = this.props; + const { data } = this.state; + + if (isSharedDashboardQuery(dsSettings.name)) { + return ; } return (
{ ); } - 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 ( @@ -305,7 +314,7 @@ export class QueryGroup extends PureComponent { } 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 { >
{this.renderTopSection(styles)} -
{this.renderQueries()}
- {this.renderAddQueryRow()} - - {isHelpOpen && ( - - - + {dsSettings && ( + <> +
{this.renderQueries(dsSettings)}
+ {this.renderAddQueryRow(dsSettings)} + {isHelpOpen && ( + + + + )} + )}
diff --git a/public/app/features/query/components/QueryGroupOptions.tsx b/public/app/features/query/components/QueryGroupOptions.tsx index b4166436fa5..ca31fef73e2 100644 --- a/public/app/features/query/components/QueryGroupOptions.tsx +++ b/public/app/features/query/components/QueryGroupOptions.tsx @@ -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; diff --git a/public/app/features/sandbox/TestStuffPage.tsx b/public/app/features/sandbox/TestStuffPage.tsx index cc7cff6cb73..ecb8184013b 100644 --- a/public/app/features/sandbox/TestStuffPage.tsx +++ b/public/app/features/sandbox/TestStuffPage.tsx @@ -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(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 = () => {
@@ -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, }, }; diff --git a/public/app/features/variables/adhoc/actions.test.ts b/public/app/features/variables/adhoc/actions.test.ts index a612e3fafb6..92deb2bd09b 100644 --- a/public/app/features/variables/adhoc/actions.test.ts +++ b/public/app/features/variables/adhoc/actions.test.ts @@ -478,6 +478,5 @@ function createDatasource(name: string, selectable = true): DataSourceSelectItem meta: { mixed: !selectable, } as DataSourcePluginMeta, - sort: '', }; } diff --git a/public/app/features/variables/datasource/actions.test.ts b/public/app/features/variables/datasource/actions.test.ts index f652545aefe..93e3a6cfdfa 100644 --- a/public/app/features/variables/datasource/actions.test.ts +++ b/public/app/features/variables/datasource/actions.test.ts @@ -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: '', }, ]; diff --git a/public/app/features/variables/editor/VariableTextAreaField.tsx b/public/app/features/variables/editor/VariableTextAreaField.tsx index 30d0d01de0c..ebdc9ea89d3 100644 --- a/public/app/features/variables/editor/VariableTextAreaField.tsx +++ b/public/app/features/variables/editor/VariableTextAreaField.tsx @@ -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 ( - - - +