mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
Annotations: add standard annotations support (and use it for flux queries) (#27375)
This commit is contained in:
parent
4707508f4b
commit
5d11d8faa3
87
packages/grafana-data/src/types/annotations.ts
Normal file
87
packages/grafana-data/src/types/annotations.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { DataQuery, QueryEditorProps } from './datasource';
|
||||
import { DataFrame } from './dataFrame';
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
/**
|
||||
* This JSON object is stored in the dashboard json model.
|
||||
*/
|
||||
export interface AnnotationQuery<TQuery extends DataQuery = DataQuery> {
|
||||
datasource: string;
|
||||
enable: boolean;
|
||||
name: string;
|
||||
iconColor: string;
|
||||
|
||||
// Standard datasource query
|
||||
target?: TQuery;
|
||||
|
||||
// Convert a dataframe to an AnnotationEvent
|
||||
mappings?: AnnotationEventMappings;
|
||||
}
|
||||
|
||||
export interface AnnotationEvent {
|
||||
id?: string;
|
||||
annotation?: any;
|
||||
dashboardId?: number;
|
||||
panelId?: number;
|
||||
userId?: number;
|
||||
login?: string;
|
||||
email?: string;
|
||||
avatarUrl?: string;
|
||||
time?: number;
|
||||
timeEnd?: number;
|
||||
isRegion?: boolean;
|
||||
title?: string;
|
||||
text?: string;
|
||||
type?: string;
|
||||
tags?: string[];
|
||||
|
||||
// Currently used to merge annotations from alerts and dashboard
|
||||
source?: any; // source.type === 'dashboard'
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha -- any value other than `field` is experimental
|
||||
*/
|
||||
export enum AnnotationEventFieldSource {
|
||||
Field = 'field', // Default -- find the value with a matching key
|
||||
Text = 'text', // Write a constant string into the value
|
||||
Skip = 'skip', // Do not include the field
|
||||
}
|
||||
|
||||
export interface AnnotationEventFieldMapping {
|
||||
source?: AnnotationEventFieldSource; // defautls to 'field'
|
||||
value?: string;
|
||||
regex?: string;
|
||||
}
|
||||
|
||||
export type AnnotationEventMappings = Partial<Record<keyof AnnotationEvent, AnnotationEventFieldMapping>>;
|
||||
|
||||
/**
|
||||
* Since Grafana 7.2
|
||||
*
|
||||
* This offers a generic approach to annotation processing
|
||||
*/
|
||||
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
|
||||
*/
|
||||
prepareAnnotation?(json: any): TAnno;
|
||||
|
||||
/**
|
||||
* Convert the stored JSON model to a standard datasource query object.
|
||||
* This query will be executed in the datasource and the results converted into events.
|
||||
* Returning an undefined result will quietly skip query execution
|
||||
*/
|
||||
prepareQuery?(anno: TAnno): TQuery | undefined;
|
||||
|
||||
/**
|
||||
* When the standard frame > event processing is insufficient, this allows explicit control of the mappings
|
||||
*/
|
||||
processEvents?(anno: TAnno, data: DataFrame): AnnotationEvent[] | undefined;
|
||||
|
||||
/**
|
||||
* Specify a custom QueryEditor for the annotation page. If not specified, the standard one will be used
|
||||
*/
|
||||
QueryEditor?: ComponentType<QueryEditorProps<any, TQuery>>;
|
||||
}
|
@ -132,27 +132,6 @@ export enum NullValueMode {
|
||||
AsZero = 'null as zero',
|
||||
}
|
||||
|
||||
export interface AnnotationEvent {
|
||||
id?: string;
|
||||
annotation?: any;
|
||||
dashboardId?: number;
|
||||
panelId?: number;
|
||||
userId?: number;
|
||||
login?: string;
|
||||
email?: string;
|
||||
avatarUrl?: string;
|
||||
time?: number;
|
||||
timeEnd?: number;
|
||||
isRegion?: boolean;
|
||||
title?: string;
|
||||
text?: string;
|
||||
type?: string;
|
||||
tags?: string[];
|
||||
|
||||
// Currently used to merge annotations from alerts and dashboard
|
||||
source?: any; // source.type === 'dashboard'
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes and API for exposing panel specific data configurations.
|
||||
*/
|
||||
|
@ -3,7 +3,8 @@ import { ComponentType } from 'react';
|
||||
import { GrafanaPlugin, PluginMeta } from './plugin';
|
||||
import { PanelData } from './panel';
|
||||
import { LogRowModel } from './logs';
|
||||
import { AnnotationEvent, KeyValue, LoadingState, TableData, TimeSeries } from './data';
|
||||
import { AnnotationEvent, AnnotationSupport } from './annotations';
|
||||
import { KeyValue, LoadingState, TableData, TimeSeries } from './data';
|
||||
import { DataFrame, DataFrameDTO } from './dataFrame';
|
||||
import { RawTimeRange, TimeRange } from './time';
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
@ -155,8 +156,7 @@ export interface DataSourceConstructor<
|
||||
*/
|
||||
export abstract class DataSourceApi<
|
||||
TQuery extends DataQuery = DataQuery,
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData,
|
||||
TAnno = TQuery // defatult to direct query
|
||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
||||
> {
|
||||
/**
|
||||
* Set in constructor
|
||||
@ -267,13 +267,23 @@ export abstract class DataSourceApi<
|
||||
|
||||
showContextToggle?(row?: LogRowModel): boolean;
|
||||
|
||||
/**
|
||||
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard. To be visible
|
||||
* in the annotation editor `annotations` capability also needs to be enabled in plugin.json.
|
||||
*/
|
||||
annotationQuery?(options: AnnotationQueryRequest<TAnno>): Promise<AnnotationEvent[]>;
|
||||
|
||||
interpolateVariablesInQueries?(queries: TQuery[], scopedVars: ScopedVars | {}): TQuery[];
|
||||
|
||||
/**
|
||||
* An annotation processor allows explict control for how annotations are managed.
|
||||
*
|
||||
* It is only necessary to configure an annotation processor if the default behavior is not desirable
|
||||
*/
|
||||
annotations?: AnnotationSupport<TQuery>;
|
||||
|
||||
/**
|
||||
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard.
|
||||
* This function will only be called if an angular {@link AnnotationsQueryCtrl} is configured and
|
||||
* the {@link annotations} is undefined
|
||||
*
|
||||
* @deprecated -- prefer using {@link AnnotationSupport}
|
||||
*/
|
||||
annotationQuery?(options: AnnotationQueryRequest<TQuery>): Promise<AnnotationEvent[]>;
|
||||
}
|
||||
|
||||
export interface MetadataInspectorProps<
|
||||
@ -473,12 +483,6 @@ export interface MetricFindValue {
|
||||
expandable?: boolean;
|
||||
}
|
||||
|
||||
export interface BaseAnnotationQuery {
|
||||
datasource: string;
|
||||
enable: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DataSourceJsonData {
|
||||
authType?: string;
|
||||
defaultRegion?: string;
|
||||
@ -547,20 +551,19 @@ export interface DataSourceSelectItem {
|
||||
|
||||
/**
|
||||
* Options passed to the datasource.annotationQuery method. See docs/plugins/developing/datasource.md
|
||||
*
|
||||
* @deprecated -- use {@link AnnotationSupport}
|
||||
*/
|
||||
export interface AnnotationQueryRequest<TAnno = {}> {
|
||||
export interface AnnotationQueryRequest<MoreOptions = {}> {
|
||||
range: TimeRange;
|
||||
rangeRaw: RawTimeRange;
|
||||
interval: string;
|
||||
intervalMs: number;
|
||||
maxDataPoints?: number;
|
||||
app: CoreApp | string;
|
||||
|
||||
// Should be DataModel but cannot import that here from the main app. Needs to be moved to package first.
|
||||
dashboard: any;
|
||||
|
||||
// The annotation query and common properties
|
||||
annotation: BaseAnnotationQuery & TAnno;
|
||||
annotation: {
|
||||
datasource: string;
|
||||
enable: boolean;
|
||||
name: string;
|
||||
} & MoreOptions;
|
||||
}
|
||||
|
||||
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
|
||||
|
@ -1,6 +1,7 @@
|
||||
export * from './data';
|
||||
export * from './dataFrame';
|
||||
export * from './dataLink';
|
||||
export * from './annotations';
|
||||
export * from './logs';
|
||||
export * from './navModel';
|
||||
export * from './select';
|
||||
|
@ -122,6 +122,8 @@ export class DataSourceWithBackend<
|
||||
/**
|
||||
* Override to skip executing a query
|
||||
*
|
||||
* @returns false if the query should be skipped
|
||||
*
|
||||
* @virtual
|
||||
*/
|
||||
filterQuery?(query: TQuery): boolean;
|
||||
|
@ -7,12 +7,30 @@ import coreModule from 'app/core/core_module';
|
||||
// Utils & Services
|
||||
import { dedupAnnotations } from './events_processing';
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../dashboard/state';
|
||||
import { AnnotationEvent, AppEvents, DataSourceApi, PanelEvents, TimeRange, CoreApp } from '@grafana/data';
|
||||
import { DashboardModel } from '../dashboard/state';
|
||||
import {
|
||||
AnnotationEvent,
|
||||
AppEvents,
|
||||
DataSourceApi,
|
||||
PanelEvents,
|
||||
rangeUtil,
|
||||
DataQueryRequest,
|
||||
CoreApp,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { getTimeSrv } from '../dashboard/services/TimeSrv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { AnnotationQueryResponse, AnnotationQueryOptions } from './types';
|
||||
import { standardAnnotationSupport, singleFrameFromPanelData } from './standardAnnotationSupport';
|
||||
import { runRequest } from '../dashboard/state/runRequest';
|
||||
|
||||
let counter = 100;
|
||||
function getNextRequestId() {
|
||||
return 'AQ' + counter++;
|
||||
}
|
||||
|
||||
export class AnnotationsSrv {
|
||||
globalAnnotationsPromise: any;
|
||||
@ -32,7 +50,7 @@ export class AnnotationsSrv {
|
||||
this.datasourcePromises = null;
|
||||
}
|
||||
|
||||
getAnnotations(options: { dashboard: DashboardModel; panel: PanelModel; range: TimeRange }) {
|
||||
getAnnotations(options: AnnotationQueryOptions) {
|
||||
return Promise.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
|
||||
.then(results => {
|
||||
// combine the annotations and flatten results
|
||||
@ -103,7 +121,7 @@ export class AnnotationsSrv {
|
||||
return this.alertStatesPromise;
|
||||
}
|
||||
|
||||
getGlobalAnnotations(options: { dashboard: DashboardModel; panel: PanelModel; range: TimeRange }) {
|
||||
getGlobalAnnotations(options: AnnotationQueryOptions) {
|
||||
const dashboard = options.dashboard;
|
||||
|
||||
if (this.globalAnnotationsPromise) {
|
||||
@ -114,9 +132,6 @@ export class AnnotationsSrv {
|
||||
const promises = [];
|
||||
const dsPromises = [];
|
||||
|
||||
// No more points than pixels
|
||||
const maxDataPoints = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
|
||||
for (const annotation of dashboard.annotations.list) {
|
||||
if (!annotation.enable) {
|
||||
continue;
|
||||
@ -130,21 +145,21 @@ export class AnnotationsSrv {
|
||||
promises.push(
|
||||
datasourcePromise
|
||||
.then((datasource: DataSourceApi) => {
|
||||
if (!datasource.annotationQuery) {
|
||||
return [];
|
||||
// Use the legacy annotationQuery unless annotation support is explicitly defined
|
||||
if (datasource.annotationQuery && !datasource.annotations) {
|
||||
return datasource.annotationQuery({
|
||||
range,
|
||||
rangeRaw: range.raw,
|
||||
annotation: annotation,
|
||||
dashboard: dashboard,
|
||||
});
|
||||
}
|
||||
|
||||
// Add interval to annotation queries
|
||||
const interval = kbn.calculateInterval(range, maxDataPoints, datasource.interval);
|
||||
|
||||
return datasource.annotationQuery({
|
||||
...interval,
|
||||
app: CoreApp.Dashboard,
|
||||
range,
|
||||
rangeRaw: range.raw,
|
||||
annotation: annotation,
|
||||
dashboard: dashboard,
|
||||
});
|
||||
// Note: future annotatoin lifecycle will use observables directly
|
||||
return executeAnnotationQuery(options, datasource, annotation)
|
||||
.toPromise()
|
||||
.then(res => {
|
||||
return res.events ?? [];
|
||||
});
|
||||
})
|
||||
.then(results => {
|
||||
// store response in annotation object if this is a snapshot call
|
||||
@ -195,4 +210,64 @@ export class AnnotationsSrv {
|
||||
}
|
||||
}
|
||||
|
||||
export function executeAnnotationQuery(
|
||||
options: AnnotationQueryOptions,
|
||||
datasource: DataSourceApi,
|
||||
savedJsonAnno: any
|
||||
): Observable<AnnotationQueryResponse> {
|
||||
const processor = {
|
||||
...standardAnnotationSupport,
|
||||
...datasource.annotations,
|
||||
};
|
||||
|
||||
const annotation = processor.prepareAnnotation!(savedJsonAnno);
|
||||
if (!annotation) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
const query = processor.prepareQuery!(annotation);
|
||||
if (!query) {
|
||||
return of({});
|
||||
}
|
||||
|
||||
// No more points than pixels
|
||||
const maxDataPoints = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
|
||||
// Add interval to annotation queries
|
||||
const interval = rangeUtil.calculateInterval(options.range, maxDataPoints, datasource.interval);
|
||||
|
||||
const scopedVars: ScopedVars = {
|
||||
__interval: { text: interval.interval, value: interval.interval },
|
||||
__interval_ms: { text: interval.intervalMs.toString(), value: interval.intervalMs },
|
||||
__annotation: { text: annotation.name, value: annotation },
|
||||
};
|
||||
|
||||
const queryRequest: DataQueryRequest = {
|
||||
startTime: Date.now(),
|
||||
requestId: getNextRequestId(),
|
||||
range: options.range,
|
||||
maxDataPoints,
|
||||
scopedVars,
|
||||
...interval,
|
||||
app: CoreApp.Dashboard,
|
||||
|
||||
timezone: options.dashboard.timezone,
|
||||
|
||||
targets: [
|
||||
{
|
||||
...query,
|
||||
refId: 'Anno',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return runRequest(datasource, queryRequest).pipe(
|
||||
map(panelData => {
|
||||
const frame = singleFrameFromPanelData(panelData);
|
||||
const events = frame ? processor.processEvents!(annotation, frame) : [];
|
||||
return { panelData, frame, events };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
coreModule.service('annotationsSrv', AnnotationsSrv);
|
||||
|
@ -0,0 +1,195 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import {
|
||||
SelectableValue,
|
||||
getFieldDisplayName,
|
||||
AnnotationEvent,
|
||||
AnnotationEventMappings,
|
||||
AnnotationEventFieldMapping,
|
||||
formattedValueToString,
|
||||
AnnotationEventFieldSource,
|
||||
getValueFormat,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { annotationEventNames, AnnotationFieldInfo } from '../standardAnnotationSupport';
|
||||
import { Select, Tooltip, Icon } from '@grafana/ui';
|
||||
import { AnnotationQueryResponse } from '../types';
|
||||
|
||||
// const valueOptions: Array<SelectableValue<AnnotationEventFieldSource>> = [
|
||||
// { value: AnnotationEventFieldSource.Field, label: 'Field', description: 'Set the field value from a response field' },
|
||||
// { value: AnnotationEventFieldSource.Text, label: 'Text', description: 'Enter direct text for the value' },
|
||||
// { value: AnnotationEventFieldSource.Skip, label: 'Skip', description: 'Hide this field' },
|
||||
// ];
|
||||
|
||||
interface Props {
|
||||
response?: AnnotationQueryResponse;
|
||||
|
||||
mappings?: AnnotationEventMappings;
|
||||
|
||||
change: (mappings?: AnnotationEventMappings) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
fieldNames: Array<SelectableValue<string>>;
|
||||
}
|
||||
|
||||
export class AnnotationFieldMapper extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fieldNames: [],
|
||||
};
|
||||
}
|
||||
|
||||
updateFields = () => {
|
||||
const frame = this.props.response?.frame;
|
||||
if (frame && frame.fields) {
|
||||
const fieldNames = frame.fields.map(f => {
|
||||
const name = getFieldDisplayName(f, frame);
|
||||
|
||||
let description = '';
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
if (i > 0) {
|
||||
description += ', ';
|
||||
}
|
||||
if (i > 2) {
|
||||
description += '...';
|
||||
break;
|
||||
}
|
||||
description += f.values.get(i);
|
||||
}
|
||||
|
||||
if (description.length > 50) {
|
||||
description = description.substring(0, 50) + '...';
|
||||
}
|
||||
|
||||
return {
|
||||
label: `${name} (${f.type})`,
|
||||
value: name,
|
||||
description,
|
||||
};
|
||||
});
|
||||
this.setState({ fieldNames });
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: Props) {
|
||||
if (oldProps.response !== this.props.response) {
|
||||
this.updateFields();
|
||||
}
|
||||
}
|
||||
|
||||
onFieldSourceChange = (k: keyof AnnotationEvent, v: SelectableValue<AnnotationEventFieldSource>) => {
|
||||
const mappings = this.props.mappings || {};
|
||||
const mapping = mappings[k] || {};
|
||||
|
||||
this.props.change({
|
||||
...mappings,
|
||||
[k]: {
|
||||
...mapping,
|
||||
source: v.value || AnnotationEventFieldSource.Field,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onFieldNameChange = (k: keyof AnnotationEvent, v: SelectableValue<string>) => {
|
||||
const mappings = this.props.mappings || {};
|
||||
const mapping = mappings[k] || {};
|
||||
|
||||
this.props.change({
|
||||
...mappings,
|
||||
[k]: {
|
||||
...mapping,
|
||||
value: v.value,
|
||||
source: AnnotationEventFieldSource.Field,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
renderRow(row: AnnotationFieldInfo, mapping: AnnotationEventFieldMapping, first?: AnnotationEvent) {
|
||||
const { fieldNames } = this.state;
|
||||
|
||||
let picker = fieldNames;
|
||||
const current = mapping.value;
|
||||
let currentValue = fieldNames.find(f => current === f.value);
|
||||
if (current) {
|
||||
picker = [...fieldNames];
|
||||
if (!currentValue) {
|
||||
picker.push({
|
||||
label: current,
|
||||
value: current,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let value = first ? first[row.key] : '';
|
||||
if (value && row.key.startsWith('time')) {
|
||||
const fmt = getValueFormat('dateTimeAsIso');
|
||||
value = formattedValueToString(fmt(value as number));
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
value = ''; // empty string
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={row.key}>
|
||||
<td>
|
||||
{row.key}{' '}
|
||||
{row.help && (
|
||||
<Tooltip content={row.help}>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
{/* <td>
|
||||
<Select
|
||||
value={valueOptions.find(v => v.value === mapping.source) || valueOptions[0]}
|
||||
options={valueOptions}
|
||||
onChange={(v: SelectableValue<AnnotationEventFieldSource>) => {
|
||||
this.onFieldSourceChange(row.key, v);
|
||||
}}
|
||||
/>
|
||||
</td> */}
|
||||
<td>
|
||||
<Select
|
||||
value={currentValue}
|
||||
options={picker}
|
||||
placeholder={row.placeholder || row.key}
|
||||
onChange={(v: SelectableValue<string>) => {
|
||||
this.onFieldNameChange(row.key, v);
|
||||
}}
|
||||
noOptionsMessage="Unknown field names"
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</td>
|
||||
<td>{`${value}`}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const first = this.props.response?.events?.[0];
|
||||
const mappings = this.props.mappings || {};
|
||||
|
||||
return (
|
||||
<table className="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Annotation</th>
|
||||
<th>From</th>
|
||||
<th>First Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{annotationEventNames.map(row => {
|
||||
return this.renderRow(row, mappings[row.key] || {}, first);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { AnnotationEventMappings, DataQuery, LoadingState, DataSourceApi, AnnotationQuery } from '@grafana/data';
|
||||
import { Spinner, Icon, IconName, Button } from '@grafana/ui';
|
||||
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { cx, css } from 'emotion';
|
||||
import { standardAnnotationSupport } from '../standardAnnotationSupport';
|
||||
import { executeAnnotationQuery } from '../annotations_srv';
|
||||
import { PanelModel } from 'app/features/dashboard/state';
|
||||
import { AnnotationQueryResponse } from '../types';
|
||||
import { AnnotationFieldMapper } from './AnnotationResultMapper';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
interface Props {
|
||||
datasource: DataSourceApi;
|
||||
annotation: AnnotationQuery<DataQuery>;
|
||||
change: (annotation: AnnotationQuery<DataQuery>) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
running?: boolean;
|
||||
response?: AnnotationQueryResponse;
|
||||
}
|
||||
|
||||
export default class StandardAnnotationQueryEditor extends PureComponent<Props, State> {
|
||||
state = {} as State;
|
||||
|
||||
componentDidMount() {
|
||||
this.verifyDataSource();
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: Props) {
|
||||
if (this.props.annotation !== oldProps.annotation) {
|
||||
this.verifyDataSource();
|
||||
}
|
||||
}
|
||||
|
||||
verifyDataSource() {
|
||||
const { datasource, annotation } = this.props;
|
||||
|
||||
// Handle any migration issues
|
||||
const processor = {
|
||||
...standardAnnotationSupport,
|
||||
...datasource.annotations,
|
||||
};
|
||||
|
||||
const fixed = processor.prepareAnnotation!(annotation);
|
||||
if (fixed !== annotation) {
|
||||
this.props.change(fixed);
|
||||
} else {
|
||||
this.onRunQuery();
|
||||
}
|
||||
}
|
||||
|
||||
onRunQuery = async () => {
|
||||
const { datasource, annotation } = this.props;
|
||||
this.setState({
|
||||
running: true,
|
||||
});
|
||||
const response = await executeAnnotationQuery(
|
||||
{
|
||||
range: getTimeSrv().timeRange(),
|
||||
panel: {} as PanelModel,
|
||||
dashboard: getDashboardSrv().getCurrent(),
|
||||
},
|
||||
datasource,
|
||||
annotation
|
||||
).toPromise();
|
||||
this.setState({
|
||||
running: false,
|
||||
response,
|
||||
});
|
||||
};
|
||||
|
||||
onQueryChange = (target: DataQuery) => {
|
||||
this.props.change({
|
||||
...this.props.annotation,
|
||||
target,
|
||||
});
|
||||
};
|
||||
|
||||
onMappingChange = (mappings: AnnotationEventMappings) => {
|
||||
this.props.change({
|
||||
...this.props.annotation,
|
||||
mappings,
|
||||
});
|
||||
};
|
||||
|
||||
renderStatus() {
|
||||
const { response, running } = this.state;
|
||||
let rowStyle = 'alert-info';
|
||||
let text = '...';
|
||||
let icon: IconName | undefined = undefined;
|
||||
|
||||
if (running || response?.panelData?.state === LoadingState.Loading || !response) {
|
||||
text = 'loading...';
|
||||
} else {
|
||||
const { events, panelData, frame } = response;
|
||||
|
||||
if (panelData?.error) {
|
||||
rowStyle = 'alert-error';
|
||||
icon = 'exclamation-triangle';
|
||||
text = panelData.error.message ?? 'error';
|
||||
} else if (!events?.length) {
|
||||
rowStyle = 'alert-warning';
|
||||
icon = 'exclamation-triangle';
|
||||
text = 'No events found';
|
||||
} else {
|
||||
text = `${events.length} events (from ${frame?.fields.length} fields)`;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
rowStyle,
|
||||
css`
|
||||
margin: 4px 0px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{icon && (
|
||||
<>
|
||||
<Icon name={icon} />
|
||||
|
||||
</>
|
||||
)}
|
||||
{text}
|
||||
</div>
|
||||
<div>
|
||||
{running ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button variant="secondary" size="xs" onClick={this.onRunQuery}>
|
||||
TEST
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { datasource, annotation } = this.props;
|
||||
const { response } = this.state;
|
||||
|
||||
// Find the annotaiton 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>;
|
||||
}
|
||||
|
||||
const query = annotation.target ?? { refId: 'Anno' };
|
||||
return (
|
||||
<>
|
||||
<QueryEditor
|
||||
key={datasource?.name}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={this.onQueryChange}
|
||||
onRunQuery={this.onRunQuery}
|
||||
data={response?.panelData}
|
||||
range={getTimeSrv().timeRange()}
|
||||
/>
|
||||
{this.renderStatus()}
|
||||
|
||||
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Careful to use a unique directive name! many plugins already use "annotationEditor" and have conflicts
|
||||
coreModule.directive('standardAnnotationEditor', [
|
||||
'reactDirective',
|
||||
(reactDirective: any) => {
|
||||
return reactDirective(StandardAnnotationQueryEditor, ['annotation', 'datasource', 'change']);
|
||||
},
|
||||
]);
|
@ -7,10 +7,13 @@ import DatasourceSrv from '../plugins/datasource_srv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { AppEvents } from '@grafana/data';
|
||||
|
||||
// Registeres the angular directive
|
||||
import './components/StandardAnnotationQueryEditor';
|
||||
|
||||
export class AnnotationsEditorCtrl {
|
||||
mode: any;
|
||||
datasources: any;
|
||||
annotations: any;
|
||||
annotations: any[];
|
||||
currentAnnotation: any;
|
||||
currentDatasource: any;
|
||||
currentIsNew: any;
|
||||
@ -69,6 +72,19 @@ export class AnnotationsEditorCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the react editor
|
||||
*/
|
||||
onAnnotationChange = (annotation: any) => {
|
||||
const currentIndex = this.dashboard.annotations.list.indexOf(this.currentAnnotation);
|
||||
if (currentIndex >= 0) {
|
||||
this.dashboard.annotations.list[currentIndex] = annotation;
|
||||
} else {
|
||||
console.warn('updating annotatoin, but not in the dashboard', annotation);
|
||||
}
|
||||
this.currentAnnotation = annotation;
|
||||
};
|
||||
|
||||
edit(annotation: any) {
|
||||
this.currentAnnotation = annotation;
|
||||
this.currentAnnotation.showIn = this.currentAnnotation.showIn || 0;
|
||||
|
@ -117,7 +117,15 @@
|
||||
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<rebuild-on-change property="ctrl.currentDatasource">
|
||||
<plugin-component type="annotations-query-ctrl"> </plugin-component>
|
||||
<!-- Legacy angular -->
|
||||
<plugin-component ng-if="!ctrl.currentDatasource.annotations" type="annotations-query-ctrl"> </plugin-component>
|
||||
|
||||
<!-- React query editor -->
|
||||
<standard-annotation-editor
|
||||
ng-if="ctrl.currentDatasource.annotations"
|
||||
annotation="ctrl.currentAnnotation"
|
||||
datasource="ctrl.currentDatasource"
|
||||
change="ctrl.onAnnotationChange" />
|
||||
</rebuild-on-change>
|
||||
|
||||
<div class="gf-form">
|
||||
|
@ -0,0 +1,83 @@
|
||||
import { toDataFrame, FieldType } from '@grafana/data';
|
||||
import { getAnnotationsFromFrame } from './standardAnnotationSupport';
|
||||
|
||||
describe('DataFrame to annotations', () => {
|
||||
test('simple conversion', () => {
|
||||
const frame = toDataFrame({
|
||||
fields: [
|
||||
{ type: FieldType.time, values: [1, 2, 3] },
|
||||
{ name: 'first string field', values: ['t1', 't2', 't3'] },
|
||||
{ name: 'tags', values: ['aaa,bbb', 'bbb,ccc', 'zyz'] },
|
||||
],
|
||||
});
|
||||
|
||||
const events = getAnnotationsFromFrame(frame);
|
||||
expect(events).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"tags": Array [
|
||||
"aaa",
|
||||
"bbb",
|
||||
],
|
||||
"text": "t1",
|
||||
"time": 1,
|
||||
},
|
||||
Object {
|
||||
"tags": Array [
|
||||
"bbb",
|
||||
"ccc",
|
||||
],
|
||||
"text": "t2",
|
||||
"time": 2,
|
||||
},
|
||||
Object {
|
||||
"tags": Array [
|
||||
"zyz",
|
||||
],
|
||||
"text": "t3",
|
||||
"time": 3,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('explicit mappins', () => {
|
||||
const frame = toDataFrame({
|
||||
fields: [
|
||||
{ name: 'time1', values: [111, 222, 333] },
|
||||
{ name: 'time2', values: [100, 200, 300] },
|
||||
{ name: 'aaaaa', values: ['a1', 'a2', 'a3'] },
|
||||
{ name: 'bbbbb', values: ['b1', 'b2', 'b3'] },
|
||||
],
|
||||
});
|
||||
|
||||
const events = getAnnotationsFromFrame(frame, {
|
||||
text: { value: 'bbbbb' },
|
||||
time: { value: 'time2' },
|
||||
timeEnd: { value: 'time1' },
|
||||
title: { value: 'aaaaa' },
|
||||
});
|
||||
expect(events).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"text": "b1",
|
||||
"time": 100,
|
||||
"timeEnd": 111,
|
||||
"title": "a1",
|
||||
},
|
||||
Object {
|
||||
"text": "b2",
|
||||
"time": 200,
|
||||
"timeEnd": 222,
|
||||
"title": "a2",
|
||||
},
|
||||
Object {
|
||||
"text": "b3",
|
||||
"time": 300,
|
||||
"timeEnd": 333,
|
||||
"title": "a3",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
188
public/app/features/annotations/standardAnnotationSupport.ts
Normal file
188
public/app/features/annotations/standardAnnotationSupport.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import {
|
||||
DataFrame,
|
||||
AnnotationQuery,
|
||||
AnnotationSupport,
|
||||
PanelData,
|
||||
transformDataFrame,
|
||||
FieldType,
|
||||
Field,
|
||||
KeyValue,
|
||||
AnnotationEvent,
|
||||
AnnotationEventMappings,
|
||||
getFieldDisplayName,
|
||||
AnnotationEventFieldSource,
|
||||
} from '@grafana/data';
|
||||
|
||||
import isString from 'lodash/isString';
|
||||
|
||||
export const standardAnnotationSupport: AnnotationSupport = {
|
||||
/**
|
||||
* Assume the stored value is standard model.
|
||||
*/
|
||||
prepareAnnotation: (json: any) => {
|
||||
if (isString(json?.query)) {
|
||||
const { query, ...rest } = json;
|
||||
return {
|
||||
...rest,
|
||||
target: {
|
||||
query,
|
||||
},
|
||||
mappings: {},
|
||||
};
|
||||
}
|
||||
return json as AnnotationQuery;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert the stored JSON model and environment to a standard datasource query object.
|
||||
* This query will be executed in the datasource and the results converted into events.
|
||||
* Returning an undefined result will quietly skip query execution
|
||||
*/
|
||||
prepareQuery: (anno: AnnotationQuery) => anno.target,
|
||||
|
||||
/**
|
||||
* When the standard frame > event processing is insufficient, this allows explicit control of the mappings
|
||||
*/
|
||||
processEvents: (anno: AnnotationQuery, data: DataFrame) => {
|
||||
return getAnnotationsFromFrame(data, anno.mappings);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Flatten all panel data into a single frame
|
||||
*/
|
||||
export function singleFrameFromPanelData(rsp: PanelData): DataFrame | undefined {
|
||||
if (!rsp?.series?.length) {
|
||||
return undefined;
|
||||
}
|
||||
if (rsp.series.length === 1) {
|
||||
return rsp.series[0];
|
||||
}
|
||||
|
||||
return transformDataFrame(
|
||||
[
|
||||
{
|
||||
id: 'seriesToColumns',
|
||||
options: { byField: 'Time' },
|
||||
},
|
||||
],
|
||||
rsp.series
|
||||
)[0];
|
||||
}
|
||||
|
||||
interface AnnotationEventFieldSetter {
|
||||
key: keyof AnnotationEvent;
|
||||
field?: Field;
|
||||
text?: string;
|
||||
regex?: RegExp;
|
||||
split?: string; // for tags
|
||||
}
|
||||
|
||||
export interface AnnotationFieldInfo {
|
||||
key: keyof AnnotationEvent;
|
||||
|
||||
split?: string;
|
||||
field?: (frame: DataFrame) => Field | undefined;
|
||||
placeholder?: string;
|
||||
help?: string;
|
||||
}
|
||||
|
||||
export const annotationEventNames: AnnotationFieldInfo[] = [
|
||||
{
|
||||
key: 'time',
|
||||
field: (frame: DataFrame) => frame.fields.find(f => f.type === FieldType.time),
|
||||
placeholder: 'time, or the first time field',
|
||||
},
|
||||
{ key: 'timeEnd', help: 'When this field is defined, the annotation will be treated as a range' },
|
||||
{
|
||||
key: 'title',
|
||||
},
|
||||
{
|
||||
key: 'text',
|
||||
field: (frame: DataFrame) => frame.fields.find(f => f.type === FieldType.string),
|
||||
placeholder: 'text, or the first text field',
|
||||
},
|
||||
{ key: 'tags', split: ',', help: 'The results will be split on comma (,)' },
|
||||
// { key: 'userId' },
|
||||
// { key: 'login' },
|
||||
// { key: 'email' },
|
||||
];
|
||||
|
||||
export function getAnnotationsFromFrame(frame: DataFrame, options?: AnnotationEventMappings): AnnotationEvent[] {
|
||||
if (!frame?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let hasTime = false;
|
||||
let hasText = false;
|
||||
const byName: KeyValue<Field> = {};
|
||||
for (const f of frame.fields) {
|
||||
const name = getFieldDisplayName(f, frame);
|
||||
byName[name.toLowerCase()] = f;
|
||||
}
|
||||
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
const fields: AnnotationEventFieldSetter[] = [];
|
||||
for (const evts of annotationEventNames) {
|
||||
const opt = options[evts.key] || {}; //AnnotationEventFieldMapping
|
||||
if (opt.source === AnnotationEventFieldSource.Skip) {
|
||||
continue;
|
||||
}
|
||||
const setter: AnnotationEventFieldSetter = { key: evts.key, split: evts.split };
|
||||
|
||||
if (opt.source === AnnotationEventFieldSource.Text) {
|
||||
setter.text = opt.value;
|
||||
} else {
|
||||
const lower = (opt.value || evts.key).toLowerCase();
|
||||
setter.field = byName[lower];
|
||||
if (!setter.field && evts.field) {
|
||||
setter.field = evts.field(frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (setter.field || setter.text) {
|
||||
fields.push(setter);
|
||||
if (setter.key === 'time') {
|
||||
hasTime = true;
|
||||
} else if (setter.key === 'text') {
|
||||
hasText = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTime || !hasText) {
|
||||
return []; // throw an error?
|
||||
}
|
||||
|
||||
// Add each value to the string
|
||||
const events: AnnotationEvent[] = [];
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
const anno: AnnotationEvent = {};
|
||||
for (const f of fields) {
|
||||
let v: any = undefined;
|
||||
if (f.text) {
|
||||
v = f.text; // TODO support templates!
|
||||
} else if (f.field) {
|
||||
v = f.field.values.get(i);
|
||||
if (v !== undefined && f.regex) {
|
||||
const match = f.regex.exec(v);
|
||||
if (match) {
|
||||
v = match[1] ? match[1] : match[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (v !== undefined) {
|
||||
if (f.split) {
|
||||
v = (v as string).split(',');
|
||||
}
|
||||
(anno as any)[f.key] = v;
|
||||
}
|
||||
}
|
||||
events.push(anno);
|
||||
}
|
||||
return events;
|
||||
}
|
25
public/app/features/annotations/types.ts
Normal file
25
public/app/features/annotations/types.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { PanelData, DataFrame, AnnotationEvent, TimeRange } from '@grafana/data';
|
||||
import { DashboardModel, PanelModel } from '../dashboard/state';
|
||||
|
||||
export interface AnnotationQueryOptions {
|
||||
dashboard: DashboardModel;
|
||||
panel: PanelModel;
|
||||
range: TimeRange;
|
||||
}
|
||||
|
||||
export interface AnnotationQueryResponse {
|
||||
/**
|
||||
* All the data flattened to a single frame
|
||||
*/
|
||||
frame?: DataFrame;
|
||||
|
||||
/**
|
||||
* The processed annotation events
|
||||
*/
|
||||
events?: AnnotationEvent[];
|
||||
|
||||
/**
|
||||
* The original panel response
|
||||
*/
|
||||
panelData?: PanelData;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { InfluxQuery } from '../types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { SelectableValue, QueryEditorProps } from '@grafana/data';
|
||||
import { cx, css } from 'emotion';
|
||||
import {
|
||||
InlineFormLabel,
|
||||
@ -12,12 +12,10 @@ import {
|
||||
CodeEditorSuggestionItemKind,
|
||||
} from '@grafana/ui';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import InfluxDatasource from '../datasource';
|
||||
|
||||
interface Props {
|
||||
target: InfluxQuery;
|
||||
change: (target: InfluxQuery) => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
// @ts-ignore -- complicated since the datasource is not really reactified yet!
|
||||
type Props = QueryEditorProps<InfluxDatasource, InfluxQuery>;
|
||||
|
||||
const samples: Array<SelectableValue<string>> = [
|
||||
{ label: 'Show buckets', description: 'List the avaliable buckets (table)', value: 'buckets()' },
|
||||
@ -87,20 +85,19 @@ v1.tagValues(
|
||||
|
||||
export class FluxQueryEditor extends PureComponent<Props> {
|
||||
onFluxQueryChange = (query: string) => {
|
||||
const { target, change } = this.props;
|
||||
change({ ...target, query });
|
||||
this.props.refresh();
|
||||
this.props.onChange({ ...this.props.query, query });
|
||||
this.props.onRunQuery();
|
||||
};
|
||||
|
||||
onSampleChange = (val: SelectableValue<string>) => {
|
||||
this.props.change({
|
||||
...this.props.target,
|
||||
this.props.onChange({
|
||||
...this.props.query,
|
||||
query: val.value!,
|
||||
});
|
||||
|
||||
// Angular HACK: Since the target does not actually change!
|
||||
this.forceUpdate();
|
||||
this.props.refresh();
|
||||
this.props.onRunQuery();
|
||||
};
|
||||
|
||||
getSuggestions = (): CodeEditorSuggestionItem[] => {
|
||||
@ -157,7 +154,7 @@ export class FluxQueryEditor extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { target } = this.props;
|
||||
const { query } = this.props;
|
||||
|
||||
const helpTooltip = (
|
||||
<div>
|
||||
@ -171,7 +168,7 @@ export class FluxQueryEditor extends PureComponent<Props> {
|
||||
<CodeEditor
|
||||
height={'200px'}
|
||||
language="sql"
|
||||
value={target.query || ''}
|
||||
value={query.query || ''}
|
||||
onBlur={this.onFluxQueryChange}
|
||||
onSave={this.onFluxQueryChange}
|
||||
showMiniMap={false}
|
||||
@ -211,6 +208,6 @@ export class FluxQueryEditor extends PureComponent<Props> {
|
||||
coreModule.directive('fluxQueryEditor', [
|
||||
'reactDirective',
|
||||
(reactDirective: any) => {
|
||||
return reactDirective(FluxQueryEditor, ['target', 'change', 'refresh']);
|
||||
return reactDirective(FluxQueryEditor, ['query', 'onChange', 'onRunQuery']);
|
||||
},
|
||||
]);
|
||||
|
@ -19,12 +19,13 @@ export default class VariableQueryEditor extends PureComponent<Props> {
|
||||
if (datasource.isFlux) {
|
||||
return (
|
||||
<FluxQueryEditor
|
||||
target={{
|
||||
datasource={datasource}
|
||||
query={{
|
||||
refId: 'A',
|
||||
query,
|
||||
}}
|
||||
refresh={this.onRefresh}
|
||||
change={v => onChange(v.query)}
|
||||
onRunQuery={this.onRefresh}
|
||||
onChange={v => onChange(v.query)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import { InfluxQueryBuilder } from './query_builder';
|
||||
import { InfluxQuery, InfluxOptions, InfluxVersion } from './types';
|
||||
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend, frameToMetricFindValue } from '@grafana/runtime';
|
||||
import { Observable, from } from 'rxjs';
|
||||
import { FluxQueryEditor } from './components/FluxQueryEditor';
|
||||
|
||||
export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery, InfluxOptions> {
|
||||
type: string;
|
||||
@ -55,6 +56,13 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
this.httpMode = settingsData.httpMode || 'GET';
|
||||
this.responseParser = new ResponseParser();
|
||||
this.isFlux = settingsData.version === InfluxVersion.Flux;
|
||||
|
||||
if (this.isFlux) {
|
||||
// When flux, use an annotation processor rather than the `annotationQuery` lifecycle
|
||||
this.annotations = {
|
||||
QueryEditor: FluxQueryEditor,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
query(request: DataQueryRequest<InfluxQuery>): Observable<DataQueryResponse> {
|
||||
@ -73,6 +81,16 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
return new InfluxQueryModel(query).render(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the query should be skipped
|
||||
*/
|
||||
filterQuery(query: InfluxQuery): boolean {
|
||||
if (this.isFlux) {
|
||||
return !!query.query;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only applied on flux queries
|
||||
*/
|
||||
@ -183,7 +201,7 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
|
||||
async annotationQuery(options: AnnotationQueryRequest<any>): Promise<AnnotationEvent[]> {
|
||||
if (this.isFlux) {
|
||||
return Promise.reject({
|
||||
message: 'Annotations are not yet supported with flux queries',
|
||||
message: 'Flux requires the standard annotation query',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
|
||||
<query-editor-row ng-if="ctrl.datasource.isFlux" query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="true">
|
||||
<flux-query-editor
|
||||
target="ctrl.target"
|
||||
change="ctrl.onChange"
|
||||
refresh="ctrl.onRunQuery"
|
||||
query="ctrl.target"
|
||||
onChange="ctrl.onChange"
|
||||
onRunQuery="ctrl.onRunQuery"
|
||||
></flux-query-editor>
|
||||
</query-editor-row>
|
||||
|
||||
|
@ -539,9 +539,6 @@ function makeAnnotationQueryRequest(): AnnotationQueryRequest<LokiQuery> {
|
||||
raw: timeRange,
|
||||
},
|
||||
rangeRaw: timeRange,
|
||||
app: 'test',
|
||||
interval: '1m',
|
||||
intervalMs: 6000,
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user