mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
c62a0aa4d0
commit
3d6380a0aa
@ -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 => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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}`,
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
|
@ -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'};
|
||||
|
@ -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} />
|
||||
) : (
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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 [];
|
||||
},
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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[] = [];
|
||||
|
||||
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
@ -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 }}
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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 => {
|
||||
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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)}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -478,6 +478,5 @@ function createDatasource(name: string, selectable = true): DataSourceSelectItem
|
||||
meta: {
|
||||
mixed: !selectable,
|
||||
} as DataSourcePluginMeta,
|
||||
sort: '',
|
||||
};
|
||||
}
|
||||
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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', () => {
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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()]);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
40
yarn.lock
40
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user