mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Annotations: Add typeahead support for tags in builtin annotations (#36377)
* Annotations: create React component, naive attempt at hooking together
* Annotations: Use query object instead of passing annotation
* Annotations: Hook up the new api to get annotation tags
* Annotations: Use InlineFieldRow instead of gf-form-inline
* Annotations: Use InlineSwitch instead of gf-form-switch
* TagFilter: Add support for allowCustomValue
* Annotations: Update to match backend api
* Annotations: Add basic tests, expose inputId on `TagFilter`
* Annotations: Fix test name and reorder tests slightly
* Annotations: Use FieldSet instead of gf-form-group
* Refactor: fixes annotation queries
* Annotations: Everything working, just types to fix...
* Annotations: Fix types?
* Revert "Annotations: Fix types?"
This reverts commit 6df0cae0c9
.
* Annotations: Fix types again?
* Annotations: Remove old angular code
* Annotations: Fix unit tests for AnnotationQueryEditor
* Annotations: Check if it's an annotation query immediately
* Annotations: Prevent TagFilter overflowing container when there are a large number of tags
* Change to new form styles
* Annotations: Add id's + fix unit tests
* Updated wording
* Annotations: Allow custom value to preserve being able to use template variables
Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
96a3cc3cd8
commit
cc4d301d50
@ -14,8 +14,10 @@ export interface TermCount {
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
allowCustomValue?: boolean;
|
||||
/** Do not show selected values inside Select. Useful when the values need to be shown in some other components */
|
||||
hideValues?: boolean;
|
||||
inputId?: string;
|
||||
isClearable?: boolean;
|
||||
onChange: (tags: string[]) => void;
|
||||
placeholder?: string;
|
||||
@ -30,7 +32,9 @@ const filterOption = (option: any, searchQuery: string) => {
|
||||
};
|
||||
|
||||
export const TagFilter: FC<Props> = ({
|
||||
allowCustomValue = false,
|
||||
hideValues,
|
||||
inputId,
|
||||
isClearable,
|
||||
onChange,
|
||||
placeholder = 'Filter by tag',
|
||||
@ -60,10 +64,12 @@ export const TagFilter: FC<Props> = ({
|
||||
const value = tags.map((tag) => ({ value: tag, label: tag, count: 0 }));
|
||||
|
||||
const selectOptions = {
|
||||
allowCustomValue,
|
||||
defaultOptions: true,
|
||||
filterOption,
|
||||
getOptionLabel: (i: any) => i.label,
|
||||
getOptionValue: (i: any) => i.value,
|
||||
inputId,
|
||||
isMulti: true,
|
||||
loadOptions: onLoadOptions,
|
||||
loadingMessage: 'Loading...',
|
||||
|
@ -18,7 +18,7 @@ export const TagOption: FC<ExtendedOptionProps> = ({ data, className, label, isF
|
||||
return (
|
||||
<div className={cx(styles.option, isFocused && styles.optionFocused)} aria-label="Tag option" {...innerProps}>
|
||||
<div className={`tag-filter-option ${className || ''}`}>
|
||||
<TagBadge label={label} removeIcon={false} count={data.count} />
|
||||
<TagBadge label={label} removeIcon={false} count={data.count ?? 0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AnnotationEvent } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { AnnotationTagsResponse } from './types';
|
||||
|
||||
export function saveAnnotation(annotation: AnnotationEvent) {
|
||||
return getBackendSrv().post('/api/annotations', annotation);
|
||||
@ -12,3 +13,11 @@ export function updateAnnotation(annotation: AnnotationEvent) {
|
||||
export function deleteAnnotation(annotation: AnnotationEvent) {
|
||||
return getBackendSrv().delete(`/api/annotations/${annotation.id}`);
|
||||
}
|
||||
|
||||
export async function getAnnotationTags() {
|
||||
const response: AnnotationTagsResponse = await getBackendSrv().get('/api/annotations/tags');
|
||||
return response.result.tags.map(({ tag, count }) => ({
|
||||
term: tag,
|
||||
count,
|
||||
}));
|
||||
}
|
||||
|
@ -175,10 +175,12 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
|
||||
data={response?.panelData}
|
||||
range={getTimeSrv().timeRange()}
|
||||
/>
|
||||
{this.renderStatus()}
|
||||
|
||||
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
|
||||
<br />
|
||||
{datasource.type !== 'datasource' && (
|
||||
<>
|
||||
{this.renderStatus()}
|
||||
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -18,3 +18,20 @@ export interface AnnotationQueryResponse {
|
||||
*/
|
||||
panelData?: PanelData;
|
||||
}
|
||||
|
||||
export interface AnnotationTag {
|
||||
/**
|
||||
* The tag name
|
||||
*/
|
||||
tag: string;
|
||||
/**
|
||||
* The number of occurences of that tag
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface AnnotationTagsResponse {
|
||||
result: {
|
||||
tags: AnnotationTag[];
|
||||
};
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { GrafanaAnnotationType } from './types';
|
||||
|
||||
export const annotationTypes: Array<SelectableValue<GrafanaAnnotationType>> = [
|
||||
{ text: 'Dashboard', value: GrafanaAnnotationType.Dashboard },
|
||||
{ text: 'Tags', value: GrafanaAnnotationType.Tags },
|
||||
];
|
||||
|
||||
export class GrafanaAnnotationsQueryCtrl {
|
||||
declare annotation: any;
|
||||
|
||||
types = annotationTypes;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope: any) {
|
||||
this.annotation = $scope.ctrl.annotation;
|
||||
this.annotation.type = this.annotation.type || GrafanaAnnotationType.Tags;
|
||||
this.annotation.limit = this.annotation.limit || 100;
|
||||
}
|
||||
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQueryType } from '../types';
|
||||
import AnnotationQueryEditor from './AnnotationQueryEditor';
|
||||
|
||||
describe('AnnotationQueryEditor', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
let mockQuery: GrafanaAnnotationQuery;
|
||||
|
||||
beforeEach(() => {
|
||||
mockQuery = {
|
||||
queryType: GrafanaQueryType.Annotations,
|
||||
refId: 'Anno',
|
||||
type: GrafanaAnnotationType.Tags,
|
||||
limit: 100,
|
||||
};
|
||||
});
|
||||
|
||||
it('has a "Filter by" input', () => {
|
||||
render(<AnnotationQueryEditor query={mockQuery} onChange={mockOnChange} />);
|
||||
const filterBy = screen.getByLabelText('Filter by');
|
||||
expect(filterBy).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('has a "Max limit" input', () => {
|
||||
render(<AnnotationQueryEditor query={mockQuery} onChange={mockOnChange} />);
|
||||
const maxLimit = screen.getByLabelText('Max limit');
|
||||
expect(maxLimit).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when the query type is "Tags" and the tags array is present', () => {
|
||||
beforeEach(() => {
|
||||
mockQuery.tags = [];
|
||||
});
|
||||
|
||||
it('has a "Match any" toggle', () => {
|
||||
render(<AnnotationQueryEditor query={mockQuery} onChange={mockOnChange} />);
|
||||
const matchAny = screen.getByLabelText(/Match any/);
|
||||
expect(matchAny).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('has a "Tags" input', () => {
|
||||
render(<AnnotationQueryEditor query={mockQuery} onChange={mockOnChange} />);
|
||||
const tags = screen.getByLabelText(/Tags/);
|
||||
expect(tags).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the query type is "Dashboard"', () => {
|
||||
beforeEach(() => {
|
||||
mockQuery.type = GrafanaAnnotationType.Dashboard;
|
||||
});
|
||||
|
||||
it('does not have a "Match any" toggle', () => {
|
||||
render(<AnnotationQueryEditor query={mockQuery} onChange={mockOnChange} />);
|
||||
const matchAny = screen.queryByLabelText('Match any');
|
||||
expect(matchAny).toBeNull();
|
||||
});
|
||||
|
||||
it('does not have a "Tags" input', () => {
|
||||
render(<AnnotationQueryEditor query={mockQuery} onChange={mockOnChange} />);
|
||||
const tags = screen.queryByLabelText('Tags');
|
||||
expect(tags).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Field, FieldSet, Select, Switch } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
|
||||
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery } from '../types';
|
||||
import { getAnnotationTags } from 'app/features/annotations/api';
|
||||
|
||||
const matchTooltipContent = 'Enabling this returns annotations that match any of the tags specified below';
|
||||
|
||||
const tagsTooltipContent = (
|
||||
<div>Specify a list of tags to match. To specify a key and value tag use `key:value` syntax.</div>
|
||||
);
|
||||
|
||||
const annotationTypes = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
value: GrafanaAnnotationType.Dashboard,
|
||||
description: 'Query for events created on this dashboard and show them in the panels where they where created',
|
||||
},
|
||||
{
|
||||
label: 'Tags',
|
||||
value: GrafanaAnnotationType.Tags,
|
||||
description: 'This will fetch any annotation events that match the tags filter',
|
||||
},
|
||||
];
|
||||
|
||||
const limitOptions = [10, 50, 100, 200, 300, 500, 1000, 2000].map((limit) => ({
|
||||
label: String(limit),
|
||||
value: limit,
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
query: GrafanaQuery;
|
||||
onChange: (newValue: GrafanaAnnotationQuery) => void;
|
||||
}
|
||||
|
||||
export default function AnnotationQueryEditor({ query, onChange }: Props) {
|
||||
const annotationQuery = query as GrafanaAnnotationQuery;
|
||||
const { limit, matchAny, tags, type } = annotationQuery;
|
||||
const styles = getStyles();
|
||||
|
||||
const onFilterByChange = (newValue: SelectableValue<GrafanaAnnotationType>) =>
|
||||
onChange({
|
||||
...annotationQuery,
|
||||
type: newValue.value!,
|
||||
});
|
||||
|
||||
const onMaxLimitChange = (newValue: SelectableValue<number>) =>
|
||||
onChange({
|
||||
...annotationQuery,
|
||||
limit: newValue.value!,
|
||||
});
|
||||
|
||||
const onMatchAnyChange = (newValue: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange({
|
||||
...annotationQuery,
|
||||
matchAny: newValue.target.checked,
|
||||
});
|
||||
|
||||
const onTagsChange = (tags: string[]) =>
|
||||
onChange({
|
||||
...annotationQuery,
|
||||
tags,
|
||||
});
|
||||
|
||||
return (
|
||||
<FieldSet className={styles.container}>
|
||||
<Field label="Filter by">
|
||||
<Select
|
||||
inputId="grafana-annotations__filter-by"
|
||||
options={annotationTypes}
|
||||
value={type}
|
||||
onChange={onFilterByChange}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Max limit">
|
||||
<Select
|
||||
inputId="grafana-annotations__limit"
|
||||
width={16}
|
||||
options={limitOptions}
|
||||
value={limit}
|
||||
onChange={onMaxLimitChange}
|
||||
/>
|
||||
</Field>
|
||||
{type === GrafanaAnnotationType.Tags && tags && (
|
||||
<>
|
||||
<Field label="Match any" description={matchTooltipContent}>
|
||||
<Switch id="grafana-annotations__match-any" value={matchAny} onChange={onMatchAnyChange} />
|
||||
</Field>
|
||||
<Field label="Tags" description={tagsTooltipContent}>
|
||||
<TagFilter
|
||||
allowCustomValue
|
||||
inputId="grafana-annotations__tags"
|
||||
onChange={onTagsChange}
|
||||
tagOptions={getAnnotationTags}
|
||||
tags={tags}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = () => {
|
||||
return {
|
||||
container: css`
|
||||
max-width: 600px;
|
||||
`,
|
||||
};
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
import { DataSourceInstanceSettings, dateTime, AnnotationQueryRequest } from '@grafana/data';
|
||||
import { AnnotationQueryRequest, DataSourceInstanceSettings, dateTime } from '@grafana/data';
|
||||
|
||||
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||
import { GrafanaDatasource } from './datasource';
|
||||
import { GrafanaQuery, GrafanaAnnotationQuery, GrafanaAnnotationType } from './types';
|
||||
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery } from './types';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||
@ -37,7 +37,7 @@ describe('grafana data source', () => {
|
||||
const options = setupAnnotationQueryOptions({ tags: ['tag1:$var'] });
|
||||
|
||||
beforeEach(() => {
|
||||
return ds.annotationQuery(options);
|
||||
return ds.getAnnotations(options);
|
||||
});
|
||||
|
||||
it('should interpolate template variables in tags in query options', () => {
|
||||
@ -49,7 +49,7 @@ describe('grafana data source', () => {
|
||||
const options = setupAnnotationQueryOptions({ tags: ['$var2'] });
|
||||
|
||||
beforeEach(() => {
|
||||
return ds.annotationQuery(options);
|
||||
return ds.getAnnotations(options);
|
||||
});
|
||||
|
||||
it('should interpolate template variables in tags in query options', () => {
|
||||
@ -68,7 +68,7 @@ describe('grafana data source', () => {
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
return ds.annotationQuery(options);
|
||||
return ds.getAnnotations(options);
|
||||
});
|
||||
|
||||
it('should remove tags from query options', () => {
|
||||
@ -80,7 +80,9 @@ describe('grafana data source', () => {
|
||||
|
||||
function setupAnnotationQueryOptions(annotation: Partial<GrafanaAnnotationQuery>, dashboard?: { id: number }) {
|
||||
return ({
|
||||
annotation,
|
||||
annotation: {
|
||||
target: annotation,
|
||||
},
|
||||
dashboard,
|
||||
range: {
|
||||
from: dateTime(1432288354),
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { from, merge, Observable, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, toDataQueryResponse } from '@grafana/runtime';
|
||||
import {
|
||||
AnnotationEvent,
|
||||
AnnotationQuery,
|
||||
AnnotationQueryRequest,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
@ -8,24 +11,51 @@ import {
|
||||
isValidLiveChannelAddress,
|
||||
parseLiveChannelAddress,
|
||||
StreamingFrameOptions,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { GrafanaQuery, GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQueryType } from './types';
|
||||
import { getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, toDataQueryResponse } from '@grafana/runtime';
|
||||
import { Observable, of, merge } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQueryType } from './types';
|
||||
import AnnotationQueryEditor from './components/AnnotationQueryEditor';
|
||||
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
||||
|
||||
let counter = 100;
|
||||
|
||||
export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
||||
constructor(instanceSettings: DataSourceInstanceSettings) {
|
||||
super(instanceSettings);
|
||||
this.annotations = {
|
||||
QueryEditor: AnnotationQueryEditor,
|
||||
prepareAnnotation(json: any): AnnotationQuery<GrafanaAnnotationQuery> {
|
||||
// Previously, these properties lived outside of target
|
||||
// This should handle migrating them
|
||||
json.target = json.target ?? {
|
||||
type: json.type ?? GrafanaAnnotationType.Dashboard,
|
||||
limit: json.limit ?? 100,
|
||||
tags: json.tags ?? [],
|
||||
matchAny: json.matchAny ?? false,
|
||||
}; // using spread syntax caused an infinite loop in StandardAnnotationQueryEditor
|
||||
return json;
|
||||
},
|
||||
prepareQuery(anno: AnnotationQuery<GrafanaAnnotationQuery>): GrafanaQuery {
|
||||
return { ...anno, refId: anno.name, queryType: GrafanaQueryType.Annotations };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
query(request: DataQueryRequest<GrafanaQuery>): Observable<DataQueryResponse> {
|
||||
const queries: Array<Observable<DataQueryResponse>> = [];
|
||||
const templateSrv = getTemplateSrv();
|
||||
for (const target of request.targets) {
|
||||
if (target.queryType === GrafanaQueryType.Annotations) {
|
||||
return from(
|
||||
this.getAnnotations({
|
||||
range: request.range,
|
||||
rangeRaw: request.range.raw,
|
||||
annotation: (target as unknown) as AnnotationQuery<GrafanaAnnotationQuery>,
|
||||
dashboard: getDashboardSrv().getCurrent(),
|
||||
})
|
||||
);
|
||||
}
|
||||
if (target.hide) {
|
||||
continue;
|
||||
}
|
||||
@ -80,21 +110,22 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
annotationQuery(options: AnnotationQueryRequest<GrafanaQuery>): Promise<AnnotationEvent[]> {
|
||||
async getAnnotations(options: AnnotationQueryRequest<GrafanaQuery>): Promise<DataQueryResponse> {
|
||||
const templateSrv = getTemplateSrv();
|
||||
const annotation = (options.annotation as unknown) as GrafanaAnnotationQuery;
|
||||
const annotation = (options.annotation as unknown) as AnnotationQuery<GrafanaAnnotationQuery>;
|
||||
const target = annotation.target!;
|
||||
const params: any = {
|
||||
from: options.range.from.valueOf(),
|
||||
to: options.range.to.valueOf(),
|
||||
limit: annotation.limit,
|
||||
tags: annotation.tags,
|
||||
matchAny: annotation.matchAny,
|
||||
limit: target.limit,
|
||||
tags: target.tags,
|
||||
matchAny: target.matchAny,
|
||||
};
|
||||
|
||||
if (annotation.type === GrafanaAnnotationType.Dashboard) {
|
||||
if (target.type === GrafanaAnnotationType.Dashboard) {
|
||||
// if no dashboard id yet return
|
||||
if (!options.dashboard.id) {
|
||||
return Promise.resolve([]);
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
// filter by dashboard id
|
||||
params.dashboardId = options.dashboard.id;
|
||||
@ -102,8 +133,8 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
||||
delete params.tags;
|
||||
} else {
|
||||
// require at least one tag
|
||||
if (!Array.isArray(annotation.tags) || annotation.tags.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
if (!Array.isArray(target.tags) || target.tags.length === 0) {
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
const delimiter = '__delimiter__';
|
||||
const tags = [];
|
||||
@ -122,11 +153,12 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
||||
params.tags = tags;
|
||||
}
|
||||
|
||||
return getBackendSrv().get(
|
||||
const annotations = await getBackendSrv().get(
|
||||
'/api/annotations',
|
||||
params,
|
||||
`grafana-data-source-annotations-${annotation.name}-${options.dashboard?.id}`
|
||||
);
|
||||
return { data: [toDataFrame(annotations)] };
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
|
@ -2,8 +2,7 @@ import { DataSourcePlugin } from '@grafana/data';
|
||||
import { GrafanaDatasource } from './datasource';
|
||||
import { QueryEditor } from './components/QueryEditor';
|
||||
import { GrafanaQuery } from './types';
|
||||
import { GrafanaAnnotationsQueryCtrl } from './annotation_ctrl';
|
||||
|
||||
export const plugin = new DataSourcePlugin<GrafanaDatasource, GrafanaQuery>(GrafanaDatasource)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setAnnotationQueryCtrl(GrafanaAnnotationsQueryCtrl);
|
||||
export const plugin = new DataSourcePlugin<GrafanaDatasource, GrafanaQuery>(GrafanaDatasource).setQueryEditor(
|
||||
QueryEditor
|
||||
);
|
||||
|
@ -1,54 +0,0 @@
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">
|
||||
Filter by
|
||||
<info-popover mode="right-normal">
|
||||
<ul>
|
||||
<li>Dashboard: This will fetch annotation and alert state changes for whole dashboard and show them only on the event's originating panel.</li>
|
||||
<li>Tags: This will fetch any annotation events that match the tags filter.</li>
|
||||
</ul>
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in ctrl.types">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Max limit</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.annotation.limit" ng-options="f for f in [10,50,100,200,300,500,1000,2000]">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-if="ctrl.annotation.type === 'tags'">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Match any"
|
||||
label-class="width-9"
|
||||
checked="ctrl.annotation.matchAny"
|
||||
on-change="ctrl.refresh()"
|
||||
tooltip="By default Grafana only shows annotations that match all tags in the query. Enabling this returns annotations that match any of the tags in the query."></gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.annotation.type === 'tags'">
|
||||
<span class="gf-form-label">
|
||||
Tags
|
||||
<info-popover mode="right-normal">
|
||||
A tag entered here as 'foo' will match
|
||||
<ul>
|
||||
<li>annotation tags 'foo'</li>
|
||||
<li>annotation key-value tags formatted as 'foo:bar'</li>
|
||||
</ul>
|
||||
</info-popover>
|
||||
</span>
|
||||
<bootstrap-tagsinput ng-model="ctrl.annotation.tags" tagclass="label label-tag" placeholder="add tags">
|
||||
</bootstrap-tagsinput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AnnotationQuery, DataQuery } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/data';
|
||||
import { LiveDataFilter } from '@grafana/runtime';
|
||||
|
||||
//----------------------------------------------
|
||||
@ -8,6 +8,7 @@ import { LiveDataFilter } from '@grafana/runtime';
|
||||
export enum GrafanaQueryType {
|
||||
RandomWalk = 'randomWalk',
|
||||
LiveMeasurements = 'measurements',
|
||||
Annotations = 'annotations',
|
||||
}
|
||||
|
||||
export interface GrafanaQuery extends DataQuery {
|
||||
@ -31,7 +32,7 @@ export enum GrafanaAnnotationType {
|
||||
Tags = 'tags',
|
||||
}
|
||||
|
||||
export interface GrafanaAnnotationQuery extends AnnotationQuery<GrafanaQuery> {
|
||||
export interface GrafanaAnnotationQuery extends GrafanaQuery {
|
||||
type: GrafanaAnnotationType; // tags
|
||||
limit: number; // 100
|
||||
tags?: string[];
|
||||
|
Loading…
Reference in New Issue
Block a user