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 {
|
export interface Props {
|
||||||
|
allowCustomValue?: boolean;
|
||||||
/** Do not show selected values inside Select. Useful when the values need to be shown in some other components */
|
/** Do not show selected values inside Select. Useful when the values need to be shown in some other components */
|
||||||
hideValues?: boolean;
|
hideValues?: boolean;
|
||||||
|
inputId?: string;
|
||||||
isClearable?: boolean;
|
isClearable?: boolean;
|
||||||
onChange: (tags: string[]) => void;
|
onChange: (tags: string[]) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@ -30,7 +32,9 @@ const filterOption = (option: any, searchQuery: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TagFilter: FC<Props> = ({
|
export const TagFilter: FC<Props> = ({
|
||||||
|
allowCustomValue = false,
|
||||||
hideValues,
|
hideValues,
|
||||||
|
inputId,
|
||||||
isClearable,
|
isClearable,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = 'Filter by tag',
|
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 value = tags.map((tag) => ({ value: tag, label: tag, count: 0 }));
|
||||||
|
|
||||||
const selectOptions = {
|
const selectOptions = {
|
||||||
|
allowCustomValue,
|
||||||
defaultOptions: true,
|
defaultOptions: true,
|
||||||
filterOption,
|
filterOption,
|
||||||
getOptionLabel: (i: any) => i.label,
|
getOptionLabel: (i: any) => i.label,
|
||||||
getOptionValue: (i: any) => i.value,
|
getOptionValue: (i: any) => i.value,
|
||||||
|
inputId,
|
||||||
isMulti: true,
|
isMulti: true,
|
||||||
loadOptions: onLoadOptions,
|
loadOptions: onLoadOptions,
|
||||||
loadingMessage: 'Loading...',
|
loadingMessage: 'Loading...',
|
||||||
|
@ -18,7 +18,7 @@ export const TagOption: FC<ExtendedOptionProps> = ({ data, className, label, isF
|
|||||||
return (
|
return (
|
||||||
<div className={cx(styles.option, isFocused && styles.optionFocused)} aria-label="Tag option" {...innerProps}>
|
<div className={cx(styles.option, isFocused && styles.optionFocused)} aria-label="Tag option" {...innerProps}>
|
||||||
<div className={`tag-filter-option ${className || ''}`}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { AnnotationEvent } from '@grafana/data';
|
import { AnnotationEvent } from '@grafana/data';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
import { AnnotationTagsResponse } from './types';
|
||||||
|
|
||||||
export function saveAnnotation(annotation: AnnotationEvent) {
|
export function saveAnnotation(annotation: AnnotationEvent) {
|
||||||
return getBackendSrv().post('/api/annotations', annotation);
|
return getBackendSrv().post('/api/annotations', annotation);
|
||||||
@ -12,3 +13,11 @@ export function updateAnnotation(annotation: AnnotationEvent) {
|
|||||||
export function deleteAnnotation(annotation: AnnotationEvent) {
|
export function deleteAnnotation(annotation: AnnotationEvent) {
|
||||||
return getBackendSrv().delete(`/api/annotations/${annotation.id}`);
|
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}
|
data={response?.panelData}
|
||||||
range={getTimeSrv().timeRange()}
|
range={getTimeSrv().timeRange()}
|
||||||
/>
|
/>
|
||||||
{this.renderStatus()}
|
{datasource.type !== 'datasource' && (
|
||||||
|
<>
|
||||||
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
|
{this.renderStatus()}
|
||||||
<br />
|
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,3 +18,20 @@ export interface AnnotationQueryResponse {
|
|||||||
*/
|
*/
|
||||||
panelData?: PanelData;
|
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 { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
|
||||||
import { GrafanaDatasource } from './datasource';
|
import { GrafanaDatasource } from './datasource';
|
||||||
import { GrafanaQuery, GrafanaAnnotationQuery, GrafanaAnnotationType } from './types';
|
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery } from './types';
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||||
@ -37,7 +37,7 @@ describe('grafana data source', () => {
|
|||||||
const options = setupAnnotationQueryOptions({ tags: ['tag1:$var'] });
|
const options = setupAnnotationQueryOptions({ tags: ['tag1:$var'] });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
return ds.annotationQuery(options);
|
return ds.getAnnotations(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should interpolate template variables in tags in query options', () => {
|
it('should interpolate template variables in tags in query options', () => {
|
||||||
@ -49,7 +49,7 @@ describe('grafana data source', () => {
|
|||||||
const options = setupAnnotationQueryOptions({ tags: ['$var2'] });
|
const options = setupAnnotationQueryOptions({ tags: ['$var2'] });
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
return ds.annotationQuery(options);
|
return ds.getAnnotations(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should interpolate template variables in tags in query options', () => {
|
it('should interpolate template variables in tags in query options', () => {
|
||||||
@ -68,7 +68,7 @@ describe('grafana data source', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
return ds.annotationQuery(options);
|
return ds.getAnnotations(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove tags from query options', () => {
|
it('should remove tags from query options', () => {
|
||||||
@ -80,7 +80,9 @@ describe('grafana data source', () => {
|
|||||||
|
|
||||||
function setupAnnotationQueryOptions(annotation: Partial<GrafanaAnnotationQuery>, dashboard?: { id: number }) {
|
function setupAnnotationQueryOptions(annotation: Partial<GrafanaAnnotationQuery>, dashboard?: { id: number }) {
|
||||||
return ({
|
return ({
|
||||||
annotation,
|
annotation: {
|
||||||
|
target: annotation,
|
||||||
|
},
|
||||||
dashboard,
|
dashboard,
|
||||||
range: {
|
range: {
|
||||||
from: dateTime(1432288354),
|
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 {
|
import {
|
||||||
AnnotationEvent,
|
AnnotationQuery,
|
||||||
AnnotationQueryRequest,
|
AnnotationQueryRequest,
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
@ -8,24 +11,51 @@ import {
|
|||||||
isValidLiveChannelAddress,
|
isValidLiveChannelAddress,
|
||||||
parseLiveChannelAddress,
|
parseLiveChannelAddress,
|
||||||
StreamingFrameOptions,
|
StreamingFrameOptions,
|
||||||
|
toDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import { GrafanaQuery, GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQueryType } from './types';
|
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQueryType } from './types';
|
||||||
import { getBackendSrv, getGrafanaLiveSrv, getTemplateSrv, toDataQueryResponse } from '@grafana/runtime';
|
import AnnotationQueryEditor from './components/AnnotationQueryEditor';
|
||||||
import { Observable, of, merge } from 'rxjs';
|
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
||||||
import { map, catchError } from 'rxjs/operators';
|
|
||||||
|
|
||||||
let counter = 100;
|
let counter = 100;
|
||||||
|
|
||||||
export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
||||||
constructor(instanceSettings: DataSourceInstanceSettings) {
|
constructor(instanceSettings: DataSourceInstanceSettings) {
|
||||||
super(instanceSettings);
|
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> {
|
query(request: DataQueryRequest<GrafanaQuery>): Observable<DataQueryResponse> {
|
||||||
const queries: Array<Observable<DataQueryResponse>> = [];
|
const queries: Array<Observable<DataQueryResponse>> = [];
|
||||||
const templateSrv = getTemplateSrv();
|
const templateSrv = getTemplateSrv();
|
||||||
for (const target of request.targets) {
|
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) {
|
if (target.hide) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -80,21 +110,22 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
|||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
annotationQuery(options: AnnotationQueryRequest<GrafanaQuery>): Promise<AnnotationEvent[]> {
|
async getAnnotations(options: AnnotationQueryRequest<GrafanaQuery>): Promise<DataQueryResponse> {
|
||||||
const templateSrv = getTemplateSrv();
|
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 = {
|
const params: any = {
|
||||||
from: options.range.from.valueOf(),
|
from: options.range.from.valueOf(),
|
||||||
to: options.range.to.valueOf(),
|
to: options.range.to.valueOf(),
|
||||||
limit: annotation.limit,
|
limit: target.limit,
|
||||||
tags: annotation.tags,
|
tags: target.tags,
|
||||||
matchAny: annotation.matchAny,
|
matchAny: target.matchAny,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (annotation.type === GrafanaAnnotationType.Dashboard) {
|
if (target.type === GrafanaAnnotationType.Dashboard) {
|
||||||
// if no dashboard id yet return
|
// if no dashboard id yet return
|
||||||
if (!options.dashboard.id) {
|
if (!options.dashboard.id) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve({ data: [] });
|
||||||
}
|
}
|
||||||
// filter by dashboard id
|
// filter by dashboard id
|
||||||
params.dashboardId = options.dashboard.id;
|
params.dashboardId = options.dashboard.id;
|
||||||
@ -102,8 +133,8 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
|||||||
delete params.tags;
|
delete params.tags;
|
||||||
} else {
|
} else {
|
||||||
// require at least one tag
|
// require at least one tag
|
||||||
if (!Array.isArray(annotation.tags) || annotation.tags.length === 0) {
|
if (!Array.isArray(target.tags) || target.tags.length === 0) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve({ data: [] });
|
||||||
}
|
}
|
||||||
const delimiter = '__delimiter__';
|
const delimiter = '__delimiter__';
|
||||||
const tags = [];
|
const tags = [];
|
||||||
@ -122,11 +153,12 @@ export class GrafanaDatasource extends DataSourceApi<GrafanaQuery> {
|
|||||||
params.tags = tags;
|
params.tags = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getBackendSrv().get(
|
const annotations = await getBackendSrv().get(
|
||||||
'/api/annotations',
|
'/api/annotations',
|
||||||
params,
|
params,
|
||||||
`grafana-data-source-annotations-${annotation.name}-${options.dashboard?.id}`
|
`grafana-data-source-annotations-${annotation.name}-${options.dashboard?.id}`
|
||||||
);
|
);
|
||||||
|
return { data: [toDataFrame(annotations)] };
|
||||||
}
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource() {
|
||||||
|
@ -2,8 +2,7 @@ import { DataSourcePlugin } from '@grafana/data';
|
|||||||
import { GrafanaDatasource } from './datasource';
|
import { GrafanaDatasource } from './datasource';
|
||||||
import { QueryEditor } from './components/QueryEditor';
|
import { QueryEditor } from './components/QueryEditor';
|
||||||
import { GrafanaQuery } from './types';
|
import { GrafanaQuery } from './types';
|
||||||
import { GrafanaAnnotationsQueryCtrl } from './annotation_ctrl';
|
|
||||||
|
|
||||||
export const plugin = new DataSourcePlugin<GrafanaDatasource, GrafanaQuery>(GrafanaDatasource)
|
export const plugin = new DataSourcePlugin<GrafanaDatasource, GrafanaQuery>(GrafanaDatasource).setQueryEditor(
|
||||||
.setQueryEditor(QueryEditor)
|
QueryEditor
|
||||||
.setAnnotationQueryCtrl(GrafanaAnnotationsQueryCtrl);
|
);
|
||||||
|
@ -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';
|
import { LiveDataFilter } from '@grafana/runtime';
|
||||||
|
|
||||||
//----------------------------------------------
|
//----------------------------------------------
|
||||||
@ -8,6 +8,7 @@ import { LiveDataFilter } from '@grafana/runtime';
|
|||||||
export enum GrafanaQueryType {
|
export enum GrafanaQueryType {
|
||||||
RandomWalk = 'randomWalk',
|
RandomWalk = 'randomWalk',
|
||||||
LiveMeasurements = 'measurements',
|
LiveMeasurements = 'measurements',
|
||||||
|
Annotations = 'annotations',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrafanaQuery extends DataQuery {
|
export interface GrafanaQuery extends DataQuery {
|
||||||
@ -31,7 +32,7 @@ export enum GrafanaAnnotationType {
|
|||||||
Tags = 'tags',
|
Tags = 'tags',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrafanaAnnotationQuery extends AnnotationQuery<GrafanaQuery> {
|
export interface GrafanaAnnotationQuery extends GrafanaQuery {
|
||||||
type: GrafanaAnnotationType; // tags
|
type: GrafanaAnnotationType; // tags
|
||||||
limit: number; // 100
|
limit: number; // 100
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
Loading…
Reference in New Issue
Block a user