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()
|
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.within(select => {
|
.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()
|
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect()
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.within(select => {
|
.within(select => {
|
||||||
|
@ -567,6 +567,7 @@ export interface DataSourceInstanceSettings<T extends DataSourceJsonData = DataS
|
|||||||
username?: string;
|
username?: string;
|
||||||
password?: string; // when access is direct, for some legacy datasources
|
password?: string; // when access is direct, for some legacy datasources
|
||||||
database?: string;
|
database?: string;
|
||||||
|
isDefault?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the full Authorization header if basic auth is enabled.
|
* This is the full Authorization header if basic auth is enabled.
|
||||||
@ -582,7 +583,6 @@ export interface DataSourceSelectItem {
|
|||||||
name: string;
|
name: string;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
meta: DataSourcePluginMeta;
|
meta: DataSourcePluginMeta;
|
||||||
sort: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { Pages } from './pages';
|
|
||||||
|
|
||||||
export const Components = {
|
export const Components = {
|
||||||
DataSource: {
|
DataSource: {
|
||||||
TestData: {
|
TestData: {
|
||||||
@ -57,8 +55,8 @@ export const Components = {
|
|||||||
},
|
},
|
||||||
OptionsPane: {
|
OptionsPane: {
|
||||||
content: 'Panel editor option pane content',
|
content: 'Panel editor option pane content',
|
||||||
close: Pages.Dashboard.Toolbar.toolbarItems('Close options pane'),
|
close: 'Dashboard navigation bar button Close options pane',
|
||||||
open: Pages.Dashboard.Toolbar.toolbarItems('Open options pane'),
|
open: 'Dashboard navigation bar button Open options pane',
|
||||||
select: 'Panel editor option pane select',
|
select: 'Panel editor option pane select',
|
||||||
tab: (title: string) => `Panel editor option pane tab ${title}`,
|
tab: (title: string) => `Panel editor option pane tab ${title}`,
|
||||||
},
|
},
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Components } from './components';
|
||||||
|
|
||||||
export const Pages = {
|
export const Pages = {
|
||||||
Login: {
|
Login: {
|
||||||
url: '/login',
|
url: '/login',
|
||||||
@ -87,7 +89,7 @@ export const Pages = {
|
|||||||
submitButton: 'Variable editor Submit button',
|
submitButton: 'Variable editor Submit button',
|
||||||
},
|
},
|
||||||
QueryVariable: {
|
QueryVariable: {
|
||||||
queryOptionsDataSourceSelect: 'Variable editor Form Query DataSource select',
|
queryOptionsDataSourceSelect: Components.DataSourcePicker.container,
|
||||||
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
|
queryOptionsRefreshSelect: 'Variable editor Form Query Refresh select',
|
||||||
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
|
queryOptionsRegExInput: 'Variable editor Form Query RegEx field',
|
||||||
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
|
queryOptionsSortSelect: 'Variable editor Form Query Sort select',
|
||||||
|
@ -16,14 +16,9 @@ export interface DataSourceSrv {
|
|||||||
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>;
|
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all data sources
|
* Get a list of data sources
|
||||||
*/
|
*/
|
||||||
getAll(): DataSourceInstanceSettings[];
|
getList(filters?: GetDataSourceListFilters): DataSourceInstanceSettings[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all data sources except for internal ones that usually should not be listed like mixed data source.
|
|
||||||
*/
|
|
||||||
getExternal(): DataSourceInstanceSettings[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get settings and plugin metadata by name or uid
|
* Get settings and plugin metadata by name or uid
|
||||||
@ -31,6 +26,17 @@ export interface DataSourceSrv {
|
|||||||
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined;
|
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;
|
let singletonInstance: DataSourceSrv;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,9 +117,11 @@ const getStyles = stylesFactory(
|
|||||||
? 0
|
? 0
|
||||||
: `-${finalSpacing}`;
|
: `-${finalSpacing}`;
|
||||||
|
|
||||||
|
const label = orientation === Orientation.Vertical ? 'vertical-group' : 'horizontal-group';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
layout: css`
|
layout: css`
|
||||||
label: HorizontalGroup;
|
label: ${label};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: ${orientation === Orientation.Vertical ? 'column' : 'row'};
|
flex-direction: ${orientation === Orientation.Vertical ? 'column' : 'row'};
|
||||||
flex-wrap: ${wrap ? 'wrap' : 'nowrap'};
|
flex-wrap: ${wrap ? 'wrap' : 'nowrap'};
|
||||||
|
@ -59,14 +59,7 @@ export const SingleValue = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<components.SingleValue {...props}>
|
<components.SingleValue {...props}>
|
||||||
<div
|
<div className={cx(styles.singleValue)}>
|
||||||
className={cx(
|
|
||||||
styles.singleValue,
|
|
||||||
css`
|
|
||||||
overflow: hidden;
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{data.imgUrl ? (
|
{data.imgUrl ? (
|
||||||
<FadeWithImage loading={loading} imgUrl={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 {
|
if isDefault, _ := dsM["isDefault"].(bool); isDefault {
|
||||||
defaultDS = n
|
defaultDS = n
|
||||||
}
|
}
|
||||||
delete(dsM, "isDefault")
|
|
||||||
|
|
||||||
meta := dsM["meta"].(*plugins.DataSourcePlugin)
|
meta := dsM["meta"].(*plugins.DataSourcePlugin)
|
||||||
if meta.Preload {
|
if meta.Preload {
|
||||||
|
@ -3,72 +3,119 @@ import React, { PureComponent } from 'react';
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { HorizontalGroup, Select } from '@grafana/ui';
|
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 { selectors } from '@grafana/e2e-selectors';
|
||||||
import { isUnsignedPluginSignature, PluginSignatureBadge } from '../../../features/plugins/PluginSignatureBadge';
|
import { isUnsignedPluginSignature, PluginSignatureBadge } from '../../../features/plugins/PluginSignatureBadge';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onChange: (ds: DataSourceSelectItem) => void;
|
onChange: (ds: DataSourceInstanceSettings) => void;
|
||||||
datasources: DataSourceSelectItem[];
|
current: string | null;
|
||||||
current?: DataSourceSelectItem | null;
|
|
||||||
hideTextValue?: boolean;
|
hideTextValue?: boolean;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
openMenuOnFocus?: boolean;
|
openMenuOnFocus?: boolean;
|
||||||
showLoading?: boolean;
|
|
||||||
placeholder?: string;
|
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> = {
|
static defaultProps: Partial<Props> = {
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
openMenuOnFocus: false,
|
openMenuOnFocus: false,
|
||||||
placeholder: 'Select datasource',
|
placeholder: 'Select datasource',
|
||||||
};
|
};
|
||||||
|
|
||||||
searchInput: HTMLElement;
|
state: State = {};
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = (item: SelectableValue<string>) => {
|
componentDidMount() {
|
||||||
const ds = this.props.datasources.find(ds => ds.name === item.value);
|
const { current } = this.props;
|
||||||
|
const dsSettings = this.dataSourceSrv.getInstanceSettings(current);
|
||||||
|
if (!dsSettings) {
|
||||||
|
this.setState({ error: 'Could not find data source ' + current });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ds) {
|
onChange = (item: SelectableValue<string>) => {
|
||||||
this.props.onChange(ds);
|
const dsSettings = this.dataSourceSrv.getInstanceSettings(item.value);
|
||||||
|
|
||||||
|
if (dsSettings) {
|
||||||
|
this.props.onChange(dsSettings);
|
||||||
|
this.setState({ error: undefined });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
private getCurrentValue() {
|
||||||
const {
|
const { current, hideTextValue, noDefault } = this.props;
|
||||||
datasources,
|
|
||||||
current,
|
|
||||||
autoFocus,
|
|
||||||
hideTextValue,
|
|
||||||
onBlur,
|
|
||||||
openMenuOnFocus,
|
|
||||||
showLoading,
|
|
||||||
placeholder,
|
|
||||||
invalid,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const options = datasources.map(ds => ({
|
if (!current && noDefault) {
|
||||||
value: ds.name,
|
return null;
|
||||||
label: ds.name,
|
}
|
||||||
imgUrl: ds.meta.info.logos.small,
|
|
||||||
meta: ds.meta,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const value = current && {
|
const ds = this.dataSourceSrv.getInstanceSettings(current);
|
||||||
label: current.name.substr(0, 37),
|
|
||||||
value: current.name,
|
if (ds) {
|
||||||
imgUrl: current.meta.info.logos.small,
|
return {
|
||||||
loading: showLoading,
|
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,
|
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 (
|
return (
|
||||||
<div aria-label={selectors.components.DataSourcePicker.container}>
|
<div aria-label={selectors.components.DataSourcePicker.container}>
|
||||||
@ -87,9 +134,9 @@ export class DataSourcePicker extends PureComponent<Props> {
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
noOptionsMessage="No datasources found"
|
noOptionsMessage="No datasources found"
|
||||||
value={value}
|
value={value}
|
||||||
invalid={invalid}
|
invalid={!!error}
|
||||||
getOptionLabel={o => {
|
getOptionLabel={o => {
|
||||||
if (isUnsignedPluginSignature(o.meta.signature) && o !== value) {
|
if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) {
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup align="center" justify="space-between">
|
<HorizontalGroup align="center" justify="space-between">
|
||||||
<span>{o.label}</span> <PluginSignatureBadge status={o.meta.signature} />
|
<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 { DataQuery, DataSourceApi, dateTimeFormat, AppEvents, urlUtil, ExploreUrlState } from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { getExploreDatasources } from '../../features/explore/state/selectors';
|
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { RichHistoryQuery } from 'app/types/explore';
|
import { RichHistoryQuery } from 'app/types/explore';
|
||||||
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
const RICH_HISTORY_KEY = 'grafana.explore.richHistory';
|
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.
|
* exploreDatasources add generic datasource image and add property isRemoved = true.
|
||||||
*/
|
*/
|
||||||
export function createDatasourcesList(queriesDatasources: string[]) {
|
export function createDatasourcesList(queriesDatasources: string[]) {
|
||||||
const exploreDatasources = getExploreDatasources();
|
|
||||||
const datasources: Array<{ label: string; value: string; imgUrl: string; isRemoved: boolean }> = [];
|
const datasources: Array<{ label: string; value: string; imgUrl: string; isRemoved: boolean }> = [];
|
||||||
|
|
||||||
queriesDatasources.forEach(queryDsName => {
|
queriesDatasources.forEach(dsName => {
|
||||||
const index = exploreDatasources.findIndex(exploreDs => exploreDs.name === queryDsName);
|
const dsSettings = getDataSourceSrv().getInstanceSettings(dsName);
|
||||||
if (index !== -1) {
|
if (dsSettings) {
|
||||||
datasources.push({
|
datasources.push({
|
||||||
label: queryDsName,
|
label: dsSettings.name,
|
||||||
value: queryDsName,
|
value: dsSettings.name,
|
||||||
imgUrl: exploreDatasources[index].meta.info.logos.small,
|
imgUrl: dsSettings.meta.info.logos.small,
|
||||||
isRemoved: false,
|
isRemoved: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
datasources.push({
|
datasources.push({
|
||||||
label: queryDsName,
|
label: dsName,
|
||||||
value: queryDsName,
|
value: dsName,
|
||||||
imgUrl: 'public/img/icn-datasource.svg',
|
imgUrl: 'public/img/icn-datasource.svg',
|
||||||
isRemoved: true,
|
isRemoved: true,
|
||||||
});
|
});
|
||||||
|
@ -22,13 +22,10 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
const getMock = jest.fn().mockResolvedValue(datasource);
|
const getMock = jest.fn().mockResolvedValue(datasource);
|
||||||
const datasourceSrv: DataSourceSrv = {
|
const datasourceSrv: DataSourceSrv = {
|
||||||
get: getMock,
|
get: getMock,
|
||||||
getExternal(): DataSourceInstanceSettings[] {
|
getList(): DataSourceInstanceSettings[] {
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
getInstanceSettings: (() => {}) as any,
|
getInstanceSettings: (() => {}) as any,
|
||||||
getAll(): DataSourceInstanceSettings[] {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const targets: ElasticsearchQuery[] = [
|
const targets: ElasticsearchQuery[] = [
|
||||||
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false },
|
||||||
@ -66,10 +63,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
return Promise.resolve(alertingDatasource);
|
return Promise.resolve(alertingDatasource);
|
||||||
},
|
},
|
||||||
getInstanceSettings: (() => {}) as any,
|
getInstanceSettings: (() => {}) as any,
|
||||||
getExternal(): DataSourceInstanceSettings[] {
|
getList(): DataSourceInstanceSettings[] {
|
||||||
return [];
|
|
||||||
},
|
|
||||||
getAll(): DataSourceInstanceSettings[] {
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -96,10 +90,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
const datasourceSrv: DataSourceSrv = {
|
const datasourceSrv: DataSourceSrv = {
|
||||||
get: getMock,
|
get: getMock,
|
||||||
getInstanceSettings: (() => {}) as any,
|
getInstanceSettings: (() => {}) as any,
|
||||||
getExternal(): DataSourceInstanceSettings[] {
|
getList(): DataSourceInstanceSettings[] {
|
||||||
return [];
|
|
||||||
},
|
|
||||||
getAll(): DataSourceInstanceSettings[] {
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -128,10 +119,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
const datasourceSrv: DataSourceSrv = {
|
const datasourceSrv: DataSourceSrv = {
|
||||||
get: getMock,
|
get: getMock,
|
||||||
getInstanceSettings: (() => {}) as any,
|
getInstanceSettings: (() => {}) as any,
|
||||||
getExternal(): DataSourceInstanceSettings[] {
|
getList(): DataSourceInstanceSettings[] {
|
||||||
return [];
|
|
||||||
},
|
|
||||||
getAll(): DataSourceInstanceSettings[] {
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -160,10 +148,7 @@ describe('getAlertingValidationMessage', () => {
|
|||||||
const datasourceSrv: DataSourceSrv = {
|
const datasourceSrv: DataSourceSrv = {
|
||||||
get: getMock,
|
get: getMock,
|
||||||
getInstanceSettings: (() => {}) as any,
|
getInstanceSettings: (() => {}) as any,
|
||||||
getExternal(): DataSourceInstanceSettings[] {
|
getList(): DataSourceInstanceSettings[] {
|
||||||
return [];
|
|
||||||
},
|
|
||||||
getAll(): DataSourceInstanceSettings[] {
|
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,6 @@ import React, { PureComponent } from 'react';
|
|||||||
import { QueryGroup } from 'app/features/query/components/QueryGroup';
|
import { QueryGroup } from 'app/features/query/components/QueryGroup';
|
||||||
import { QueryGroupOptions } from 'app/features/query/components/QueryGroupOptions';
|
import { QueryGroupOptions } from 'app/features/query/components/QueryGroupOptions';
|
||||||
import { PanelModel } from '../../state';
|
import { PanelModel } from '../../state';
|
||||||
import { DataQuery, DataSourceSelectItem } from '@grafana/data';
|
|
||||||
import { getLocationSrv } from '@grafana/runtime';
|
import { getLocationSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -22,6 +21,10 @@ export class PanelEditorQueries extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
buildQueryOptions({ panel }: Props): QueryGroupOptions {
|
buildQueryOptions({ panel }: Props): QueryGroupOptions {
|
||||||
return {
|
return {
|
||||||
|
dataSource: {
|
||||||
|
name: panel.datasource,
|
||||||
|
},
|
||||||
|
queries: panel.targets,
|
||||||
maxDataPoints: panel.maxDataPoints,
|
maxDataPoints: panel.maxDataPoints,
|
||||||
minInterval: panel.interval,
|
minInterval: panel.interval,
|
||||||
timeRange: {
|
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 = () => {
|
onRunQueries = () => {
|
||||||
this.props.panel.refresh();
|
this.props.panel.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
onQueriesChange = (queries: DataQuery[]) => {
|
|
||||||
const { panel } = this.props;
|
|
||||||
|
|
||||||
panel.targets = queries;
|
|
||||||
panel.refresh();
|
|
||||||
|
|
||||||
this.forceUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
onOpenQueryInspector = () => {
|
onOpenQueryInspector = () => {
|
||||||
getLocationSrv().update({
|
getLocationSrv().update({
|
||||||
query: { inspect: this.props.panel.id, inspectTab: 'query' },
|
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;
|
const { panel } = this.props;
|
||||||
|
|
||||||
|
panel.datasource = options.dataSource.default ? null : options.dataSource.name!;
|
||||||
|
panel.targets = options.queries;
|
||||||
panel.timeFrom = options.timeRange?.from;
|
panel.timeFrom = options.timeRange?.from;
|
||||||
panel.timeShift = options.timeRange?.shift;
|
panel.timeShift = options.timeRange?.shift;
|
||||||
panel.hideTimeOverride = options.timeRange?.hide;
|
panel.hideTimeOverride = options.timeRange?.hide;
|
||||||
@ -81,15 +67,11 @@ export class PanelEditorQueries extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryGroup
|
<QueryGroup
|
||||||
dataSourceName={panel.datasource}
|
|
||||||
options={options}
|
options={options}
|
||||||
queryRunner={panel.getQueryRunner()}
|
queryRunner={panel.getQueryRunner()}
|
||||||
queries={panel.targets}
|
|
||||||
onQueriesChange={this.onQueriesChange}
|
|
||||||
onDataSourceChange={this.onDataSourceChange}
|
|
||||||
onRunQueries={this.onRunQueries}
|
onRunQueries={this.onRunQueries}
|
||||||
onOpenQueryInspector={this.onOpenQueryInspector}
|
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 { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { Icon, IconButton, LegacyForms, SetInterval, Tooltip } from '@grafana/ui';
|
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 { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||||
@ -24,7 +24,6 @@ import { LiveTailButton } from './LiveTailButton';
|
|||||||
import { ResponsiveButton } from './ResponsiveButton';
|
import { ResponsiveButton } from './ResponsiveButton';
|
||||||
import { RunButton } from './RunButton';
|
import { RunButton } from './RunButton';
|
||||||
import { LiveTailControls } from './useLiveTailControls';
|
import { LiveTailControls } from './useLiveTailControls';
|
||||||
import { getExploreDatasources } from './state/selectors';
|
|
||||||
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
|
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
|
||||||
import { cancelQueries, clearQueries, runQueries } from './state/query';
|
import { cancelQueries, clearQueries, runQueries } from './state/query';
|
||||||
|
|
||||||
@ -81,8 +80,8 @@ interface DispatchProps {
|
|||||||
type Props = StateProps & DispatchProps & OwnProps;
|
type Props = StateProps & DispatchProps & OwnProps;
|
||||||
|
|
||||||
export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||||
onChangeDatasource = async (option: { value: any }) => {
|
onChangeDatasource = async (dsSettings: DataSourceInstanceSettings) => {
|
||||||
this.props.changeDatasource(this.props.exploreId, option.value, { importQueries: true });
|
this.props.changeDatasource(this.props.exploreId, dsSettings.name, { importQueries: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
onClearAll = () => {
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
@ -214,8 +207,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
>
|
>
|
||||||
<DataSourcePicker
|
<DataSourcePicker
|
||||||
onChange={this.onChangeDatasource}
|
onChange={this.onChangeDatasource}
|
||||||
datasources={getExploreDatasources()}
|
current={this.props.datasourceName}
|
||||||
current={this.getSelectedDatasource()}
|
|
||||||
hideTextValue={showSmallDataSourcePicker}
|
hideTextValue={showSmallDataSourcePicker}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,14 +13,8 @@ describe('createSpanLinkFactory', () => {
|
|||||||
|
|
||||||
it('returns undefined if there is no loki data source', () => {
|
it('returns undefined if there is no loki data source', () => {
|
||||||
setDataSourceSrv({
|
setDataSourceSrv({
|
||||||
getExternal() {
|
getList() {
|
||||||
return [
|
return [];
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
id: 'not loki',
|
|
||||||
},
|
|
||||||
} as DataSourceInstanceSettings,
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
} as any);
|
} as any);
|
||||||
const splitOpenFn = jest.fn();
|
const splitOpenFn = jest.fn();
|
||||||
@ -30,7 +24,7 @@ describe('createSpanLinkFactory', () => {
|
|||||||
|
|
||||||
it('creates correct link', () => {
|
it('creates correct link', () => {
|
||||||
setDataSourceSrv({
|
setDataSourceSrv({
|
||||||
getExternal() {
|
getList() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'loki1',
|
name: 'loki1',
|
||||||
|
@ -15,9 +15,7 @@ export function createSpanLinkFactory(splitOpenFn: (options: { datasourceUid: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Right now just hardcoded for first loki DS we can find
|
// Right now just hardcoded for first loki DS we can find
|
||||||
const lokiDs = getDataSourceSrv()
|
const lokiDs = getDataSourceSrv().getList({ pluginId: 'loki' })[0];
|
||||||
.getExternal()
|
|
||||||
.find(ds => ds.meta.id === 'loki');
|
|
||||||
|
|
||||||
if (!lokiDs) {
|
if (!lokiDs) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -220,10 +220,12 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
|
|||||||
const dsSettings = options?.datasources || defaultDatasources;
|
const dsSettings = options?.datasources || defaultDatasources;
|
||||||
|
|
||||||
setDataSourceSrv({
|
setDataSourceSrv({
|
||||||
getExternal(): DataSourceInstanceSettings[] {
|
getList(): DataSourceInstanceSettings[] {
|
||||||
return dsSettings.map(d => d.settings);
|
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> {
|
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> {
|
||||||
return Promise.resolve((name ? dsSettings.find(d => d.api.name === name) : dsSettings[0])!.api);
|
return Promise.resolve((name ? dsSettings.find(d => d.api.name === name) : dsSettings[0])!.api);
|
||||||
},
|
},
|
||||||
|
@ -10,26 +10,9 @@ import {
|
|||||||
refreshExplore,
|
refreshExplore,
|
||||||
} from './explorePane';
|
} from './explorePane';
|
||||||
import { setQueriesAction } from './query';
|
import { setQueriesAction } from './query';
|
||||||
import * as DatasourceSrv from 'app/features/plugins/datasource_srv';
|
|
||||||
import { makeExplorePaneState, makeInitialUpdateState } from './utils';
|
import { makeExplorePaneState, makeInitialUpdateState } from './utils';
|
||||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
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)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('../../dashboard/services/TimeSrv', () => ({
|
jest.mock('../../dashboard/services/TimeSrv', () => ({
|
||||||
getTimeSrv: jest.fn().mockReturnValue({
|
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 setup = (updateOverides?: Partial<ExploreUpdateState>) => {
|
||||||
const exploreId = ExploreId.left;
|
const exploreId = ExploreId.left;
|
||||||
const containerWidth = 1920;
|
const containerWidth = 1920;
|
||||||
|
@ -32,7 +32,7 @@ import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
|
|||||||
import { runQueries, setQueriesAction } from './query';
|
import { runQueries, setQueriesAction } from './query';
|
||||||
import { updateTime } from './time';
|
import { updateTime } from './time';
|
||||||
import { toRawTimeRange } from '../utils/time';
|
import { toRawTimeRange } from '../utils/time';
|
||||||
import { getExploreDatasources } from './selectors';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Actions and Payloads
|
// Actions and Payloads
|
||||||
@ -131,7 +131,7 @@ export function initializeExplore(
|
|||||||
originPanelId?: number | null
|
originPanelId?: number | null
|
||||||
): ThunkResult<void> {
|
): ThunkResult<void> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const exploreDatasources = getExploreDatasources();
|
const exploreDatasources = getDataSourceSrv().getList();
|
||||||
let instance = undefined;
|
let instance = undefined;
|
||||||
let history: HistoryItem[] = [];
|
let history: HistoryItem[] = [];
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { ExploreItemState } from 'app/types';
|
import { ExploreItemState } from 'app/types';
|
||||||
import { filterLogLevels, dedupLogRows } from 'app/core/logs_model';
|
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 logsRowsSelector = (state: ExploreItemState) => state.logsResult && state.logsResult.rows;
|
||||||
const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels;
|
const hiddenLogLevelsSelector = (state: ExploreItemState) => state.hiddenLogLevels;
|
||||||
@ -19,16 +17,3 @@ export const deduplicatedRowsSelector = createSelector(
|
|||||||
return dedupLogRows(filteredRows, dedupStrategy);
|
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,
|
Legend,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
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 { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers';
|
||||||
import { validateTitle, validateUid } from '../utils/validation';
|
import { validateTitle, validateUid } from '../utils/validation';
|
||||||
|
|
||||||
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState' | 'watch'> {
|
interface Props extends Omit<FormAPI<ImportDashboardDTO>, 'formState'> {
|
||||||
uidReset: boolean;
|
uidReset: boolean;
|
||||||
inputs: DashboardInputs;
|
inputs: DashboardInputs;
|
||||||
initialFolderId: number;
|
initialFolderId: number;
|
||||||
@ -36,8 +36,10 @@ export const ImportDashboardForm: FC<Props> = ({
|
|||||||
onUidReset,
|
onUidReset,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
watch,
|
||||||
}) => {
|
}) => {
|
||||||
const [isSubmitted, setSubmitted] = useState(false);
|
const [isSubmitted, setSubmitted] = useState(false);
|
||||||
|
const watchDataSources = watch('dataSources');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This useEffect is needed for overwriting a dashboard. It
|
This useEffect is needed for overwriting a dashboard. It
|
||||||
@ -96,6 +98,7 @@ export const ImportDashboardForm: FC<Props> = ({
|
|||||||
{inputs.dataSources &&
|
{inputs.dataSources &&
|
||||||
inputs.dataSources.map((input: DataSourceInput, index: number) => {
|
inputs.dataSources.map((input: DataSourceInput, index: number) => {
|
||||||
const dataSourceOption = `dataSources[${index}]`;
|
const dataSourceOption = `dataSources[${index}]`;
|
||||||
|
const current = watchDataSources ?? [];
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
label={input.label}
|
label={input.label}
|
||||||
@ -105,8 +108,10 @@ export const ImportDashboardForm: FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
as={DataSourcePicker}
|
as={DataSourcePicker}
|
||||||
|
noDefault={true}
|
||||||
|
pluginId={input.pluginId}
|
||||||
name={`${dataSourceOption}`}
|
name={`${dataSourceOption}`}
|
||||||
datasources={input.options}
|
current={current[index]?.name}
|
||||||
control={control}
|
control={control}
|
||||||
placeholder={input.info}
|
placeholder={input.info}
|
||||||
rules={{ required: true }}
|
rules={{ required: true }}
|
||||||
|
@ -87,7 +87,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
|||||||
validateFieldsOnMount={['title', 'uid']}
|
validateFieldsOnMount={['title', 'uid']}
|
||||||
validateOn="onChange"
|
validateOn="onChange"
|
||||||
>
|
>
|
||||||
{({ register, errors, control, getValues }) => (
|
{({ register, errors, control, watch, getValues }) => (
|
||||||
<ImportDashboardForm
|
<ImportDashboardForm
|
||||||
register={register}
|
register={register}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
@ -98,6 +98,7 @@ class ImportDashboardOverviewUnConnected extends PureComponent<Props, State> {
|
|||||||
onCancel={this.onCancel}
|
onCancel={this.onCancel}
|
||||||
onUidReset={this.onUidReset}
|
onUidReset={this.onUidReset}
|
||||||
onSubmit={this.onSubmit}
|
onSubmit={this.onSubmit}
|
||||||
|
watch={watch}
|
||||||
initialFolderId={folder.id}
|
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 { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import config from 'app/core/config';
|
|
||||||
import {
|
import {
|
||||||
clearDashboard,
|
clearDashboard,
|
||||||
setInputs,
|
setInputs,
|
||||||
@ -13,6 +12,7 @@ import { updateLocation } from 'app/core/actions';
|
|||||||
import { ThunkResult, FolderInfo, DashboardDTO, DashboardDataDTO } from 'app/types';
|
import { ThunkResult, FolderInfo, DashboardDTO, DashboardDataDTO } from 'app/types';
|
||||||
import { appEvents } from '../../../core/core';
|
import { appEvents } from '../../../core/core';
|
||||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
export function fetchGcomDashboard(id: string): ThunkResult<void> {
|
export function fetchGcomDashboard(id: string): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
@ -73,13 +73,13 @@ export function importDashboard(importDashboardForm: ImportDashboardDTO): ThunkR
|
|||||||
const inputs = getState().importDashboard.inputs;
|
const inputs = getState().importDashboard.inputs;
|
||||||
|
|
||||||
let inputsToPersist = [] as any[];
|
let inputsToPersist = [] as any[];
|
||||||
importDashboardForm.dataSources?.forEach((dataSource: DataSourceSelectItem, index: number) => {
|
importDashboardForm.dataSources?.forEach((dataSource: DataSourceInstanceSettings, index: number) => {
|
||||||
const input = inputs.dataSources[index];
|
const input = inputs.dataSources[index];
|
||||||
inputsToPersist.push({
|
inputsToPersist.push({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
type: input.type,
|
type: input.type,
|
||||||
pluginId: input.pluginId,
|
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 getDataSourceOptions = (input: { pluginId: string; pluginName: string }, inputModel: any) => {
|
||||||
const sources = Object.values(config.datasources).filter(
|
const sources = getDataSourceSrv().getList({ pluginId: input.pluginId });
|
||||||
(val: DataSourceInstanceSettings) => val.type === input.pluginId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sources.length === 0) {
|
if (sources.length === 0) {
|
||||||
inputModel.info = 'No data sources of type ' + input.pluginName + ' found';
|
inputModel.info = 'No data sources of type ' + input.pluginName + ' found';
|
||||||
} else if (!inputModel.info) {
|
} else if (!inputModel.info) {
|
||||||
inputModel.info = 'Select a ' + input.pluginName + ' data source';
|
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) {
|
export function moveDashboards(dashboardUids: string[], toFolder: FolderInfo) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { DataSourceSelectItem } from '@grafana/data';
|
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||||
|
|
||||||
export enum DashboardSource {
|
export enum DashboardSource {
|
||||||
Gcom = 0,
|
Gcom = 0,
|
||||||
@ -11,7 +11,7 @@ export interface ImportDashboardDTO {
|
|||||||
uid: string;
|
uid: string;
|
||||||
gnetId: string;
|
gnetId: string;
|
||||||
constants: string[];
|
constants: string[];
|
||||||
dataSources: DataSourceSelectItem[];
|
dataSources: DataSourceInstanceSettings[];
|
||||||
folder: { id: number; title?: string };
|
folder: { id: number; title?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,6 @@ export interface DashboardInput {
|
|||||||
|
|
||||||
export interface DataSourceInput extends DashboardInput {
|
export interface DataSourceInput extends DashboardInput {
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
options: DataSourceSelectItem[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardInputs {
|
export interface DashboardInputs {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import { importDataSourcePlugin } from './plugin_loader';
|
import { importDataSourcePlugin } from './plugin_loader';
|
||||||
import {
|
import {
|
||||||
|
GetDataSourceListFilters,
|
||||||
DataSourceSrv as DataSourceService,
|
DataSourceSrv as DataSourceService,
|
||||||
getDataSourceSrv as getDataSourceService,
|
getDataSourceSrv as getDataSourceService,
|
||||||
TemplateSrv,
|
TemplateSrv,
|
||||||
@ -15,6 +15,7 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
|||||||
// Pretend Datasource
|
// Pretend Datasource
|
||||||
import { expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
import { expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
||||||
import { DataSourceVariableModel } from '../variables/types';
|
import { DataSourceVariableModel } from '../variables/types';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
export class DatasourceSrv implements DataSourceService {
|
export class DatasourceSrv implements DataSourceService {
|
||||||
private datasources: Record<string, DataSourceApi> = {};
|
private datasources: Record<string, DataSourceApi> = {};
|
||||||
@ -49,6 +50,20 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
return this.settingsMapByName[this.defaultName];
|
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];
|
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
|
// Interpolation here is to support template variable in data source selection
|
||||||
nameOrUid = this.templateSrv.replace(nameOrUid, scopedVars, (value: any[]) => {
|
nameOrUid = this.templateSrv.replace(nameOrUid, scopedVars, variableInterpolation);
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value[0];
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nameOrUid === 'default') {
|
if (nameOrUid === 'default') {
|
||||||
return this.get(this.defaultName);
|
return this.get(this.defaultName);
|
||||||
@ -130,88 +140,109 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
return Object.values(this.settingsMapByName);
|
return Object.values(this.settingsMapByName);
|
||||||
}
|
}
|
||||||
|
|
||||||
getExternal(): DataSourceInstanceSettings[] {
|
getList(filters: GetDataSourceListFilters = {}): DataSourceInstanceSettings[] {
|
||||||
const datasources = this.getAll().filter(ds => !ds.meta.builtIn);
|
const base = Object.values(this.settingsMapByName).filter(x => {
|
||||||
return sortBy(datasources, ['name']);
|
if (x.meta.id === 'grafana' || x.meta.id === 'mixed' || x.meta.id === 'dashboard') {
|
||||||
}
|
return false;
|
||||||
|
|
||||||
getAnnotationSources() {
|
|
||||||
const sources: any[] = [];
|
|
||||||
|
|
||||||
this.addDataSourceVariables(sources);
|
|
||||||
|
|
||||||
Object.values(this.settingsMapByName).forEach(value => {
|
|
||||||
if (value.meta?.annotations) {
|
|
||||||
sources.push(value);
|
|
||||||
}
|
}
|
||||||
|
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 }) {
|
if (dsSettings) {
|
||||||
const metricSources: DataSourceSelectItem[] = [];
|
const key = `$\{${variable.name}\}`;
|
||||||
|
base.push({
|
||||||
Object.entries(this.settingsMapByName).forEach(([key, value]) => {
|
...dsSettings,
|
||||||
if (value.meta?.metrics) {
|
name: key,
|
||||||
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 (!options || !options.skipVariables) {
|
|
||||||
this.addDataSourceVariables(metricSources);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
metricSources.sort((a, b) => {
|
const sorted = base.sort((a, b) => {
|
||||||
if (a.sort.toLowerCase() > b.sort.toLowerCase()) {
|
if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (a.sort.toLowerCase() < b.sort.toLowerCase()) {
|
if (a.name.toLowerCase() < b.name.toLowerCase()) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
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
|
* @deprecated use getList
|
||||||
this.templateSrv
|
* */
|
||||||
.getVariables()
|
getExternal(): DataSourceInstanceSettings[] {
|
||||||
.filter(variable => variable.type === 'datasource')
|
return this.getList();
|
||||||
.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
|
||||||
|
* */
|
||||||
|
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 => {
|
export const getDatasourceSrv = (): DatasourceSrv => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'app/features/plugins/datasource_srv';
|
import 'app/features/plugins/datasource_srv';
|
||||||
import { DatasourceSrv } from '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'
|
// Datasource variable $datasource with current value 'BBB'
|
||||||
const templateSrv: any = {
|
const templateSrv: any = {
|
||||||
@ -13,7 +13,9 @@ const templateSrv: any = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
replace: (v: string) => v,
|
replace: (v: string) => {
|
||||||
|
return v.replace('${datasource}', 'BBB');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class TestDataSource {
|
class TestDataSource {
|
||||||
@ -27,120 +29,184 @@ jest.mock('../plugin_loader', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('datasource_srv', () => {
|
describe('datasource_srv', () => {
|
||||||
const _datasourceSrv = new DatasourceSrv({} as any, {} as any, templateSrv);
|
const dataSourceSrv = new DatasourceSrv({} as any, {} as any, templateSrv);
|
||||||
const datasources = {
|
const dataSourceInit = {
|
||||||
buildIn: {
|
mmm: {
|
||||||
id: 1,
|
type: 'test-db',
|
||||||
uid: '1',
|
name: 'mmm',
|
||||||
type: 'b',
|
uid: 'uid-code-mmm',
|
||||||
name: 'buildIn',
|
meta: { metrics: true, annotations: true } as any,
|
||||||
meta: { builtIn: true } as DataSourcePluginMeta,
|
|
||||||
jsonData: {},
|
|
||||||
},
|
},
|
||||||
external1: {
|
'-- Grafana --': {
|
||||||
id: 2,
|
type: 'grafana',
|
||||||
uid: '2',
|
name: '-- Grafana --',
|
||||||
type: 'e',
|
meta: { builtIn: true, metrics: true, id: 'grafana' },
|
||||||
name: 'external1',
|
|
||||||
meta: { builtIn: false } as DataSourcePluginMeta,
|
|
||||||
jsonData: {},
|
|
||||||
},
|
},
|
||||||
external2: {
|
'-- Dashboard --': {
|
||||||
id: 3,
|
type: 'dashboard',
|
||||||
uid: '3',
|
name: '-- Dashboard --',
|
||||||
type: 'e2',
|
meta: { builtIn: true, metrics: true, id: 'dashboard' },
|
||||||
name: 'external2',
|
},
|
||||||
meta: {} as PluginMeta,
|
'-- Mixed --': {
|
||||||
jsonData: {},
|
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(() => {
|
describe('Given a list of data sources', () => {
|
||||||
_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;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
_datasourceSrv.init(
|
dataSourceSrv.init(dataSourceInit as any, 'BBB');
|
||||||
{
|
|
||||||
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({});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a list of sources sorted case insensitively with builtin sources last', () => {
|
describe('when getting data source class instance', () => {
|
||||||
expect(metricSources[1].name).toBe('aaa');
|
it('should load plugin and create instance and set meta', async () => {
|
||||||
expect(metricSources[2].name).toBe('BBB');
|
const ds = (await dataSourceSrv.get('mmm')) as any;
|
||||||
expect(metricSources[3].name).toBe('default');
|
expect(ds.meta).toBe(dataSourceInit.mmm.meta);
|
||||||
expect(metricSources[4].name).toBe('mmm');
|
expect(ds.instanceSettings).toBe(dataSourceInit.mmm);
|
||||||
expect(metricSources[5].name).toBe('ZZZ');
|
|
||||||
expect(metricSources[6].name).toBe('--Grafana--');
|
// validate that it caches instance
|
||||||
expect(metricSources[7].name).toBe('--Mixed--');
|
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', () => {
|
describe('when getting instance settings', () => {
|
||||||
expect(metricSources[3].name).toBe('default');
|
it('should work by name or uid', () => {
|
||||||
expect(metricSources[3].sort).toBe('BBB');
|
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', () => {
|
describe('when getting external metric sources', () => {
|
||||||
expect(metricSources[0].name).toBe('$datasource');
|
it('should return list of explore sources', () => {
|
||||||
expect(metricSources[0].sort).toBe('$datasource');
|
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,
|
TimeRange,
|
||||||
toLegacyResponseData,
|
toLegacyResponseData,
|
||||||
EventBusExtended,
|
EventBusExtended,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { QueryEditorRowTitle } from './QueryEditorRowTitle';
|
import { QueryEditorRowTitle } from './QueryEditorRowTitle';
|
||||||
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||||
@ -27,8 +28,7 @@ import { PanelModel } from 'app/features/dashboard/state';
|
|||||||
interface Props {
|
interface Props {
|
||||||
data: PanelData;
|
data: PanelData;
|
||||||
query: DataQuery;
|
query: DataQuery;
|
||||||
dataSourceValue: string | null;
|
dsSettings: DataSourceInstanceSettings;
|
||||||
inMixedMode?: boolean;
|
|
||||||
id: string;
|
id: string;
|
||||||
index: number;
|
index: number;
|
||||||
onAddQuery: (query?: DataQuery) => void;
|
onAddQuery: (query?: DataQuery) => void;
|
||||||
@ -38,7 +38,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
loadedDataSourceValue: string | null | undefined;
|
loadedDataSourceIdentifier?: string | null;
|
||||||
datasource: DataSourceApi | null;
|
datasource: DataSourceApi | null;
|
||||||
hasTextEditMode: boolean;
|
hasTextEditMode: boolean;
|
||||||
data?: PanelData;
|
data?: PanelData;
|
||||||
@ -52,7 +52,6 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
datasource: null,
|
datasource: null,
|
||||||
loadedDataSourceValue: undefined,
|
|
||||||
hasTextEditMode: false,
|
hasTextEditMode: false,
|
||||||
data: undefined,
|
data: undefined,
|
||||||
isOpen: true,
|
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() {
|
async loadDatasource() {
|
||||||
const { query, dataSourceValue } = this.props;
|
|
||||||
const dataSourceSrv = getDatasourceSrv();
|
const dataSourceSrv = getDatasourceSrv();
|
||||||
let datasource;
|
let datasource: DataSourceApi;
|
||||||
|
const dataSourceIdentifier = this.getQueryDataSourceIdentifier();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const datasourceName = dataSourceValue || query.datasource;
|
datasource = await dataSourceSrv.get(dataSourceIdentifier);
|
||||||
datasource = await dataSourceSrv.get(datasourceName);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
datasource = await dataSourceSrv.get();
|
datasource = await dataSourceSrv.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
datasource,
|
datasource,
|
||||||
loadedDataSourceValue: this.props.dataSourceValue,
|
loadedDataSourceIdentifier: dataSourceIdentifier,
|
||||||
hasTextEditMode: _.has(datasource, 'components.QueryCtrl.prototype.toggleEditorMode'),
|
hasTextEditMode: _.has(datasource, 'components.QueryCtrl.prototype.toggleEditorMode'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
const { loadedDataSourceValue } = this.state;
|
const { datasource, loadedDataSourceIdentifier } = this.state;
|
||||||
const { data, query } = this.props;
|
const { data, query } = this.props;
|
||||||
|
|
||||||
if (data !== prevProps.data) {
|
if (data !== prevProps.data) {
|
||||||
@ -125,7 +128,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if we need to load another datasource
|
// check if we need to load another datasource
|
||||||
if (loadedDataSourceValue !== this.props.dataSourceValue) {
|
if (datasource && loadedDataSourceIdentifier !== this.getQueryDataSourceIdentifier()) {
|
||||||
if (this.angularQueryEditor) {
|
if (this.angularQueryEditor) {
|
||||||
this.angularQueryEditor.destroy();
|
this.angularQueryEditor.destroy();
|
||||||
this.angularQueryEditor = null;
|
this.angularQueryEditor = null;
|
||||||
@ -137,6 +140,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
if (!this.element || this.angularQueryEditor) {
|
if (!this.element || this.angularQueryEditor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderAngularQueryEditor();
|
this.renderAngularQueryEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,14 +263,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderTitle = (props: { isOpen: boolean; openRow: () => void }) => {
|
renderTitle = (props: { isOpen: boolean; openRow: () => void }) => {
|
||||||
const { query, inMixedMode } = this.props;
|
const { query, dsSettings } = this.props;
|
||||||
const { datasource } = this.state;
|
const { datasource } = this.state;
|
||||||
const isDisabled = query.hide;
|
const isDisabled = query.hide;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryEditorRowTitle
|
<QueryEditorRowTitle
|
||||||
query={query}
|
query={query}
|
||||||
inMixedMode={inMixedMode}
|
inMixedMode={dsSettings.meta.mixed}
|
||||||
datasource={datasource!}
|
datasource={datasource!}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={e => this.onToggleEditMode(e, props)}
|
onClick={e => this.onToggleEditMode(e, props)}
|
||||||
|
@ -2,14 +2,14 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataQuery, PanelData, DataSourceSelectItem } from '@grafana/data';
|
import { DataQuery, DataSourceInstanceSettings, PanelData } from '@grafana/data';
|
||||||
import { QueryEditorRow } from './QueryEditorRow';
|
import { QueryEditorRow } from './QueryEditorRow';
|
||||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// The query configuration
|
// The query configuration
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
datasource: DataSourceSelectItem;
|
dsSettings: DataSourceInstanceSettings;
|
||||||
|
|
||||||
// Query editing
|
// Query editing
|
||||||
onQueriesChange: (queries: DataQuery[]) => void;
|
onQueriesChange: (queries: DataQuery[]) => void;
|
||||||
@ -67,7 +67,7 @@ export class QueryEditorRows extends PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props } = this;
|
const { dsSettings, data, queries } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={this.onDragEnd}>
|
<DragDropContext onDragEnd={this.onDragEnd}>
|
||||||
@ -75,19 +75,18 @@ export class QueryEditorRows extends PureComponent<Props> {
|
|||||||
{provided => {
|
{provided => {
|
||||||
return (
|
return (
|
||||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
{props.queries.map((query, index) => (
|
{queries.map((query, index) => (
|
||||||
<QueryEditorRow
|
<QueryEditorRow
|
||||||
dataSourceValue={query.datasource || props.datasource.value}
|
dsSettings={dsSettings}
|
||||||
id={query.refId}
|
id={query.refId}
|
||||||
index={index}
|
index={index}
|
||||||
key={query.refId}
|
key={query.refId}
|
||||||
data={props.data}
|
data={data}
|
||||||
query={query}
|
query={query}
|
||||||
onChange={query => this.onChangeQuery(query, index)}
|
onChange={query => this.onChangeQuery(query, index)}
|
||||||
onRemoveQuery={this.onRemoveQuery}
|
onRemoveQuery={this.onRemoveQuery}
|
||||||
onAddQuery={this.props.onAddQuery}
|
onAddQuery={this.props.onAddQuery}
|
||||||
onRunQuery={this.props.onRunQueries}
|
onRunQuery={this.props.onRunQueries}
|
||||||
inMixedMode={props.datasource.meta.mixed}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
@ -2,21 +2,20 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
// Components
|
// Components
|
||||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
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 { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { QueryEditorRows } from './QueryEditorRows';
|
import { QueryEditorRows } from './QueryEditorRows';
|
||||||
// Services
|
// Services
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataSourceSelectItem,
|
|
||||||
DefaultTimeRange,
|
DefaultTimeRange,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
PanelData,
|
PanelData,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||||
import { addQuery } from 'app/core/utils/query';
|
import { addQuery } from 'app/core/utils/query';
|
||||||
@ -30,20 +29,15 @@ import { css } from 'emotion';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
queryRunner: PanelQueryRunner;
|
queryRunner: PanelQueryRunner;
|
||||||
queries: DataQuery[];
|
|
||||||
dataSourceName: string | null;
|
|
||||||
options: QueryGroupOptions;
|
options: QueryGroupOptions;
|
||||||
onOpenQueryInspector?: () => void;
|
onOpenQueryInspector?: () => void;
|
||||||
onRunQueries: () => void;
|
onRunQueries: () => void;
|
||||||
onQueriesChange: (queries: DataQuery[]) => void;
|
|
||||||
onDataSourceChange: (ds: DataSourceSelectItem, queries: DataQuery[]) => void;
|
|
||||||
onOptionsChange: (options: QueryGroupOptions) => void;
|
onOptionsChange: (options: QueryGroupOptions) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
dataSource?: DataSourceApi;
|
dataSource?: DataSourceApi;
|
||||||
dataSourceItem: DataSourceSelectItem;
|
dsSettings?: DataSourceInstanceSettings;
|
||||||
dataSourceError?: string;
|
|
||||||
helpContent: React.ReactNode;
|
helpContent: React.ReactNode;
|
||||||
isLoadingHelp: boolean;
|
isLoadingHelp: boolean;
|
||||||
isPickerOpen: boolean;
|
isPickerOpen: boolean;
|
||||||
@ -54,13 +48,12 @@ interface State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class QueryGroup extends PureComponent<Props, State> {
|
export class QueryGroup extends PureComponent<Props, State> {
|
||||||
datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
|
|
||||||
backendSrv = backendSrv;
|
backendSrv = backendSrv;
|
||||||
|
dataSourceSrv = getDataSourceSrv();
|
||||||
querySubscription: Unsubscribable | null;
|
querySubscription: Unsubscribable | null;
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
isLoadingHelp: false,
|
isLoadingHelp: false,
|
||||||
dataSourceItem: this.findCurrentDataSource(this.props.dataSourceName),
|
|
||||||
helpContent: null,
|
helpContent: null,
|
||||||
isPickerOpen: false,
|
isPickerOpen: false,
|
||||||
isAddingMixed: false,
|
isAddingMixed: false,
|
||||||
@ -74,19 +67,18 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const { queryRunner, dataSourceName: datasourceName } = this.props;
|
const { queryRunner, options } = this.props;
|
||||||
|
|
||||||
this.querySubscription = queryRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({
|
this.querySubscription = queryRunner.getData({ withTransforms: false, withFieldConfig: false }).subscribe({
|
||||||
next: (data: PanelData) => this.onPanelDataUpdate(data),
|
next: (data: PanelData) => this.onPanelDataUpdate(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ds = await getDataSourceSrv().get(datasourceName);
|
const ds = await this.dataSourceSrv.get(options.dataSource.name);
|
||||||
this.setState({ dataSource: ds });
|
const dsSettings = this.dataSourceSrv.getInstanceSettings(options.dataSource.name);
|
||||||
|
this.setState({ dataSource: ds, dsSettings });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const ds = await getDataSourceSrv().get();
|
console.log('failed to load data source', error);
|
||||||
const dataSourceItem = this.findCurrentDataSource(ds.name);
|
|
||||||
this.setState({ dataSource: ds, dataSourceError: error?.message, dataSourceItem });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,62 +93,73 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
this.setState({ data });
|
this.setState({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
findCurrentDataSource(dataSourceName: string | null): DataSourceSelectItem {
|
onChangeDataSource = async (newSettings: DataSourceInstanceSettings) => {
|
||||||
return this.datasources.find(datasource => datasource.value === dataSourceName) || this.datasources[0];
|
let { queries } = this.props.options;
|
||||||
}
|
const { dsSettings } = this.state;
|
||||||
|
|
||||||
onChangeDataSource = async (newDsItem: DataSourceSelectItem) => {
|
|
||||||
let { queries } = this.props;
|
|
||||||
const { dataSourceItem } = this.state;
|
|
||||||
|
|
||||||
// switching to mixed
|
// switching to mixed
|
||||||
if (newDsItem.meta.mixed) {
|
if (newSettings.meta.mixed) {
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
if (query.datasource !== ExpressionDatasourceID) {
|
if (query.datasource !== ExpressionDatasourceID) {
|
||||||
query.datasource = query.datasource;
|
query.datasource = dsSettings?.name;
|
||||||
if (!query.datasource) {
|
if (!query.datasource) {
|
||||||
query.datasource = config.defaultDatasource;
|
query.datasource = config.defaultDatasource;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (dataSourceItem) {
|
} else if (dsSettings) {
|
||||||
// if switching from mixed
|
// if switching from mixed
|
||||||
if (dataSourceItem.meta.mixed) {
|
if (dsSettings.meta.mixed) {
|
||||||
// Remove the explicit datasource
|
// Remove the explicit datasource
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
if (query.datasource !== ExpressionDatasourceID) {
|
if (query.datasource !== ExpressionDatasourceID) {
|
||||||
delete query.datasource;
|
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
|
// we are changing data source type, clear queries
|
||||||
queries = [{ refId: 'A' }];
|
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({
|
this.setState({
|
||||||
dataSourceItem: newDsItem,
|
|
||||||
dataSource: dataSource,
|
dataSource: dataSource,
|
||||||
dataSourceError: undefined,
|
dsSettings: newSettings,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onAddQueryClick = () => {
|
onAddQueryClick = () => {
|
||||||
if (this.state.dataSourceItem.meta.mixed) {
|
if (this.state.dsSettings?.meta.mixed) {
|
||||||
this.setState({ isAddingMixed: true });
|
this.setState({ isAddingMixed: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onQueriesChange(addQuery(this.props.queries));
|
this.onChange({ queries: addQuery(this.props.options.queries) });
|
||||||
this.onScrollBottom();
|
this.onScrollBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onChange(changedProps: Partial<QueryGroupOptions>) {
|
||||||
|
this.props.onOptionsChange({
|
||||||
|
...this.props.options,
|
||||||
|
...changedProps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onAddExpressionClick = () => {
|
onAddExpressionClick = () => {
|
||||||
this.props.onQueriesChange(addQuery(this.props.queries, expressionDatasource.newQuery()));
|
this.onChange({
|
||||||
|
queries: addQuery(this.props.options.queries, expressionDatasource.newQuery()),
|
||||||
|
});
|
||||||
this.onScrollBottom();
|
this.onScrollBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -166,45 +169,51 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
renderTopSection(styles: QueriesTabStyls) {
|
renderTopSection(styles: QueriesTabStyls) {
|
||||||
const { onOpenQueryInspector, options, onOptionsChange } = this.props;
|
const { onOpenQueryInspector, options, onOptionsChange } = this.props;
|
||||||
const { dataSourceItem, dataSource, dataSourceError, data } = this.state;
|
const { dataSource, data } = this.state;
|
||||||
|
|
||||||
if (!dataSource) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.dataSourceRow}>
|
<div className={styles.dataSourceRow}>
|
||||||
<div className={styles.dataSourceRowItem}>
|
<div className={styles.dataSourceRowItem}>
|
||||||
<Field invalid={!!dataSourceError} error={dataSourceError}>
|
<DataSourcePicker
|
||||||
<DataSourcePicker
|
onChange={this.onChangeDataSource}
|
||||||
datasources={this.datasources}
|
current={options.dataSource.name}
|
||||||
onChange={this.onChangeDataSource}
|
metrics={true}
|
||||||
current={dataSourceItem}
|
mixed={true}
|
||||||
/>
|
dashboard={true}
|
||||||
</Field>
|
variables={true}
|
||||||
</div>
|
|
||||||
<div className={styles.dataSourceRowItem}>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
icon="question-circle"
|
|
||||||
title="Open data source help"
|
|
||||||
onClick={this.onOpenHelp}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.dataSourceRowItemOptions}>
|
{dataSource && (
|
||||||
<QueryGroupOptionsEditor options={options} dataSource={dataSource} data={data} onChange={onOptionsChange} />
|
<>
|
||||||
</div>
|
<div className={styles.dataSourceRowItem}>
|
||||||
{onOpenQueryInspector && (
|
<Button
|
||||||
<div className={styles.dataSourceRowItem}>
|
variant="secondary"
|
||||||
<Button
|
icon="question-circle"
|
||||||
variant="secondary"
|
title="Open data source help"
|
||||||
onClick={onOpenQueryInspector}
|
onClick={this.onOpenHelp}
|
||||||
aria-label={selectors.components.QueryTab.queryInspectorButton}
|
/>
|
||||||
>
|
</div>
|
||||||
Query inspector
|
<div className={styles.dataSourceRowItemOptions}>
|
||||||
</Button>
|
<QueryGroupOptionsEditor
|
||||||
</div>
|
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>
|
||||||
</div>
|
</div>
|
||||||
@ -220,13 +229,9 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderMixedPicker = () => {
|
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 (
|
return (
|
||||||
<DataSourcePicker
|
<DataSourcePicker
|
||||||
datasources={filteredDsList}
|
mixed={false}
|
||||||
onChange={this.onAddMixedQuery}
|
onChange={this.onAddMixedQuery}
|
||||||
current={null}
|
current={null}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
@ -246,8 +251,8 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onAddQuery = (query: Partial<DataQuery>) => {
|
onAddQuery = (query: Partial<DataQuery>) => {
|
||||||
const { queries, onQueriesChange } = this.props;
|
const { queries } = this.props.options;
|
||||||
onQueriesChange(addQuery(queries, query));
|
this.onChange({ queries: addQuery(queries, query) });
|
||||||
this.onScrollBottom();
|
this.onScrollBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -256,20 +261,24 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
this.setState({ scrollTop: target.scrollTop });
|
this.setState({ scrollTop: target.scrollTop });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderQueries() {
|
onQueriesChange = (queries: DataQuery[]) => {
|
||||||
const { onQueriesChange, queries, onRunQueries } = this.props;
|
this.onChange({ queries });
|
||||||
const { dataSourceItem, data } = this.state;
|
};
|
||||||
|
|
||||||
if (isSharedDashboardQuery(dataSourceItem.name)) {
|
renderQueries(dsSettings: DataSourceInstanceSettings) {
|
||||||
return <DashboardQueryEditor queries={queries} panelData={data} onChange={onQueriesChange} />;
|
const { options, onRunQueries } = this.props;
|
||||||
|
const { data } = this.state;
|
||||||
|
|
||||||
|
if (isSharedDashboardQuery(dsSettings.name)) {
|
||||||
|
return <DashboardQueryEditor queries={options.queries} panelData={data} onChange={this.onQueriesChange} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-label={selectors.components.QueryTab.content}>
|
<div aria-label={selectors.components.QueryTab.content}>
|
||||||
<QueryEditorRows
|
<QueryEditorRows
|
||||||
queries={queries}
|
queries={options.queries}
|
||||||
datasource={dataSourceItem}
|
dsSettings={dsSettings}
|
||||||
onQueriesChange={onQueriesChange}
|
onQueriesChange={this.onQueriesChange}
|
||||||
onAddQuery={this.onAddQuery}
|
onAddQuery={this.onAddQuery}
|
||||||
onRunQueries={onRunQueries}
|
onRunQueries={onRunQueries}
|
||||||
data={data}
|
data={data}
|
||||||
@ -278,9 +287,9 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAddQueryRow() {
|
renderAddQueryRow(dsSettings: DataSourceInstanceSettings) {
|
||||||
const { dataSourceItem, isAddingMixed } = this.state;
|
const { isAddingMixed } = this.state;
|
||||||
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(dataSourceItem.name));
|
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(dsSettings.name));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing="md" align="flex-start">
|
<HorizontalGroup spacing="md" align="flex-start">
|
||||||
@ -305,7 +314,7 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { scrollTop, isHelpOpen } = this.state;
|
const { scrollTop, isHelpOpen, dsSettings } = this.state;
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -318,13 +327,16 @@ export class QueryGroup extends PureComponent<Props, State> {
|
|||||||
>
|
>
|
||||||
<div className={styles.innerWrapper}>
|
<div className={styles.innerWrapper}>
|
||||||
{this.renderTopSection(styles)}
|
{this.renderTopSection(styles)}
|
||||||
<div className={styles.queriesWrapper}>{this.renderQueries()}</div>
|
{dsSettings && (
|
||||||
{this.renderAddQueryRow()}
|
<>
|
||||||
|
<div className={styles.queriesWrapper}>{this.renderQueries(dsSettings)}</div>
|
||||||
{isHelpOpen && (
|
{this.renderAddQueryRow(dsSettings)}
|
||||||
<Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}>
|
{isHelpOpen && (
|
||||||
<PluginHelp plugin={this.state.dataSourceItem.meta} type="query_help" />
|
<Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}>
|
||||||
</Modal>
|
<PluginHelp plugin={dsSettings.meta} type="query_help" />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
|
import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { rangeUtil, PanelData, DataSourceApi } from '@grafana/data';
|
import { rangeUtil, PanelData, DataSourceApi, DataQuery } from '@grafana/data';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { Switch, Input, InlineField, InlineFormLabel, stylesFactory } from '@grafana/ui';
|
import { Switch, Input, InlineField, InlineFormLabel, stylesFactory } from '@grafana/ui';
|
||||||
@ -13,6 +13,8 @@ import { config } from 'app/core/config';
|
|||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
|
||||||
export interface QueryGroupOptions {
|
export interface QueryGroupOptions {
|
||||||
|
queries: DataQuery[];
|
||||||
|
dataSource: QueryGroupDataSource;
|
||||||
maxDataPoints?: number | null;
|
maxDataPoints?: number | null;
|
||||||
minInterval?: string | null;
|
minInterval?: string | null;
|
||||||
cacheTimeout?: string | null;
|
cacheTimeout?: string | null;
|
||||||
@ -23,6 +25,12 @@ export interface QueryGroupOptions {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface QueryGroupDataSource {
|
||||||
|
name?: string | null;
|
||||||
|
uid?: string;
|
||||||
|
default?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
options: QueryGroupOptions;
|
options: QueryGroupOptions;
|
||||||
dataSource: DataSourceApi;
|
dataSource: DataSourceApi;
|
||||||
|
@ -1,12 +1,4 @@
|
|||||||
import {
|
import { ApplyFieldOverrideOptions, DataTransformerConfig, dateMath, FieldColorModeId, PanelData } from '@grafana/data';
|
||||||
ApplyFieldOverrideOptions,
|
|
||||||
DataQuery,
|
|
||||||
DataSourceSelectItem,
|
|
||||||
DataTransformerConfig,
|
|
||||||
dateMath,
|
|
||||||
FieldColorModeId,
|
|
||||||
PanelData,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { GraphNG, Table } from '@grafana/ui';
|
import { GraphNG, Table } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import React, { FC, useMemo, useState } from 'react';
|
import React, { FC, useMemo, useState } from 'react';
|
||||||
@ -16,43 +8,29 @@ import { QueryGroupOptions } from '../query/components/QueryGroupOptions';
|
|||||||
import { PanelQueryRunner } from '../query/state/PanelQueryRunner';
|
import { PanelQueryRunner } from '../query/state/PanelQueryRunner';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
queries: DataQuery[];
|
|
||||||
queryRunner: PanelQueryRunner;
|
queryRunner: PanelQueryRunner;
|
||||||
dataSourceName: string | null;
|
|
||||||
queryOptions: QueryGroupOptions;
|
queryOptions: QueryGroupOptions;
|
||||||
data?: PanelData;
|
data?: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TestStuffPage: FC = () => {
|
export const TestStuffPage: FC = () => {
|
||||||
const [state, setState] = useState<State>(getDefaultState());
|
const [state, setState] = useState<State>(getDefaultState());
|
||||||
const { queryOptions, queryRunner, queries, dataSourceName } = state;
|
const { queryOptions, queryRunner } = state;
|
||||||
|
|
||||||
const onDataSourceChange = (ds: DataSourceSelectItem, queries: DataQuery[]) => {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
dataSourceName: ds.value,
|
|
||||||
queries: queries,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRunQueries = () => {
|
const onRunQueries = () => {
|
||||||
const timeRange = { from: 'now-1h', to: 'now' };
|
const timeRange = { from: 'now-1h', to: 'now' };
|
||||||
|
|
||||||
queryRunner.run({
|
queryRunner.run({
|
||||||
queries,
|
queries: queryOptions.queries,
|
||||||
|
datasource: queryOptions.dataSource.name!,
|
||||||
timezone: 'browser',
|
timezone: 'browser',
|
||||||
datasource: dataSourceName,
|
|
||||||
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
|
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
|
||||||
maxDataPoints: queryOptions.maxDataPoints ?? 100,
|
maxDataPoints: queryOptions.maxDataPoints ?? 100,
|
||||||
minInterval: queryOptions.minInterval,
|
minInterval: queryOptions.minInterval,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onQueriesChange = (queries: DataQuery[]) => {
|
const onOptionsChange = (queryOptions: QueryGroupOptions) => {
|
||||||
setState({ ...state, queries: queries });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onQueryOptionsChange = (queryOptions: QueryGroupOptions) => {
|
|
||||||
setState({ ...state, queryOptions });
|
setState({ ...state, queryOptions });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,13 +46,9 @@ export const TestStuffPage: FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<QueryGroup
|
<QueryGroup
|
||||||
options={queryOptions}
|
options={queryOptions}
|
||||||
dataSourceName={dataSourceName}
|
|
||||||
queryRunner={queryRunner}
|
queryRunner={queryRunner}
|
||||||
queries={queries}
|
|
||||||
onDataSourceChange={onDataSourceChange}
|
|
||||||
onRunQueries={onRunQueries}
|
onRunQueries={onRunQueries}
|
||||||
onQueriesChange={onQueriesChange}
|
onOptionsChange={onOptionsChange}
|
||||||
onOptionsChange={onQueryOptionsChange}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -109,10 +83,12 @@ export function getDefaultState(): State {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
queries: [],
|
|
||||||
dataSourceName: 'gdev-testdata',
|
|
||||||
queryRunner: new PanelQueryRunner(dataConfig),
|
queryRunner: new PanelQueryRunner(dataConfig),
|
||||||
queryOptions: {
|
queryOptions: {
|
||||||
|
queries: [],
|
||||||
|
dataSource: {
|
||||||
|
name: 'gdev-testdata',
|
||||||
|
},
|
||||||
maxDataPoints: 100,
|
maxDataPoints: 100,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -478,6 +478,5 @@ function createDatasource(name: string, selectable = true): DataSourceSelectItem
|
|||||||
meta: {
|
meta: {
|
||||||
mixed: !selectable,
|
mixed: !selectable,
|
||||||
} as DataSourcePluginMeta,
|
} as DataSourcePluginMeta,
|
||||||
sort: '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -27,13 +27,11 @@ describe('data source actions', () => {
|
|||||||
name: 'first-name',
|
name: 'first-name',
|
||||||
value: 'first-value',
|
value: 'first-value',
|
||||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
||||||
sort: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'second-name',
|
name: 'second-name',
|
||||||
value: 'second-value',
|
value: 'second-value',
|
||||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
||||||
sort: '',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -80,13 +78,11 @@ describe('data source actions', () => {
|
|||||||
name: 'first-name',
|
name: 'first-name',
|
||||||
value: 'first-value',
|
value: 'first-value',
|
||||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
||||||
sort: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'second-name',
|
name: 'second-name',
|
||||||
value: 'second-value',
|
value: 'second-value',
|
||||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
||||||
sort: '',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -134,13 +130,11 @@ describe('data source actions', () => {
|
|||||||
name: 'first-name',
|
name: 'first-name',
|
||||||
value: 'first-value',
|
value: 'first-value',
|
||||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
||||||
sort: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'second-name',
|
name: 'second-name',
|
||||||
value: 'second-value',
|
value: 'second-value',
|
||||||
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
meta: getMockPlugin({ name: 'mock-data-name', id: 'mock-data-id' }),
|
||||||
sort: '',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'mixed-name',
|
name: 'mixed-name',
|
||||||
@ -150,7 +144,6 @@ describe('data source actions', () => {
|
|||||||
id: 'mixed-data-id',
|
id: 'mixed-data-id',
|
||||||
mixed: true,
|
mixed: true,
|
||||||
} as unknown) as DataSourcePluginMeta),
|
} as unknown) as DataSourcePluginMeta),
|
||||||
sort: '',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { FormEvent, PropsWithChildren, ReactElement, useCallback } from 'react';
|
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 { GrafanaTheme } from '@grafana/data';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
|
||||||
@ -38,16 +38,7 @@ export function VariableTextAreaField({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing="none">
|
<InlineField label={name} labelWidth={labelWidth ?? 12} tooltip={tooltip}>
|
||||||
<InlineField
|
|
||||||
label={name}
|
|
||||||
labelWidth={labelWidth ?? 12}
|
|
||||||
grow={false}
|
|
||||||
tooltip={tooltip}
|
|
||||||
className={styles.inlineFieldOverride}
|
|
||||||
>
|
|
||||||
<span hidden />
|
|
||||||
</InlineField>
|
|
||||||
<TextArea
|
<TextArea
|
||||||
rows={getLineCount(value)}
|
rows={getLineCount(value)}
|
||||||
value={value}
|
value={value}
|
||||||
@ -59,15 +50,12 @@ export function VariableTextAreaField({
|
|||||||
cols={width}
|
cols={width}
|
||||||
className={styles.textarea}
|
className={styles.textarea}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</InlineField>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme) {
|
function getStyles(theme: GrafanaTheme) {
|
||||||
return {
|
return {
|
||||||
inlineFieldOverride: css`
|
|
||||||
margin: 0;
|
|
||||||
`,
|
|
||||||
textarea: css`
|
textarea: css`
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
min-height: 32px;
|
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 { describe, expect } from '../../../../test/lib/common';
|
||||||
import { NEW_VARIABLE_ID } from '../state/types';
|
import { NEW_VARIABLE_ID } from '../state/types';
|
||||||
import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
|
import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
|
||||||
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
const setupTestContext = (options: Partial<Props>) => {
|
const setupTestContext = (options: Partial<Props>) => {
|
||||||
const defaults: Props = {
|
const defaults: Props = {
|
||||||
@ -21,7 +22,6 @@ const setupTestContext = (options: Partial<Props>) => {
|
|||||||
...initialVariableEditorState,
|
...initialVariableEditorState,
|
||||||
extended: {
|
extended: {
|
||||||
VariableQueryEditor: LegacyVariableQueryEditor,
|
VariableQueryEditor: LegacyVariableQueryEditor,
|
||||||
dataSources: [],
|
|
||||||
dataSource: ({} as unknown) as DataSourceApi,
|
dataSource: ({} as unknown) as DataSourceApi,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -34,6 +34,11 @@ const setupTestContext = (options: Partial<Props>) => {
|
|||||||
return { rerender, props };
|
return { rerender, props };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setDataSourceSrv({
|
||||||
|
getInstanceSettings: () => null,
|
||||||
|
getList: () => [],
|
||||||
|
} as any);
|
||||||
|
|
||||||
describe('QueryVariableEditor', () => {
|
describe('QueryVariableEditor', () => {
|
||||||
describe('when the component is mounted', () => {
|
describe('when the component is mounted', () => {
|
||||||
it('then it should call initQueryVariableEditor', () => {
|
it('then it should call initQueryVariableEditor', () => {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { ChangeEvent, PureComponent } from 'react';
|
import React, { ChangeEvent, PureComponent } from 'react';
|
||||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
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 { selectors } from '@grafana/e2e-selectors';
|
||||||
import { getTemplateSrv } from '@grafana/runtime';
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import { LoadingState, SelectableValue } from '@grafana/data';
|
import { DataSourceInstanceSettings, LoadingState, SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
|
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
|
||||||
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
|
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
|
||||||
@ -20,9 +20,9 @@ import { isLegacyQueryEditor, isQueryEditor } from '../guard';
|
|||||||
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
|
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
|
||||||
import { VariableTextField } from '../editor/VariableTextField';
|
import { VariableTextField } from '../editor/VariableTextField';
|
||||||
import { VariableSwitchField } from '../editor/VariableSwitchField';
|
import { VariableSwitchField } from '../editor/VariableSwitchField';
|
||||||
import { QueryVariableDatasourceSelect } from './QueryVariableDatasourceSelect';
|
|
||||||
import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
|
import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
|
||||||
import { QueryVariableSortSelect } from './QueryVariableSortSelect';
|
import { QueryVariableSortSelect } from './QueryVariableSortSelect';
|
||||||
|
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
|
|
||||||
export interface OwnProps extends VariableEditorProps<QueryVariableModel> {}
|
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: '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) => {
|
onLegacyQueryChange = async (query: any, definition: string) => {
|
||||||
@ -182,19 +185,19 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
|||||||
return (
|
return (
|
||||||
<VerticalGroup spacing="xs">
|
<VerticalGroup spacing="xs">
|
||||||
<VariableSectionHeader name="Query Options" />
|
<VariableSectionHeader name="Query Options" />
|
||||||
<VerticalGroup spacing="md">
|
<VerticalGroup spacing="lg">
|
||||||
<VerticalGroup spacing="none">
|
<VerticalGroup spacing="none">
|
||||||
<VerticalGroup spacing="xs">
|
<InlineFieldRow>
|
||||||
<InlineFieldRow>
|
<InlineField label="Data source" labelWidth={20}>
|
||||||
<QueryVariableDatasourceSelect
|
<DataSourcePicker
|
||||||
|
current={this.props.variable.datasource}
|
||||||
onChange={this.onDataSourceChange}
|
onChange={this.onDataSourceChange}
|
||||||
datasource={this.props.variable.datasource}
|
variables={true}
|
||||||
dataSources={this.props.editor.extended?.dataSources}
|
|
||||||
/>
|
/>
|
||||||
<QueryVariableRefreshSelect onChange={this.onRefreshChange} refresh={this.props.variable.refresh} />
|
</InlineField>
|
||||||
</InlineFieldRow>
|
<QueryVariableRefreshSelect onChange={this.onRefreshChange} refresh={this.props.variable.refresh} />
|
||||||
<div style={{ flexDirection: 'column' }}>{this.renderQueryEditor()}</div>
|
</InlineFieldRow>
|
||||||
</VerticalGroup>
|
<div style={{ flexDirection: 'column' }}>{this.renderQueryEditor()}</div>
|
||||||
<VariableTextField
|
<VariableTextField
|
||||||
value={this.state.regex ?? this.props.variable.regex}
|
value={this.state.regex ?? this.props.variable.regex}
|
||||||
name="Regex"
|
name="Regex"
|
||||||
|
@ -37,25 +37,22 @@ import { notifyApp } from '../../../core/reducers/appNotification';
|
|||||||
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
||||||
import { getTimeSrv, setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
import { getTimeSrv, setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
|
||||||
import { setVariableQueryRunner, VariableQueryRunner } from './VariableQueryRunner';
|
import { setVariableQueryRunner, VariableQueryRunner } from './VariableQueryRunner';
|
||||||
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
const mocks: Record<string, any> = {
|
const mocks: Record<string, any> = {
|
||||||
datasource: {
|
datasource: {
|
||||||
metricFindQuery: jest.fn().mockResolvedValue([]),
|
metricFindQuery: jest.fn().mockResolvedValue([]),
|
||||||
},
|
},
|
||||||
datasourceSrv: {
|
dataSourceSrv: {
|
||||||
getMetricSources: jest.fn().mockReturnValue([]),
|
get: (name: string) => Promise.resolve(mocks[name]),
|
||||||
|
getList: jest.fn().mockReturnValue([]),
|
||||||
},
|
},
|
||||||
pluginLoader: {
|
pluginLoader: {
|
||||||
importDataSourcePlugin: jest.fn().mockResolvedValue({ components: {} }),
|
importDataSourcePlugin: jest.fn().mockResolvedValue({ components: {} }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('../../plugins/datasource_srv', () => ({
|
setDataSourceSrv(mocks.dataSourceSrv as any);
|
||||||
getDatasourceSrv: jest.fn(() => ({
|
|
||||||
get: jest.fn((name: string) => mocks[name]),
|
|
||||||
getMetricSources: () => mocks.datasourceSrv.getMetricSources(),
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../../plugins/plugin_loader', () => ({
|
jest.mock('../../plugins/plugin_loader', () => ({
|
||||||
importDataSourcePlugin: () => mocks.pluginLoader.importDataSourcePlugin(),
|
importDataSourcePlugin: () => mocks.pluginLoader.importDataSourcePlugin(),
|
||||||
@ -272,11 +269,10 @@ describe('query actions', () => {
|
|||||||
describe('when initQueryVariableEditor is dispatched', () => {
|
describe('when initQueryVariableEditor is dispatched', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
const variable = createVariable({ includeAll: true, useTags: false });
|
const variable = createVariable({ includeAll: true, useTags: false });
|
||||||
const defaultMetricSource = { name: '', value: '', meta: {}, sort: '' };
|
const testMetricSource = { name: 'test', value: 'test', meta: {} };
|
||||||
const testMetricSource = { name: 'test', value: 'test', meta: {}, sort: '' };
|
|
||||||
const editor = {};
|
const editor = {};
|
||||||
|
|
||||||
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([testMetricSource]);
|
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([testMetricSource]);
|
||||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||||
components: { VariableQueryEditor: editor },
|
components: { VariableQueryEditor: editor },
|
||||||
});
|
});
|
||||||
@ -287,12 +283,9 @@ describe('query actions', () => {
|
|||||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||||
|
|
||||||
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
||||||
const [updateDatasources, setDatasource, setEditor] = actions;
|
const [setDatasource, setEditor] = actions;
|
||||||
const expectedNumberOfActions = 3;
|
const expectedNumberOfActions = 2;
|
||||||
|
|
||||||
expect(updateDatasources).toEqual(
|
|
||||||
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultMetricSource, testMetricSource] })
|
|
||||||
);
|
|
||||||
expect(setDatasource).toEqual(
|
expect(setDatasource).toEqual(
|
||||||
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
||||||
);
|
);
|
||||||
@ -305,11 +298,10 @@ describe('query actions', () => {
|
|||||||
describe('when initQueryVariableEditor is dispatched and metricsource without value is available', () => {
|
describe('when initQueryVariableEditor is dispatched and metricsource without value is available', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
const variable = createVariable({ includeAll: true, useTags: false });
|
const variable = createVariable({ includeAll: true, useTags: false });
|
||||||
const defaultMetricSource = { name: '', value: '', meta: {}, sort: '' };
|
const testMetricSource = { name: 'test', value: (null as unknown) as string, meta: {} };
|
||||||
const testMetricSource = { name: 'test', value: (null as unknown) as string, meta: {}, sort: '' };
|
|
||||||
const editor = {};
|
const editor = {};
|
||||||
|
|
||||||
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([testMetricSource]);
|
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([testMetricSource]);
|
||||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||||
components: { VariableQueryEditor: editor },
|
components: { VariableQueryEditor: editor },
|
||||||
});
|
});
|
||||||
@ -320,12 +312,9 @@ describe('query actions', () => {
|
|||||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||||
|
|
||||||
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
||||||
const [updateDatasources, setDatasource, setEditor] = actions;
|
const [setDatasource, setEditor] = actions;
|
||||||
const expectedNumberOfActions = 3;
|
const expectedNumberOfActions = 2;
|
||||||
|
|
||||||
expect(updateDatasources).toEqual(
|
|
||||||
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultMetricSource] })
|
|
||||||
);
|
|
||||||
expect(setDatasource).toEqual(
|
expect(setDatasource).toEqual(
|
||||||
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
||||||
);
|
);
|
||||||
@ -338,10 +327,9 @@ describe('query actions', () => {
|
|||||||
describe('when initQueryVariableEditor is dispatched and no metric sources was found', () => {
|
describe('when initQueryVariableEditor is dispatched and no metric sources was found', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
const variable = createVariable({ includeAll: true, useTags: false });
|
const variable = createVariable({ includeAll: true, useTags: false });
|
||||||
const defaultDatasource = { name: '', value: '', meta: {}, sort: '' };
|
|
||||||
const editor = {};
|
const editor = {};
|
||||||
|
|
||||||
mocks.datasourceSrv.getMetricSources = jest.fn().mockReturnValue([]);
|
mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([]);
|
||||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||||
components: { VariableQueryEditor: editor },
|
components: { VariableQueryEditor: editor },
|
||||||
});
|
});
|
||||||
@ -352,12 +340,9 @@ describe('query actions', () => {
|
|||||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||||
|
|
||||||
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
||||||
const [updateDatasources, setDatasource, setEditor] = actions;
|
const [setDatasource, setEditor] = actions;
|
||||||
const expectedNumberOfActions = 3;
|
const expectedNumberOfActions = 2;
|
||||||
|
|
||||||
expect(updateDatasources).toEqual(
|
|
||||||
changeVariableEditorExtended({ propName: 'dataSources', propValue: [defaultDatasource] })
|
|
||||||
);
|
|
||||||
expect(setDatasource).toEqual(
|
expect(setDatasource).toEqual(
|
||||||
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
changeVariableEditorExtended({ propName: 'dataSource', propValue: mocks['datasource'] })
|
||||||
);
|
);
|
||||||
@ -370,7 +355,6 @@ describe('query actions', () => {
|
|||||||
describe('when initQueryVariableEditor is dispatched and variable dont have datasource', () => {
|
describe('when initQueryVariableEditor is dispatched and variable dont have datasource', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
const variable = createVariable({ datasource: undefined });
|
const variable = createVariable({ datasource: undefined });
|
||||||
const ds = { name: '', value: '', meta: {}, sort: '' };
|
|
||||||
|
|
||||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||||
.givenRootReducer(getRootReducer())
|
.givenRootReducer(getRootReducer())
|
||||||
@ -378,10 +362,10 @@ describe('query actions', () => {
|
|||||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||||
|
|
||||||
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
tester.thenDispatchedActionsPredicateShouldEqual(actions => {
|
||||||
const [updateDatasources] = actions;
|
const [setDatasource] = actions;
|
||||||
const expectedNumberOfActions = 1;
|
const expectedNumberOfActions = 1;
|
||||||
|
|
||||||
expect(updateDatasources).toEqual(changeVariableEditorExtended({ propName: 'dataSources', propValue: [ds] }));
|
expect(setDatasource).toEqual(changeVariableEditorExtended({ propName: 'dataSource', propValue: undefined }));
|
||||||
return actions.length === expectedNumberOfActions;
|
return actions.length === expectedNumberOfActions;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { DataSourcePluginMeta, DataSourceSelectItem } from '@grafana/data';
|
|
||||||
import { toDataQueryError } from '@grafana/runtime';
|
import { toDataQueryError } from '@grafana/runtime';
|
||||||
|
|
||||||
import { updateOptions } from '../state/actions';
|
import { updateOptions } from '../state/actions';
|
||||||
import { QueryVariableModel } from '../types';
|
import { QueryVariableModel } from '../types';
|
||||||
import { ThunkResult } from '../../../types';
|
import { ThunkResult } from '../../../types';
|
||||||
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { getVariable } from '../state/selectors';
|
import { getVariable } from '../state/selectors';
|
||||||
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
|
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
|
||||||
import { changeVariableProp } from '../state/sharedReducer';
|
import { changeVariableProp } from '../state/sharedReducer';
|
||||||
@ -24,7 +22,7 @@ export const updateQueryVariableOptions = (
|
|||||||
if (getState().templating.editor.id === variableInState.id) {
|
if (getState().templating.editor.id === variableInState.id) {
|
||||||
dispatch(removeVariableEditorError({ errorProp: 'update' }));
|
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
|
// 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
|
// variable will have the wrong current value as input
|
||||||
@ -53,18 +51,7 @@ export const initQueryVariableEditor = (identifier: VariableIdentifier): ThunkRe
|
|||||||
dispatch,
|
dispatch,
|
||||||
getState
|
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());
|
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||||
if (!variable.datasource) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
|
await dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -74,7 +61,7 @@ export const changeQueryVariableDataSource = (
|
|||||||
): ThunkResult<void> => {
|
): ThunkResult<void> => {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
try {
|
try {
|
||||||
const dataSource = await getDatasourceSrv().get(name ?? '');
|
const dataSource = await getDataSourceSrv().get(name ?? '');
|
||||||
dispatch(changeVariableEditorExtended({ propName: 'dataSource', propValue: dataSource }));
|
dispatch(changeVariableEditorExtended({ propName: 'dataSource', propValue: dataSource }));
|
||||||
|
|
||||||
const VariableQueryEditor = await getVariableQueryEditor(dataSource);
|
const VariableQueryEditor = await getVariableQueryEditor(dataSource);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { DataSourceApi, DataSourceSelectItem, MetricFindValue, stringToJsRegex } from '@grafana/data';
|
import { DataSourceApi, MetricFindValue, stringToJsRegex } from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
initialVariableModelState,
|
initialVariableModelState,
|
||||||
@ -29,7 +29,6 @@ interface VariableOptionsUpdate {
|
|||||||
|
|
||||||
export interface QueryVariableEditorState {
|
export interface QueryVariableEditorState {
|
||||||
VariableQueryEditor: VariableQueryEditorType;
|
VariableQueryEditor: VariableQueryEditorType;
|
||||||
dataSources: DataSourceSelectItem[];
|
|
||||||
dataSource: DataSourceApi | null;
|
dataSource: DataSourceApi | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ import { expect } from '../../../../test/lib/common';
|
|||||||
import { ConstantVariableModel, VariableRefresh } from '../types';
|
import { ConstantVariableModel, VariableRefresh } from '../types';
|
||||||
import { updateVariableOptions } from '../query/reducer';
|
import { updateVariableOptions } from '../query/reducer';
|
||||||
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
|
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
|
||||||
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
variableAdapters.setInit(() => [
|
variableAdapters.setInit(() => [
|
||||||
createQueryVariableAdapter(),
|
createQueryVariableAdapter(),
|
||||||
@ -82,12 +83,10 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('app/features/plugins/datasource_srv', () => ({
|
setDataSourceSrv({
|
||||||
getDatasourceSrv: jest.fn(() => ({
|
get: getDatasource,
|
||||||
get: getDatasource,
|
getList: getMetricSources,
|
||||||
getMetricSources,
|
} as any);
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('shared actions', () => {
|
describe('shared actions', () => {
|
||||||
describe('when initDashboardTemplating is dispatched', () => {
|
describe('when initDashboardTemplating is dispatched', () => {
|
||||||
|
@ -14,6 +14,7 @@ import { updateVariableOptions } from '../query/reducer';
|
|||||||
import { customBuilder, queryBuilder } from '../shared/testing/builders';
|
import { customBuilder, queryBuilder } from '../shared/testing/builders';
|
||||||
import { variablesInitTransaction } from './transactionReducer';
|
import { variablesInitTransaction } from './transactionReducer';
|
||||||
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
|
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
|
||||||
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
|
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
|
||||||
getTimeSrv: jest.fn().mockReturnValue({
|
getTimeSrv: jest.fn().mockReturnValue({
|
||||||
@ -28,39 +29,37 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('app/features/plugins/datasource_srv', () => ({
|
setDataSourceSrv({
|
||||||
getDatasourceSrv: () => ({
|
get: jest.fn().mockResolvedValue({
|
||||||
get: jest.fn().mockResolvedValue({
|
metricFindQuery: jest.fn().mockImplementation((query, options) => {
|
||||||
metricFindQuery: jest.fn().mockImplementation((query, options) => {
|
if (query === '$custom.*') {
|
||||||
if (query === '$custom.*') {
|
return Promise.resolve([
|
||||||
return Promise.resolve([
|
{ value: 'AA', text: 'AA' },
|
||||||
{ value: 'AA', text: 'AA' },
|
{ value: 'AB', text: 'AB' },
|
||||||
{ value: 'AB', text: 'AB' },
|
{ value: 'AC', text: 'AC' },
|
||||||
{ value: 'AC', text: 'AC' },
|
]);
|
||||||
]);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (query === '$custom.$queryDependsOnCustom.*') {
|
if (query === '$custom.$queryDependsOnCustom.*') {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ value: 'AAA', text: 'AAA' },
|
{ value: 'AAA', text: 'AAA' },
|
||||||
{ value: 'AAB', text: 'AAB' },
|
{ value: 'AAB', text: 'AAB' },
|
||||||
{ value: 'AAC', text: 'AAC' },
|
{ value: 'AAC', text: 'AAC' },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query === '*') {
|
if (query === '*') {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ value: 'A', text: 'A' },
|
{ value: 'A', text: 'A' },
|
||||||
{ value: 'B', text: 'B' },
|
{ value: 'B', text: 'B' },
|
||||||
{ value: 'C', text: 'C' },
|
{ value: 'C', text: 'C' },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}));
|
} as any);
|
||||||
|
|
||||||
variableAdapters.setInit(() => [createCustomVariableAdapter(), createQueryVariableAdapter()]);
|
variableAdapters.setInit(() => [createCustomVariableAdapter(), createQueryVariableAdapter()]);
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { DataSourceSelectItem, VariableSuggestion } from '@grafana/data';
|
import { VariableSuggestion } from '@grafana/data';
|
||||||
import { Button, LegacyForms, DataLinkInput, stylesFactory } from '@grafana/ui';
|
import { Button, LegacyForms, DataLinkInput, stylesFactory } from '@grafana/ui';
|
||||||
const { FormField, Switch } = LegacyForms;
|
const { FormField, Switch } = LegacyForms;
|
||||||
import { DataLinkConfig } from '../types';
|
import { DataLinkConfig } from '../types';
|
||||||
import { usePrevious } from 'react-use';
|
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(() => ({
|
const getStyles = stylesFactory(() => ({
|
||||||
firstRow: css`
|
firstRow: css`
|
||||||
@ -107,14 +106,16 @@ export const DataLink = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{showInternalLink && (
|
{showInternalLink && (
|
||||||
<DataSourceSection
|
<DataSourcePicker
|
||||||
onChange={datasourceUid => {
|
tracing={true}
|
||||||
|
// Uid and value should be always set in the db and so in the items.
|
||||||
|
onChange={ds => {
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
datasourceUid,
|
datasourceUid: ds.uid,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
datasourceUid={value.datasourceUid}
|
current={value.datasourceUid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>>] {
|
function useInternalLink(datasourceUid?: string): [boolean, Dispatch<SetStateAction<boolean>>] {
|
||||||
const [showInternalLink, setShowInternalLink] = useState<boolean>(!!datasourceUid);
|
const [showInternalLink, setShowInternalLink] = useState<boolean>(!!datasourceUid);
|
||||||
const previousUid = usePrevious(datasourceUid);
|
const previousUid = usePrevious(datasourceUid);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { DerivedField } from './DerivedField';
|
import { DerivedField } from './DerivedField';
|
||||||
import DataSourcePicker from '../../../../core/components/Select/DataSourcePicker';
|
import { DataSourcePicker } from '../../../../core/components/Select/DataSourcePicker';
|
||||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||||
|
|
||||||
jest.mock('app/features/plugins/datasource_srv', () => ({
|
jest.mock('app/features/plugins/datasource_srv', () => ({
|
||||||
@ -41,12 +41,7 @@ describe('DerivedField', () => {
|
|||||||
};
|
};
|
||||||
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
|
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
|
||||||
|
|
||||||
expect(
|
expect(wrapper.find(DataSourcePicker).length).toBe(1);
|
||||||
wrapper
|
|
||||||
.find('DataSourceSection')
|
|
||||||
.dive()
|
|
||||||
.find(DataSourcePicker).length
|
|
||||||
).toBe(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows url link if uid is not set', () => {
|
it('shows url link if uid is not set', () => {
|
||||||
@ -56,7 +51,7 @@ describe('DerivedField', () => {
|
|||||||
url: 'test',
|
url: 'test',
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
|
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', () => {
|
it('shows only tracing datasources for internal link', () => {
|
||||||
@ -66,13 +61,6 @@ describe('DerivedField', () => {
|
|||||||
datasourceUid: 'test',
|
datasourceUid: 'test',
|
||||||
};
|
};
|
||||||
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
|
const wrapper = shallow(<DerivedField value={value} onChange={() => {}} onDelete={() => {}} suggestions={[]} />);
|
||||||
const dsSection = wrapper.find('DataSourceSection').dive();
|
expect(wrapper.find(DataSourcePicker).props().tracing).toEqual(true);
|
||||||
expect(dsSection.find(DataSourcePicker).props().datasources).toEqual([
|
|
||||||
{
|
|
||||||
meta: { tracing: true },
|
|
||||||
name: 'tracing_ds',
|
|
||||||
value: 'tracing',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { Button, DataLinkInput, stylesFactory, LegacyForms } from '@grafana/ui';
|
import { Button, DataLinkInput, stylesFactory, LegacyForms } from '@grafana/ui';
|
||||||
const { Switch, FormField } = LegacyForms;
|
|
||||||
import { VariableSuggestion } from '@grafana/data';
|
import { VariableSuggestion } from '@grafana/data';
|
||||||
import { DataSourceSelectItem } from '@grafana/data';
|
|
||||||
|
|
||||||
import { DerivedFieldConfig } from '../types';
|
import { DerivedFieldConfig } from '../types';
|
||||||
import DataSourcePicker from 'app/core/components/Select/DataSourcePicker';
|
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
|
||||||
import { usePrevious } from 'react-use';
|
import { usePrevious } from 'react-use';
|
||||||
|
|
||||||
|
const { Switch, FormField } = LegacyForms;
|
||||||
|
|
||||||
const getStyles = stylesFactory(() => ({
|
const getStyles = stylesFactory(() => ({
|
||||||
row: css`
|
row: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -128,48 +126,18 @@ export const DerivedField = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{showInternalLink && (
|
{showInternalLink && (
|
||||||
<DataSourceSection
|
<DataSourcePicker
|
||||||
onChange={datasourceUid => {
|
tracing={true}
|
||||||
|
onChange={ds =>
|
||||||
onChange({
|
onChange({
|
||||||
...value,
|
...value,
|
||||||
datasourceUid,
|
datasourceUid: ds.uid,
|
||||||
});
|
})
|
||||||
}}
|
}
|
||||||
datasourceUid={value.datasourceUid}
|
current={value.datasourceUid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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"
|
slash "^2.0.0"
|
||||||
source-map "^0.6.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:
|
jest-util@^26.1.0, jest-util@^26.6.2:
|
||||||
version "26.6.2"
|
version "26.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1"
|
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:
|
dependencies:
|
||||||
source-map "0.1.32"
|
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"
|
version "0.5.19"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
||||||
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
|
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
|
||||||
@ -25203,23 +25192,6 @@ ts-jest@26.4.4:
|
|||||||
semver "7.x"
|
semver "7.x"
|
||||||
yargs-parser "20.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:
|
ts-loader@6.2.1:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.1.tgz#67939d5772e8a8c6bdaf6277ca023a4812da02ef"
|
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"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
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:
|
tslib@2.0.3, tslib@^2.0.0, tslib@^2.0.1:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
|
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"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
|
||||||
integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
|
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:
|
yargs-parser@^11.1.1:
|
||||||
version "11.1.1"
|
version "11.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
|
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
|
||||||
|
Loading…
Reference in New Issue
Block a user