mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Event Annotations] Fetching annotations (#138618)
* allow handleEsaggsRequest to be used externally for event_annotation plugin * fetch annotation expression * fix skippedCount * add tests * added textField * timezone fix * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * query to query point * cr * code review changes Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
78e96c100c
commit
76dc50cefe
39 changed files with 2506 additions and 323 deletions
|
@ -30,7 +30,11 @@ export function annotationLayerFunction(): ExpressionFunctionDefinition<
|
|||
help: strings.getAnnotationLayerSimpleViewHelp(),
|
||||
},
|
||||
annotations: {
|
||||
types: ['manual_point_event_annotation', 'manual_range_event_annotation'],
|
||||
types: [
|
||||
'manual_point_event_annotation',
|
||||
'manual_range_event_annotation',
|
||||
'query_point_event_annotation',
|
||||
],
|
||||
help: strings.getAnnotationLayerAnnotationsHelp(),
|
||||
multi: true,
|
||||
},
|
||||
|
|
|
@ -30,7 +30,11 @@ export function extendedAnnotationLayerFunction(): ExpressionFunctionDefinition<
|
|||
help: strings.getAnnotationLayerSimpleViewHelp(),
|
||||
},
|
||||
annotations: {
|
||||
types: ['manual_point_event_annotation', 'manual_range_event_annotation'],
|
||||
types: [
|
||||
'manual_point_event_annotation',
|
||||
'manual_range_event_annotation',
|
||||
'query_point_event_annotation',
|
||||
],
|
||||
help: strings.getAnnotationLayerAnnotationsHelp(),
|
||||
multi: true,
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@ exports[`XYChart component annotations should render basic line annotation 1`] =
|
|||
<Marker
|
||||
config={
|
||||
Object {
|
||||
"id": "annotation",
|
||||
"label": "Annotation",
|
||||
"position": "bottom",
|
||||
"roundedTimestamp": 1647591917100,
|
||||
|
@ -115,6 +116,7 @@ exports[`XYChart component annotations should render grouped line annotations pr
|
|||
"color": "red",
|
||||
"customTooltipDetails": [Function],
|
||||
"icon": "3",
|
||||
"id": "event1",
|
||||
"label": "Event 1",
|
||||
"lineStyle": "dashed",
|
||||
"lineWidth": 3,
|
||||
|
@ -174,6 +176,7 @@ exports[`XYChart component annotations should render grouped line annotations wi
|
|||
"color": "#f04e98",
|
||||
"customTooltipDetails": [Function],
|
||||
"icon": "2",
|
||||
"id": "event1",
|
||||
"label": "Event 1",
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1,
|
||||
|
|
|
@ -22,6 +22,7 @@ import moment from 'moment';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import type {
|
||||
ManualPointEventAnnotationArgs,
|
||||
ManualPointEventAnnotationOutput,
|
||||
ManualRangeEventAnnotationOutput,
|
||||
} from '@kbn/event-annotation-plugin/common';
|
||||
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
|
@ -63,7 +64,10 @@ const groupVisibleConfigsByInterval = (
|
|||
) => {
|
||||
return layers
|
||||
.flatMap(({ annotations }) =>
|
||||
annotations.filter((a) => !a.isHidden && a.type === 'manual_point_event_annotation')
|
||||
annotations.filter(
|
||||
(a): a is ManualPointEventAnnotationOutput =>
|
||||
!a.isHidden && a.type === 'manual_point_event_annotation'
|
||||
)
|
||||
)
|
||||
.sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf())
|
||||
.reduce<Record<string, ManualPointEventAnnotationArgs[]>>((acc, current) => {
|
||||
|
|
|
@ -3045,6 +3045,7 @@ describe('XYChart component', () => {
|
|||
|
||||
describe('annotations', () => {
|
||||
const customLineStaticAnnotation: EventAnnotationOutput = {
|
||||
id: 'event1',
|
||||
time: '2022-03-18T08:25:00.000Z',
|
||||
label: 'Event 1',
|
||||
icon: 'triangle',
|
||||
|
@ -3055,11 +3056,13 @@ describe('XYChart component', () => {
|
|||
};
|
||||
|
||||
const defaultLineStaticAnnotation = {
|
||||
id: 'annotation',
|
||||
time: '2022-03-18T08:25:17.140Z',
|
||||
label: 'Annotation',
|
||||
type: 'manual_point_event_annotation' as const,
|
||||
};
|
||||
const defaultRangeStaticAnnotation = {
|
||||
id: 'range_annotation',
|
||||
time: '2022-03-18T08:25:17.140Z',
|
||||
endTime: '2022-03-31T08:25:17.140Z',
|
||||
label: 'Event range',
|
||||
|
|
|
@ -97,5 +97,5 @@ export const getEsaggsMeta: () => Omit<EsaggsExpressionFunctionDefinition, 'fn'>
|
|||
},
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export { handleRequest as handleEsaggsRequest };
|
||||
export type { RequestHandlerParams } from './request_handler';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { IAggConfigs } from '../../aggs';
|
|||
import { ISearchStartSearchSource } from '../../search_source';
|
||||
import { tabifyAggResponse } from '../../tabify';
|
||||
|
||||
interface RequestHandlerParams {
|
||||
export interface RequestHandlerParams {
|
||||
abortSignal?: AbortSignal;
|
||||
aggs: IAggConfigs;
|
||||
filters?: Filter[];
|
||||
|
|
|
@ -9,17 +9,17 @@
|
|||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IndexPatternExpressionType } from '@kbn/data-views-plugin/common';
|
||||
import type { EventAnnotationOutput } from '../manual_event_annotation/types';
|
||||
import type { EventAnnotationOutput } from '../types';
|
||||
|
||||
export interface EventAnnotationGroupOutput {
|
||||
type: 'event_annotation_group';
|
||||
annotations: EventAnnotationOutput[];
|
||||
index?: IndexPatternExpressionType;
|
||||
dataView: IndexPatternExpressionType;
|
||||
}
|
||||
|
||||
export interface EventAnnotationGroupArgs {
|
||||
annotations: EventAnnotationOutput[];
|
||||
index?: IndexPatternExpressionType;
|
||||
dataView: IndexPatternExpressionType;
|
||||
}
|
||||
|
||||
export function eventAnnotationGroup(): ExpressionFunctionDefinition<
|
||||
|
@ -37,18 +37,23 @@ export function eventAnnotationGroup(): ExpressionFunctionDefinition<
|
|||
defaultMessage: 'Event annotation group',
|
||||
}),
|
||||
args: {
|
||||
index: {
|
||||
dataView: {
|
||||
types: ['index_pattern'],
|
||||
required: false,
|
||||
help: i18n.translate('eventAnnotation.group.args.annotationConfigs.index.help', {
|
||||
required: true,
|
||||
help: i18n.translate('eventAnnotation.group.args.annotationConfigs.dataView.help', {
|
||||
defaultMessage: 'Data view retrieved with indexPatternLoad',
|
||||
}),
|
||||
},
|
||||
annotations: {
|
||||
types: ['manual_point_event_annotation', 'manual_range_event_annotation'],
|
||||
types: [
|
||||
'manual_point_event_annotation',
|
||||
'manual_range_event_annotation',
|
||||
'query_point_event_annotation',
|
||||
],
|
||||
help: i18n.translate('eventAnnotation.group.args.annotationConfigs', {
|
||||
defaultMessage: 'Annotation configs',
|
||||
}),
|
||||
required: true,
|
||||
multi: true,
|
||||
},
|
||||
},
|
||||
|
@ -56,7 +61,7 @@ export function eventAnnotationGroup(): ExpressionFunctionDefinition<
|
|||
return {
|
||||
type: 'event_annotation_group',
|
||||
annotations: args.annotations,
|
||||
index: args.index,
|
||||
dataView: args.dataView,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ExpressionValueSearchContext } from '@kbn/data-plugin/common';
|
||||
import { FetchEventAnnotationsArgs, fetchEventAnnotations } from '.';
|
||||
|
||||
const mockHandlers = {
|
||||
abortSignal: jest.fn() as unknown as jest.Mocked<AbortSignal>,
|
||||
getSearchContext: jest.fn(),
|
||||
getSearchSessionId: jest.fn().mockReturnValue('abc123'),
|
||||
getExecutionContext: jest.fn(),
|
||||
inspectorAdapters: jest.fn(),
|
||||
variables: {},
|
||||
types: {},
|
||||
};
|
||||
|
||||
const args = {
|
||||
interval: '30m',
|
||||
group: [
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [
|
||||
{
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-07-05T11:12:00Z',
|
||||
},
|
||||
{
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-07-05T01:18:00Z',
|
||||
},
|
||||
{
|
||||
type: 'manual_range_event_annotation',
|
||||
time: '2022-07-03T05:00:00Z',
|
||||
endTime: '2022-07-05T00:01:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [
|
||||
{
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-07-05T04:34:00Z',
|
||||
label: 'custom',
|
||||
color: '#9170b8',
|
||||
lineWidth: 3,
|
||||
lineStyle: 'dotted',
|
||||
icon: 'triangle',
|
||||
textVisibility: true,
|
||||
},
|
||||
{
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-07-05T05:55:00Z',
|
||||
isHidden: true,
|
||||
},
|
||||
{
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-08-05T12:48:10Z',
|
||||
},
|
||||
{
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-06-05T12:48:10Z',
|
||||
},
|
||||
{
|
||||
type: 'manual_range_event_annotation',
|
||||
time: '2022-06-03T05:00:00Z',
|
||||
endTime: '2022-06-05T00:01:00Z',
|
||||
},
|
||||
{
|
||||
type: 'manual_range_event_annotation',
|
||||
time: '2022-08-03T05:00:00Z',
|
||||
endTime: '2022-08-05T00:01:00Z',
|
||||
},
|
||||
{
|
||||
type: 'manual_range_event_annotation',
|
||||
time: '2022-06-03T05:00:00Z',
|
||||
endTime: '2022-08-05T00:01:00Z',
|
||||
label: 'Event range',
|
||||
color: '#F04E981A',
|
||||
outside: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as FetchEventAnnotationsArgs;
|
||||
|
||||
const input = {
|
||||
type: 'kibana_context',
|
||||
query: [],
|
||||
filters: [],
|
||||
timeRange: {
|
||||
type: 'timerange',
|
||||
from: '2022-07-01T00:00:00Z',
|
||||
to: '2022-07-31T00:00:00Z',
|
||||
},
|
||||
} as ExpressionValueSearchContext;
|
||||
|
||||
describe('fetchEventAnnotations', () => {
|
||||
test('Sorts annotations by time, assigns correct timebuckets, filters out hidden and out of range annotations', async () => {
|
||||
const result = await fetchEventAnnotations().fn(input, args, mockHandlers).toPromise();
|
||||
expect(result!.rows).toEqual([
|
||||
{
|
||||
type: 'range',
|
||||
time: '2022-06-03T05:00:00Z',
|
||||
endTime: '2022-08-05T00:01:00Z',
|
||||
label: 'Event range',
|
||||
color: '#F04E981A',
|
||||
outside: false,
|
||||
timebucket: '2022-06-03T05:00:00.000Z',
|
||||
},
|
||||
{
|
||||
type: 'range',
|
||||
time: '2022-07-03T05:00:00Z',
|
||||
endTime: '2022-07-05T00:01:00Z',
|
||||
timebucket: '2022-07-03T05:00:00.000Z',
|
||||
},
|
||||
{
|
||||
type: 'point',
|
||||
time: '2022-07-05T01:18:00Z',
|
||||
timebucket: '2022-07-05T01:00:00.000Z',
|
||||
},
|
||||
{
|
||||
type: 'point',
|
||||
time: '2022-07-05T04:34:00Z',
|
||||
label: 'custom',
|
||||
color: '#9170b8',
|
||||
lineWidth: 3,
|
||||
lineStyle: 'dotted',
|
||||
icon: 'triangle',
|
||||
textVisibility: true,
|
||||
timebucket: '2022-07-05T04:30:00.000Z',
|
||||
},
|
||||
{
|
||||
type: 'point',
|
||||
time: '2022-07-05T11:12:00Z',
|
||||
timebucket: '2022-07-05T11:00:00.000Z',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FetchEventAnnotationsExpressionFunctionDefinition } from './types';
|
||||
|
||||
/** @internal */
|
||||
export const getFetchEventAnnotationsMeta: () => Omit<
|
||||
FetchEventAnnotationsExpressionFunctionDefinition,
|
||||
'fn'
|
||||
> = () => ({
|
||||
name: 'fetch_event_annotations',
|
||||
aliases: [],
|
||||
type: 'datatable',
|
||||
inputTypes: ['kibana_context', 'null'],
|
||||
help: i18n.translate('eventAnnotation.fetchEventAnnotations.description', {
|
||||
defaultMessage: 'Fetch event annotations',
|
||||
}),
|
||||
args: {
|
||||
timezone: {
|
||||
aliases: ['tz'],
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.fetchEventAnnotations.timezone.help', {
|
||||
defaultMessage: 'The timezone to use for date operations. Valid IANA format.',
|
||||
}),
|
||||
},
|
||||
groups: {
|
||||
types: ['event_annotation_group'],
|
||||
help: i18n.translate('eventAnnotation.fetchEventAnnotations.args.annotationConfigs', {
|
||||
defaultMessage: 'Annotation configs',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
interval: {
|
||||
required: true,
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.fetchEventAnnotations.args.interval.help', {
|
||||
defaultMessage: 'Interval to use for this aggregation',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { handleEsaggsRequest, RequestHandlerParams } from '@kbn/data-plugin/common';
|
||||
|
||||
// in a separate file to solve a mocking problem for tests
|
||||
export const handleRequest = (args: RequestHandlerParams) => handleEsaggsRequest(args);
|
|
@ -6,98 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { defer, switchMap, Observable } from 'rxjs';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionValueSearchContext, parseEsInterval } from '@kbn/data-plugin/common';
|
||||
import type { ExpressionFunctionDefinition, Datatable } from '@kbn/expressions-plugin/common';
|
||||
import moment from 'moment';
|
||||
import { ESCalendarInterval, ESFixedInterval, roundDateToESInterval } from '@elastic/charts';
|
||||
import { EventAnnotationGroupOutput } from '../event_annotation_group';
|
||||
import { annotationColumns, EventAnnotationOutput } from '../manual_event_annotation/types';
|
||||
import { filterOutOfTimeRange, isManualPointAnnotation, sortByTime } from './utils';
|
||||
|
||||
export interface FetchEventAnnotationsDatatable {
|
||||
annotations: EventAnnotationOutput[];
|
||||
type: 'fetch_event_annotations';
|
||||
}
|
||||
|
||||
export type FetchEventAnnotationsOutput = Observable<Datatable>;
|
||||
|
||||
export interface FetchEventAnnotationsArgs {
|
||||
group: EventAnnotationGroupOutput[];
|
||||
interval: string;
|
||||
}
|
||||
|
||||
export type FetchEventAnnotationsExpressionFunctionDefinition = ExpressionFunctionDefinition<
|
||||
'fetch_event_annotations',
|
||||
ExpressionValueSearchContext | null,
|
||||
FetchEventAnnotationsArgs,
|
||||
FetchEventAnnotationsOutput
|
||||
>;
|
||||
|
||||
export function fetchEventAnnotations(): FetchEventAnnotationsExpressionFunctionDefinition {
|
||||
return {
|
||||
name: 'fetch_event_annotations',
|
||||
aliases: [],
|
||||
type: 'datatable',
|
||||
inputTypes: ['kibana_context', 'null'],
|
||||
help: i18n.translate('eventAnnotation.fetchEventAnnotations.description', {
|
||||
defaultMessage: 'Fetch event annotations',
|
||||
}),
|
||||
args: {
|
||||
group: {
|
||||
types: ['event_annotation_group'],
|
||||
help: i18n.translate('eventAnnotation.fetchEventAnnotations.args.annotationConfigs', {
|
||||
defaultMessage: 'Annotation configs',
|
||||
}),
|
||||
multi: true,
|
||||
},
|
||||
interval: {
|
||||
required: true,
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.fetchEventAnnotations.args.interval.help', {
|
||||
defaultMessage: 'Interval to use for this aggregation',
|
||||
}),
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
return defer(async () => {
|
||||
const annotations = args.group
|
||||
.flatMap((group) => group.annotations)
|
||||
.filter(
|
||||
(annotation) =>
|
||||
!annotation.isHidden && filterOutOfTimeRange(annotation, input?.timeRange)
|
||||
);
|
||||
// TODO: fetching for Query annotations goes here
|
||||
|
||||
return { annotations };
|
||||
}).pipe(
|
||||
switchMap(({ annotations }) => {
|
||||
const datatable: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: annotationColumns,
|
||||
rows: annotations.sort(sortByTime).map((annotation) => {
|
||||
const initialDate = moment(annotation.time).valueOf();
|
||||
const snappedDate = roundDateToESInterval(
|
||||
initialDate,
|
||||
parseEsInterval(args.interval) as ESCalendarInterval | ESFixedInterval,
|
||||
'start',
|
||||
'UTC'
|
||||
);
|
||||
return {
|
||||
...annotation,
|
||||
type: isManualPointAnnotation(annotation) ? 'point' : 'range',
|
||||
timebucket: moment(snappedDate).toISOString(),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
return new Observable<Datatable>((subscriber) => {
|
||||
subscriber.next(datatable);
|
||||
subscriber.complete();
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
export { getFetchEventAnnotationsMeta } from './fetch_event_annotations_fn';
|
||||
export { requestEventAnnotations } from './request_event_annotations';
|
||||
export * from './types';
|
||||
|
|
|
@ -0,0 +1,353 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { defer, firstValueFrom } from 'rxjs';
|
||||
import { partition } from 'lodash';
|
||||
import {
|
||||
AggsStart,
|
||||
DataViewsContract,
|
||||
DataViewSpec,
|
||||
ExpressionValueSearchContext,
|
||||
parseEsInterval,
|
||||
AggConfigs,
|
||||
IndexPatternExpressionType,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { ExecutionContext } from '@kbn/expressions-plugin/common';
|
||||
import moment from 'moment';
|
||||
import { ESCalendarInterval, ESFixedInterval, roundDateToESInterval } from '@elastic/charts';
|
||||
import { Adapters } from '@kbn/inspector-plugin/common';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { handleRequest } from './handle_request';
|
||||
import {
|
||||
ANNOTATIONS_PER_BUCKET,
|
||||
isInRange,
|
||||
isManualAnnotation,
|
||||
isManualPointAnnotation,
|
||||
postprocessAnnotations,
|
||||
sortByTime,
|
||||
wrapRowsInDatatable,
|
||||
} from './utils';
|
||||
import type { ManualEventAnnotationOutput } from '../manual_event_annotation/types';
|
||||
import { QueryPointEventAnnotationOutput } from '../query_point_event_annotation/types';
|
||||
import { FetchEventAnnotationsArgs, FetchEventAnnotationsStartDependencies } from './types';
|
||||
|
||||
interface ManualGroup {
|
||||
type: 'manual';
|
||||
annotations: ManualEventAnnotationOutput[];
|
||||
}
|
||||
|
||||
interface QueryGroup {
|
||||
type: 'query';
|
||||
annotations: QueryPointEventAnnotationOutput[];
|
||||
allFields?: string[];
|
||||
dataView: IndexPatternExpressionType;
|
||||
timeField: string;
|
||||
}
|
||||
|
||||
export const requestEventAnnotations = (
|
||||
input: ExpressionValueSearchContext | null,
|
||||
args: FetchEventAnnotationsArgs,
|
||||
{
|
||||
inspectorAdapters,
|
||||
abortSignal,
|
||||
getSearchSessionId,
|
||||
getExecutionContext,
|
||||
}: ExecutionContext<Adapters, SerializableRecord>,
|
||||
getStartDependencies: () => Promise<FetchEventAnnotationsStartDependencies>
|
||||
) => {
|
||||
return defer(async () => {
|
||||
const [manualGroups, queryGroups] = partition(
|
||||
regroupForRequestOptimization(args, input),
|
||||
isManualSubGroup
|
||||
);
|
||||
|
||||
const manualAnnotationDatatableRows = manualGroups.length
|
||||
? convertManualToDatatableRows(manualGroups[0], args.interval, args.timezone)
|
||||
: [];
|
||||
if (!queryGroups.length) {
|
||||
return manualAnnotationDatatableRows.length
|
||||
? wrapRowsInDatatable(manualAnnotationDatatableRows)
|
||||
: null;
|
||||
}
|
||||
|
||||
const { aggs, dataViews, searchSource, getNow } = await getStartDependencies();
|
||||
|
||||
const createEsaggsSingleRequest = async ({
|
||||
dataView,
|
||||
aggConfigs,
|
||||
timeFields,
|
||||
}: {
|
||||
dataView: any;
|
||||
aggConfigs: AggConfigs;
|
||||
timeFields: string[];
|
||||
}) =>
|
||||
firstValueFrom(
|
||||
handleRequest({
|
||||
aggs: aggConfigs,
|
||||
indexPattern: dataView,
|
||||
timeFields,
|
||||
filters: input?.filters,
|
||||
query: input?.query as any,
|
||||
timeRange: input?.timeRange,
|
||||
abortSignal,
|
||||
inspectorAdapters,
|
||||
searchSessionId: getSearchSessionId(),
|
||||
searchSourceService: searchSource,
|
||||
getNow,
|
||||
executionContext: getExecutionContext(),
|
||||
})
|
||||
);
|
||||
|
||||
const esaggsGroups = await prepareEsaggsForQueryGroups(
|
||||
queryGroups,
|
||||
args.interval,
|
||||
dataViews,
|
||||
aggs
|
||||
);
|
||||
|
||||
const allQueryAnnotationsConfigs = queryGroups.flatMap((group) => group.annotations);
|
||||
|
||||
const esaggsResponses = await Promise.all(
|
||||
esaggsGroups.map(async ({ esaggsParams, fieldsColIdMap }) => ({
|
||||
response: await createEsaggsSingleRequest(esaggsParams),
|
||||
fieldsColIdMap,
|
||||
}))
|
||||
);
|
||||
|
||||
return postprocessAnnotations(
|
||||
esaggsResponses,
|
||||
allQueryAnnotationsConfigs,
|
||||
manualAnnotationDatatableRows
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const isManualSubGroup = (group: ManualGroup | QueryGroup): group is ManualGroup => {
|
||||
return group.type === 'manual';
|
||||
};
|
||||
|
||||
const convertManualToDatatableRows = (
|
||||
manualGroup: ManualGroup,
|
||||
interval: string,
|
||||
timezone: string
|
||||
) => {
|
||||
const datatableRows = manualGroup.annotations
|
||||
.map((annotation) => {
|
||||
const initialDate = moment(annotation.time).valueOf();
|
||||
const snappedDate = roundDateToESInterval(
|
||||
initialDate,
|
||||
parseEsInterval(interval) as ESCalendarInterval | ESFixedInterval,
|
||||
'start',
|
||||
timezone
|
||||
);
|
||||
return {
|
||||
timebucket: moment(snappedDate).toISOString(),
|
||||
...annotation,
|
||||
type: isManualPointAnnotation(annotation) ? 'point' : 'range',
|
||||
};
|
||||
})
|
||||
.sort(sortByTime);
|
||||
|
||||
return datatableRows;
|
||||
};
|
||||
|
||||
const prepareEsaggsForQueryGroups = async (
|
||||
queryGroups: QueryGroup[],
|
||||
interval: string,
|
||||
dataViews: DataViewsContract,
|
||||
aggs: AggsStart
|
||||
) => {
|
||||
const uniqueDataViewsToLoad = queryGroups
|
||||
.map((g) => g.dataView.value)
|
||||
.reduce<DataViewSpec[]>((acc, current) => {
|
||||
if (acc.find((el) => el.id === current.id)) return acc;
|
||||
return [...acc, current];
|
||||
}, []);
|
||||
|
||||
const loadedDataViews = await Promise.all(
|
||||
uniqueDataViewsToLoad.map((dataView) => dataViews.create(dataView, true))
|
||||
);
|
||||
|
||||
return queryGroups.map((group) => {
|
||||
const dataView = loadedDataViews.find((dv) => dv.id === group.dataView.value.id)!;
|
||||
|
||||
const annotationsFilters = {
|
||||
type: 'agg_type',
|
||||
value: {
|
||||
enabled: true,
|
||||
schema: 'bucket',
|
||||
type: 'filters',
|
||||
params: {
|
||||
filters: group.annotations.map((annotation) => ({
|
||||
label: annotation.id,
|
||||
input: { ...annotation.filter },
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const dateHistogram = {
|
||||
type: 'agg_type',
|
||||
value: {
|
||||
enabled: true,
|
||||
schema: 'bucket',
|
||||
type: 'date_histogram',
|
||||
params: {
|
||||
useNormalizedEsInterval: true,
|
||||
field: group.timeField,
|
||||
interval,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const count = {
|
||||
type: 'agg_type',
|
||||
value: {
|
||||
enabled: true,
|
||||
schema: 'metric',
|
||||
type: 'count',
|
||||
},
|
||||
};
|
||||
|
||||
const timefieldTopMetric = {
|
||||
type: 'agg_type',
|
||||
value: {
|
||||
enabled: true,
|
||||
type: 'top_metrics',
|
||||
params: {
|
||||
field: group.timeField,
|
||||
size: ANNOTATIONS_PER_BUCKET,
|
||||
sortOrder: 'asc',
|
||||
sortField: group.timeField,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const fieldsTopMetric = (group.allFields || []).map((field) => ({
|
||||
type: 'agg_type',
|
||||
value: {
|
||||
enabled: true,
|
||||
type: 'top_metrics',
|
||||
params: {
|
||||
field,
|
||||
size: ANNOTATIONS_PER_BUCKET,
|
||||
sortOrder: 'asc',
|
||||
sortField: group.timeField,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const aggregations = [
|
||||
annotationsFilters,
|
||||
dateHistogram,
|
||||
count,
|
||||
timefieldTopMetric,
|
||||
...fieldsTopMetric,
|
||||
];
|
||||
|
||||
const aggConfigs = aggs.createAggConfigs(dataView, aggregations?.map((agg) => agg.value) ?? []);
|
||||
return {
|
||||
esaggsParams: { dataView, aggConfigs, timeFields: [group.timeField] },
|
||||
fieldsColIdMap:
|
||||
group.allFields?.reduce<Record<string, string>>(
|
||||
(acc, fieldName, i) => ({
|
||||
...acc,
|
||||
// esaggs names the columns col-0-1 (filters), col-1-2(date histogram), col-2-3(timefield), col-3-4(count), col-4-5 (all the extra fields, that's why we start with `col-${i + 4}-${i + 5}`)
|
||||
[fieldName]: `col-${i + 4}-${i + 5}`,
|
||||
}),
|
||||
{}
|
||||
) || {},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function regroupForRequestOptimization(
|
||||
{ groups }: FetchEventAnnotationsArgs,
|
||||
input: ExpressionValueSearchContext | null
|
||||
) {
|
||||
const outputGroups = groups
|
||||
.map((g) => {
|
||||
return g.annotations.reduce<Record<string, ManualGroup | QueryGroup>>((acc, current) => {
|
||||
if (current.isHidden) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (isManualAnnotation(current)) {
|
||||
if (!isInRange(current, input?.timeRange)) {
|
||||
return acc;
|
||||
}
|
||||
if (!acc.manual) {
|
||||
acc.manual = { type: 'manual', annotations: [] };
|
||||
}
|
||||
(acc.manual as ManualGroup).annotations.push(current);
|
||||
return acc;
|
||||
} else {
|
||||
const key = `${g.dataView.value.id}-${current.timeField}`;
|
||||
const subGroup = acc[key] as QueryGroup;
|
||||
if (subGroup) {
|
||||
let allFields = [...(subGroup.allFields || []), ...(current.extraFields || [])];
|
||||
if (current.textField) {
|
||||
allFields = [...allFields, current.textField];
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[key]: {
|
||||
...subGroup,
|
||||
allFields: [...new Set(allFields)],
|
||||
annotations: [...subGroup.annotations, current],
|
||||
},
|
||||
};
|
||||
}
|
||||
let allFields = current.extraFields || [];
|
||||
if (current.textField) {
|
||||
allFields = [...allFields, current.textField];
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[key]: {
|
||||
type: 'query',
|
||||
dataView: g.dataView,
|
||||
timeField: current.timeField,
|
||||
allFields,
|
||||
annotations: [current],
|
||||
},
|
||||
};
|
||||
}
|
||||
}, {});
|
||||
})
|
||||
.reduce((acc, currentGroup) => {
|
||||
Object.keys(currentGroup).forEach((key) => {
|
||||
if (acc[key]) {
|
||||
const currentSubGroup = currentGroup[key];
|
||||
const requestGroup = acc[key];
|
||||
|
||||
if (isManualSubGroup(currentSubGroup) || isManualSubGroup(requestGroup)) {
|
||||
acc[key] = {
|
||||
...requestGroup,
|
||||
annotations: [...requestGroup.annotations, ...currentSubGroup.annotations],
|
||||
} as ManualGroup;
|
||||
} else {
|
||||
acc[key] = {
|
||||
...requestGroup,
|
||||
annotations: [...requestGroup.annotations, ...currentSubGroup.annotations],
|
||||
allFields: [
|
||||
...new Set([
|
||||
...(requestGroup.allFields || []),
|
||||
...(currentSubGroup.allFields || []),
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
acc[key] = currentGroup[key];
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.values(outputGroups);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
AggsStart,
|
||||
DataViewsContract,
|
||||
ExpressionValueSearchContext,
|
||||
ISearchStartSearchSource,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { ExpressionFunctionDefinition, Datatable } from '@kbn/expressions-plugin/common';
|
||||
import { EventAnnotationGroupOutput } from '../event_annotation_group';
|
||||
|
||||
export type FetchEventAnnotationsOutput = Observable<Datatable | null>;
|
||||
|
||||
export interface FetchEventAnnotationsArgs {
|
||||
groups: EventAnnotationGroupOutput[];
|
||||
interval: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export type FetchEventAnnotationsExpressionFunctionDefinition = ExpressionFunctionDefinition<
|
||||
'fetch_event_annotations',
|
||||
ExpressionValueSearchContext | null,
|
||||
FetchEventAnnotationsArgs,
|
||||
FetchEventAnnotationsOutput
|
||||
>;
|
||||
|
||||
/** @internal */
|
||||
export interface FetchEventAnnotationsStartDependencies {
|
||||
aggs: AggsStart;
|
||||
dataViews: DataViewsContract;
|
||||
searchSource: ISearchStartSearchSource;
|
||||
getNow?: () => Date;
|
||||
}
|
|
@ -7,12 +7,22 @@
|
|||
*/
|
||||
|
||||
import { TimeRange } from '@kbn/data-plugin/common';
|
||||
|
||||
import { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/common';
|
||||
import { omit, pick } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EventAnnotationOutput,
|
||||
ManualEventAnnotationOutput,
|
||||
ManualPointEventAnnotationOutput,
|
||||
ManualRangeEventAnnotationOutput,
|
||||
} from '../manual_event_annotation/types';
|
||||
import { QueryPointEventAnnotationOutput } from '../query_point_event_annotation/types';
|
||||
import {
|
||||
annotationColumns,
|
||||
AvailableAnnotationIcon,
|
||||
EventAnnotationOutput,
|
||||
LineStyle,
|
||||
PointStyleProps,
|
||||
} from '../types';
|
||||
|
||||
export const isRangeAnnotation = (
|
||||
annotation: EventAnnotationOutput
|
||||
|
@ -26,7 +36,12 @@ export const isManualPointAnnotation = (
|
|||
return 'time' in annotation && !('endTime' in annotation);
|
||||
};
|
||||
|
||||
export const filterOutOfTimeRange = (annotation: EventAnnotationOutput, timerange?: TimeRange) => {
|
||||
export const isManualAnnotation = (
|
||||
annotation: EventAnnotationOutput
|
||||
): annotation is ManualPointEventAnnotationOutput | ManualRangeEventAnnotationOutput =>
|
||||
isRangeAnnotation(annotation) || isManualPointAnnotation(annotation);
|
||||
|
||||
export const isInRange = (annotation: ManualEventAnnotationOutput, timerange?: TimeRange) => {
|
||||
if (!timerange) {
|
||||
return false;
|
||||
}
|
||||
|
@ -36,8 +51,175 @@ export const filterOutOfTimeRange = (annotation: EventAnnotationOutput, timerang
|
|||
if (isManualPointAnnotation(annotation)) {
|
||||
return annotation.time >= timerange.from && annotation.time <= timerange.to;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const sortByTime = (a: EventAnnotationOutput, b: EventAnnotationOutput) => {
|
||||
return a.time.localeCompare(b.time);
|
||||
export const sortByTime = (a: DatatableRow, b: DatatableRow) => {
|
||||
return 'time' in a && 'time' in b ? a.time.localeCompare(b.time) : 0;
|
||||
};
|
||||
|
||||
export const wrapRowsInDatatable = (rows: DatatableRow[], columns = annotationColumns) => {
|
||||
const datatable: Datatable = {
|
||||
type: 'datatable',
|
||||
columns,
|
||||
rows,
|
||||
};
|
||||
return datatable;
|
||||
};
|
||||
|
||||
export const ANNOTATIONS_PER_BUCKET = 10;
|
||||
|
||||
export const postprocessAnnotations = (
|
||||
esaggsResponses: Array<{
|
||||
response: Datatable;
|
||||
fieldsColIdMap: Record<string, string>;
|
||||
}>,
|
||||
queryAnnotationConfigs: QueryPointEventAnnotationOutput[],
|
||||
manualAnnotationDatatableRows: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
time: string;
|
||||
label: string;
|
||||
color?: string | undefined;
|
||||
icon?: AvailableAnnotationIcon | undefined;
|
||||
lineWidth?: number | undefined;
|
||||
lineStyle?: LineStyle | undefined;
|
||||
textVisibility?: boolean | undefined;
|
||||
isHidden?: boolean | undefined;
|
||||
timebucket: string;
|
||||
}> // todo: simplify types
|
||||
) => {
|
||||
const datatableColumns: DatatableColumn[] = esaggsResponses
|
||||
.flatMap(({ response, fieldsColIdMap }) => {
|
||||
const swappedFieldsColIdMap = Object.fromEntries(
|
||||
Object.entries(fieldsColIdMap).map(([k, v]) => [v, k])
|
||||
);
|
||||
return response.columns
|
||||
.filter((col) => swappedFieldsColIdMap[col.id])
|
||||
.map((col) => {
|
||||
return {
|
||||
...col,
|
||||
id: `field:${swappedFieldsColIdMap[col.id]}`,
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce<DatatableColumn[]>((acc, col) => {
|
||||
if (!acc.find((c) => c.id === col.id)) {
|
||||
acc.push(col);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
.concat(annotationColumns);
|
||||
|
||||
const modifiedRows = esaggsResponses
|
||||
.flatMap(({ response, fieldsColIdMap }) =>
|
||||
response.rows.map((row) => {
|
||||
const annotationConfig = queryAnnotationConfigs.find(({ id }) => id === row['col-0-1']);
|
||||
if (!annotationConfig) {
|
||||
throw new Error(`Could not find annotation config for id: ${row['col-0-1']}`);
|
||||
}
|
||||
|
||||
let modifiedRow: TimebucketRow = {
|
||||
...passStylesFromAnnotationConfig(annotationConfig),
|
||||
id: row['col-0-1'],
|
||||
timebucket: moment(row['col-1-2']).toISOString(),
|
||||
time: row['col-3-4'],
|
||||
type: 'point',
|
||||
label: annotationConfig.textField
|
||||
? row[fieldsColIdMap[annotationConfig.textField]]
|
||||
: annotationConfig.label,
|
||||
};
|
||||
const countRow = row['col-2-3'];
|
||||
if (countRow > ANNOTATIONS_PER_BUCKET) {
|
||||
modifiedRow = {
|
||||
skippedCount: countRow - ANNOTATIONS_PER_BUCKET,
|
||||
...modifiedRow,
|
||||
};
|
||||
}
|
||||
|
||||
if (annotationConfig?.extraFields?.length) {
|
||||
modifiedRow.extraFields = annotationConfig.extraFields.reduce(
|
||||
(acc, field) => ({ ...acc, [`field:${field}`]: row[fieldsColIdMap[field]] }),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
return modifiedRow;
|
||||
})
|
||||
)
|
||||
.concat(...manualAnnotationDatatableRows)
|
||||
.sort((a, b) => a.timebucket.localeCompare(b.timebucket));
|
||||
|
||||
const skippedCountPerBucket = getSkippedCountPerBucket(modifiedRows);
|
||||
|
||||
const flattenedRows = modifiedRows
|
||||
.reduce<DatatableRow[]>((acc, row) => {
|
||||
if (!Array.isArray(row.time)) {
|
||||
acc.push({
|
||||
...omit(row, ['extraFields', 'skippedCount']),
|
||||
...row.extraFields,
|
||||
});
|
||||
} else {
|
||||
row.time.forEach((time, index) => {
|
||||
const extraFields: Record<string, string | number | boolean> = {};
|
||||
if (row.extraFields) {
|
||||
Object.entries(row?.extraFields).forEach(([fieldKey, fieldValue]) => {
|
||||
extraFields[fieldKey] = Array.isArray(fieldValue) ? fieldValue[index] : fieldValue;
|
||||
});
|
||||
}
|
||||
|
||||
acc.push({
|
||||
...omit(row, ['extraFields', 'skippedCount']),
|
||||
...extraFields,
|
||||
label: Array.isArray(row.label) ? row.label[index] : row.label,
|
||||
time,
|
||||
});
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
.sort(sortByTime)
|
||||
.reduce<DatatableRow[]>((acc, row, index, arr) => {
|
||||
if (index === arr.length - 1 || row.timebucket !== arr[index + 1].timebucket) {
|
||||
acc.push({ ...row, skippedCount: skippedCountPerBucket[row.timebucket] });
|
||||
return acc;
|
||||
}
|
||||
acc.push(row);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return wrapRowsInDatatable(flattenedRows, datatableColumns);
|
||||
};
|
||||
|
||||
type TimebucketRow = {
|
||||
id: string;
|
||||
timebucket: string;
|
||||
time: string;
|
||||
type: string;
|
||||
skippedCount?: number;
|
||||
extraFields?: Record<string, string | number | string[] | number[]>;
|
||||
} & PointStyleProps;
|
||||
|
||||
function getSkippedCountPerBucket(rows: TimebucketRow[]) {
|
||||
return rows.reduce<Record<string, number>>((acc, current) => {
|
||||
if (current.skippedCount) {
|
||||
acc[current.timebucket] = (acc[current.timebucket] || 0) + current.skippedCount;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function passStylesFromAnnotationConfig(
|
||||
annotationConfig: QueryPointEventAnnotationOutput
|
||||
): PointStyleProps {
|
||||
return {
|
||||
...pick(annotationConfig, [
|
||||
`label`,
|
||||
`color`,
|
||||
`icon`,
|
||||
`lineWidth`,
|
||||
`lineStyle`,
|
||||
`textVisibility`,
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,20 +7,26 @@
|
|||
*/
|
||||
|
||||
export type {
|
||||
EventAnnotationArgs,
|
||||
EventAnnotationOutput,
|
||||
ManualPointEventAnnotationArgs,
|
||||
ManualPointEventAnnotationOutput,
|
||||
ManualRangeEventAnnotationArgs,
|
||||
ManualRangeEventAnnotationOutput,
|
||||
} from './manual_event_annotation/types';
|
||||
export type {
|
||||
QueryPointEventAnnotationArgs,
|
||||
QueryPointEventAnnotationOutput,
|
||||
} from './query_point_event_annotation/types';
|
||||
export type { EventAnnotationArgs, EventAnnotationOutput } from './types';
|
||||
export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation';
|
||||
export { queryPointEventAnnotation } from './query_point_event_annotation';
|
||||
export { eventAnnotationGroup } from './event_annotation_group';
|
||||
export type { EventAnnotationGroupArgs } from './event_annotation_group';
|
||||
export { fetchEventAnnotations } from './fetch_event_annotations';
|
||||
export type { FetchEventAnnotationsArgs } from './fetch_event_annotations';
|
||||
|
||||
export type { FetchEventAnnotationsArgs } from './fetch_event_annotations/types';
|
||||
export type {
|
||||
EventAnnotationConfig,
|
||||
RangeEventAnnotationConfig,
|
||||
PointInTimeEventAnnotationConfig,
|
||||
QueryPointEventAnnotationConfig,
|
||||
AvailableAnnotationIcon,
|
||||
} from './types';
|
||||
|
|
|
@ -31,6 +31,12 @@ export const manualPointEventAnnotation: ExpressionFunctionDefinition<
|
|||
}),
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
id: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.manualAnnotation.args.id', {
|
||||
defaultMessage: `Id for annotation`,
|
||||
}),
|
||||
},
|
||||
time: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.manualAnnotation.args.time', {
|
||||
|
@ -100,20 +106,26 @@ export const manualRangeEventAnnotation: ExpressionFunctionDefinition<
|
|||
name: 'manual_range_event_annotation',
|
||||
aliases: [],
|
||||
type: 'manual_range_event_annotation',
|
||||
help: i18n.translate('eventAnnotation.manualAnnotation.description', {
|
||||
help: i18n.translate('eventAnnotation.rangeAnnotation.description', {
|
||||
defaultMessage: `Configure manual annotation`,
|
||||
}),
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
id: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.rangeAnnotation.args.id', {
|
||||
defaultMessage: `Id for annotation`,
|
||||
}),
|
||||
},
|
||||
time: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.manualAnnotation.args.time', {
|
||||
help: i18n.translate('eventAnnotation.rangeAnnotation.args.time', {
|
||||
defaultMessage: `Timestamp for annotation`,
|
||||
}),
|
||||
},
|
||||
endTime: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.manualAnnotation.args.endTime', {
|
||||
help: i18n.translate('eventAnnotation.rangeAnnotation.args.endTime', {
|
||||
defaultMessage: `Timestamp for range annotation`,
|
||||
}),
|
||||
required: false,
|
||||
|
@ -125,19 +137,19 @@ export const manualRangeEventAnnotation: ExpressionFunctionDefinition<
|
|||
},
|
||||
label: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.manualAnnotation.args.label', {
|
||||
help: i18n.translate('eventAnnotation.rangeAnnotation.args.label', {
|
||||
defaultMessage: `The name of the annotation`,
|
||||
}),
|
||||
},
|
||||
color: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.manualAnnotation.args.color', {
|
||||
help: i18n.translate('eventAnnotation.rangeAnnotation.args.color', {
|
||||
defaultMessage: 'The color of the line',
|
||||
}),
|
||||
},
|
||||
isHidden: {
|
||||
types: ['boolean'],
|
||||
help: i18n.translate('eventAnnotation.manualAnnotation.args.isHidden', {
|
||||
help: i18n.translate('eventAnnotation.rangeAnnotation.args.isHidden', {
|
||||
defaultMessage: `Switch to hide annotation`,
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { PointStyleProps, RangeStyleProps } from '../types';
|
||||
|
||||
export type ManualPointEventAnnotationArgs = {
|
||||
id: string;
|
||||
time: string;
|
||||
} & PointStyleProps;
|
||||
|
||||
|
@ -18,6 +18,7 @@ export type ManualPointEventAnnotationOutput = ManualPointEventAnnotationArgs &
|
|||
};
|
||||
|
||||
export type ManualRangeEventAnnotationArgs = {
|
||||
id: string;
|
||||
time: string;
|
||||
endTime: string;
|
||||
} & RangeStyleProps;
|
||||
|
@ -26,22 +27,6 @@ export type ManualRangeEventAnnotationOutput = ManualRangeEventAnnotationArgs &
|
|||
type: 'manual_range_event_annotation';
|
||||
};
|
||||
|
||||
export type EventAnnotationArgs = ManualPointEventAnnotationArgs | ManualRangeEventAnnotationArgs;
|
||||
export type EventAnnotationOutput =
|
||||
export type ManualEventAnnotationOutput =
|
||||
| ManualPointEventAnnotationOutput
|
||||
| ManualRangeEventAnnotationOutput;
|
||||
|
||||
export const annotationColumns: DatatableColumn[] = [
|
||||
{ id: 'time', name: 'time', meta: { type: 'string' } },
|
||||
{ id: 'endTime', name: 'endTime', meta: { type: 'string' } },
|
||||
{ id: 'timebucket', name: 'timebucket', meta: { type: 'string' } },
|
||||
{ id: 'type', name: 'type', meta: { type: 'string' } },
|
||||
{ id: 'label', name: 'label', meta: { type: 'string' } },
|
||||
{ id: 'color', name: 'color', meta: { type: 'string' } },
|
||||
{ id: 'lineStyle', name: 'lineStyle', meta: { type: 'string' } },
|
||||
{ id: 'lineWidth', name: 'lineWidth', meta: { type: 'number' } },
|
||||
{ id: 'icon', name: 'icon', meta: { type: 'string' } },
|
||||
{ id: 'textVisibility', name: 'textVisibility', meta: { type: 'boolean' } },
|
||||
{ id: 'outside', name: 'outside', meta: { type: 'number' } },
|
||||
{ id: 'skippedAnnotationsCount', name: 'skippedAnnotationsCount', meta: { type: 'number' } },
|
||||
];
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AvailableAnnotationIcons } from '../constants';
|
||||
|
||||
import type { QueryPointEventAnnotationArgs, QueryPointEventAnnotationOutput } from './types';
|
||||
|
||||
export const queryPointEventAnnotation: ExpressionFunctionDefinition<
|
||||
'query_point_event_annotation',
|
||||
null,
|
||||
QueryPointEventAnnotationArgs,
|
||||
QueryPointEventAnnotationOutput
|
||||
> = {
|
||||
name: 'query_point_event_annotation',
|
||||
aliases: [],
|
||||
type: 'query_point_event_annotation',
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.description', {
|
||||
defaultMessage: `Configure manual annotation`,
|
||||
}),
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
filter: {
|
||||
types: ['kibana_query'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.filter', {
|
||||
defaultMessage: `Annotation filter`,
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
extraFields: {
|
||||
multi: true,
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.field', {
|
||||
defaultMessage: `The extra fields of the annotation`,
|
||||
}),
|
||||
},
|
||||
id: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.id', {
|
||||
defaultMessage: `The id of the annotation`,
|
||||
}),
|
||||
},
|
||||
timeField: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.timeField', {
|
||||
defaultMessage: `The time field of the annotation`,
|
||||
}),
|
||||
},
|
||||
label: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.label', {
|
||||
defaultMessage: `The name of the annotation`,
|
||||
}),
|
||||
},
|
||||
color: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.color', {
|
||||
defaultMessage: 'The color of the line',
|
||||
}),
|
||||
},
|
||||
lineStyle: {
|
||||
types: ['string'],
|
||||
options: ['solid', 'dotted', 'dashed'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.lineStyle', {
|
||||
defaultMessage: 'The style of the annotation line',
|
||||
}),
|
||||
},
|
||||
lineWidth: {
|
||||
types: ['number'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.lineWidth', {
|
||||
defaultMessage: 'The width of the annotation line',
|
||||
}),
|
||||
},
|
||||
icon: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.icon', {
|
||||
defaultMessage: 'An optional icon used for annotation lines',
|
||||
}),
|
||||
options: [...Object.values(AvailableAnnotationIcons)],
|
||||
strict: true,
|
||||
},
|
||||
textVisibility: {
|
||||
types: ['boolean'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.textVisibility', {
|
||||
defaultMessage: 'Visibility of the label on the annotation line',
|
||||
}),
|
||||
},
|
||||
textField: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.textField', {
|
||||
defaultMessage: `Field name used for the annotation label`,
|
||||
}),
|
||||
},
|
||||
isHidden: {
|
||||
types: ['boolean'],
|
||||
help: i18n.translate('eventAnnotation.queryAnnotation.args.isHidden', {
|
||||
defaultMessage: `Switch to hide annotation`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
fn: function fn(input: unknown, args: QueryPointEventAnnotationArgs) {
|
||||
return {
|
||||
type: 'query_point_event_annotation',
|
||||
...args,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { KibanaQueryOutput } from '@kbn/data-plugin/common';
|
||||
import { PointStyleProps } from '../types';
|
||||
|
||||
export type QueryPointEventAnnotationArgs = {
|
||||
id: string;
|
||||
filter: KibanaQueryOutput;
|
||||
timeField: string;
|
||||
extraFields?: string[];
|
||||
textField?: string;
|
||||
} & PointStyleProps;
|
||||
|
||||
export type QueryPointEventAnnotationOutput = QueryPointEventAnnotationArgs & {
|
||||
type: 'query_point_event_annotation';
|
||||
};
|
|
@ -6,8 +6,19 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { KibanaQueryOutput } from '@kbn/data-plugin/common';
|
||||
import { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { $Values } from '@kbn/utility-types';
|
||||
import { AvailableAnnotationIcons } from './constants';
|
||||
import {
|
||||
ManualEventAnnotationOutput,
|
||||
ManualPointEventAnnotationArgs,
|
||||
ManualRangeEventAnnotationArgs,
|
||||
} from './manual_event_annotation/types';
|
||||
import {
|
||||
QueryPointEventAnnotationArgs,
|
||||
QueryPointEventAnnotationOutput,
|
||||
} from './query_point_event_annotation/types';
|
||||
|
||||
export type LineStyle = 'solid' | 'dashed' | 'dotted';
|
||||
export type Fill = 'inside' | 'outside' | 'none';
|
||||
|
@ -50,4 +61,43 @@ export type RangeEventAnnotationConfig = {
|
|||
|
||||
export type StyleProps = PointStyleProps & RangeStyleProps;
|
||||
|
||||
export type EventAnnotationConfig = PointInTimeEventAnnotationConfig | RangeEventAnnotationConfig;
|
||||
export type QueryPointEventAnnotationConfig = {
|
||||
id: string;
|
||||
filter: KibanaQueryOutput;
|
||||
timeField: string;
|
||||
textField: string;
|
||||
extraFields?: string[];
|
||||
key: {
|
||||
type: 'point_in_time';
|
||||
};
|
||||
} & PointStyleProps;
|
||||
|
||||
export type EventAnnotationConfig =
|
||||
| PointInTimeEventAnnotationConfig
|
||||
| RangeEventAnnotationConfig
|
||||
| QueryPointEventAnnotationConfig;
|
||||
|
||||
export type EventAnnotationArgs =
|
||||
| ManualPointEventAnnotationArgs
|
||||
| ManualRangeEventAnnotationArgs
|
||||
| QueryPointEventAnnotationArgs;
|
||||
|
||||
export type EventAnnotationOutput = ManualEventAnnotationOutput | QueryPointEventAnnotationOutput;
|
||||
|
||||
export const annotationColumns: DatatableColumn[] = [
|
||||
{ id: 'id', name: 'id', meta: { type: 'string' } },
|
||||
{ id: 'time', name: 'time', meta: { type: 'string' } },
|
||||
{ id: 'endTime', name: 'endTime', meta: { type: 'string' } },
|
||||
{ id: 'timebucket', name: 'timebucket', meta: { type: 'string' } },
|
||||
{ id: 'type', name: 'type', meta: { type: 'string' } },
|
||||
{ id: 'label', name: 'label', meta: { type: 'string' } },
|
||||
{ id: 'color', name: 'color', meta: { type: 'string' } },
|
||||
{ id: 'lineStyle', name: 'lineStyle', meta: { type: 'string' } },
|
||||
{ id: 'lineWidth', name: 'lineWidth', meta: { type: 'number' } },
|
||||
{ id: 'icon', name: 'icon', meta: { type: 'string' } },
|
||||
{ id: 'textVisibility', name: 'textVisibility', meta: { type: 'boolean' } },
|
||||
{ id: 'textField', name: 'textField', meta: { type: 'string' } },
|
||||
{ id: 'outside', name: 'outside', meta: { type: 'number' } },
|
||||
{ id: 'type', name: 'type', meta: { type: 'string' } },
|
||||
{ id: 'skippedCount', name: 'skippedCount', meta: { type: 'number' } },
|
||||
];
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { euiLightVars } from '@kbn/ui-theme';
|
||||
import { EventAnnotationConfig, RangeEventAnnotationConfig } from '../../common';
|
||||
import {
|
||||
EventAnnotationConfig,
|
||||
RangeEventAnnotationConfig,
|
||||
PointInTimeEventAnnotationConfig,
|
||||
QueryPointEventAnnotationConfig,
|
||||
} from '../../common';
|
||||
export const defaultAnnotationColor = euiLightVars.euiColorAccent;
|
||||
export const defaultAnnotationRangeColor = `#F04E981A`; // defaultAnnotationColor with opacity 0.1
|
||||
|
||||
|
@ -18,8 +23,20 @@ export const defaultAnnotationLabel = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const isRangeAnnotation = (
|
||||
export const isRangeAnnotationConfig = (
|
||||
annotation?: EventAnnotationConfig
|
||||
): annotation is RangeEventAnnotationConfig => {
|
||||
return Boolean(annotation && annotation?.key.type === 'range');
|
||||
};
|
||||
|
||||
export const isManualPointAnnotationConfig = (
|
||||
annotation?: EventAnnotationConfig
|
||||
): annotation is PointInTimeEventAnnotationConfig => {
|
||||
return Boolean(annotation && 'timestamp' in annotation?.key);
|
||||
};
|
||||
|
||||
export const isQueryAnnotationConfig = (
|
||||
annotation?: EventAnnotationConfig
|
||||
): annotation is QueryPointEventAnnotationConfig => {
|
||||
return Boolean(annotation && 'filter' in annotation);
|
||||
};
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { queryToAst } from '@kbn/data-plugin/common';
|
||||
import { EventAnnotationServiceType } from './types';
|
||||
import {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
defaultAnnotationLabel,
|
||||
isQueryAnnotationConfig,
|
||||
} from './helpers';
|
||||
import { EventAnnotationConfig } from '../../common';
|
||||
import { RangeEventAnnotationConfig } from '../../common/types';
|
||||
|
@ -48,6 +50,42 @@ export function getEventAnnotationService(): EventAnnotationServiceType {
|
|||
},
|
||||
],
|
||||
};
|
||||
} else if (isQueryAnnotationConfig(annotation)) {
|
||||
const {
|
||||
extraFields,
|
||||
label,
|
||||
isHidden,
|
||||
color,
|
||||
lineStyle,
|
||||
lineWidth,
|
||||
icon,
|
||||
filter,
|
||||
textVisibility,
|
||||
timeField,
|
||||
textField,
|
||||
} = annotation;
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'query_point_event_annotation',
|
||||
arguments: {
|
||||
filter: filter ? [queryToAst(filter)] : [],
|
||||
timeField: [timeField],
|
||||
textField: [textField],
|
||||
label: [label || defaultAnnotationLabel],
|
||||
color: [color || defaultAnnotationColor],
|
||||
lineWidth: [lineWidth || 1],
|
||||
lineStyle: [lineStyle || 'solid'],
|
||||
icon: hasIcon(icon) ? [icon] : ['triangle'],
|
||||
textVisibility: [textVisibility || false],
|
||||
isHidden: [Boolean(isHidden)],
|
||||
extraFields: extraFields || [],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility } =
|
||||
annotation;
|
||||
|
|
|
@ -0,0 +1,386 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getFetchEventAnnotations Manual annotations Sorts annotations by time, assigns correct timebuckets, filters out hidden and out of range annotations 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"color": "#F04E981A",
|
||||
"endTime": "2022-08-05T00:01:00Z",
|
||||
"id": "mann10",
|
||||
"label": "Event range",
|
||||
"outside": false,
|
||||
"time": "2022-06-03T05:00:00Z",
|
||||
"timebucket": "2022-06-03T05:00:00.000Z",
|
||||
"type": "range",
|
||||
},
|
||||
Object {
|
||||
"endTime": "2022-07-05T00:01:00Z",
|
||||
"id": "mann3",
|
||||
"time": "2022-07-03T05:00:00Z",
|
||||
"timebucket": "2022-07-03T05:00:00.000Z",
|
||||
"type": "range",
|
||||
},
|
||||
Object {
|
||||
"id": "mann2",
|
||||
"time": "2022-07-05T01:18:00Z",
|
||||
"timebucket": "2022-07-05T01:00:00.000Z",
|
||||
"type": "point",
|
||||
},
|
||||
Object {
|
||||
"color": "#9170b8",
|
||||
"icon": "triangle",
|
||||
"id": "mann4",
|
||||
"label": "custom",
|
||||
"lineStyle": "dotted",
|
||||
"lineWidth": 3,
|
||||
"textVisibility": true,
|
||||
"time": "2022-07-05T04:34:00Z",
|
||||
"timebucket": "2022-07-05T04:30:00.000Z",
|
||||
"type": "point",
|
||||
},
|
||||
Object {
|
||||
"id": "mann1",
|
||||
"time": "2022-07-05T11:12:00Z",
|
||||
"timebucket": "2022-07-05T11:00:00.000Z",
|
||||
"type": "point",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getFetchEventAnnotations Query annotations runs handleRequest only for query annotations when manual and query are defined 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"input": Object {
|
||||
"language": "kuery",
|
||||
"query": "products.base_price < 7",
|
||||
"type": "kibana_query",
|
||||
},
|
||||
"label": "ann1",
|
||||
},
|
||||
Object {
|
||||
"input": Object {
|
||||
"language": "kuery",
|
||||
"query": "products.base_price > 700",
|
||||
"type": "kibana_query",
|
||||
},
|
||||
"label": "ann2",
|
||||
},
|
||||
],
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "filters",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "order_date",
|
||||
"interval": "3d",
|
||||
"useNormalizedEsInterval": true,
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "date_histogram",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"schema": "metric",
|
||||
"type": "count",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "order_date",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "price",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "currency",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "total_quantity",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getFetchEventAnnotations Query annotations runs handleRequest only for query annotations when manual and query are defined 2`] = `
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"input": Object {
|
||||
"language": "kuery",
|
||||
"query": "AvgTicketPrice > 900",
|
||||
"type": "kibana_query",
|
||||
},
|
||||
"label": "ann4",
|
||||
},
|
||||
Object {
|
||||
"input": Object {
|
||||
"language": "kuery",
|
||||
"query": "AvgTicketPrice = 800",
|
||||
"type": "kibana_query",
|
||||
},
|
||||
"label": "ann5",
|
||||
},
|
||||
],
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "filters",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "timestamp",
|
||||
"interval": "3d",
|
||||
"useNormalizedEsInterval": true,
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "date_histogram",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"schema": "metric",
|
||||
"type": "count",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "timestamp",
|
||||
"size": 10,
|
||||
"sortField": "timestamp",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "extraField",
|
||||
"size": 10,
|
||||
"sortField": "timestamp",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getFetchEventAnnotations Query annotations runs single handleRequest for query annotations with the same data view and timeField and creates aggregation for each extraField 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"input": Object {
|
||||
"language": "kuery",
|
||||
"query": "products.base_price > 700",
|
||||
"type": "kibana_query",
|
||||
},
|
||||
"label": "ann2",
|
||||
},
|
||||
Object {
|
||||
"input": Object {
|
||||
"language": "kuery",
|
||||
"query": "products.base_price < 7",
|
||||
"type": "kibana_query",
|
||||
},
|
||||
"label": "ann1",
|
||||
},
|
||||
],
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "filters",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "order_date",
|
||||
"interval": "3d",
|
||||
"useNormalizedEsInterval": true,
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "date_histogram",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"schema": "metric",
|
||||
"type": "count",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "order_date",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "price",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "currency",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "total_quantity",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getFetchEventAnnotations Query annotations runs two separate handleRequests if timeField is different 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"input": Object {
|
||||
"language": "kuery",
|
||||
"query": "products.base_price < 7",
|
||||
"type": "kibana_query",
|
||||
},
|
||||
"label": "ann1",
|
||||
},
|
||||
],
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "filters",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "order_date",
|
||||
"interval": "3d",
|
||||
"useNormalizedEsInterval": true,
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "date_histogram",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"schema": "metric",
|
||||
"type": "count",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "order_date",
|
||||
"size": 10,
|
||||
"sortField": "order_date",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getFetchEventAnnotations Query annotations runs two separate handleRequests if timeField is different 2`] = `
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"filters": Array [
|
||||
Object {
|
||||
"input": Object {
|
||||
"language": "kuery",
|
||||
"query": "AvgTicketPrice > 900",
|
||||
"type": "kibana_query",
|
||||
},
|
||||
"label": "ann4",
|
||||
},
|
||||
],
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "filters",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "timestamp",
|
||||
"interval": "3d",
|
||||
"useNormalizedEsInterval": true,
|
||||
},
|
||||
"schema": "bucket",
|
||||
"type": "date_histogram",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"schema": "metric",
|
||||
"type": "count",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "timestamp",
|
||||
"size": 10,
|
||||
"sortField": "timestamp",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
Object {
|
||||
"enabled": true,
|
||||
"params": Object {
|
||||
"field": "extraField",
|
||||
"size": 10,
|
||||
"sortField": "timestamp",
|
||||
"sortOrder": "asc",
|
||||
},
|
||||
"type": "top_metrics",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,431 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
|
||||
import {
|
||||
AggsStart,
|
||||
DataViewsContract,
|
||||
ExpressionValueSearchContext,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { EventAnnotationService } from '..';
|
||||
import { getFetchEventAnnotations } from '.';
|
||||
import { FetchEventAnnotationsArgs, QueryPointEventAnnotationOutput } from '../../common';
|
||||
import { EventAnnotationStartDependencies } from '../plugin';
|
||||
import { of as mockOf } from 'rxjs';
|
||||
import { handleRequest } from '../../common/fetch_event_annotations/handle_request';
|
||||
jest.mock('../../common/fetch_event_annotations/handle_request', () => {
|
||||
const original = jest.requireActual('../../common/fetch_event_annotations/handle_request');
|
||||
return {
|
||||
...original,
|
||||
handleRequest: jest.fn(() =>
|
||||
mockOf({
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: 'col-0-annotations',
|
||||
name: 'filters',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'col-1-1',
|
||||
name: 'order_date per day',
|
||||
meta: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'col-2-2',
|
||||
name: 'Count',
|
||||
meta: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'col-3-3',
|
||||
name: 'First 10 order_date',
|
||||
meta: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'col-4-4',
|
||||
name: 'First 10 category.keyword',
|
||||
meta: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
'col-0-1': 'ann1',
|
||||
'col-1-2': 1657922400000,
|
||||
'col-2-3': 1,
|
||||
'col-3-4': '2022-07-16T15:27:22.000Z',
|
||||
'col-4-5': "Women's Clothing",
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
};
|
||||
});
|
||||
// import { adaptEsaggsResponseToAnnotations } from '../../common/fetch_event_annotations/utils';
|
||||
|
||||
// jest.mock('../../common/fetch_event_annotations/utils', () => {
|
||||
// const original = jest.requireActual('../../common/fetch_event_annotations/utils');
|
||||
// return {
|
||||
// ...original,
|
||||
// adaptEsaggsResponseToAnnotations: jest.fn(),
|
||||
// };
|
||||
// });
|
||||
|
||||
// test postprocess and preprocess separately?
|
||||
|
||||
const dataView1 = {
|
||||
type: 'index_pattern',
|
||||
value: {
|
||||
id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
name: 'Kibana Sample Data eCommerce',
|
||||
},
|
||||
};
|
||||
|
||||
const dataView2 = {
|
||||
type: 'index_pattern',
|
||||
value: {
|
||||
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
name: 'Kibana Sample Data Logs',
|
||||
},
|
||||
};
|
||||
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
|
||||
const mockHandlers = {
|
||||
abortSignal: jest.fn() as unknown as jest.Mocked<AbortSignal>,
|
||||
getSearchContext: jest.fn(),
|
||||
getSearchSessionId: jest.fn().mockReturnValue('abc123'),
|
||||
getExecutionContext: jest.fn(),
|
||||
inspectorAdapters: jest.fn(),
|
||||
variables: {},
|
||||
types: {},
|
||||
};
|
||||
|
||||
const startServices = [
|
||||
{},
|
||||
{
|
||||
data: {
|
||||
...dataMock,
|
||||
search: {
|
||||
...dataMock.search,
|
||||
aggs: {
|
||||
createAggConfigs: jest.fn((_, arg) => arg),
|
||||
} as unknown as AggsStart,
|
||||
},
|
||||
dataViews: {
|
||||
...dataMock.dataViews,
|
||||
create: jest.fn().mockResolvedValue({}),
|
||||
} as DataViewsContract,
|
||||
},
|
||||
},
|
||||
{},
|
||||
] as [CoreStart, EventAnnotationStartDependencies, EventAnnotationService];
|
||||
|
||||
const getStartServices = async () => startServices;
|
||||
|
||||
const manualAnnotationSamples = {
|
||||
point1: {
|
||||
id: 'mann1',
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-07-05T11:12:00Z',
|
||||
},
|
||||
point2: {
|
||||
id: 'mann2',
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-07-05T01:18:00Z',
|
||||
},
|
||||
range: {
|
||||
id: 'mann3',
|
||||
type: 'manual_range_event_annotation',
|
||||
time: '2022-07-03T05:00:00Z',
|
||||
endTime: '2022-07-05T00:01:00Z',
|
||||
},
|
||||
customPoint: {
|
||||
id: 'mann4',
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-07-05T04:34:00Z',
|
||||
label: 'custom',
|
||||
color: '#9170b8',
|
||||
lineWidth: 3,
|
||||
lineStyle: 'dotted',
|
||||
icon: 'triangle',
|
||||
textVisibility: true,
|
||||
},
|
||||
hiddenPoint: {
|
||||
id: 'mann5',
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-07-05T05:55:00Z',
|
||||
isHidden: true,
|
||||
},
|
||||
tooLatePoint: {
|
||||
id: 'mann6',
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-08-05T12:48:10Z',
|
||||
},
|
||||
tooOldPoint: {
|
||||
id: 'mann7',
|
||||
type: 'manual_point_event_annotation',
|
||||
time: '2022-06-05T12:48:10Z',
|
||||
},
|
||||
tooOldRange: {
|
||||
id: 'mann8',
|
||||
type: 'manual_range_event_annotation',
|
||||
time: '2022-06-03T05:00:00Z',
|
||||
endTime: '2022-06-05T00:01:00Z',
|
||||
},
|
||||
toLateRange: {
|
||||
id: 'mann9',
|
||||
type: 'manual_range_event_annotation',
|
||||
time: '2022-08-03T05:00:00Z',
|
||||
endTime: '2022-08-05T00:01:00Z',
|
||||
},
|
||||
customRange: {
|
||||
id: 'mann10',
|
||||
type: 'manual_range_event_annotation',
|
||||
time: '2022-06-03T05:00:00Z',
|
||||
endTime: '2022-08-05T00:01:00Z',
|
||||
label: 'Event range',
|
||||
color: '#F04E981A',
|
||||
outside: false,
|
||||
},
|
||||
};
|
||||
|
||||
const queryAnnotationSamples: Record<string, QueryPointEventAnnotationOutput> = {
|
||||
noExtraFields: {
|
||||
type: 'query_point_event_annotation',
|
||||
id: 'ann1',
|
||||
filter: {
|
||||
type: 'kibana_query',
|
||||
language: 'kuery',
|
||||
query: 'products.base_price < 7',
|
||||
},
|
||||
timeField: 'order_date',
|
||||
label: 'Ann1',
|
||||
},
|
||||
extraFields: {
|
||||
type: 'query_point_event_annotation',
|
||||
id: 'ann2',
|
||||
label: 'Ann2',
|
||||
filter: {
|
||||
type: 'kibana_query',
|
||||
language: 'kuery',
|
||||
query: 'products.base_price > 700',
|
||||
},
|
||||
extraFields: ['price', 'currency', 'total_quantity'],
|
||||
timeField: 'order_date',
|
||||
color: '#9170b8',
|
||||
lineWidth: 3,
|
||||
lineStyle: 'dotted',
|
||||
icon: 'triangle',
|
||||
textVisibility: true,
|
||||
},
|
||||
hidden: {
|
||||
type: 'query_point_event_annotation',
|
||||
id: 'ann3',
|
||||
label: 'Ann3',
|
||||
timeField: 'timestamp',
|
||||
filter: {
|
||||
type: 'kibana_query',
|
||||
language: 'kuery',
|
||||
query: 'category = "accessories"',
|
||||
},
|
||||
extraFields: [],
|
||||
isHidden: true,
|
||||
},
|
||||
differentTimeField: {
|
||||
type: 'query_point_event_annotation',
|
||||
filter: {
|
||||
type: 'kibana_query',
|
||||
language: 'kuery',
|
||||
query: 'AvgTicketPrice > 900',
|
||||
},
|
||||
extraFields: ['extraField'],
|
||||
timeField: 'timestamp',
|
||||
id: 'ann4',
|
||||
label: 'Ann4',
|
||||
},
|
||||
ann5: {
|
||||
type: 'query_point_event_annotation',
|
||||
filter: {
|
||||
type: 'kibana_query',
|
||||
language: 'kuery',
|
||||
query: 'AvgTicketPrice = 800',
|
||||
},
|
||||
extraFields: [],
|
||||
timeField: 'timestamp',
|
||||
id: 'ann5',
|
||||
label: 'Ann5',
|
||||
},
|
||||
};
|
||||
|
||||
const input = {
|
||||
type: 'kibana_context',
|
||||
query: [],
|
||||
filters: [],
|
||||
timeRange: {
|
||||
type: 'timerange',
|
||||
from: '2022-07-01T00:00:00Z',
|
||||
to: '2022-07-31T00:00:00Z',
|
||||
},
|
||||
} as ExpressionValueSearchContext;
|
||||
|
||||
const runGetFetchEventAnnotations = async (args: FetchEventAnnotationsArgs) => {
|
||||
return await getFetchEventAnnotations({ getStartServices })
|
||||
.fn(input, args, mockHandlers)
|
||||
.toPromise();
|
||||
};
|
||||
|
||||
describe('getFetchEventAnnotations', () => {
|
||||
afterEach(() => {
|
||||
(startServices[1].data.dataViews.create as jest.Mock).mockClear();
|
||||
(handleRequest as jest.Mock).mockClear();
|
||||
});
|
||||
test('Returns null for empty groups', async () => {
|
||||
const result = await runGetFetchEventAnnotations({
|
||||
interval: '2h',
|
||||
groups: [],
|
||||
timezone: 'Europe/Madrid',
|
||||
});
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
describe('Manual annotations', () => {
|
||||
const manualOnlyArgs = {
|
||||
timezone: 'Europe/Madrid',
|
||||
interval: '30m',
|
||||
groups: [
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
dataView: dataView1,
|
||||
annotations: [
|
||||
manualAnnotationSamples.point1,
|
||||
manualAnnotationSamples.point2,
|
||||
manualAnnotationSamples.range,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
dataView: dataView1,
|
||||
annotations: [
|
||||
manualAnnotationSamples.customPoint,
|
||||
manualAnnotationSamples.hiddenPoint,
|
||||
manualAnnotationSamples.tooLatePoint,
|
||||
manualAnnotationSamples.tooOldPoint,
|
||||
manualAnnotationSamples.tooOldRange,
|
||||
manualAnnotationSamples.toLateRange,
|
||||
manualAnnotationSamples.customRange,
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as FetchEventAnnotationsArgs;
|
||||
|
||||
test(`Doesn't run dataViews.create for manual annotations groups only`, async () => {
|
||||
await runGetFetchEventAnnotations(manualOnlyArgs);
|
||||
expect(startServices[1].data.dataViews.create).not.toHaveBeenCalled();
|
||||
});
|
||||
test('Sorts annotations by time, assigns correct timebuckets, filters out hidden and out of range annotations', async () => {
|
||||
const result = await runGetFetchEventAnnotations(manualOnlyArgs);
|
||||
expect(result!.rows).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query annotations', () => {
|
||||
test('runs handleRequest only for query annotations when manual and query are defined', async () => {
|
||||
const sampleArgs = {
|
||||
timezone: 'Europe/Madrid',
|
||||
interval: '3d',
|
||||
groups: [
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [manualAnnotationSamples.point1],
|
||||
dataView1,
|
||||
},
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [
|
||||
manualAnnotationSamples.customPoint,
|
||||
queryAnnotationSamples.noExtraFields,
|
||||
queryAnnotationSamples.extraFields,
|
||||
],
|
||||
dataView: dataView1,
|
||||
},
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [
|
||||
queryAnnotationSamples.differentTimeField,
|
||||
queryAnnotationSamples.ann5,
|
||||
manualAnnotationSamples.range,
|
||||
],
|
||||
dataView: dataView2,
|
||||
},
|
||||
],
|
||||
} as unknown as FetchEventAnnotationsArgs;
|
||||
await runGetFetchEventAnnotations(sampleArgs);
|
||||
expect(startServices[1].data.dataViews.create).toBeCalledTimes(2);
|
||||
expect(handleRequest).toBeCalledTimes(2);
|
||||
expect((handleRequest as jest.Mock).mock.calls[0][0]!.aggs).toMatchSnapshot();
|
||||
expect((handleRequest as jest.Mock).mock.calls[1][0]!.aggs).toMatchSnapshot();
|
||||
});
|
||||
test('runs single handleRequest for query annotations with the same data view and timeField and creates aggregation for each extraField', async () => {
|
||||
const sampleArgs = {
|
||||
timezone: 'Europe/Madrid',
|
||||
interval: '3d',
|
||||
groups: [
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [queryAnnotationSamples.extraFields],
|
||||
dataView: dataView1,
|
||||
},
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [queryAnnotationSamples.noExtraFields],
|
||||
dataView: dataView1,
|
||||
},
|
||||
],
|
||||
} as unknown as FetchEventAnnotationsArgs;
|
||||
await runGetFetchEventAnnotations(sampleArgs);
|
||||
expect(startServices[1].data.dataViews.create).toBeCalledTimes(1);
|
||||
expect(handleRequest).toBeCalledTimes(1);
|
||||
expect((handleRequest as jest.Mock).mock.calls[0][0]!.aggs).toMatchSnapshot();
|
||||
});
|
||||
test('runs two separate handleRequests if timeField is different', async () => {
|
||||
const sampleArgs = {
|
||||
timezone: 'Europe/Madrid',
|
||||
interval: '3d',
|
||||
groups: [
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [queryAnnotationSamples.noExtraFields],
|
||||
dataView: dataView1,
|
||||
},
|
||||
{
|
||||
type: 'event_annotation_group',
|
||||
annotations: [queryAnnotationSamples.differentTimeField],
|
||||
dataView: dataView1,
|
||||
},
|
||||
],
|
||||
} as unknown as FetchEventAnnotationsArgs;
|
||||
await runGetFetchEventAnnotations(sampleArgs);
|
||||
expect(startServices[1].data.dataViews.create).toBeCalledTimes(1);
|
||||
expect(handleRequest).toBeCalledTimes(2); // how many times and with what params
|
||||
expect((handleRequest as jest.Mock).mock.calls[0][0]!.aggs).toMatchSnapshot();
|
||||
expect((handleRequest as jest.Mock).mock.calls[1][0]!.aggs).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { StartServicesAccessor } from '@kbn/core/public';
|
||||
import { EventAnnotationPluginStart, EventAnnotationStartDependencies } from '../plugin';
|
||||
|
||||
import {
|
||||
FetchEventAnnotationsExpressionFunctionDefinition,
|
||||
FetchEventAnnotationsStartDependencies,
|
||||
getFetchEventAnnotationsMeta,
|
||||
requestEventAnnotations,
|
||||
} from '../../common/fetch_event_annotations';
|
||||
|
||||
export function fetchEventAnnotations({
|
||||
getStartDependencies,
|
||||
}: {
|
||||
getStartDependencies: () => Promise<FetchEventAnnotationsStartDependencies>;
|
||||
}): FetchEventAnnotationsExpressionFunctionDefinition {
|
||||
return {
|
||||
...getFetchEventAnnotationsMeta(),
|
||||
fn: (input, args, context) => {
|
||||
return requestEventAnnotations(input, args, context, getStartDependencies);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getFetchEventAnnotations({
|
||||
getStartServices,
|
||||
}: {
|
||||
getStartServices: StartServicesAccessor<
|
||||
EventAnnotationStartDependencies,
|
||||
EventAnnotationPluginStart
|
||||
>;
|
||||
}) {
|
||||
return fetchEventAnnotations({
|
||||
getStartDependencies: async () => {
|
||||
const [
|
||||
,
|
||||
{
|
||||
data: { search, dataViews, nowProvider },
|
||||
},
|
||||
] = await getStartServices();
|
||||
return {
|
||||
aggs: search.aggs,
|
||||
searchSource: search.searchSource,
|
||||
dataViews,
|
||||
getNow: () => nowProvider.get(),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
|
@ -17,5 +17,6 @@ export { EventAnnotationService } from './event_annotation_service';
|
|||
export {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
isRangeAnnotation,
|
||||
isRangeAnnotationConfig,
|
||||
isManualPointAnnotationConfig,
|
||||
} from './event_annotation_service/helpers';
|
||||
|
|
|
@ -6,41 +6,54 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from '@kbn/core/public';
|
||||
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
import { ExpressionsSetup } from '@kbn/expressions-plugin/public';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { EventAnnotationService } from './event_annotation_service';
|
||||
import {
|
||||
manualPointEventAnnotation,
|
||||
manualRangeEventAnnotation,
|
||||
queryPointEventAnnotation,
|
||||
eventAnnotationGroup,
|
||||
fetchEventAnnotations,
|
||||
} from '../common';
|
||||
import { EventAnnotationService } from './event_annotation_service';
|
||||
import { getFetchEventAnnotations } from './fetch_event_annotations';
|
||||
|
||||
export interface EventAnnotationStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
|
||||
interface SetupDependencies {
|
||||
expressions: ExpressionsSetup;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type EventAnnotationPluginStart = EventAnnotationService;
|
||||
export type EventAnnotationPluginSetup = EventAnnotationService;
|
||||
|
||||
/** @public */
|
||||
export type EventAnnotationPluginStart = EventAnnotationService;
|
||||
|
||||
/** @public */
|
||||
export class EventAnnotationPlugin
|
||||
implements Plugin<EventAnnotationPluginSetup, EventAnnotationPluginStart>
|
||||
implements Plugin<EventAnnotationPluginSetup, EventAnnotationService>
|
||||
{
|
||||
private readonly eventAnnotationService = new EventAnnotationService();
|
||||
|
||||
public setup(core: CoreSetup, dependencies: SetupDependencies): EventAnnotationPluginSetup {
|
||||
public setup(
|
||||
core: CoreSetup<EventAnnotationStartDependencies, EventAnnotationService>,
|
||||
dependencies: SetupDependencies
|
||||
) {
|
||||
dependencies.expressions.registerFunction(manualPointEventAnnotation);
|
||||
dependencies.expressions.registerFunction(manualRangeEventAnnotation);
|
||||
dependencies.expressions.registerFunction(queryPointEventAnnotation);
|
||||
dependencies.expressions.registerFunction(eventAnnotationGroup);
|
||||
dependencies.expressions.registerFunction(fetchEventAnnotations);
|
||||
dependencies.expressions.registerFunction(
|
||||
getFetchEventAnnotations({ getStartServices: core.getStartServices })
|
||||
);
|
||||
return this.eventAnnotationService;
|
||||
}
|
||||
|
||||
public start(): EventAnnotationPluginStart {
|
||||
public start(
|
||||
core: CoreStart,
|
||||
startDependencies: EventAnnotationStartDependencies
|
||||
): EventAnnotationService {
|
||||
return this.eventAnnotationService;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
// TODO: implement this on the server
|
||||
|
||||
// import { StartServicesAccessor } from '@kbn/core/server';
|
||||
// import { EventAnnotationStartDependencies } from '../plugin';
|
||||
|
||||
// import {
|
||||
// FetchEventAnnotationsExpressionFunctionDefinition,
|
||||
// FetchEventAnnotationsStartDependencies,
|
||||
// getFetchEventAnnotationsMeta,
|
||||
// requestEventAnnotations,
|
||||
// } from '../../common/fetch_event_annotations';
|
||||
|
||||
// export function fetchEventAnnotations({
|
||||
// getStartDependencies,
|
||||
// }: {
|
||||
// getStartDependencies: () => Promise<FetchEventAnnotationsStartDependencies>;
|
||||
// }): FetchEventAnnotationsExpressionFunctionDefinition {
|
||||
// return {
|
||||
// ...getFetchEventAnnotationsMeta(),
|
||||
// fn: (input, args, context) => {
|
||||
// return requestEventAnnotations(input, args, context, getStartDependencies);
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function getFetchEventAnnotations({
|
||||
// getStartServices,
|
||||
// }: {
|
||||
// getStartServices: StartServicesAccessor<EventAnnotationStartDependencies, object>;
|
||||
// }) {
|
||||
// return fetchEventAnnotations({
|
||||
// getStartDependencies: async () => {
|
||||
// const [
|
||||
// ,
|
||||
// {
|
||||
// data: { search, indexPatterns: dataViews },
|
||||
// },
|
||||
// ] = await getStartServices();
|
||||
|
||||
// return {
|
||||
// aggs: search.aggs,
|
||||
// searchSource: search.searchSource,
|
||||
// dataViews,
|
||||
// };
|
||||
// },
|
||||
// });
|
||||
// }
|
|
@ -8,21 +8,34 @@
|
|||
|
||||
import { CoreSetup, Plugin } from '@kbn/core/server';
|
||||
import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server';
|
||||
import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server';
|
||||
import {
|
||||
manualPointEventAnnotation,
|
||||
eventAnnotationGroup,
|
||||
manualRangeEventAnnotation,
|
||||
queryPointEventAnnotation,
|
||||
} from '../common';
|
||||
// import { getFetchEventAnnotations } from './fetch_event_annotations';
|
||||
|
||||
interface SetupDependencies {
|
||||
expressions: ExpressionsServerSetup;
|
||||
}
|
||||
export interface EventAnnotationStartDependencies {
|
||||
data: DataPluginStart;
|
||||
}
|
||||
|
||||
export class EventAnnotationServerPlugin implements Plugin<object, object> {
|
||||
public setup(core: CoreSetup, dependencies: SetupDependencies) {
|
||||
public setup(
|
||||
core: CoreSetup<EventAnnotationStartDependencies, object>,
|
||||
dependencies: SetupDependencies
|
||||
) {
|
||||
dependencies.expressions.registerFunction(manualPointEventAnnotation);
|
||||
dependencies.expressions.registerFunction(manualRangeEventAnnotation);
|
||||
dependencies.expressions.registerFunction(queryPointEventAnnotation);
|
||||
dependencies.expressions.registerFunction(eventAnnotationGroup);
|
||||
// dependencies.expressions.registerFunction(
|
||||
// getFetchEventAnnotations({ getStartServices: core.getStartServices })
|
||||
// );
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { omit } from 'lodash';
|
||||
import { ExpectExpression, expectExpressionProvider } from '../helpers';
|
||||
import { FtrProviderContext } from '../../../../functional/ftr_provider_context';
|
||||
import { expectedResult } from './fetch_event_annotations_result';
|
||||
|
||||
export default function ({
|
||||
getService,
|
||||
updateBaselines,
|
||||
}: FtrProviderContext & { updateBaselines: boolean }) {
|
||||
let expectExpression: ExpectExpression;
|
||||
|
||||
describe('fetch event annotation tests', () => {
|
||||
before(() => {
|
||||
expectExpression = expectExpressionProvider({ getService, updateBaselines });
|
||||
});
|
||||
|
||||
const timeRange = {
|
||||
from: '2015-09-21T00:00:00Z',
|
||||
to: '2015-09-22T00:00:00Z',
|
||||
};
|
||||
|
||||
it(`manual annotations from different groups`, async () => {
|
||||
const expression = `
|
||||
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
|
||||
| fetch_event_annotations timezone="Australia/Darwin"
|
||||
interval="1w"
|
||||
groups={event_annotation_group dataView={indexPatternLoad id="logstash-*"} annotations={
|
||||
manual_point_event_annotation id="ann1" label="Manual1" color="red" icon="bolt" time="2015-09-21T12:15:00Z" lineWidth="3" lineStyle="solid" textVisibility=true
|
||||
}
|
||||
annotations={
|
||||
manual_point_event_annotation id="ann2" label="ManualHidden" color="pink" icon="triangle" time="2015-09-21T12:30:00Z" isHidden=true
|
||||
}
|
||||
}
|
||||
groups={event_annotation_group dataView={indexPatternLoad id="logstash-*"} annotations={
|
||||
manual_range_event_annotation id="ann3" label="Range" color="blue" time="2015-09-21T07:30:00Z" endTime="2015-09-21T12:30:00Z"
|
||||
}
|
||||
}
|
||||
`;
|
||||
const result = await expectExpression('fetch_event_annotations', expression).getResponse();
|
||||
|
||||
expect(result.rows.length).to.equal(2); // filters out hidden annotations
|
||||
expect(result.rows).to.eql([
|
||||
{
|
||||
id: 'ann3',
|
||||
time: '2015-09-21T07:30:00Z',
|
||||
endTime: '2015-09-21T12:30:00Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'range',
|
||||
label: 'Range',
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: 'ann1',
|
||||
time: '2015-09-21T12:15:00Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z', // time bucket is correctly assigned
|
||||
type: 'point',
|
||||
label: 'Manual1',
|
||||
lineStyle: 'solid', // styles and label are passed
|
||||
color: 'red',
|
||||
icon: 'bolt',
|
||||
lineWidth: 3,
|
||||
textVisibility: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('query and manual annotations', () => {
|
||||
it('calculates correct timebuckets, counts skippedCount, passes fields and styles for single group with only query annotations', async () => {
|
||||
const expression = `
|
||||
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
|
||||
| fetch_event_annotations interval="1d" timezone="Australia/Darwin" groups={event_annotation_group
|
||||
dataView={indexPatternLoad id="logstash-*"}
|
||||
annotations={query_point_event_annotation id="server_errors" filter={kql q="response.raw === 503"} extraFields='response.raw' extraFields='extension.raw' extraFields='bytes' timeField="@timestamp" label="503" color="red"}
|
||||
annotations={query_point_event_annotation id="client_errors" filter={kql q="response.raw === 404"} extraFields='response.raw' textField="ip" timeField="@timestamp" label="404" color="orange"}}
|
||||
`;
|
||||
|
||||
const result = await expectExpression('fetch_event_annotations', expression).getResponse();
|
||||
expect(result.rows).to.eql(expectedResult);
|
||||
});
|
||||
it('calculates correct timebuckets, counts skippedCount, passes fields and styles for multiple groups with only query annotations', async () => {
|
||||
const expression = `
|
||||
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
|
||||
| fetch_event_annotations interval="1d" timezone="Australia/Darwin" groups={event_annotation_group
|
||||
dataView={indexPatternLoad id="logstash-*"}
|
||||
annotations={query_point_event_annotation id="server_errors" filter={kql q="response.raw === 503"} extraFields='response.raw' extraFields='extension.raw' extraFields='bytes' timeField="@timestamp" label="503" color="red"}}
|
||||
groups={event_annotation_group
|
||||
dataView={indexPatternLoad id="logstash-*"}
|
||||
annotations={query_point_event_annotation id="client_errors" filter={kql q="response.raw === 404"} extraFields='response.raw' textField="ip" timeField="@timestamp" label="404" color="orange"}}
|
||||
`;
|
||||
|
||||
const result = await expectExpression('fetch_event_annotations', expression).getResponse();
|
||||
expect(result.rows).to.eql(expectedResult);
|
||||
});
|
||||
it('calculates correct timebuckets, counts skippedCount, passes fields and styles for multiple groups with query and manual annotations', async () => {
|
||||
const expression = `
|
||||
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
|
||||
| fetch_event_annotations interval="1d" timezone="Australia/Darwin" groups={event_annotation_group
|
||||
dataView={indexPatternLoad id="logstash-*"}
|
||||
annotations={query_point_event_annotation id="server_errors" filter={kql q="response.raw === 503"} extraFields='response.raw' extraFields='extension.raw' extraFields='bytes' timeField="@timestamp" label="503" color="red"}}
|
||||
groups={event_annotation_group
|
||||
dataView={indexPatternLoad id="logstash-*"}
|
||||
annotations={
|
||||
manual_point_event_annotation id="ann1" label="Manual1" color="red" icon="bolt" time="2015-09-21T12:15:00Z" lineWidth="3" lineStyle="solid" textVisibility=true
|
||||
}
|
||||
annotations={query_point_event_annotation id="client_errors" filter={kql q="response.raw === 404"} extraFields='response.raw' timeField="@timestamp" textField="ip" label="404" color="orange"}
|
||||
annotations={
|
||||
manual_range_event_annotation id="ann3" label="Range" color="blue" time="2015-09-21T07:30:00Z" endTime="2015-09-21T12:30:00Z"
|
||||
}}
|
||||
`;
|
||||
|
||||
const result = await expectExpression('fetch_event_annotations', expression).getResponse();
|
||||
expect(result.rows).to.eql([
|
||||
...expectedResult.slice(0, 19),
|
||||
omit(expectedResult[19], 'skippedCount'), // skippedCount is moved to the last row of the timebucket
|
||||
{
|
||||
color: 'blue',
|
||||
endTime: '2015-09-21T12:30:00Z',
|
||||
id: 'ann3',
|
||||
label: 'Range',
|
||||
time: '2015-09-21T07:30:00Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'range',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
icon: 'bolt',
|
||||
id: 'ann1',
|
||||
label: 'Manual1',
|
||||
lineStyle: 'solid',
|
||||
lineWidth: 3,
|
||||
skippedCount: 269,
|
||||
textVisibility: true,
|
||||
time: '2015-09-21T12:15:00Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
|
||||
...expectedResult.slice(-20),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,412 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const expectedResult = [
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '252.63.51.8',
|
||||
time: '2015-09-21T00:00:00.000Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T00:58:25.823Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T00:59:00.367Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '185.39.79.51',
|
||||
time: '2015-09-21T01:55:32.632Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '113.43.75.122',
|
||||
time: '2015-09-21T02:45:59.636Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '200',
|
||||
id: 'client_errors',
|
||||
label: '137.230.105.32',
|
||||
time: '2015-09-21T02:54:47.500Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'gif',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T03:17:30.141Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'png',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T03:26:55.232Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '124.187.220.168',
|
||||
time: '2015-09-21T03:46:06.209Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '99.216.169.176',
|
||||
time: '2015-09-21T04:17:57.312Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '191.204.236.159',
|
||||
time: '2015-09-21T04:19:58.195Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T04:26:43.432Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '1.59.159.138',
|
||||
time: '2015-09-21T05:07:31.817Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '112.75.33.146',
|
||||
time: '2015-09-21T05:12:59.470Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'gif',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T05:34:13.304Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '197.222.12.184',
|
||||
time: '2015-09-21T05:36:00.717Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T06:48:38.946Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'php',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T06:57:46.610Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T06:58:27.922Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
skippedCount: 269,
|
||||
time: '2015-09-21T07:11:00.754Z',
|
||||
timebucket: '2015-09-20T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '55.75.54.137',
|
||||
time: '2015-09-21T14:30:35.524Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T14:35:33.669Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'gif',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T14:35:49.990Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '9.85.51.238',
|
||||
time: '2015-09-21T14:37:30.895Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '146.86.123.109',
|
||||
time: '2015-09-21T14:37:55.120Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '117.112.55.75',
|
||||
time: '2015-09-21T14:38:21.637Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T14:38:58.747Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '128.248.96.80',
|
||||
time: '2015-09-21T14:39:45.330Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '103.57.26.210',
|
||||
time: '2015-09-21T14:41:08.984Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '68.41.209.99',
|
||||
time: '2015-09-21T14:46:57.158Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '40.160.62.179',
|
||||
time: '2015-09-21T14:47:29.295Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'css',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T14:51:33.395Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '18.113.253.141',
|
||||
time: '2015-09-21T14:51:40.391Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'orange',
|
||||
'field:response.raw': '404',
|
||||
id: 'client_errors',
|
||||
label: '16.166.96.38',
|
||||
time: '2015-09-21T14:57:25.160Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T15:15:42.547Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'png',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T15:22:45.564Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T15:25:05.797Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T15:47:55.678Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
time: '2015-09-21T15:49:56.270Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
{
|
||||
color: 'red',
|
||||
'field:bytes': 0,
|
||||
'field:extension.raw': 'jpg',
|
||||
'field:response.raw': '503',
|
||||
id: 'server_errors',
|
||||
label: '503',
|
||||
skippedCount: 72,
|
||||
time: '2015-09-21T15:54:46.498Z',
|
||||
timebucket: '2015-09-21T14:30:00.000Z',
|
||||
type: 'point',
|
||||
},
|
||||
];
|
|
@ -50,6 +50,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
|
|||
loadTestFile(require.resolve('./esaggs_rareterms'));
|
||||
loadTestFile(require.resolve('./esaggs_topmetrics'));
|
||||
loadTestFile(require.resolve('./esaggs_histogram'));
|
||||
loadTestFile(require.resolve('./event_annotation/fetch_event_annotations'));
|
||||
loadTestFile(require.resolve('./essql'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import moment from 'moment';
|
|||
import {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
isRangeAnnotation,
|
||||
isRangeAnnotationConfig,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
|
||||
import { IconChartBarAnnotations } from '@kbn/chart-icons';
|
||||
|
@ -359,7 +359,7 @@ export const getSingleColorAnnotationConfig = (annotation: EventAnnotationConfig
|
|||
triggerIcon: annotation.isHidden ? ('invisible' as const) : ('color' as const),
|
||||
color:
|
||||
annotation?.color ||
|
||||
(isRangeAnnotation(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor),
|
||||
(isRangeAnnotationConfig(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor),
|
||||
});
|
||||
|
||||
export const getAnnotationsAccessorColorConfig = (layer: XYAnnotationLayerConfig) =>
|
||||
|
|
|
@ -12,7 +12,7 @@ import { euiLightVars } from '@kbn/ui-theme';
|
|||
import {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
isRangeAnnotation,
|
||||
isRangeAnnotationConfig,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import type { AccessorConfig, FramePublicAPI } from '../../types';
|
||||
import { getColumnToLabelMap } from './state_helpers';
|
||||
|
@ -126,7 +126,9 @@ export function getAssignedColorConfig(
|
|||
return {
|
||||
columnId: accessor,
|
||||
triggerIcon: annotation?.isHidden ? ('invisible' as const) : ('color' as const),
|
||||
color: isRangeAnnotation(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor,
|
||||
color: isRangeAnnotationConfig(annotation)
|
||||
? defaultAnnotationRangeColor
|
||||
: defaultAnnotationColor,
|
||||
};
|
||||
}
|
||||
const layerContainsSplits = isDataLayer(layer) && !layer.collapseFn && layer.splitAccessor;
|
||||
|
|
|
@ -32,7 +32,8 @@ import { search } from '@kbn/data-plugin/public';
|
|||
import {
|
||||
defaultAnnotationColor,
|
||||
defaultAnnotationRangeColor,
|
||||
isRangeAnnotation,
|
||||
isRangeAnnotationConfig,
|
||||
isManualPointAnnotationConfig,
|
||||
} from '@kbn/event-annotation-plugin/public';
|
||||
import Color from 'color';
|
||||
import { getDataLayers } from '../../visualization_helpers';
|
||||
|
@ -95,7 +96,7 @@ export const getEndTimestamp = (
|
|||
};
|
||||
|
||||
const sanitizeProperties = (annotation: EventAnnotationConfig) => {
|
||||
if (isRangeAnnotation(annotation)) {
|
||||
if (isRangeAnnotationConfig(annotation)) {
|
||||
const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [
|
||||
'label',
|
||||
'key',
|
||||
|
@ -105,7 +106,7 @@ const sanitizeProperties = (annotation: EventAnnotationConfig) => {
|
|||
'outside',
|
||||
]);
|
||||
return rangeAnnotation;
|
||||
} else {
|
||||
} else if (isManualPointAnnotationConfig(annotation)) {
|
||||
const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [
|
||||
'id',
|
||||
'label',
|
||||
|
@ -119,6 +120,7 @@ const sanitizeProperties = (annotation: EventAnnotationConfig) => {
|
|||
]);
|
||||
return lineAnnotation;
|
||||
}
|
||||
return annotation; // todo: sanitize for the query annotations here
|
||||
};
|
||||
|
||||
export const AnnotationsPanel = (
|
||||
|
@ -143,7 +145,8 @@ export const AnnotationsPanel = (
|
|||
|
||||
const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor);
|
||||
|
||||
const isRange = isRangeAnnotation(currentAnnotation);
|
||||
const isRange = isRangeAnnotationConfig(currentAnnotation);
|
||||
const isManualPoint = isManualPointAnnotationConfig(currentAnnotation);
|
||||
|
||||
const setAnnotations = useCallback(
|
||||
(annotation) => {
|
||||
|
@ -240,7 +243,7 @@ export const AnnotationsPanel = (
|
|||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
) : isManualPoint ? (
|
||||
<ConfigPanelRangeDatePicker
|
||||
dataTestSubj="lns-xyAnnotation-time"
|
||||
label={i18n.translate('xpack.lens.xyChart.annotationDate', {
|
||||
|
@ -258,7 +261,7 @@ export const AnnotationsPanel = (
|
|||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<ConfigPanelApplyAsRangeSwitch
|
||||
annotation={currentAnnotation}
|
||||
|
@ -385,7 +388,8 @@ const ConfigPanelApplyAsRangeSwitch = ({
|
|||
frame: FramePublicAPI;
|
||||
state: XYState;
|
||||
}) => {
|
||||
const isRange = isRangeAnnotation(annotation);
|
||||
const isRange = isRangeAnnotationConfig(annotation);
|
||||
const isManualPoint = isManualPointAnnotationConfig(annotation);
|
||||
return (
|
||||
<EuiFormRow display="columnCompressed" className="lnsRowCompressedMargin">
|
||||
<EuiSwitch
|
||||
|
@ -414,8 +418,8 @@ const ConfigPanelApplyAsRangeSwitch = ({
|
|||
isHidden: annotation.isHidden,
|
||||
};
|
||||
onChange(newPointAnnotation);
|
||||
} else if (annotation) {
|
||||
const fromTimestamp = moment(annotation?.key.timestamp);
|
||||
} else if (isManualPoint) {
|
||||
const fromTimestamp = moment(annotation?.key?.timestamp);
|
||||
const dataLayers = getDataLayers(state.layers);
|
||||
const newRangeAnnotation: RangeEventAnnotationConfig = {
|
||||
key: {
|
||||
|
|
|
@ -33780,10 +33780,8 @@
|
|||
"eventAnnotation.fetchEventAnnotations.args.interval.help": "Intervalle à utiliser pour cette agrégation",
|
||||
"eventAnnotation.fetchEventAnnotations.description": "Récupérer les annotations d’événement",
|
||||
"eventAnnotation.group.args.annotationConfigs": "Configurations d'annotations",
|
||||
"eventAnnotation.group.args.annotationConfigs.index.help": "Vue de données extraite avec indexPatternLoad",
|
||||
"eventAnnotation.group.description": "Groupe d'annotations d'événement",
|
||||
"eventAnnotation.manualAnnotation.args.color": "Couleur de la ligne",
|
||||
"eventAnnotation.manualAnnotation.args.endTime": "Horodatage de l'annotation de plage",
|
||||
"eventAnnotation.manualAnnotation.args.icon": "Icône facultative utilisée pour les lignes d'annotation",
|
||||
"eventAnnotation.manualAnnotation.args.isHidden": "Basculer pour masquer l'annotation",
|
||||
"eventAnnotation.manualAnnotation.args.label": "Nom de l'annotation",
|
||||
|
@ -34126,4 +34124,4 @@
|
|||
"xpack.painlessLab.title": "Painless Lab",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "Présentation"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33755,10 +33755,8 @@
|
|||
"eventAnnotation.fetchEventAnnotations.args.interval.help": "このアグリゲーションで使用する間隔",
|
||||
"eventAnnotation.fetchEventAnnotations.description": "イベント注釈を取得",
|
||||
"eventAnnotation.group.args.annotationConfigs": "注釈構成",
|
||||
"eventAnnotation.group.args.annotationConfigs.index.help": "indexPatternLoad で取得されたデータビュー",
|
||||
"eventAnnotation.group.description": "イベント注釈グループ",
|
||||
"eventAnnotation.manualAnnotation.args.color": "行の色",
|
||||
"eventAnnotation.manualAnnotation.args.endTime": "範囲注釈のタイムスタンプ",
|
||||
"eventAnnotation.manualAnnotation.args.icon": "注釈行で使用される任意のアイコン",
|
||||
"eventAnnotation.manualAnnotation.args.isHidden": "注釈を非表示",
|
||||
"eventAnnotation.manualAnnotation.args.label": "注釈の名前",
|
||||
|
@ -34101,4 +34099,4 @@
|
|||
"xpack.painlessLab.title": "Painless Lab",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "実地検証"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33789,10 +33789,8 @@
|
|||
"eventAnnotation.fetchEventAnnotations.args.interval.help": "要用于此聚合的时间间隔",
|
||||
"eventAnnotation.fetchEventAnnotations.description": "提取事件标注",
|
||||
"eventAnnotation.group.args.annotationConfigs": "标注配置",
|
||||
"eventAnnotation.group.args.annotationConfigs.index.help": "使用 indexPatternLoad 检索的数据视图",
|
||||
"eventAnnotation.group.description": "事件标注组",
|
||||
"eventAnnotation.manualAnnotation.args.color": "线条的颜色",
|
||||
"eventAnnotation.manualAnnotation.args.endTime": "范围标注的时间戳",
|
||||
"eventAnnotation.manualAnnotation.args.icon": "用于标注线条的可选图标",
|
||||
"eventAnnotation.manualAnnotation.args.isHidden": "切换到隐藏标注",
|
||||
"eventAnnotation.manualAnnotation.args.label": "标注的名称",
|
||||
|
@ -34135,4 +34133,4 @@
|
|||
"xpack.painlessLab.title": "Painless 实验室",
|
||||
"xpack.painlessLab.walkthroughButtonLabel": "指导"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue