Prometheus: Migrate annotation editor to react (#48814)

* Modify the annotation support api

* Migrate annotation editor component

* Update public/app/features/annotations/standardAnnotationSupport.ts

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>

* Move the escape hatches out of the public API

* Fix props transforms

* Break import cycle

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Andrej Ocenas
2022-05-24 17:43:58 +02:00
committed by GitHub
co-authored by Ryan McKinley
parent 1c0019285f
commit 4124294011
13 changed files with 324 additions and 72 deletions
@@ -77,6 +77,12 @@ export interface AnnotationEventFieldMapping {
}
export type AnnotationEventMappings = Partial<Record<keyof AnnotationEvent, AnnotationEventFieldMapping>>;
type AnnotationQueryEditorProps<TQuery extends DataQuery> = QueryEditorProps<any, TQuery> & {
// Needs to be optional otherwise component not using these cannot be used, even though they are passed on and can be
// just ignored if not used.
annotation?: AnnotationQuery<TQuery>;
onAnnotationChange?: (annotation: AnnotationQuery<TQuery>) => void;
};
/**
* Since Grafana 7.2
@@ -86,7 +92,7 @@ export type AnnotationEventMappings = Partial<Record<keyof AnnotationEvent, Anno
export interface AnnotationSupport<TQuery extends DataQuery = DataQuery, TAnno = AnnotationQuery<TQuery>> {
/**
* This hook lets you manipulate any existing stored values before running them though the processor.
* This is particularly helpful when dealing with migrating old formats. ie query as a string vs object
* This is particularly helpful when dealing with migrating old formats. ie query as a string vs object.
*/
prepareAnnotation?(json: any): TAnno;
@@ -105,5 +111,5 @@ export interface AnnotationSupport<TQuery extends DataQuery = DataQuery, TAnno =
/**
* Specify a custom QueryEditor for the annotation page. If not specified, the standard one will be used
*/
QueryEditor?: ComponentType<QueryEditorProps<any, TQuery>>;
QueryEditor?: ComponentType<AnnotationQueryEditorProps<TQuery>>;
}
@@ -9,7 +9,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { PanelModel } from 'app/features/dashboard/state';
import { executeAnnotationQuery } from '../executeAnnotationQuery';
import { standardAnnotationSupport } from '../standardAnnotationSupport';
import { shouldUseLegacyRunner, shouldUseMappingUI, standardAnnotationSupport } from '../standardAnnotationSupport';
import { AnnotationQueryResponse } from '../types';
import { AnnotationFieldMapper } from './AnnotationResultMapper';
@@ -33,7 +33,7 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
}
componentDidUpdate(oldProps: Props) {
if (this.props.annotation !== oldProps.annotation) {
if (this.props.annotation !== oldProps.annotation && !shouldUseLegacyRunner(this.props.datasource)) {
this.verifyDataSource();
}
}
@@ -57,6 +57,13 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
onRunQuery = async () => {
const { datasource, annotation } = this.props;
if (shouldUseLegacyRunner(datasource)) {
// In the new UI the running of query is done so the data can be mapped. In the legacy annotations this does
// not exist as the annotationQuery already returns annotation events which cannot be mapped. This means that
// right now running a query for data source with legacy runner does not make much sense.
return;
}
const dashboard = getDashboardSrv().getCurrent();
if (!dashboard) {
return;
@@ -156,11 +163,15 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
);
}
onAnnotationChange = (annotation: AnnotationQuery) => {
this.props.onChange(annotation);
};
render() {
const { datasource, annotation } = this.props;
const { response } = this.state;
// Find the annotaiton runner
// Find the annotation runner
let QueryEditor = datasource.annotations?.QueryEditor || datasource.components?.QueryEditor;
if (!QueryEditor) {
return <div>Annotations are not supported. This datasource needs to export a QueryEditor</div>;
@@ -177,8 +188,10 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
onRunQuery={this.onRunQuery}
data={response?.panelData}
range={getTimeSrv().timeRange()}
annotation={annotation}
onAnnotationChange={this.onAnnotationChange}
/>
{datasource.type !== 'datasource' && (
{shouldUseMappingUI(datasource) && (
<>
{this.renderStatus()}
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
@@ -9,6 +9,7 @@ import {
AnnotationQuery,
AnnotationSupport,
DataFrame,
DataSourceApi,
Field,
FieldType,
getFieldDisplayName,
@@ -26,6 +27,7 @@ export const standardAnnotationSupport: AnnotationSupport = {
return {
...rest,
target: {
refId: 'annotation_query',
query,
},
mappings: {},
@@ -35,14 +37,12 @@ export const standardAnnotationSupport: AnnotationSupport = {
},
/**
* Convert the stored JSON model and environment to a standard data source query object.
* This query will be executed in the data source and the results converted into events.
* Returning an undefined result will quietly skip query execution
* Default will just return target from the annotation.
*/
prepareQuery: (anno: AnnotationQuery) => anno.target,
/**
* When the standard frame > event processing is insufficient, this allows explicit control of the mappings
* Provides default processing from dataFrame to annotation events.
*/
processEvents: (anno: AnnotationQuery, data: DataFrame[]) => {
return getAnnotationsFromData(data, anno.mappings);
@@ -50,7 +50,7 @@ export const standardAnnotationSupport: AnnotationSupport = {
};
/**
* Flatten all panel data into a single frame
* Flatten all frames into a single frame with mergeTransformer.
*/
export function singleFrameFromPanelData(): OperatorFunction<DataFrame[], DataFrame | undefined> {
@@ -226,3 +226,21 @@ export function getAnnotationsFromData(
})
);
}
// These opt outs are here only for quicker and easier migration to react based annotations editors and because
// annotation support API needs some work to support less "standard" editors like prometheus and here it is not
// polluting public API.
/**
* Opt out of using the default mapping functionality on frontend.
*/
export function shouldUseMappingUI(datasource: DataSourceApi): boolean {
return datasource.type !== 'prometheus';
}
/**
* Use legacy runner. Used only as an escape hatch for easier transition to React based annotation editor.
*/
export function shouldUseLegacyRunner(datasource: DataSourceApi): boolean {
return datasource.type === 'prometheus';
}
@@ -15,7 +15,7 @@ export class AnnotationsQueryRunner implements AnnotationQueryRunner {
return false;
}
return !Boolean(datasource.annotationQuery && !datasource.annotations);
return Boolean(!datasource.annotationQuery || datasource.annotations);
}
run({ annotation, datasource, dashboard, range }: AnnotationQueryRunnerOptions): Observable<AnnotationEvent[]> {
@@ -2,6 +2,7 @@ import { from, Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AnnotationEvent, DataSourceApi } from '@grafana/data';
import { shouldUseLegacyRunner } from 'app/features/annotations/standardAnnotationSupport';
import { AnnotationQueryRunner, AnnotationQueryRunnerOptions } from './types';
import { handleAnnotationQueryRunnerError } from './utils';
@@ -12,6 +13,10 @@ export class LegacyAnnotationQueryRunner implements AnnotationQueryRunner {
return false;
}
if (shouldUseLegacyRunner(datasource)) {
return true;
}
return Boolean(datasource.annotationQuery && !datasource.annotations);
}
@@ -99,7 +99,15 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
>
Run query
</Button>
<QueryEditorModeToggle mode={editorMode!} onChange={onEditorModeChange} />
<QueryEditorModeToggle
mode={editorMode!}
onChange={onEditorModeChange}
uiOptions={{
[QueryEditorMode.Explain]: true,
[QueryEditorMode.Code]: true,
[QueryEditorMode.Builder]: true,
}}
/>
</EditorHeader>
<Space v={0.5} />
<EditorRows>
@@ -0,0 +1,121 @@
import React from 'react';
import { AnnotationQuery } from '@grafana/data';
import { EditorRow, EditorField, EditorSwitch, Space } from '@grafana/experimental';
import { Input } from '@grafana/ui';
import { PromQueryEditorSelector } from '../querybuilder/components/PromQueryEditorSelector';
import { QueryEditorMode } from '../querybuilder/shared/types';
import { PromQuery } from '../types';
import { PromQueryEditorProps } from './types';
type Props = PromQueryEditorProps & {
annotation?: AnnotationQuery<PromQuery>;
onAnnotationChange?: (annotation: AnnotationQuery<PromQuery>) => void;
};
export function AnnotationQueryEditor(props: Props) {
// This is because of problematic typing. See AnnotationQueryEditorProps in grafana-data/annotations.ts.
const annotation = props.annotation!;
const onAnnotationChange = props.onAnnotationChange!;
return (
<>
<PromQueryEditorSelector
{...props}
query={{ expr: annotation.expr, refId: annotation.name, interval: annotation.step }}
onChange={(query) =>
onAnnotationChange({
...annotation,
expr: query.expr,
step: query.interval,
})
}
uiOptions={{
modes: {
[QueryEditorMode.Explain]: false,
[QueryEditorMode.Code]: true,
[QueryEditorMode.Builder]: true,
},
runQueryButton: false,
options: {
exemplars: false,
type: false,
format: false,
minStep: true,
legend: false,
resolution: false,
},
}}
/>
<Space v={0.5} />
<EditorRow>
<EditorField
label="Title"
tooltip={
'Use either the name or a pattern. For example, {{instance}} is replaced with label value for the label instance.'
}
>
<Input
type="text"
placeholder="alertname"
value={annotation.titleFormat}
onChange={(event) => {
onAnnotationChange({
...annotation,
titleFormat: event.currentTarget.value,
});
}}
/>
</EditorField>
<EditorField label="Tags">
<Input
type="text"
placeholder="label1,label2"
value={annotation.tagKeys}
onChange={(event) => {
onAnnotationChange({
...annotation,
tagKeys: event.currentTarget.value,
});
}}
/>
</EditorField>
<EditorField
label="Text"
tooltip={
'Use either the name or a pattern. For example, {{instance}} is replaced with label value for the label instance.'
}
>
<Input
type="text"
placeholder="instance"
value={annotation.textFormat}
onChange={(event) => {
onAnnotationChange({
...annotation,
textFormat: event.currentTarget.value,
});
}}
/>
</EditorField>
<EditorField
label="Series value as timestamp"
tooltip={
'The unit of timestamp is milliseconds. If the unit of the series value is seconds, multiply its range vector by 1000.'
}
>
<EditorSwitch
value={annotation.useValueForTime}
onChange={(event) => {
onAnnotationChange({
...annotation,
useValueForTime: event.currentTarget.value,
});
}}
/>
</EditorField>
</EditorRow>
</>
);
}
@@ -40,6 +40,7 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr
import { PromApplication, PromApiFeatures } from 'app/types/unified-alerting-dto';
import { addLabelToQuery } from './add_label_to_query';
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
import PrometheusLanguageProvider from './language_provider';
import { expandRecordingRules } from './language_utils';
import { renderLegendFormat } from './legend';
@@ -119,6 +120,14 @@ export class PrometheusDatasource
this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters);
this.variables = new PrometheusVariableSupport(this, this.templateSrv, this.timeSrv);
this.exemplarsAvailable = true;
// This needs to be here and cannot be static because of how annotations typing affects casting of data source
// objects to DataSourceApi types.
// We don't use the default processing for prometheus.
// See standardAnnotationSupport.ts/[shouldUseMappingUI|shouldUseLegacyRunner]
this.annotations = {
QueryEditor: AnnotationQueryEditor,
};
}
init = async () => {
@@ -3,7 +3,6 @@ import React, { useState } from 'react';
import { DataSourceApi, SelectableValue, toOption } from '@grafana/data';
import { Select } from '@grafana/ui';
import { PrometheusDatasource } from '../../datasource';
import { promQueryModeller } from '../PromQueryModeller';
import { getOperationParamId } from '../shared/operationUtils';
import { QueryBuilderLabelFilter, QueryBuilderOperationParamEditorProps } from '../shared/types';
@@ -49,8 +48,8 @@ async function loadGroupByLabels(
): Promise<Array<SelectableValue<any>>> {
let labels: QueryBuilderLabelFilter[] = query.labels;
// This function is used by both Prometheus and Loki and this the only difference
if (datasource instanceof PrometheusDatasource) {
// This function is used by both Prometheus and Loki and this the only difference.
if (datasource.type === 'prometheus') {
labels = [{ label: '__name__', op: '=', value: query.metric }, ...query.labels];
}
@@ -78,6 +78,14 @@ function setup(queryOverrides: Partial<PromQuery> = {}) {
},
onRunQuery: jest.fn(),
onChange: jest.fn(),
uiOptions: {
exemplars: true,
type: true,
format: true,
minStep: true,
legend: true,
resolution: true,
},
};
const { container } = render(<PromQueryBuilderOptions {...props} />);
@@ -12,14 +12,24 @@ import { QueryOptionGroup } from '../shared/QueryOptionGroup';
import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor';
export interface UIOptions {
exemplars: boolean;
type: boolean;
format: boolean;
minStep: boolean;
legend: boolean;
resolution: boolean;
}
export interface Props {
query: PromQuery;
app?: CoreApp;
onChange: (update: PromQuery) => void;
onRunQuery: () => void;
uiOptions: UIOptions;
}
export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange, onRunQuery }) => {
export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange, onRunQuery, uiOptions }) => {
const onChangeFormat = (value: SelectableValue<string>) => {
onChange({ ...query, format: value.value });
onRunQuery();
@@ -50,42 +60,53 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
return (
<EditorRow>
<QueryOptionGroup title="Options" collapsedInfo={getCollapsedInfo(query, formatOption.label!, queryTypeLabel)}>
<PromQueryLegendEditor
legendFormat={query.legendFormat}
onChange={(legendFormat) => onChange({ ...query, legendFormat })}
onRunQuery={onRunQuery}
/>
<EditorField
label="Min step"
tooltip={
<>
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
<code>$__interval</code> and <code>$__rate_interval</code> variables.
</>
}
>
<AutoSizeInput
type="text"
aria-label="Set lower limit for the step parameter"
placeholder={'auto'}
minWidth={10}
onCommitChange={onChangeStep}
defaultValue={query.interval}
<QueryOptionGroup
title="Options"
collapsedInfo={getCollapsedInfo(query, formatOption.label!, queryTypeLabel, uiOptions)}
>
{uiOptions.legend && (
<PromQueryLegendEditor
legendFormat={query.legendFormat}
onChange={(legendFormat) => onChange({ ...query, legendFormat })}
onRunQuery={onRunQuery}
/>
</EditorField>
<EditorField label="Format">
<Select value={formatOption} allowCustomValue onChange={onChangeFormat} options={FORMAT_OPTIONS} />
</EditorField>
<EditorField label="Type">
<RadioButtonGroup options={queryTypeOptions} value={queryTypeValue} onChange={onQueryTypeChange} />
</EditorField>
{shouldShowExemplarSwitch(query, app) && (
)}
{uiOptions.minStep && (
<EditorField
label="Min step"
tooltip={
<>
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
<code>$__interval</code> and <code>$__rate_interval</code> variables.
</>
}
>
<AutoSizeInput
type="text"
aria-label="Set lower limit for the step parameter"
placeholder={'auto'}
minWidth={10}
onCommitChange={onChangeStep}
defaultValue={query.interval}
/>
</EditorField>
)}
{uiOptions.format && (
<EditorField label="Format">
<Select value={formatOption} allowCustomValue onChange={onChangeFormat} options={FORMAT_OPTIONS} />
</EditorField>
)}
{uiOptions.type && (
<EditorField label="Type">
<RadioButtonGroup options={queryTypeOptions} value={queryTypeValue} onChange={onQueryTypeChange} />
</EditorField>
)}
{uiOptions.exemplars && shouldShowExemplarSwitch(query, app) && (
<EditorField label="Exemplars">
<EditorSwitch value={query.exemplar} onChange={onExemplarChange} />
</EditorField>
)}
{query.intervalFactor && query.intervalFactor > 1 && (
{uiOptions.resolution && query.intervalFactor && query.intervalFactor > 1 && (
<EditorField label="Resolution">
<Select
aria-label="Select resolution"
@@ -113,19 +134,23 @@ function getQueryTypeValue(query: PromQuery) {
return query.range && query.instant ? 'both' : query.instant ? 'instant' : 'range';
}
function getCollapsedInfo(query: PromQuery, formatOption: string, queryType: string): string[] {
function getCollapsedInfo(query: PromQuery, formatOption: string, queryType: string, uiOptions: UIOptions): string[] {
const items: string[] = [];
items.push(`Legend: ${getLegendModeLabel(query.legendFormat)}`);
items.push(`Format: ${formatOption}`);
if (query.interval) {
if (uiOptions.legend) {
items.push(`Legend: ${getLegendModeLabel(query.legendFormat)}`);
}
if (uiOptions.format) {
items.push(`Format: ${formatOption}`);
}
if (uiOptions.minStep && query.interval) {
items.push(`Step ${query.interval}`);
}
if (uiOptions.type) {
items.push(`Type: ${queryType}`);
}
items.push(`Type: ${queryType}`);
if (query.exemplar) {
if (uiOptions.exemplars && query.exemplar) {
items.push(`Exemplars: true`);
}
@@ -10,17 +10,43 @@ import { PromQuery } from '../../types';
import { promQueryModeller } from '../PromQueryModeller';
import { buildVisualQueryFromString } from '../parsing';
import { FeedbackLink } from '../shared/FeedbackLink';
import { QueryEditorModeToggle } from '../shared/QueryEditorModeToggle';
import { QueryEditorModeToggle, UIOptions as ModeToggleUIOptions } from '../shared/QueryEditorModeToggle';
import { QueryHeaderSwitch } from '../shared/QueryHeaderSwitch';
import { QueryEditorMode } from '../shared/types';
import { changeEditorMode, getQueryWithDefaults } from '../state';
import { PromQueryBuilderContainer } from './PromQueryBuilderContainer';
import { PromQueryBuilderExplained } from './PromQueryBuilderExplained';
import { PromQueryBuilderOptions } from './PromQueryBuilderOptions';
import { PromQueryBuilderOptions, UIOptions as OptionsUIOptions } from './PromQueryBuilderOptions';
import { PromQueryCodeEditor } from './PromQueryCodeEditor';
export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props) => {
interface UIOptions {
modes: ModeToggleUIOptions;
runQueryButton: boolean;
options: OptionsUIOptions;
}
const defaultOptions: UIOptions = {
modes: {
[QueryEditorMode.Explain]: true,
[QueryEditorMode.Code]: true,
[QueryEditorMode.Builder]: true,
},
runQueryButton: true,
options: {
exemplars: true,
type: true,
format: true,
minStep: true,
legend: true,
resolution: true,
},
};
type Props = PromQueryEditorProps & { uiOptions?: UIOptions };
export const PromQueryEditorSelector = React.memo<Props>((props) => {
const uiOptions = props.uiOptions || defaultOptions;
const { onChange, onRunQuery, data, app } = props;
const [parseModalOpen, setParseModalOpen] = useState(false);
const [dataIsStale, setDataIsStale] = useState(false);
@@ -104,16 +130,18 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
<FeedbackLink feedbackUrl="https://github.com/grafana/grafana/discussions/47693" />
)}
<FlexItem grow={1} />
<Button
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
Run query
</Button>
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} />
{uiOptions.runQueryButton && (
<Button
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
Run query
</Button>
)}
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} uiOptions={uiOptions.modes} />
</EditorHeader>
<Space v={0.5} />
<EditorRows>
@@ -129,7 +157,13 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
)}
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={query.expr} />}
{editorMode !== QueryEditorMode.Explain && (
<PromQueryBuilderOptions query={query} app={props.app} onChange={onChange} onRunQuery={onRunQuery} />
<PromQueryBuilderOptions
query={query}
app={props.app}
onChange={onChange}
onRunQuery={onRunQuery}
uiOptions={uiOptions.options}
/>
)}
</EditorRows>
</>
@@ -5,9 +5,14 @@ import { RadioButtonGroup, Tag } from '@grafana/ui';
import { QueryEditorMode } from './types';
export type UIOptions = {
[key in QueryEditorMode]: boolean;
};
export interface Props {
mode: QueryEditorMode;
onChange: (mode: QueryEditorMode) => void;
uiOptions: UIOptions;
}
const editorModes = [
@@ -30,10 +35,11 @@ const editorModes = [
{ label: 'Code', value: QueryEditorMode.Code },
];
export function QueryEditorModeToggle({ mode, onChange }: Props) {
export function QueryEditorModeToggle({ mode, onChange, uiOptions }: Props) {
const modes = editorModes.filter((m) => uiOptions[m.value]);
return (
<div data-testid={'QueryEditorModeToggle'}>
<RadioButtonGroup options={editorModes} size="sm" value={mode} onChange={onChange} />
<RadioButtonGroup options={modes} size="sm" value={mode} onChange={onChange} />
</div>
);
}