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:
co-authored by
Ryan McKinley
parent
1c0019285f
commit
4124294011
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -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 () => {
|
||||
|
||||
+2
-3
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -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} />);
|
||||
|
||||
+65
-40
@@ -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`);
|
||||
}
|
||||
|
||||
|
||||
+48
-14
@@ -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>
|
||||
</>
|
||||
|
||||
+8
-2
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user