mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: app specific query editors for Loki and Prometheus (#34365)
* Adding simplified version of query editor based on app flag. * cleaned up the absolute time range. * changing placeholder text. * updated snapshot. * added some tests. * adding loki query editor tests. * updating snapshots.
This commit is contained in:
parent
f48708bb9e
commit
8ddf6de6e7
@ -7,6 +7,7 @@ export enum CoreApp {
|
|||||||
Dashboard = 'dashboard',
|
Dashboard = 'dashboard',
|
||||||
Explore = 'explore',
|
Explore = 'explore',
|
||||||
Unknown = 'unknown',
|
Unknown = 'unknown',
|
||||||
|
CloudAlerting = 'cloud-alerting',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppRootProps<T = KeyValue> {
|
export interface AppRootProps<T = KeyValue> {
|
||||||
|
@ -367,6 +367,7 @@ export interface QueryEditorProps<
|
|||||||
exploreId?: any;
|
exploreId?: any;
|
||||||
history?: HistoryItem[];
|
history?: HistoryItem[];
|
||||||
queries?: DataQuery[];
|
queries?: DataQuery[];
|
||||||
|
app?: CoreApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: not really needed but used as type in some data sources and in DataQueryRequest
|
// TODO: not really needed but used as type in some data sources and in DataQueryRequest
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { TextArea } from '@grafana/ui';
|
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||||
import React, { FC } from 'react';
|
import { noop } from 'lodash';
|
||||||
|
import { CoreApp, DataQuery } from '@grafana/data';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
|
||||||
|
import { LokiQuery } from 'app/plugins/datasource/loki/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value?: string;
|
value?: string;
|
||||||
@ -7,13 +12,62 @@ interface Props {
|
|||||||
dataSourceName: string; // will be a prometheus or loki datasource
|
dataSourceName: string; // will be a prometheus or loki datasource
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO implement proper prom/loki query editor here
|
|
||||||
export const ExpressionEditor: FC<Props> = ({ value, onChange, dataSourceName }) => {
|
export const ExpressionEditor: FC<Props> = ({ value, onChange, dataSourceName }) => {
|
||||||
|
const { mapToValue, mapToQuery } = useQueryMappers(dataSourceName);
|
||||||
|
const [query, setQuery] = useState(mapToQuery({ refId: 'A', hide: false }, value));
|
||||||
|
const { error, loading, value: dataSource } = useAsync(() => {
|
||||||
|
return getDataSourceSrv().get(dataSourceName);
|
||||||
|
}, [dataSourceName]);
|
||||||
|
|
||||||
|
const onChangeQuery = useCallback(
|
||||||
|
(query: DataQuery) => {
|
||||||
|
setQuery(query);
|
||||||
|
onChange(mapToValue(query));
|
||||||
|
},
|
||||||
|
[onChange, mapToValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading || dataSource?.name !== dataSourceName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !dataSource || !dataSource?.components?.QueryEditor) {
|
||||||
|
const errorMessage = error?.message || 'Data source plugin does not export any Query Editor component';
|
||||||
|
return <div>Could not load query editor due to: {errorMessage}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueryEditor = dataSource?.components?.QueryEditor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextArea
|
<QueryEditor
|
||||||
placeholder="Enter a promql expression"
|
query={query}
|
||||||
value={value}
|
queries={[query]}
|
||||||
onChange={(evt) => onChange((evt.target as HTMLTextAreaElement).value)}
|
app={CoreApp.CloudAlerting}
|
||||||
|
onChange={onChangeQuery}
|
||||||
|
onRunQuery={noop}
|
||||||
|
datasource={dataSource}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type QueryMappers<T extends DataQuery = DataQuery> = {
|
||||||
|
mapToValue: (query: T) => string;
|
||||||
|
mapToQuery: (existing: T, value: string | undefined) => T;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useQueryMappers(dataSourceName: string): QueryMappers {
|
||||||
|
return useMemo(() => {
|
||||||
|
const settings = getDataSourceSrv().getInstanceSettings(dataSourceName);
|
||||||
|
|
||||||
|
switch (settings?.type) {
|
||||||
|
case 'loki':
|
||||||
|
case 'prometheus':
|
||||||
|
return {
|
||||||
|
mapToValue: (query: PromQuery | LokiQuery) => query.expr,
|
||||||
|
mapToQuery: (existing: DataQuery, value: string | undefined) => ({ ...existing, expr: value }),
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`${dataSourceName} is not supported as an expression editor`);
|
||||||
|
}
|
||||||
|
}, [dataSourceName]);
|
||||||
|
}
|
||||||
|
@ -17,12 +17,6 @@ interface Props {
|
|||||||
export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEditor(props: Props) {
|
export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEditor(props: Props) {
|
||||||
const { expr, maxLines, instant, datasource, onChange } = props;
|
const { expr, maxLines, instant, datasource, onChange } = props;
|
||||||
|
|
||||||
// Timerange to get existing labels from. Hard coding like this seems to be good enough right now.
|
|
||||||
const absoluteRange = {
|
|
||||||
from: Date.now() - 10000,
|
|
||||||
to: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryWithRefId: LokiQuery = {
|
const queryWithRefId: LokiQuery = {
|
||||||
refId: '',
|
refId: '',
|
||||||
expr,
|
expr,
|
||||||
@ -38,7 +32,6 @@ export const LokiAnnotationsQueryEditor = memo(function LokiAnnotationQueryEdito
|
|||||||
onRunQuery={() => {}}
|
onRunQuery={() => {}}
|
||||||
onBlur={() => {}}
|
onBlur={() => {}}
|
||||||
history={[]}
|
history={[]}
|
||||||
absoluteRange={absoluteRange}
|
|
||||||
ExtraFieldElement={
|
ExtraFieldElement={
|
||||||
<LokiOptionFields
|
<LokiOptionFields
|
||||||
queryType={queryWithRefId.instant ? 'instant' : 'range'}
|
queryType={queryWithRefId.instant ? 'instant' : 'range'}
|
||||||
|
@ -11,8 +11,7 @@ import { LokiOptionFields } from './LokiOptionFields';
|
|||||||
type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>;
|
type Props = ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions>;
|
||||||
|
|
||||||
export function LokiExploreQueryEditor(props: Props) {
|
export function LokiExploreQueryEditor(props: Props) {
|
||||||
const { range, query, data, datasource, history, onChange, onRunQuery } = props;
|
const { query, data, datasource, history, onChange, onRunQuery } = props;
|
||||||
const absoluteTimeRange = { from: range!.from!.valueOf(), to: range!.to!.valueOf() }; // Range here is never optional
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LokiQueryField
|
<LokiQueryField
|
||||||
@ -23,7 +22,6 @@ export function LokiExploreQueryEditor(props: Props) {
|
|||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
history={history}
|
history={history}
|
||||||
data={data}
|
data={data}
|
||||||
absoluteRange={absoluteTimeRange}
|
|
||||||
ExtraFieldElement={
|
ExtraFieldElement={
|
||||||
<LokiOptionFields
|
<LokiOptionFields
|
||||||
queryType={query.instant ? 'instant' : 'range'}
|
queryType={query.instant ? 'instant' : 'range'}
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { memo } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { QueryEditorProps } from '@grafana/data';
|
|
||||||
import { InlineFormLabel } from '@grafana/ui';
|
import { InlineFormLabel } from '@grafana/ui';
|
||||||
import { LokiDatasource } from '../datasource';
|
|
||||||
import { LokiQuery, LokiOptions } from '../types';
|
|
||||||
import { LokiQueryField } from './LokiQueryField';
|
import { LokiQueryField } from './LokiQueryField';
|
||||||
import { LokiOptionFields } from './LokiOptionFields';
|
import { LokiOptionFields } from './LokiOptionFields';
|
||||||
|
import { LokiQueryEditorProps } from './types';
|
||||||
|
|
||||||
type Props = QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions>;
|
export function LokiQueryEditor(props: LokiQueryEditorProps) {
|
||||||
|
const { query, data, datasource, onChange, onRunQuery } = props;
|
||||||
export function LokiQueryEditor(props: Props) {
|
|
||||||
const { range, query, data, datasource, onChange, onRunQuery } = props;
|
|
||||||
const absoluteTimeRange = { from: range!.from!.valueOf(), to: range!.to!.valueOf() }; // Range here is never optional
|
|
||||||
|
|
||||||
const onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
const onLegendChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
const nextQuery = { ...query, legendFormat: e.currentTarget.value };
|
const nextQuery = { ...query, legendFormat: e.currentTarget.value };
|
||||||
@ -51,7 +46,7 @@ export function LokiQueryEditor(props: Props) {
|
|||||||
onBlur={onRunQuery}
|
onBlur={onRunQuery}
|
||||||
history={[]}
|
history={[]}
|
||||||
data={data}
|
data={data}
|
||||||
absoluteRange={absoluteTimeRange}
|
data-testid={testIds.editor}
|
||||||
ExtraFieldElement={
|
ExtraFieldElement={
|
||||||
<>
|
<>
|
||||||
<LokiOptionFields
|
<LokiOptionFields
|
||||||
@ -69,4 +64,6 @@ export function LokiQueryEditor(props: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(LokiQueryEditor);
|
export const testIds = {
|
||||||
|
editor: 'loki-editor',
|
||||||
|
};
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, RenderResult } from '@testing-library/react';
|
||||||
|
import { CoreApp } from '@grafana/data';
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
import { LokiDatasource } from '../datasource';
|
||||||
|
import { testIds as alertingTestIds } from './LokiQueryEditorForAlerting';
|
||||||
|
import { testIds as regularTestIds } from './LokiQueryEditor';
|
||||||
|
import LokiQueryEditorByApp from './LokiQueryEditorByApp';
|
||||||
|
|
||||||
|
function setup(app: CoreApp): RenderResult {
|
||||||
|
const dataSource = ({
|
||||||
|
languageProvider: {
|
||||||
|
start: () => Promise.resolve([]),
|
||||||
|
getSyntax: () => {},
|
||||||
|
getLabelKeys: () => [],
|
||||||
|
metrics: [],
|
||||||
|
},
|
||||||
|
} as unknown) as LokiDatasource;
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<LokiQueryEditorByApp
|
||||||
|
app={app}
|
||||||
|
onChange={noop}
|
||||||
|
onRunQuery={noop}
|
||||||
|
datasource={dataSource}
|
||||||
|
query={{ refId: 'A', expr: '' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('LokiQueryEditorByApp', () => {
|
||||||
|
it('should render simplified query editor for cloud alerting', () => {
|
||||||
|
const { getByTestId, queryByTestId } = setup(CoreApp.CloudAlerting);
|
||||||
|
|
||||||
|
expect(getByTestId(alertingTestIds.editor)).toBeInTheDocument();
|
||||||
|
expect(queryByTestId(regularTestIds.editor)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render regular query editor for unkown apps', () => {
|
||||||
|
const { getByTestId, queryByTestId } = setup(CoreApp.Unknown);
|
||||||
|
|
||||||
|
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
|
||||||
|
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render regular query editor for explore', () => {
|
||||||
|
const { getByTestId, queryByTestId } = setup(CoreApp.Explore);
|
||||||
|
|
||||||
|
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
|
||||||
|
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render regular query editor for dashboard', () => {
|
||||||
|
const { getByTestId, queryByTestId } = setup(CoreApp.Dashboard);
|
||||||
|
|
||||||
|
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
|
||||||
|
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import { CoreApp } from '@grafana/data';
|
||||||
|
import { LokiQueryEditorProps } from './types';
|
||||||
|
import { LokiQueryEditor } from './LokiQueryEditor';
|
||||||
|
import { LokiQueryEditorForAlerting } from './LokiQueryEditorForAlerting';
|
||||||
|
|
||||||
|
export function LokiQueryEditorByApp(props: LokiQueryEditorProps) {
|
||||||
|
const { app } = props;
|
||||||
|
|
||||||
|
switch (app) {
|
||||||
|
case CoreApp.CloudAlerting:
|
||||||
|
return <LokiQueryEditorForAlerting {...props} />;
|
||||||
|
default:
|
||||||
|
return <LokiQueryEditor {...props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(LokiQueryEditorByApp);
|
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LokiQueryField } from './LokiQueryField';
|
||||||
|
import { LokiQueryEditorProps } from './types';
|
||||||
|
|
||||||
|
export function LokiQueryEditorForAlerting(props: LokiQueryEditorProps) {
|
||||||
|
const { query, data, datasource, onChange, onRunQuery } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LokiQueryField
|
||||||
|
datasource={datasource}
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
onBlur={onRunQuery}
|
||||||
|
history={[]}
|
||||||
|
data={data}
|
||||||
|
placeholder="Enter a Loki query"
|
||||||
|
data-testid={testIds.editor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
editor: 'loki-editor-cloud-alerting',
|
||||||
|
};
|
@ -18,7 +18,7 @@ import { Plugin, Node } from 'slate';
|
|||||||
import { LokiLabelBrowser } from './LokiLabelBrowser';
|
import { LokiLabelBrowser } from './LokiLabelBrowser';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { ExploreQueryFieldProps, AbsoluteTimeRange } from '@grafana/data';
|
import { ExploreQueryFieldProps } from '@grafana/data';
|
||||||
import { LokiQuery, LokiOptions } from '../types';
|
import { LokiQuery, LokiOptions } from '../types';
|
||||||
import { LanguageMap, languages as prismLanguages } from 'prismjs';
|
import { LanguageMap, languages as prismLanguages } from 'prismjs';
|
||||||
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
|
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
|
||||||
@ -63,8 +63,9 @@ function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadTe
|
|||||||
|
|
||||||
export interface LokiQueryFieldProps extends ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions> {
|
export interface LokiQueryFieldProps extends ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions> {
|
||||||
history: LokiHistoryItem[];
|
history: LokiHistoryItem[];
|
||||||
absoluteRange: AbsoluteTimeRange;
|
|
||||||
ExtraFieldElement?: ReactNode;
|
ExtraFieldElement?: ReactNode;
|
||||||
|
placeholder?: string;
|
||||||
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LokiQueryFieldState {
|
interface LokiQueryFieldState {
|
||||||
@ -138,7 +139,13 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ExtraFieldElement, query, datasource } = this.props;
|
const {
|
||||||
|
ExtraFieldElement,
|
||||||
|
query,
|
||||||
|
datasource,
|
||||||
|
placeholder = 'Enter a Loki query (run with Shift+Enter)',
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const { labelsLoaded, labelBrowserVisible } = this.state;
|
const { labelsLoaded, labelBrowserVisible } = this.state;
|
||||||
const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
|
const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
|
||||||
const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
|
const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
|
||||||
@ -148,7 +155,10 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1">
|
<div
|
||||||
|
className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"
|
||||||
|
data-testid={this.props['data-testid']}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="gf-form-label query-keyword pointer"
|
className="gf-form-label query-keyword pointer"
|
||||||
onClick={this.onClickChooserButton}
|
onClick={this.onClickChooserButton}
|
||||||
@ -167,7 +177,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
|
|||||||
onChange={this.onChangeQuery}
|
onChange={this.onChangeQuery}
|
||||||
onBlur={this.props.onBlur}
|
onBlur={this.props.onBlur}
|
||||||
onRunQuery={this.props.onRunQuery}
|
onRunQuery={this.props.onRunQuery}
|
||||||
placeholder="Enter a Loki query (run with Shift+Enter)"
|
placeholder={placeholder}
|
||||||
portalOrigin="loki"
|
portalOrigin="loki"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,12 +17,6 @@ exports[`LokiExploreQueryEditor should render component 1`] = `
|
|||||||
queryType="range"
|
queryType="range"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
absoluteRange={
|
|
||||||
Object {
|
|
||||||
"from": 1577836800000,
|
|
||||||
"to": 1577923200000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data={
|
data={
|
||||||
Object {
|
Object {
|
||||||
"request": Object {
|
"request": Object {
|
||||||
|
@ -43,12 +43,7 @@ exports[`Render LokiQueryEditor with legend should render 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
absoluteRange={
|
data-testid="loki-editor"
|
||||||
Object {
|
|
||||||
"from": 1577836800000,
|
|
||||||
"to": 1577923200000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
datasource={Object {}}
|
datasource={Object {}}
|
||||||
history={Array []}
|
history={Array []}
|
||||||
onBlur={[MockFunction]}
|
onBlur={[MockFunction]}
|
||||||
@ -107,12 +102,7 @@ exports[`Render LokiQueryEditor with legend should update timerange 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
absoluteRange={
|
data-testid="loki-editor"
|
||||||
Object {
|
|
||||||
"from": 1546300800000,
|
|
||||||
"to": 1577923200000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
datasource={Object {}}
|
datasource={Object {}}
|
||||||
history={Array []}
|
history={Array []}
|
||||||
onBlur={[MockFunction]}
|
onBlur={[MockFunction]}
|
||||||
|
5
public/app/plugins/datasource/loki/components/types.ts
Normal file
5
public/app/plugins/datasource/loki/components/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { QueryEditorProps } from '@grafana/data';
|
||||||
|
import LokiDatasource from '../datasource';
|
||||||
|
import { LokiOptions, LokiQuery } from '../types';
|
||||||
|
|
||||||
|
export type LokiQueryEditorProps = QueryEditorProps<LokiDatasource, LokiQuery, LokiOptions>;
|
@ -3,12 +3,12 @@ import Datasource from './datasource';
|
|||||||
|
|
||||||
import LokiCheatSheet from './components/LokiCheatSheet';
|
import LokiCheatSheet from './components/LokiCheatSheet';
|
||||||
import LokiExploreQueryEditor from './components/LokiExploreQueryEditor';
|
import LokiExploreQueryEditor from './components/LokiExploreQueryEditor';
|
||||||
import LokiQueryEditor from './components/LokiQueryEditor';
|
import LokiQueryEditorByApp from './components/LokiQueryEditorByApp';
|
||||||
import { LokiAnnotationsQueryCtrl } from './LokiAnnotationsQueryCtrl';
|
import { LokiAnnotationsQueryCtrl } from './LokiAnnotationsQueryCtrl';
|
||||||
import { ConfigEditor } from './configuration/ConfigEditor';
|
import { ConfigEditor } from './configuration/ConfigEditor';
|
||||||
|
|
||||||
export const plugin = new DataSourcePlugin(Datasource)
|
export const plugin = new DataSourcePlugin(Datasource)
|
||||||
.setQueryEditor(LokiQueryEditor)
|
.setQueryEditor(LokiQueryEditorByApp)
|
||||||
.setConfigEditor(ConfigEditor)
|
.setConfigEditor(ConfigEditor)
|
||||||
.setExploreQueryField(LokiExploreQueryEditor)
|
.setExploreQueryField(LokiExploreQueryEditor)
|
||||||
.setQueryEditorHelp(LokiCheatSheet)
|
.setQueryEditorHelp(LokiCheatSheet)
|
||||||
|
@ -3,18 +3,16 @@ import React, { PureComponent } from 'react';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
|
import { InlineFormLabel, LegacyForms, Select } from '@grafana/ui';
|
||||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { PrometheusDatasource } from '../datasource';
|
import { PromQuery } from '../types';
|
||||||
import { PromOptions, PromQuery } from '../types';
|
|
||||||
|
|
||||||
import PromQueryField from './PromQueryField';
|
import PromQueryField from './PromQueryField';
|
||||||
import PromLink from './PromLink';
|
import PromLink from './PromLink';
|
||||||
import { PromExemplarField } from './PromExemplarField';
|
import { PromExemplarField } from './PromExemplarField';
|
||||||
|
import { PromQueryEditorProps } from './types';
|
||||||
|
|
||||||
const { Switch } = LegacyForms;
|
const { Switch } = LegacyForms;
|
||||||
|
|
||||||
export type Props = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>;
|
|
||||||
|
|
||||||
const FORMAT_OPTIONS: Array<SelectableValue<string>> = [
|
const FORMAT_OPTIONS: Array<SelectableValue<string>> = [
|
||||||
{ label: 'Time series', value: 'time_series' },
|
{ label: 'Time series', value: 'time_series' },
|
||||||
{ label: 'Table', value: 'table' },
|
{ label: 'Table', value: 'table' },
|
||||||
@ -35,11 +33,11 @@ interface State {
|
|||||||
exemplar: boolean;
|
exemplar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PromQueryEditor extends PureComponent<Props, State> {
|
export class PromQueryEditor extends PureComponent<PromQueryEditorProps, State> {
|
||||||
// Query target to be modified and used for queries
|
// Query target to be modified and used for queries
|
||||||
query: PromQuery;
|
query: PromQuery;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: PromQueryEditorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
// Use default query to prevent undefined input values
|
// Use default query to prevent undefined input values
|
||||||
const defaultQuery: Partial<PromQuery> = { expr: '', legendFormat: '', interval: '', exemplar: true };
|
const defaultQuery: Partial<PromQuery> = { expr: '', legendFormat: '', interval: '', exemplar: true };
|
||||||
@ -118,6 +116,7 @@ export class PromQueryEditor extends PureComponent<Props, State> {
|
|||||||
onChange={this.onFieldChange}
|
onChange={this.onFieldChange}
|
||||||
history={[]}
|
history={[]}
|
||||||
data={data}
|
data={data}
|
||||||
|
data-testid={testIds.editor}
|
||||||
ExtraFieldElement={
|
ExtraFieldElement={
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
@ -198,3 +197,7 @@ export class PromQueryEditor extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
editor: 'prom-editor',
|
||||||
|
};
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, RenderResult } from '@testing-library/react';
|
||||||
|
import { PromQueryEditorByApp } from './PromQueryEditorByApp';
|
||||||
|
import { CoreApp } from '@grafana/data';
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { PrometheusDatasource } from '../datasource';
|
||||||
|
import { testIds as alertingTestIds } from './PromQueryEditorForAlerting';
|
||||||
|
import { testIds as regularTestIds } from './PromQueryEditor';
|
||||||
|
|
||||||
|
function setup(app: CoreApp): RenderResult {
|
||||||
|
const dataSource = ({
|
||||||
|
createQuery: jest.fn((q) => q),
|
||||||
|
getPrometheusTime: jest.fn((date, roundup) => 123),
|
||||||
|
languageProvider: {
|
||||||
|
start: () => Promise.resolve([]),
|
||||||
|
syntax: () => {},
|
||||||
|
getLabelKeys: () => [],
|
||||||
|
metrics: [],
|
||||||
|
},
|
||||||
|
exemplarErrors: new Observable().pipe(first()),
|
||||||
|
} as unknown) as PrometheusDatasource;
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<PromQueryEditorByApp
|
||||||
|
app={app}
|
||||||
|
onChange={noop}
|
||||||
|
onRunQuery={noop}
|
||||||
|
datasource={dataSource}
|
||||||
|
query={{ refId: 'A', expr: '' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PromQueryEditorByApp', () => {
|
||||||
|
it('should render simplified query editor for cloud alerting', () => {
|
||||||
|
const { getByTestId, queryByTestId } = setup(CoreApp.CloudAlerting);
|
||||||
|
|
||||||
|
expect(getByTestId(alertingTestIds.editor)).toBeInTheDocument();
|
||||||
|
expect(queryByTestId(regularTestIds.editor)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render regular query editor for unkown apps', () => {
|
||||||
|
const { getByTestId, queryByTestId } = setup(CoreApp.Unknown);
|
||||||
|
|
||||||
|
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
|
||||||
|
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render regular query editor for explore', () => {
|
||||||
|
const { getByTestId, queryByTestId } = setup(CoreApp.Explore);
|
||||||
|
|
||||||
|
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
|
||||||
|
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render regular query editor for dashboard', () => {
|
||||||
|
const { getByTestId, queryByTestId } = setup(CoreApp.Dashboard);
|
||||||
|
|
||||||
|
expect(getByTestId(regularTestIds.editor)).toBeInTheDocument();
|
||||||
|
expect(queryByTestId(alertingTestIds.editor)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import { CoreApp } from '@grafana/data';
|
||||||
|
import { PromQueryEditorProps } from './types';
|
||||||
|
import { PromQueryEditor } from './PromQueryEditor';
|
||||||
|
import { PromQueryEditorForAlerting } from './PromQueryEditorForAlerting';
|
||||||
|
|
||||||
|
export function PromQueryEditorByApp(props: PromQueryEditorProps) {
|
||||||
|
const { app } = props;
|
||||||
|
|
||||||
|
switch (app) {
|
||||||
|
case CoreApp.CloudAlerting:
|
||||||
|
return <PromQueryEditorForAlerting {...props} />;
|
||||||
|
default:
|
||||||
|
return <PromQueryEditor {...props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PromQueryEditorByApp);
|
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PromQueryField from './PromQueryField';
|
||||||
|
import { PromQueryEditorProps } from './types';
|
||||||
|
|
||||||
|
export function PromQueryEditorForAlerting(props: PromQueryEditorProps) {
|
||||||
|
const { datasource, query, range, data, onChange, onRunQuery } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PromQueryField
|
||||||
|
datasource={datasource}
|
||||||
|
query={query}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
onChange={onChange}
|
||||||
|
history={[]}
|
||||||
|
range={range}
|
||||||
|
data={data}
|
||||||
|
placeholder="Enter a PromQL query"
|
||||||
|
data-testid={testIds.editor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
editor: 'prom-editor-cloud-alerting',
|
||||||
|
};
|
@ -77,6 +77,8 @@ export function willApplySuggestion(suggestion: string, { typeaheadContext, type
|
|||||||
interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> {
|
interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasource, PromQuery, PromOptions> {
|
||||||
history: Array<HistoryItem<PromQuery>>;
|
history: Array<HistoryItem<PromQuery>>;
|
||||||
ExtraFieldElement?: ReactNode;
|
ExtraFieldElement?: ReactNode;
|
||||||
|
placeholder?: string;
|
||||||
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PromQueryFieldState {
|
interface PromQueryFieldState {
|
||||||
@ -271,7 +273,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
datasource: { languageProvider },
|
datasource: { languageProvider },
|
||||||
query,
|
query,
|
||||||
ExtraFieldElement,
|
ExtraFieldElement,
|
||||||
|
placeholder = 'Enter a PromQL query (run with Shift+Enter)',
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { labelBrowserVisible, syntaxLoaded, hint } = this.state;
|
const { labelBrowserVisible, syntaxLoaded, hint } = this.state;
|
||||||
const cleanText = languageProvider ? languageProvider.cleanText : undefined;
|
const cleanText = languageProvider ? languageProvider.cleanText : undefined;
|
||||||
const hasMetrics = languageProvider.metrics.length > 0;
|
const hasMetrics = languageProvider.metrics.length > 0;
|
||||||
@ -280,7 +284,10 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1">
|
<div
|
||||||
|
className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"
|
||||||
|
data-testid={this.props['data-testid']}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="gf-form-label query-keyword pointer"
|
className="gf-form-label query-keyword pointer"
|
||||||
onClick={this.onClickChooserButton}
|
onClick={this.onClickChooserButton}
|
||||||
@ -300,7 +307,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
onBlur={this.props.onBlur}
|
onBlur={this.props.onBlur}
|
||||||
onChange={this.onChangeQuery}
|
onChange={this.onChangeQuery}
|
||||||
onRunQuery={this.props.onRunQuery}
|
onRunQuery={this.props.onRunQuery}
|
||||||
placeholder="Enter a PromQL query (run with Shift+Enter)"
|
placeholder={placeholder}
|
||||||
portalOrigin="prometheus"
|
portalOrigin="prometheus"
|
||||||
syntaxLoaded={syntaxLoaded}
|
syntaxLoaded={syntaxLoaded}
|
||||||
/>
|
/>
|
||||||
|
@ -178,6 +178,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
data-testid="prom-editor"
|
||||||
datasource={
|
datasource={
|
||||||
Object {
|
Object {
|
||||||
"createQuery": [MockFunction],
|
"createQuery": [MockFunction],
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { QueryEditorProps } from '@grafana/data';
|
||||||
|
import { PrometheusDatasource } from '../datasource';
|
||||||
|
import { PromOptions, PromQuery } from '../types';
|
||||||
|
|
||||||
|
export type PromQueryEditorProps = QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>;
|
@ -1,7 +1,7 @@
|
|||||||
import { DataSourcePlugin } from '@grafana/data';
|
import { DataSourcePlugin } from '@grafana/data';
|
||||||
import { ANNOTATION_QUERY_STEP_DEFAULT, PrometheusDatasource } from './datasource';
|
import { ANNOTATION_QUERY_STEP_DEFAULT, PrometheusDatasource } from './datasource';
|
||||||
|
|
||||||
import { PromQueryEditor } from './components/PromQueryEditor';
|
import PromQueryEditorByApp from './components/PromQueryEditorByApp';
|
||||||
import PromCheatSheet from './components/PromCheatSheet';
|
import PromCheatSheet from './components/PromCheatSheet';
|
||||||
import PromExploreQueryEditor from './components/PromExploreQueryEditor';
|
import PromExploreQueryEditor from './components/PromExploreQueryEditor';
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ class PrometheusAnnotationsQueryCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const plugin = new DataSourcePlugin(PrometheusDatasource)
|
export const plugin = new DataSourcePlugin(PrometheusDatasource)
|
||||||
.setQueryEditor(PromQueryEditor)
|
.setQueryEditor(PromQueryEditorByApp)
|
||||||
.setConfigEditor(ConfigEditor)
|
.setConfigEditor(ConfigEditor)
|
||||||
.setExploreMetricsQueryField(PromExploreQueryEditor)
|
.setExploreMetricsQueryField(PromExploreQueryEditor)
|
||||||
.setAnnotationQueryCtrl(PrometheusAnnotationsQueryCtrl)
|
.setAnnotationQueryCtrl(PrometheusAnnotationsQueryCtrl)
|
||||||
|
@ -50,11 +50,9 @@ export class TempoQueryField extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { query, onChange, range } = this.props;
|
const { query, onChange } = this.props;
|
||||||
const { linkedDatasource } = this.state;
|
const { linkedDatasource } = this.state;
|
||||||
|
|
||||||
const absoluteTimeRange = { from: range!.from!.valueOf(), to: range!.to!.valueOf() }; // Range here is never optional
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
@ -87,7 +85,6 @@ export class TempoQueryField extends React.PureComponent<Props, State> {
|
|||||||
onRunQuery={this.onRunLinkedQuery}
|
onRunQuery={this.onRunLinkedQuery}
|
||||||
query={this.linkedQuery as any}
|
query={this.linkedQuery as any}
|
||||||
history={[]}
|
history={[]}
|
||||||
absoluteRange={absoluteTimeRange}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
Loading…
Reference in New Issue
Block a user