[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:
Marta Bondyra 2022-08-29 17:10:26 +02:00 committed by GitHub
parent 78e96c100c
commit 76dc50cefe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 2506 additions and 323 deletions

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,

View file

@ -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) => {

View file

@ -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',

View file

@ -97,5 +97,5 @@ export const getEsaggsMeta: () => Omit<EsaggsExpressionFunctionDefinition, 'fn'>
},
});
/** @internal */
export { handleRequest as handleEsaggsRequest };
export type { RequestHandlerParams } from './request_handler';

View file

@ -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[];

View file

@ -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,
};
},
};

View file

@ -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',
},
]);
});
});

View file

@ -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',
}),
},
},
});

View file

@ -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);

View file

@ -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';

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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`,
]),
};
}

View file

@ -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';

View file

@ -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`,
}),
},

View file

@ -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' } },
];

View file

@ -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,
};
},
};

View file

@ -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';
};

View file

@ -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' } },
];

View file

@ -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);
};

View file

@ -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;

View file

@ -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",
},
]
`;

View file

@ -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();
});
});
});

View file

@ -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(),
};
},
});
}

View file

@ -17,5 +17,6 @@ export { EventAnnotationService } from './event_annotation_service';
export {
defaultAnnotationColor,
defaultAnnotationRangeColor,
isRangeAnnotation,
isRangeAnnotationConfig,
isManualPointAnnotationConfig,
} from './event_annotation_service/helpers';

View file

@ -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;
}
}

View file

@ -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,
// };
// },
// });
// }

View file

@ -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 {};
}

View file

@ -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),
]);
});
});
});
}

View file

@ -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',
},
];

View file

@ -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'));
});
}

View file

@ -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) =>

View file

@ -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;

View file

@ -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: {

View file

@ -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"
}
}
}

View file

@ -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": "実地検証"
}
}
}

View file

@ -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": "指导"
}
}
}