[Event annotations] Individual annotation editing from library (#163346)

## Summary

Resolve https://github.com/elastic/kibana/issues/158774
Part of https://github.com/elastic/kibana/issues/159053

<img width="1920" alt="Screenshot 2023-09-13 at 2 00 25 PM"
src="69cfe07e-d442-462b-91c5-395d6040c383">

<img width="1920" alt="Screenshot 2023-09-13 at 2 00 09 PM"
src="260aedbe-31d0-415a-b387-10a9b13bf9a6">

<img width="1920" alt="Screenshot 2023-09-13 at 2 01 07 PM"
src="9672010b-d49b-4041-acf1-33d3baec1e9a">


### Known issues
- [ ] ~Responsive layout~ **Proposal:** don't optimize for mobile
- [x] Recovering embeddable from problematic data view state
- [x] margin around dimension buttons
- [x] Functional test coverage

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Drew Tate 2023-09-20 01:09:17 -06:00 committed by GitHub
parent f94b4c2755
commit 172de682c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 2739 additions and 554 deletions

1
.github/CODEOWNERS vendored
View file

@ -364,6 +364,7 @@ packages/kbn-eslint-plugin-telemetry @elastic/actionable-observability
x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security
packages/kbn-event-annotation-common @elastic/kibana-visualizations
packages/kbn-event-annotation-components @elastic/kibana-visualizations
src/plugins/event_annotation_listing @elastic/kibana-visualizations
src/plugins/event_annotation @elastic/kibana-visualizations
x-pack/test/plugin_api_integration/plugins/event_log @elastic/response-ops
x-pack/plugins/event_log @elastic/response-ops

View file

@ -6,17 +6,12 @@
"apmOss": "src/plugins/apm_oss",
"autocomplete": "packages/kbn-securitysolution-autocomplete/src",
"bfetch": "src/plugins/bfetch",
"cases": [
"packages/kbn-cases-components"
],
"cases": ["packages/kbn-cases-components"],
"cellActions": "packages/kbn-cell-actions",
"charts": "src/plugins/charts",
"console": "src/plugins/console",
"contentManagement": "packages/content-management",
"core": [
"src/core",
"packages/core"
],
"core": ["src/core", "packages/core"],
"customIntegrations": "src/plugins/custom_integrations",
"customIntegrationsPackage": "packages/kbn-custom-integrations",
"dashboard": "src/plugins/dashboard",
@ -27,10 +22,7 @@
"dataViews": "src/plugins/data_views",
"defaultNavigation": "packages/default-nav",
"devTools": "src/plugins/dev_tools",
"discover": [
"src/plugins/discover",
"packages/kbn-discover-utils"
],
"discover": ["src/plugins/discover", "packages/kbn-discover-utils"],
"savedSearch": "src/plugins/saved_search",
"embeddableApi": "src/plugins/embeddable",
"embeddableExamples": "examples/embeddable_examples",
@ -52,6 +44,7 @@
"expressionShape": "src/plugins/expression_shape",
"expressionTagcloud": "src/plugins/chart_expressions/expression_tagcloud",
"eventAnnotation": "src/plugins/event_annotation",
"eventAnnotationListing": "src/plugins/event_annotation_listing",
"eventAnnotationCommon": "packages/kbn-event-annotation-common",
"eventAnnotationComponents": "packages/kbn-event-annotation-components",
"fieldFormats": "src/plugins/field_formats",
@ -110,17 +103,9 @@
"languageDocumentationPopover": "packages/kbn-language-documentation-popover/src",
"textBasedLanguages": "src/plugins/text_based_languages",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": [
"src/plugins/telemetry",
"src/plugins/telemetry_management_section"
],
"timelion": [
"src/plugins/vis_types/timelion"
],
"uiActions": [
"src/plugins/ui_actions",
"packages/kbn-ui-actions-browser"
],
"telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"],
"timelion": ["src/plugins/vis_types/timelion"],
"uiActions": ["src/plugins/ui_actions", "packages/kbn-ui-actions-browser"],
"uiActionsEnhanced": "src/plugins/ui_actions_enhanced",
"uiActionsExamples": "examples/ui_action_examples",
"usageCollection": "src/plugins/usage_collection",
@ -140,10 +125,7 @@
"visTypeXy": "src/plugins/vis_types/xy",
"visualizations": "src/plugins/visualizations",
"visualizationUiComponents": "packages/kbn-visualization-ui-components",
"unifiedDocViewer": [
"src/plugins/unified_doc_viewer",
"packages/kbn-unified-doc-viewer"
],
"unifiedDocViewer": ["src/plugins/unified_doc_viewer", "packages/kbn-unified-doc-viewer"],
"unifiedSearch": "src/plugins/unified_search",
"unifiedFieldList": "packages/kbn-unified-field-list",
"unifiedHistogram": "src/plugins/unified_histogram",

View file

@ -102,6 +102,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a
|The Event Annotation service contains expressions for event annotations
|{kib-repo}blob/{branch}/src/plugins/event_annotation_listing/README.md[eventAnnotationListing]
|This plugin contains the library listing page for event annotation groups.
|{kib-repo}blob/{branch}/src/plugins/expression_error/README.md[expressionError]
|Expression Error plugin adds an error renderer to the expression plugin. The renderer will display the error image.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -222,9 +222,11 @@ Annotations support two placement types:
* *Custom query* &mdash; Displays annotations based on custom {es} queries. For detailed information about queries, check <<semi-structured-search>>.
Create the annotation layer.
Any annotation layer can be saved as an annotation group to the *Visualize Library* in order to reuse it in other visualizations. Any changes made to the annotation group will be reflected in all visualizations to which it is added.
. In the layer pane, click *Add layer > Annotations*.
Create a new annotation layer.
. In the layer pane, click *Add layer > Annotations > New annotation*.
. Select the {data-source} for the annotation.
@ -263,6 +265,20 @@ Specify the annotation appearance.
. To close, click *X*.
Save the annotation group to the library.
. In the layer pane, on your annotation layer, click image:images/lens_saveAnnotationLayerButton_8.9.0.png[Save button on annotations layer].
. Enter the *Title*, *Description*, and add any applicable <<managing-tags,*Tags*>>.
. Click *Save group*.
Add a library annotation group to a visualization.
. In the layer pane, click *Add layer > Annotations > Load from library*.
. Select the annotation group you want to use.
[float]
[[add-reference-lines]]
==== Add reference lines

View file

@ -401,6 +401,7 @@
"@kbn/eso-plugin": "link:x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin",
"@kbn/event-annotation-common": "link:packages/kbn-event-annotation-common",
"@kbn/event-annotation-components": "link:packages/kbn-event-annotation-components",
"@kbn/event-annotation-listing-plugin": "link:src/plugins/event_annotation_listing",
"@kbn/event-annotation-plugin": "link:src/plugins/event_annotation",
"@kbn/event-log-fixture-plugin": "link:x-pack/test/plugin_api_integration/plugins/event_log",
"@kbn/event-log-plugin": "link:x-pack/plugins/event_log",

View file

@ -946,7 +946,7 @@ function TableListViewTableComp<T extends UserContentCommonSchema>({
if (!showFetchError && hasNoItems) {
return (
<PageTemplate panelled isEmptyState={true}>
<PageTemplate isEmptyState={true}>
<KibanaPageTemplate.Section
aria-labelledby={hasInitialFetchReturned ? headingId : undefined}
>

View file

@ -322,7 +322,6 @@ const AnnotationEditorControls = ({
)}
<ColorPicker
overwriteColor={currentAnnotation.color}
defaultColor={isRange ? defaultAnnotationRangeColor : defaultAnnotationColor}
showAlpha={isRange}
setConfig={update}

View file

@ -97,6 +97,7 @@ export const ConfigPanelQueryAnnotation = ({
})}
>
<FieldPicker
compressed
options={options}
selectedOptions={
selectedField

View file

@ -181,6 +181,7 @@ export function TooltipSection({
data-test-subj={`lnsXY-annotation-tooltip-${index}`}
>
<FieldPicker
compressed
selectedOptions={
value
? [

View file

@ -1,152 +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 {
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
htmlIdGenerator,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
import type { QueryInputServices } from '@kbn/visualization-ui-components';
import type {
EventAnnotationConfig,
EventAnnotationGroupConfig,
} from '@kbn/event-annotation-common';
import { GroupEditorControls, isGroupValid } from './group_editor_controls';
export const GroupEditorFlyout = ({
group,
updateGroup,
onClose: parentOnClose,
onSave,
savedObjectsTagging,
dataViews,
createDataView,
queryInputServices,
}: {
group: EventAnnotationGroupConfig;
updateGroup: (newGroup: EventAnnotationGroupConfig) => void;
onClose: () => void;
onSave: () => void;
savedObjectsTagging: SavedObjectsTaggingApi;
dataViews: DataView[];
createDataView: (spec: DataViewSpec) => Promise<DataView>;
queryInputServices: QueryInputServices;
}) => {
const flyoutHeadingId = useMemo(() => htmlIdGenerator()(), []);
const flyoutBodyOverflowRef = useRef<Element | null>(null);
useEffect(() => {
if (!flyoutBodyOverflowRef.current) {
flyoutBodyOverflowRef.current = document.querySelector('.euiFlyoutBody__overflow');
}
}, []);
const [hasAttemptedSave, setHasAttemptedSave] = useState(false);
const resetContentScroll = useCallback(
() => flyoutBodyOverflowRef.current && flyoutBodyOverflowRef.current.scroll(0, 0),
[]
);
const [selectedAnnotation, _setSelectedAnnotation] = useState<EventAnnotationConfig>();
const setSelectedAnnotation = useCallback(
(newValue: EventAnnotationConfig | undefined) => {
if ((!newValue && selectedAnnotation) || (newValue && !selectedAnnotation))
resetContentScroll();
_setSelectedAnnotation(newValue);
},
[resetContentScroll, selectedAnnotation]
);
const onClose = () => (selectedAnnotation ? setSelectedAnnotation(undefined) : parentOnClose());
return (
<EuiFlyout onClose={onClose} size={'s'}>
<EuiFlyoutHeader hasBorder aria-labelledby={flyoutHeadingId}>
<EuiTitle size="s">
<h2 id={flyoutHeadingId}>
<FormattedMessage
id="eventAnnotationComponents.groupEditorFlyout.title"
defaultMessage="Edit annotation group"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<GroupEditorControls
group={group}
update={updateGroup}
selectedAnnotation={selectedAnnotation}
setSelectedAnnotation={setSelectedAnnotation}
TagSelector={savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector}
dataViews={dataViews}
createDataView={createDataView}
queryInputServices={queryInputServices}
showValidation={hasAttemptedSave}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
{selectedAnnotation ? (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="arrowLeft"
data-test-subj="backToGroupSettings"
onClick={() => setSelectedAnnotation(undefined)}
>
<FormattedMessage id="eventAnnotationComponents.edit.back" defaultMessage="Back" />
</EuiButtonEmpty>
</EuiFlexItem>
) : (
<>
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelGroupEdit" onClick={onClose}>
<FormattedMessage
id="eventAnnotationComponents.edit.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
iconType="save"
data-test-subj="saveAnnotationGroup"
fill
onClick={() => {
setHasAttemptedSave(true);
if (isGroupValid(group)) {
onSave();
}
}}
>
<FormattedMessage
id="eventAnnotationComponents.edit.save"
defaultMessage="Save annotation group"
/>
</EuiButton>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -9,8 +9,4 @@
// TODO - is this file needed?
export { AnnotationEditorControls, annotationsIconSet } from './annotation_editor_controls';
export * from './group_editor_controls';
export * from './get_annotation_accessor';
export * from './table_list';

View file

@ -10,7 +10,7 @@ export {
AnnotationEditorControls,
annotationsIconSet,
} from './components/annotation_editor_controls';
export { EventAnnotationGroupTableList, getAnnotationAccessor } from './components';
export { getAnnotationAccessor } from './components';
export {
defaultAnnotationColor,
defaultAnnotationRangeColor,

View file

@ -10,4 +10,5 @@ module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-event-annotation-components'],
setupFiles: ['jest-canvas-mock'],
};

View file

@ -22,19 +22,11 @@
"@kbn/unified-field-list",
"@kbn/data-views-plugin",
"@kbn/data-plugin",
"@kbn/test-jest-helpers",
"@kbn/saved-objects-tagging-oss-plugin",
"@kbn/core-ui-settings-browser",
"@kbn/dom-drag-drop",
"@kbn/content-management-table-list-view-table",
"@kbn/content-management-tabbed-table-list-view",
"@kbn/content-management-plugin",
"@kbn/event-annotation-common",
"@kbn/i18n-react",
"@kbn/saved-objects-finder-plugin",
"@kbn/core-notifications-browser-mocks",
"@kbn/core-notifications-browser",
"@kbn/core-saved-objects-api-browser",
"@kbn/expressions-plugin",
"@kbn/content-management-plugin",
]
}

View file

@ -40,7 +40,8 @@ pageLoadAssetSize:
embeddableEnhanced: 22107
enterpriseSearch: 50858
esUiShared: 326654
eventAnnotation: 48565
eventAnnotation: 30000
eventAnnotationListing: 25841
exploratoryView: 74673
expressionError: 22127
expressionGauge: 25000

View file

@ -97,12 +97,12 @@ export const ColorPicker = ({
onChange={handleColor}
color={isDisabled ? '' : colorText}
disabled={isDisabled}
placeholder={
defaultColor?.toUpperCase() ||
i18n.translate('visualizationUiComponents.colorPicker.seriesColor.auto', {
defaultMessage: 'Auto',
})
}
placeholder={' '}
onBlur={() => {
if (!colorText) {
setColorText(overwriteColor ?? defaultColor);
}
}}
aria-label={inputLabel}
showAlpha={showAlpha}
swatches={

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { ReactNode } from 'react';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { isPromise } from '@kbn/std';
@ -21,83 +21,45 @@ interface Props {
input?: EmbeddableInput;
}
interface State {
node?: ReactNode;
}
export class EmbeddableRoot extends React.Component<Props, State> {
private root?: React.RefObject<HTMLDivElement>;
private alreadyMounted: boolean = false;
constructor(props: Props) {
super(props);
this.root = React.createRef();
this.state = {};
}
private updateNode = (node: MaybePromise<ReactNode>) => {
if (isPromise(node)) {
node.then(this.updateNode);
export const EmbeddableRoot: React.FC<Props> = ({ embeddable, loading, error, input }) => {
const [node, setNode] = useState<ReactNode | undefined>();
const [embeddableHasMounted, setEmbeddableHasMounted] = useState(false);
const rootRef = useRef<HTMLDivElement | null>(null);
const updateNode = useCallback((newNode: MaybePromise<ReactNode>) => {
if (isPromise(newNode)) {
newNode.then(updateNode);
return;
}
this.setState({ node });
};
setNode(newNode);
}, []);
public componentDidMount() {
if (!this.root?.current || !this.props.embeddable) {
useEffect(() => {
if (!rootRef.current || !embeddable) {
return;
}
this.alreadyMounted = true;
this.updateNode(this.props.embeddable.render(this.root.current) ?? undefined);
}
setEmbeddableHasMounted(true);
updateNode(embeddable.render(rootRef.current) ?? undefined);
embeddable.render(rootRef.current);
}, [updateNode, embeddable]);
public componentDidUpdate(prevProps?: Props) {
let justRendered = false;
if (this.root?.current && this.props.embeddable && !this.alreadyMounted) {
this.alreadyMounted = true;
this.updateNode(this.props.embeddable.render(this.root.current) ?? undefined);
justRendered = true;
useEffect(() => {
if (input && embeddable && embeddableHasMounted) {
embeddable.updateInput(input);
}
}, [input, embeddable, embeddableHasMounted]);
if (
!justRendered &&
this.root &&
this.root.current &&
this.props.embeddable &&
this.alreadyMounted &&
this.props.input &&
prevProps?.input !== this.props.input
) {
this.props.embeddable.updateInput(this.props.input);
}
}
public shouldComponentUpdate({ embeddable, error, input, loading }: Props, { node }: State) {
return Boolean(
error !== this.props.error ||
loading !== this.props.loading ||
embeddable !== this.props.embeddable ||
(this.root && this.root.current && embeddable && !this.alreadyMounted) ||
input !== this.props.input ||
node !== this.state.node
);
}
public render() {
return (
<React.Fragment>
<div ref={this.root}>{this.state.node}</div>
{this.props.loading && <EuiLoadingSpinner data-test-subj="embedSpinner" />}
{this.props.error && (
<EmbeddableErrorHandler embeddable={this.props.embeddable} error={this.props.error}>
{({ message }) => <EuiText data-test-subj="embedError">{message}</EuiText>}
</EmbeddableErrorHandler>
)}
</React.Fragment>
);
}
}
return (
<>
<div ref={rootRef}>{node}</div>
{loading && <EuiLoadingSpinner data-test-subj="embedSpinner" />}
{error && (
<EmbeddableErrorHandler embeddable={embeddable} error={error}>
{({ message }) => <EuiText data-test-subj="embedError">{message}</EuiText>}
</EmbeddableErrorHandler>
)}
</>
);
};

View file

@ -24,7 +24,7 @@ export { eventAnnotationGroup } from './event_annotation_group';
export type { EventAnnotationGroupArgs } from './event_annotation_group';
export type { FetchEventAnnotationsArgs } from './fetch_event_annotations/types';
export type { EventAnnotationArgs, EventAnnotationOutput } from './types';
export type { EventAnnotationOutput } from './types';
export type {
EventAnnotationGroupGetIn,

View file

@ -24,7 +24,6 @@
"data",
"savedObjectsFinder",
"dataViews",
"kibanaReact",
],
"extraPublicDirs": [
"common"

View file

@ -10,7 +10,6 @@ import type { Plugin, CoreSetup, CoreStart } from '@kbn/core/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public';
import type { ExpressionsSetup } from '@kbn/expressions-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import {
ContentManagementPublicSetup,
ContentManagementPublicStart,
@ -28,8 +27,6 @@ import {
eventAnnotationGroup,
} from '../common';
import { getFetchEventAnnotations } from './fetch_event_annotations';
import type { EventAnnotationListingPageServices } from './get_table_list';
import { ANNOTATIONS_LISTING_VIEW_ID } from '../common/constants';
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
export interface EventAnnotationStartDependencies {
@ -76,46 +73,6 @@ export class EventAnnotationPlugin
defaultMessage: 'Annotation group',
}),
});
dependencies.visualizations.listingViewRegistry.add({
title: i18n.translate('eventAnnotation.listingViewTitle', {
defaultMessage: 'Annotation groups',
}),
id: ANNOTATIONS_LISTING_VIEW_ID,
getTableList: async (props) => {
const [coreStart, pluginsStart] = await core.getStartServices();
const eventAnnotationService = await new EventAnnotationService(
coreStart,
pluginsStart.contentManagement
).getService();
const ids = await pluginsStart.dataViews.getIds();
const dataViews = await Promise.all(ids.map((id) => pluginsStart.dataViews.get(id)));
const services: EventAnnotationListingPageServices = {
core: coreStart,
savedObjectsTagging: pluginsStart.savedObjectsTagging,
eventAnnotationService,
PresentationUtilContextProvider: pluginsStart.presentationUtil.ContextProvider,
dataViews,
createDataView: pluginsStart.dataViews.create.bind(pluginsStart.dataViews),
queryInputServices: {
http: coreStart.http,
docLinks: coreStart.docLinks,
notifications: coreStart.notifications,
uiSettings: coreStart.uiSettings,
dataViews: pluginsStart.dataViews,
unifiedSearch: pluginsStart.unifiedSearch,
data: pluginsStart.data,
storage: new Storage(localStorage),
},
};
const { getTableList } = await import('./get_table_list');
return getTableList(props, services);
},
});
}
public start(

View file

@ -34,13 +34,6 @@
"@kbn/core-saved-objects-api-server",
"@kbn/event-annotation-components",
"@kbn/event-annotation-common",
"@kbn/kibana-react-plugin",
"@kbn/content-management-table-list-view-table",
"@kbn/content-management-tabbed-table-list-view",
"@kbn/core-lifecycle-browser",
"@kbn/saved-objects-tagging-oss-plugin",
"@kbn/dom-drag-drop",
"@kbn/kibana-utils-plugin",
"@kbn/content-management-utils"
],
"exclude": [

View file

@ -0,0 +1,6 @@
{
"prefix": "eventAnnotationsApplication",
"paths": {
"eventAnnotationsApplication": "."
}
}

View file

@ -0,0 +1,3 @@
# Event Annotation listing
This plugin contains the library listing page for event annotation groups.

View file

@ -0,0 +1,19 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/event_annotation_listing'],
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/src/plugins/event_annotation_listing',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/src/plugins/event_annotation_listing/{common,public,server}/**/*.{ts,tsx}',
],
setupFiles: ['jest-canvas-mock'],
};

View file

@ -0,0 +1,30 @@
{
"type": "plugin",
"id": "@kbn/event-annotation-listing-plugin",
"owner": "@elastic/kibana-visualizations",
"description": "The listing page for event annotations.",
"plugin": {
"id": "eventAnnotationListing",
"server": false,
"browser": true,
"requiredPlugins": [
"savedObjectsManagement",
"eventAnnotation",
"data",
"presentationUtil",
"visualizations",
"dataViews",
"unifiedSearch",
"kibanaUtils",
"contentManagement",
],
"optionalPlugins": [
"savedObjectsTagging",
"lens",
],
"requiredBundles": [
"kibanaReact",
],
"extraPublicDirs": []
}
}

View file

@ -3,7 +3,6 @@
exports[`group editor flyout renders controls 1`] = `
Object {
"TagSelector": [MockFunction],
"createDataView": [MockFunction],
"dataViews": Array [
Object {
"id": "some-id",
@ -29,6 +28,7 @@ Object {
"tags": Array [],
"title": "My group",
},
"isAdHocDataView": [Function],
"queryInputServices": Object {},
"selectedAnnotation": undefined,
"setSelectedAnnotation": [Function],

View file

@ -24,7 +24,7 @@ import { euiThemeVars } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
import type { EventAnnotationConfig } from '@kbn/event-annotation-common';
import { createCopiedAnnotation } from '@kbn/event-annotation-common';
import { getAnnotationAccessor } from '..';
import { getAnnotationAccessor } from '@kbn/event-annotation-components';
export const AnnotationList = ({
annotations,
@ -40,7 +40,7 @@ export const AnnotationList = ({
setNewAnnotationId(uuidv4());
}, [annotations.length]);
const addAnnotationText = i18n.translate('eventAnnotationComponents.annotationList.add', {
const addAnnotationText = i18n.translate('eventAnnotationListing.annotationList.add', {
defaultMessage: 'Add annotation',
});
@ -88,14 +88,26 @@ export const AnnotationList = ({
const [{ dragging }] = useDragDropContext();
return (
<div>
<div
css={css`
background-color: ${euiThemeVars.euiColorLightestShade};
padding: ${euiThemeVars.euiSizeS};
border-radius: ${euiThemeVars.euiBorderRadius};
overflow: hidden;
.domDragDrop-isActiveGroup {
padding: ${euiThemeVars.euiSizeS};
margin: -${euiThemeVars.euiSizeS} -${euiThemeVars.euiSizeS} 0 -${euiThemeVars.euiSizeS};
}
`}
>
<ReorderProvider>
{annotations.map((annotation, index) => (
<div
key={index}
css={css`
margin-top: ${euiThemeVars.euiSizeS};
position: relative; // this is to properly contain the absolutely-positioned drop target in DragDrop
margin-bottom: ${euiThemeVars.euiSizeS};
`}
>
<DragDrop
@ -119,7 +131,7 @@ export const AnnotationList = ({
}}
>
<DimensionButton
groupLabel={i18n.translate('eventAnnotationComponents.groupEditor.addAnnotation', {
groupLabel={i18n.translate('eventAnnotationListing.groupEditor.addAnnotation', {
defaultMessage: 'Annotations',
})}
onClick={() => selectAnnotation(annotation)}
@ -136,34 +148,28 @@ export const AnnotationList = ({
))}
</ReorderProvider>
<div
css={css`
margin-top: ${euiThemeVars.euiSizeS};
`}
<DragDrop
order={[annotations.length]}
getCustomDropTarget={DropTargetSwapDuplicateCombine.getCustomDropTarget}
getAdditionalClassesOnDroppable={
DropTargetSwapDuplicateCombine.getAdditionalClassesOnDroppable
}
dropTypes={dragging ? ['duplicate_compatible'] : []}
value={{
id: 'addAnnotation',
humanData: {
label: addAnnotationText,
},
}}
onDrop={({ id: sourceId }) => addNewAnnotation(sourceId)}
>
<DragDrop
order={[annotations.length]}
getCustomDropTarget={DropTargetSwapDuplicateCombine.getCustomDropTarget}
getAdditionalClassesOnDroppable={
DropTargetSwapDuplicateCombine.getAdditionalClassesOnDroppable
}
dropTypes={dragging ? ['duplicate_compatible'] : []}
value={{
id: 'addAnnotation',
humanData: {
label: addAnnotationText,
},
}}
onDrop={({ id: sourceId }) => addNewAnnotation(sourceId)}
>
<EmptyDimensionButton
dataTestSubj="addAnnotation"
label={addAnnotationText}
ariaLabel={addAnnotationText}
onClick={() => addNewAnnotation()}
/>
</DragDrop>
</div>
<EmptyDimensionButton
dataTestSubj="addAnnotation"
label={addAnnotationText}
ariaLabel={addAnnotationText}
onClick={() => addNewAnnotation()}
/>
</DragDrop>
</div>
);
};

View file

@ -16,7 +16,7 @@ import { EuiTextAreaProps, EuiTextProps } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/common';
import { act } from 'react-dom/test-utils';
import type { QueryInputServices } from '@kbn/visualization-ui-components';
import { AnnotationEditorControls, ENABLE_INDIVIDUAL_ANNOTATION_EDITING } from '..';
import { AnnotationEditorControls } from '@kbn/event-annotation-components';
jest.mock('@elastic/eui', () => {
return {
@ -68,15 +68,9 @@ describe('event annotation group editor', () => {
}
selectedAnnotation={undefined}
setSelectedAnnotation={setSelectedAnnotationMock}
createDataView={(spec) =>
Promise.resolve({
id: spec.id,
title: spec.title,
toSpec: () => spec,
} as unknown as DataView)
}
queryInputServices={{} as QueryInputServices}
showValidation={false}
isAdHocDataView={() => false}
/>
);
@ -174,63 +168,54 @@ describe('event annotation group editor', () => {
`);
});
if (ENABLE_INDIVIDUAL_ANNOTATION_EDITING) {
it('adds a new annotation group', () => {
act(() => {
wrapper.find('button[data-test-subj="addAnnotation"]').simulate('click');
});
// it('adds a new annotation group', () => {
// act(() => {
// wrapper.find('button[data-test-subj="addAnnotation"]').simulate('click');
// });
expect(updateMock).toHaveBeenCalledTimes(2);
const newAnnotations = (updateMock.mock.calls[0][0] as EventAnnotationGroupConfig)
.annotations;
expect(newAnnotations.length).toBe(group.annotations.length + 1);
expect(wrapper.exists(AnnotationEditorControls)); // annotation controls opened
// expect(updateMock).toHaveBeenCalledTimes(2);
// const newAnnotations = (updateMock.mock.calls[0][0] as EventAnnotationGroupConfig).annotations;
// expect(newAnnotations.length).toBe(group.annotations.length + 1);
// expect(wrapper.exists(AnnotationEditorControls)); // annotation controls opened
// });
it('incorporates annotation updates into group', () => {
const annotations = [getDefaultManualAnnotation('1', ''), getDefaultManualAnnotation('2', '')];
act(() => {
wrapper.setProps({
selectedAnnotation: annotations[0],
group: { ...group, annotations },
});
});
it('incorporates annotation updates into group', () => {
const annotations = [
getDefaultManualAnnotation('1', ''),
getDefaultManualAnnotation('2', ''),
];
act(() => {
wrapper.setProps({
selectedAnnotation: annotations[0],
group: { ...group, annotations },
});
});
wrapper.find(AnnotationEditorControls).prop('onAnnotationChange')({
...annotations[0],
color: 'newColor',
});
expect(updateMock).toHaveBeenCalledTimes(1);
expect(updateMock.mock.calls[0][0].annotations[0].color).toBe('newColor');
expect(setSelectedAnnotationMock).toHaveBeenCalledTimes(1);
wrapper.find(AnnotationEditorControls).prop('onAnnotationChange')({
...annotations[0],
color: 'newColor',
});
it('removes an annotation from a group', () => {
const annotations = [
getDefaultManualAnnotation('1', ''),
getDefaultManualAnnotation('2', ''),
];
expect(updateMock).toHaveBeenCalledTimes(1);
expect(updateMock.mock.calls[0][0].annotations[0].color).toBe('newColor');
expect(setSelectedAnnotationMock).toHaveBeenCalledTimes(1);
});
act(() => {
wrapper.setProps({
group: { ...group, annotations },
});
it('removes an annotation from a group', () => {
const annotations = [getDefaultManualAnnotation('1', ''), getDefaultManualAnnotation('2', '')];
act(() => {
wrapper.setProps({
group: { ...group, annotations },
});
act(() => {
wrapper
.find('button[data-test-subj="indexPattern-dimension-remove"]')
.last()
.simulate('click');
});
expect(updateMock).toHaveBeenCalledTimes(1);
expect(updateMock.mock.calls[0][0].annotations).toEqual(annotations.slice(0, 1));
});
}
act(() => {
wrapper
.find('button[data-test-subj="indexPattern-dimension-remove"]')
.last()
.simulate('click');
});
expect(updateMock).toHaveBeenCalledTimes(1);
expect(updateMock.mock.calls[0][0].annotations).toEqual(annotations.slice(0, 1));
});
});

View file

@ -16,26 +16,30 @@ import {
EuiTitle,
} from '@elastic/eui';
import { css } from '@emotion/react';
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
import type { DataView } from '@kbn/data-views-plugin/common';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { SavedObjectsTaggingApiUiComponent } from '@kbn/saved-objects-tagging-oss-plugin/public';
import { euiThemeVars } from '@kbn/ui-theme';
import { QueryInputServices } from '@kbn/visualization-ui-components';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import type {
EventAnnotationConfig,
EventAnnotationGroupConfig,
} from '@kbn/event-annotation-common';
import { EVENT_ANNOTATION_APP_NAME } from '../../constants';
import { AnnotationEditorControls } from '../annotation_editor_controls';
import {
EVENT_ANNOTATION_APP_NAME,
AnnotationEditorControls,
} from '@kbn/event-annotation-components';
import { AnnotationList } from './annotation_list';
export const ENABLE_INDIVIDUAL_ANNOTATION_EDITING = false;
const isTitleValid = (title: string) => Boolean(title.length);
export const isGroupValid = (group: EventAnnotationGroupConfig) => isTitleValid(group.title);
const isDataViewValid = (dataView: DataView | undefined) => Boolean(dataView?.id);
export const isGroupValid = (group: EventAnnotationGroupConfig, dataViews: DataView[]) =>
isTitleValid(group.title) &&
isDataViewValid(dataViews.find(({ id }) => id === group.indexPatternId));
export const GroupEditorControls = ({
group,
@ -43,10 +47,10 @@ export const GroupEditorControls = ({
setSelectedAnnotation: _setSelectedAnnotation,
selectedAnnotation,
TagSelector,
dataViews: globalDataViews,
createDataView,
dataViews,
queryInputServices,
showValidation,
isAdHocDataView,
}: {
group: EventAnnotationGroupConfig;
update: (group: EventAnnotationGroupConfig) => void;
@ -54,19 +58,10 @@ export const GroupEditorControls = ({
setSelectedAnnotation: (annotation: EventAnnotationConfig) => void;
TagSelector: SavedObjectsTaggingApiUiComponent['SavedObjectSaveModalTagSelector'];
dataViews: DataView[];
createDataView: (spec: DataViewSpec) => Promise<DataView>;
queryInputServices: QueryInputServices;
showValidation: boolean;
isAdHocDataView: (id: string) => boolean;
}) => {
// save the spec for the life of the component since the user might change their mind after selecting another data view
const [adHocDataView, setAdHocDataView] = useState<DataView>();
useEffect(() => {
if (group.dataViewSpec) {
createDataView(group.dataViewSpec).then(setAdHocDataView);
}
}, [createDataView, group.dataViewSpec]);
const setSelectedAnnotation = useCallback(
(newSelection: EventAnnotationConfig) => {
update({
@ -80,45 +75,38 @@ export const GroupEditorControls = ({
[_setSelectedAnnotation, group, update]
);
const dataViews = useMemo(() => {
const items = [...globalDataViews];
if (adHocDataView) {
items.push(adHocDataView);
}
return items;
}, [adHocDataView, globalDataViews]);
const currentDataView = useMemo(
() => dataViews.find((dataView) => dataView.id === group.indexPatternId) || dataViews[0],
() => dataViews.find((dataView) => dataView.id === group.indexPatternId),
[dataViews, group.indexPatternId]
);
return !selectedAnnotation ? (
<>
<EuiTitle
size="xs"
size="xxs"
css={css`
margin-bottom: ${euiThemeVars.euiSize};
`}
>
<h4>
<h3>
<FormattedMessage
id="eventAnnotationComponents.groupEditor.details"
id="eventAnnotationListing.groupEditor.details"
defaultMessage="Details"
/>
</h4>
</h3>
</EuiTitle>
<EuiForm>
<EuiFormRow
label={i18n.translate('eventAnnotationComponents.groupEditor.title', {
label={i18n.translate('eventAnnotationListing.groupEditor.title', {
defaultMessage: 'Title',
})}
isInvalid={showValidation && !isTitleValid(group.title)}
error={i18n.translate('eventAnnotationComponents.groupEditor.titleRequired', {
error={i18n.translate('eventAnnotationListing.groupEditor.titleRequired', {
defaultMessage: 'A title is required.',
})}
>
<EuiFieldText
compressed
data-test-subj="annotationGroupTitle"
value={group.title}
isInvalid={showValidation && !isTitleValid(group.title)}
@ -131,19 +119,20 @@ export const GroupEditorControls = ({
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('eventAnnotationComponents.groupEditor.description', {
label={i18n.translate('eventAnnotationListing.groupEditor.description', {
defaultMessage: 'Description',
})}
labelAppend={
<EuiText color="subdued" size="xs">
<FormattedMessage
id="eventAnnotationComponents.groupEditor.optional"
id="eventAnnotationListing.groupEditor.optional"
defaultMessage="Optional"
/>
</EuiText>
}
>
<EuiTextArea
compressed
data-test-subj="annotationGroupDescription"
value={group.description}
onChange={({ target: { value } }) =>
@ -158,6 +147,7 @@ export const GroupEditorControls = ({
<TagSelector
initialSelection={group.tags}
markOptional
compressed
onTagsSelected={(tags: string[]) =>
update({
...group,
@ -166,46 +156,92 @@ export const GroupEditorControls = ({
}
/>
</EuiFormRow>
{ENABLE_INDIVIDUAL_ANNOTATION_EDITING && (
<>
<EuiFormRow
label={i18n.translate('eventAnnotationComponents.groupEditor.dataView', {
defaultMessage: 'Data view',
})}
>
<EuiSelect
data-test-subj="annotationDataViewSelection"
options={dataViews.map(({ id: value, title, name }) => ({
value,
text: name ?? title,
}))}
value={group.indexPatternId}
onChange={({ target: { value } }) =>
update({
...group,
indexPatternId: value,
dataViewSpec:
value === adHocDataView?.id ? adHocDataView.toSpec(false) : undefined,
})
}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('eventAnnotationComponents.groupEditor.addAnnotation', {
defaultMessage: 'Annotations',
})}
>
<AnnotationList
annotations={group.annotations}
selectAnnotation={setSelectedAnnotation}
update={(newAnnotations) => update({ ...group, annotations: newAnnotations })}
/>
</EuiFormRow>
</>
)}
<EuiFormRow
label={i18n.translate('eventAnnotationListing.groupEditor.dataView', {
defaultMessage: 'Data view',
})}
isInvalid={!isDataViewValid(currentDataView)}
error={
!isDataViewValid(currentDataView)
? i18n.translate('eventAnnotationListing.groupEditor.dataViewMissingError', {
defaultMessage: 'The previously selected data view no longer exists.',
})
: ''
}
>
<EuiSelect
compressed
data-test-subj="annotationDataViewSelection"
isInvalid={!isDataViewValid(currentDataView)}
options={dataViews.map(({ id: value, title, name }) => ({
value,
text: name ?? title,
}))}
value={isDataViewValid(currentDataView) ? group.indexPatternId : undefined}
hasNoInitialSelection={true}
onChange={({ target: { value } }) => {
const selectedDataView = dataViews.find(({ id }) => id === value);
if (!selectedDataView?.id) {
return;
}
update({
...group,
indexPatternId: value,
dataViewSpec: isAdHocDataView(selectedDataView.id)
? selectedDataView.toSpec(false)
: undefined,
});
}}
/>
</EuiFormRow>
</EuiForm>
<div
css={css`
margin-top: ${euiThemeVars.euiSize};
padding-top: ${euiThemeVars.euiSize};
position: relative;
&:before {
content: '';
position: absolute;
top: 0;
right: -${euiThemeVars.euiSize};
left: -${euiThemeVars.euiSize};
border-top: 1px solid ${euiThemeVars.euiColorLightShade};
}
`}
>
<EuiTitle
size="xxs"
css={css`
margin-bottom: ${euiThemeVars.euiSize};
`}
>
<h3>
<FormattedMessage
id="eventAnnotationListing.groupEditor.annotations"
defaultMessage="Annotations"
/>
</h3>
</EuiTitle>
<EuiForm>
<EuiFormRow
label={i18n.translate('eventAnnotationListing.groupEditor.annotationGroupLabel', {
defaultMessage: 'Date histogram axis',
})}
>
<AnnotationList
annotations={group.annotations}
selectAnnotation={currentDataView ? setSelectedAnnotation : () => {}}
update={(newAnnotations) => update({ ...group, annotations: newAnnotations })}
/>
</EuiFormRow>
</EuiForm>
</div>
</>
) : (
) : currentDataView ? (
<AnnotationEditorControls
annotation={selectedAnnotation}
onAnnotationChange={(changes) => setSelectedAnnotation({ ...selectedAnnotation, ...changes })}
@ -214,5 +250,5 @@ export const GroupEditorControls = ({
queryInputServices={queryInputServices}
appName={EVENT_ANNOTATION_APP_NAME}
/>
);
) : null;
};

View file

@ -16,6 +16,7 @@ import { GroupEditorControls } from './group_editor_controls';
import { GroupEditorFlyout } from './group_editor_flyout';
import { DataView } from '@kbn/data-views-plugin/common';
import type { QueryInputServices } from '@kbn/visualization-ui-components';
import { EmbeddableComponent } from '@kbn/lens-plugin/public';
const simulateButtonClick = (component: ShallowWrapper, selector: string) => {
(component.find(selector) as ShallowWrapper<Parameters<typeof EuiButton>[0]>).prop('onClick')!(
@ -27,6 +28,7 @@ const SELECTORS = {
SAVE_BUTTON: '[data-test-subj="saveAnnotationGroup"]',
CANCEL_BUTTON: '[data-test-subj="cancelGroupEdit"]',
BACK_BUTTON: '[data-test-subj="backToGroupSettings"]',
TOP_BACK_BUTTON: '[data-test-subj="backToGroupSettingsTop"]',
};
const assertGroupEditingState = (component: ShallowWrapper) => {
@ -59,14 +61,15 @@ describe('group editor flyout', () => {
let onSave: jest.Mock;
let onClose: jest.Mock;
let updateGroup: jest.Mock;
const LensEmbeddableComponent: EmbeddableComponent = jest.fn();
beforeEach(() => {
const mountComponent = (groupToUse: EventAnnotationGroupConfig) => {
onSave = jest.fn();
onClose = jest.fn();
updateGroup = jest.fn();
component = shallow(
return shallow(
<GroupEditorFlyout
group={group}
group={groupToUse}
onSave={onSave}
onClose={onClose}
updateGroup={updateGroup}
@ -79,8 +82,16 @@ describe('group editor flyout', () => {
savedObjectsTagging={mockTaggingApi}
createDataView={jest.fn()}
queryInputServices={{} as QueryInputServices}
LensEmbeddableComponent={LensEmbeddableComponent}
searchSessionId={'searchSessionId'}
refreshSearchSession={jest.fn()}
timePickerQuickRanges={[]}
/>
);
};
beforeEach(() => {
component = mountComponent(group);
});
it('renders controls', () => {
@ -112,17 +123,20 @@ describe('group editor flyout', () => {
expect(updateGroup).toHaveBeenCalledWith(newGroup);
});
test('specific annotation editing', () => {
assertGroupEditingState(component);
test.each([SELECTORS.BACK_BUTTON, SELECTORS.TOP_BACK_BUTTON])(
'specific annotation editing',
(backButtonSelector) => {
assertGroupEditingState(component);
component.find(GroupEditorControls).prop('setSelectedAnnotation')(annotation);
component.find(GroupEditorControls).prop('setSelectedAnnotation')(annotation);
assertAnnotationEditingState(component);
assertAnnotationEditingState(component);
component.find(SELECTORS.BACK_BUTTON).simulate('click');
component.find(backButtonSelector).simulate('click');
assertGroupEditingState(component);
});
assertGroupEditingState(component);
}
);
it('removes active annotation instead of signaling close', () => {
component.find(GroupEditorControls).prop('setSelectedAnnotation')(annotation);

View file

@ -0,0 +1,253 @@
/*
* 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 {
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
htmlIdGenerator,
useIsWithinBreakpoints,
EuiButtonIcon,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
import type { QueryInputServices } from '@kbn/visualization-ui-components';
import type {
EventAnnotationConfig,
EventAnnotationGroupConfig,
} from '@kbn/event-annotation-common';
import { css } from '@emotion/react';
import type { EmbeddableComponent as LensEmbeddableComponent } from '@kbn/lens-plugin/public';
import { euiThemeVars } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
import { GroupEditorControls, isGroupValid } from './group_editor_controls';
import { GroupPreview } from './group_preview';
export const GroupEditorFlyout = ({
group,
updateGroup,
onClose: parentOnClose,
onSave,
savedObjectsTagging,
dataViews: globalDataViews,
createDataView,
LensEmbeddableComponent,
queryInputServices,
searchSessionId,
refreshSearchSession,
timePickerQuickRanges,
}: {
group: EventAnnotationGroupConfig;
updateGroup: (newGroup: EventAnnotationGroupConfig) => void;
onClose: () => void;
onSave: () => void;
savedObjectsTagging: SavedObjectsTaggingApi;
dataViews: DataView[];
createDataView: (spec: DataViewSpec) => Promise<DataView>;
LensEmbeddableComponent: LensEmbeddableComponent;
queryInputServices: QueryInputServices;
searchSessionId: string;
refreshSearchSession: () => void;
timePickerQuickRanges: Array<{ from: string; to: string; display: string }> | undefined;
}) => {
const flyoutHeadingId = useMemo(() => htmlIdGenerator()(), []);
const flyoutBodyOverflowRef = useRef<Element | null>(null);
useEffect(() => {
if (!flyoutBodyOverflowRef.current) {
flyoutBodyOverflowRef.current = document.querySelector('.euiFlyoutBody__overflow');
}
}, []);
const [hasAttemptedSave, setHasAttemptedSave] = useState(false);
const resetContentScroll = useCallback(
() => flyoutBodyOverflowRef.current && flyoutBodyOverflowRef.current.scroll(0, 0),
[]
);
// save the spec for the life of the component since the user might change their mind after selecting another data view
const [adHocDataView, setAdHocDataView] = useState<DataView>();
useEffect(() => {
if (group.dataViewSpec) {
createDataView(group.dataViewSpec).then(setAdHocDataView);
}
}, [createDataView, group.dataViewSpec]);
const dataViews = useMemo(() => {
const items = [...globalDataViews];
if (adHocDataView) {
items.push(adHocDataView);
}
return items;
}, [adHocDataView, globalDataViews]);
const [selectedAnnotation, _setSelectedAnnotation] = useState<EventAnnotationConfig>();
const setSelectedAnnotation = useCallback(
(newValue: EventAnnotationConfig | undefined) => {
if ((!newValue && selectedAnnotation) || (newValue && !selectedAnnotation))
resetContentScroll();
_setSelectedAnnotation(newValue);
},
[resetContentScroll, selectedAnnotation]
);
const onClose = () => (selectedAnnotation ? setSelectedAnnotation(undefined) : parentOnClose());
const showPreview = !useIsWithinBreakpoints(['xs', 's', 'm']);
return (
<EuiFlyout
onClose={onClose}
paddingSize="m"
size="l"
hideCloseButton
outsideClickCloses={false}
>
<EuiFlexGroup
css={css`
height: 100%;
overflow-y: auto;
`}
gutterSize="none"
>
<EuiFlexItem
grow={false}
css={css`
${showPreview ? 'width: 360px;' : ''}
border-right: 1px solid ${euiThemeVars.euiColorLightShade};
`}
>
<EuiFlyoutHeader hasBorder aria-labelledby={flyoutHeadingId}>
<EuiTitle size="xs">
<h2 id={flyoutHeadingId}>
{selectedAnnotation ? (
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="text"
iconType="sortLeft"
aria-label={i18n.translate('eventAnnotationListing.edit.back', {
defaultMessage: 'Back',
})}
onClick={() => setSelectedAnnotation(undefined)}
data-test-subj="backToGroupSettingsTop"
/>
</EuiFlexItem>
<EuiFlexItem>
<FormattedMessage
id="eventAnnotationListing.groupEditorFlyout.titleWithAnnotation"
defaultMessage="Date histogram axis annotation"
/>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<FormattedMessage
id="eventAnnotationListing.groupEditorFlyout.title"
defaultMessage="Edit annotation group"
/>
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<GroupEditorControls
group={group}
update={updateGroup}
selectedAnnotation={selectedAnnotation}
setSelectedAnnotation={setSelectedAnnotation}
TagSelector={savedObjectsTagging.ui.components.SavedObjectSaveModalTagSelector}
dataViews={dataViews}
queryInputServices={queryInputServices}
showValidation={hasAttemptedSave}
isAdHocDataView={(id) => id === adHocDataView?.id}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
{selectedAnnotation ? (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
flush="both"
iconType="sortLeft"
data-test-subj="backToGroupSettings"
onClick={() => setSelectedAnnotation(undefined)}
>
<FormattedMessage id="eventAnnotationListing.edit.back" defaultMessage="Back" />
</EuiButtonEmpty>
</EuiFlexItem>
) : (
<>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="cancelGroupEdit"
onClick={onClose}
size="s"
flush="both"
>
<FormattedMessage
id="eventAnnotationListing.edit.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
size="s"
iconType="save"
data-test-subj="saveAnnotationGroup"
fill
onClick={() => {
setHasAttemptedSave(true);
if (isGroupValid(group, dataViews)) {
onSave();
}
}}
>
<FormattedMessage
id="eventAnnotationListing.edit.save"
defaultMessage="Save annotation group"
/>
</EuiButton>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlexItem>
{showPreview && (
<EuiFlexItem
css={css`
background-color: ${euiThemeVars.euiColorLightestShade};
`}
>
<GroupPreview
group={group}
dataViews={dataViews}
LensEmbeddableComponent={LensEmbeddableComponent}
searchSessionId={searchSessionId}
refreshSearchSession={refreshSearchSession}
timePickerQuickRanges={timePickerQuickRanges}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyout>
);
};

View file

@ -0,0 +1,307 @@
/*
* 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 { getDefaultManualAnnotation } from '@kbn/event-annotation-common';
import type { EventAnnotationGroupConfig } from '@kbn/event-annotation-common';
import React from 'react';
import {
DataView,
DataViewField,
DataViewFieldMap,
IIndexPatternFieldList,
} from '@kbn/data-views-plugin/common';
import {
EmbeddableComponent,
FieldBasedIndexPatternColumn,
TypedLensByValueInput,
} from '@kbn/lens-plugin/public';
import { Datatable } from '@kbn/expressions-plugin/common';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import { I18nProvider } from '@kbn/i18n-react';
import { GroupPreview } from './group_preview';
import { LensByValueInput } from '@kbn/lens-plugin/public/embeddable';
import { DATA_LAYER_ID, DATE_HISTOGRAM_COLUMN_ID, getCurrentTimeField } from './lens_attributes';
import moment from 'moment';
class EuiSuperDatePickerTestHarness {
public static get currentCommonlyUsedRange() {
return screen.queryByTestId('superDatePickerShowDatesButton')?.textContent ?? '';
}
// TODO - add assertion with date formatting
public static get currentRange() {
if (screen.queryByTestId('superDatePickerShowDatesButton')) {
// showing a commonly-used range
return { from: '', to: '' };
}
return {
from: screen.getByTestId('superDatePickerstartDatePopoverButton').textContent,
to: screen.getByTestId('superDatePickerendDatePopoverButton').textContent,
};
}
static togglePopover() {
userEvent.click(screen.getByRole('button', { name: 'Date quick select' }));
}
static async selectCommonlyUsedRange(label: string) {
if (!screen.queryByText('Commonly used')) this.togglePopover();
// Using fireEvent here because userEvent erroneously claims that
// pointer-events is set to 'none'.
//
// I have verified that this fixed on the latest version of the @testing-library/user-event package
fireEvent.click(await screen.findByText(label));
}
static refresh() {
userEvent.click(screen.getByRole('button', { name: 'Refresh' }));
}
}
describe('group editor preview', () => {
const annotation = getDefaultManualAnnotation('my-id', 'some-timestamp');
const group: EventAnnotationGroupConfig = {
annotations: [annotation],
description: '',
tags: [],
indexPatternId: 'some-id',
title: 'My group',
ignoreGlobalFilters: false,
};
const BRUSH_RANGE = [0, 100];
const LensEmbeddableComponent: EmbeddableComponent = (props) => (
<div>
<div data-test-subj="chartTimeRange">{JSON.stringify(props.timeRange)}</div>
<div data-test-subj="chartSearchSessionId">{props.searchSessionId}</div>
<div data-test-subj="lensAttributes">
{JSON.stringify((props as LensByValueInput).attributes)}
</div>
<button
data-test-subj="brushEnd"
onClick={() =>
props.onBrushEnd?.({
table: {} as Datatable,
range: BRUSH_RANGE,
column: 0,
preventDefault: jest.fn(),
})
}
/>
</div>
);
const getEmbeddableTimeRange = () => {
const serialized = screen.getByTestId('chartTimeRange').textContent;
return serialized ? JSON.parse(serialized) : null;
};
const getEmbeddableSearchSessionId = () => {
return screen.getByTestId('chartSearchSessionId').textContent;
};
const getLensAttributes = () => {
const serialized = screen.queryByTestId('lensAttributes')?.textContent;
return serialized ? JSON.parse(serialized) : null;
};
let rerender: (ui: React.ReactElement<any, string | React.JSXElementConstructor<any>>) => void;
const defaultProps: Parameters<typeof GroupPreview>[0] = {
group,
dataViews: [
{
id: 'some-id',
title: 'My Data View',
timeFieldName: '@timestamp',
fields: {
getByType: jest.fn<DataViewField[], []>(() => [
{
type: 'date',
name: '@timestamp',
} as DataViewField,
{
type: 'date',
name: 'other-time-field',
} as DataViewField,
]),
} as unknown as IIndexPatternFieldList & { toSpec: () => DataViewFieldMap },
} as DataView,
{
id: 'a-different-id',
title: 'My Data View',
timeFieldName: 'other-time-field',
fields: {
getByType: jest.fn<DataViewField[], []>(() => [
{
type: 'date',
name: '@timestamp',
} as DataViewField,
{
type: 'date',
name: 'other-time-field',
} as DataViewField,
]),
} as unknown as IIndexPatternFieldList & { toSpec: () => DataViewFieldMap },
} as DataView,
],
LensEmbeddableComponent,
searchSessionId: 'some-search-session-id',
refreshSearchSession: jest.fn(),
timePickerQuickRanges: [{ from: 'now/d', to: 'now/d', display: 'Today' }],
};
beforeEach(() => {
const renderResult = render(
<I18nProvider>
<GroupPreview {...defaultProps} />
</I18nProvider>
);
rerender = renderResult.rerender;
});
it('updates the chart time range', async () => {
// default
expect(EuiSuperDatePickerTestHarness.currentCommonlyUsedRange).toBe('Last 15 minutes');
expect(getEmbeddableTimeRange()).toEqual({ from: 'now-15m', to: 'now' });
// from time picker
await EuiSuperDatePickerTestHarness.selectCommonlyUsedRange('Today');
expect(EuiSuperDatePickerTestHarness.currentCommonlyUsedRange).toBe('Today');
expect(getEmbeddableTimeRange()).toEqual({ from: 'now/d', to: 'now/d' });
// from chart brush
userEvent.click(screen.getByTestId('brushEnd'));
const format = 'MMM D, YYYY @ HH:mm:ss.SSS'; // from https://github.com/elastic/eui/blob/6a30eba7c2a154691c96a1d17c8b2f3506d351a3/src/components/date_picker/super_date_picker/super_date_picker.tsx#L222;
expect(EuiSuperDatePickerTestHarness.currentRange).toEqual({
from: moment(BRUSH_RANGE[0]).format(format),
to: moment(BRUSH_RANGE[1]).format(format),
});
expect(getEmbeddableTimeRange()).toEqual({
from: new Date(BRUSH_RANGE[0]).toISOString(),
to: new Date(BRUSH_RANGE[1]).toISOString(),
});
});
it('updates the time field', async () => {
EuiSuperDatePickerTestHarness.togglePopover();
const select = screen.getByRole('combobox', { name: 'Time field' });
expect(select).toHaveValue('@timestamp');
expect(getCurrentTimeField(getLensAttributes())).toBe('@timestamp');
userEvent.selectOptions(select, 'other-time-field');
expect(select).toHaveValue('other-time-field');
await waitFor(() => {
expect(getCurrentTimeField(getLensAttributes())).toBe('other-time-field');
});
});
it('refreshes the chart data', () => {
expect(defaultProps.refreshSearchSession).not.toHaveBeenCalled();
expect(getEmbeddableSearchSessionId()).toBe(defaultProps.searchSessionId);
EuiSuperDatePickerTestHarness.refresh();
expect(defaultProps.refreshSearchSession).toHaveBeenCalled();
rerender(
<I18nProvider>
<GroupPreview {...defaultProps} searchSessionId="new-search-session-id" />
</I18nProvider>
);
expect(getEmbeddableSearchSessionId()).toBe('new-search-session-id');
});
describe('data views', () => {
const assertDataView = (id: string, attributes: TypedLensByValueInput['attributes']) =>
expect(attributes.references[0].id).toBe(id);
const assertTimeField = (fieldName: string, attributes: TypedLensByValueInput['attributes']) =>
expect(
(
attributes.state.datasourceStates.formBased.layers[DATA_LAYER_ID].columns[
DATE_HISTOGRAM_COLUMN_ID
] as FieldBasedIndexPatternColumn
).sourceField
).toBe(fieldName);
it('uses correct data view', async () => {
assertDataView(group.indexPatternId, getLensAttributes());
rerender(
<I18nProvider>
<GroupPreview {...defaultProps} group={{ ...group, indexPatternId: 'a-different-id' }} />
</I18nProvider>
);
await waitFor(() => {
assertDataView('a-different-id', getLensAttributes());
assertTimeField('other-time-field', getLensAttributes());
});
});
it('supports ad-hoc data view', () => {
const adHocDataView = {
id: 'adhoc-1',
title: 'my-pattern*',
timeFieldName: '@timestamp',
sourceFilters: [],
fieldFormats: {},
runtimeFieldMap: {},
fieldAttrs: {},
allowNoIndex: false,
name: 'My ad-hoc data view',
};
rerender(
<I18nProvider>
<GroupPreview
{...defaultProps}
group={{ ...group, indexPatternId: '', dataViewSpec: adHocDataView }}
/>
</I18nProvider>
);
waitFor(() => {
const attributes = getLensAttributes();
expect(attributes.references).toHaveLength(0);
expect(attributes.state.adHocDataViews![adHocDataView.id!]).toEqual(adHocDataView);
expect(attributes.state.internalReferences).toHaveLength(1);
expect(attributes.state.internalReferences![0].id).toBe(adHocDataView.id);
});
});
it('handles missing data view', async () => {
rerender(
<I18nProvider>
<GroupPreview {...defaultProps} group={{ ...group, indexPatternId: 'doesnt-exist' }} />
</I18nProvider>
);
await waitFor(() => {
expect(
screen.getByRole('heading', { name: 'Select a valid data view' })
).toBeInTheDocument();
});
expect(getLensAttributes()).toBeNull(); // chart shouldn't be rendered
});
});
});

View file

@ -0,0 +1,260 @@
/*
* 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 {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiSelect,
EuiSuperDatePicker,
EuiSuperDatePickerProps,
EuiTitle,
} from '@elastic/eui';
import { TimeRange } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import type {
EmbeddableComponent as LensEmbeddableComponent,
TypedLensByValueInput,
} from '@kbn/lens-plugin/public';
import { EventAnnotationGroupConfig } from '@kbn/event-annotation-common';
import { css } from '@emotion/react';
import { DataView } from '@kbn/data-views-plugin/common';
import { i18n } from '@kbn/i18n';
import { getLensAttributes } from './lens_attributes';
export const GroupPreview = ({
group,
dataViews,
LensEmbeddableComponent,
searchSessionId,
refreshSearchSession,
timePickerQuickRanges,
}: {
group: EventAnnotationGroupConfig;
dataViews: DataView[];
LensEmbeddableComponent: LensEmbeddableComponent;
searchSessionId: string;
refreshSearchSession: () => void;
timePickerQuickRanges: Array<{ from: string; to: string; display: string }> | undefined;
}) => {
const [chartTimeRange, setChartTimeRange] = useState<TimeRange>({ from: 'now-15m', to: 'now' });
const commonlyUsedRanges = useMemo(
() =>
timePickerQuickRanges?.map(
({ from, to, display }: { from: string; to: string; display: string }) => {
return {
start: from,
end: to,
label: display,
};
}
) ?? [],
[timePickerQuickRanges]
);
const customQuickSelectRender = useCallback<
Required<EuiSuperDatePickerProps>['customQuickSelectRender']
>(
({ quickSelect, commonlyUsedRanges: ranges, customQuickSelectPanels }) =>
(
<>
{customQuickSelectPanels}
{quickSelect}
{ranges}
</>
) as React.ReactNode,
[]
);
const currentDataView = useMemo(
() => dataViews.find((dataView) => dataView.id === group.indexPatternId),
[dataViews, group.indexPatternId]
);
const timeFieldNames = useMemo(
() => currentDataView?.fields.getByType('date').map((field) => field.name) ?? [],
[currentDataView?.fields]
);
// We can assume that there is at least one time field because we don't allow annotation groups to be created without one
const defaultTimeFieldName = useMemo(
() => currentDataView?.timeFieldName ?? timeFieldNames[0],
[currentDataView?.timeFieldName, timeFieldNames]
);
const [currentTimeFieldName, setCurrentTimeFieldName] = useState<string>(defaultTimeFieldName);
const [lensAttributes, setLensAttributes] = useState<TypedLensByValueInput['attributes']>(
getLensAttributes(group, currentTimeFieldName)
);
// we don't use currentDataView directly to hide/show the missing prompt because we want to delay
// the embeddable render until the lensAttributes have been updated in useDebounce
// in the case that the user selects a new data view
const [showMissingDataViewPrompt, setShowMissingDataViewPrompt] = useState<boolean>(
!currentDataView
);
useEffect(() => {
setCurrentTimeFieldName(defaultTimeFieldName);
}, [defaultTimeFieldName]);
useDebounce(
() => {
setLensAttributes(getLensAttributes(group, currentTimeFieldName));
setShowMissingDataViewPrompt(!currentDataView);
},
250,
[group, currentTimeFieldName]
);
return (
<>
<EuiFlyoutHeader>
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
<EuiFlexItem>
<EuiTitle size="xs">
<h3
css={css`
white-space: nowrap;
`}
>
<FormattedMessage
id="eventAnnotationListing.groupPreview.preview"
defaultMessage="Preview"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
css={css`
width: 336px;
margin-top: -5px;
margin-bottom: -5px;
`}
grow={false}
>
<EuiSuperDatePicker
onTimeChange={({ start: from, end: to }) => setChartTimeRange({ from, to })}
onRefresh={({ start: from, end: to }) => {
setChartTimeRange({ from, to });
refreshSearchSession();
}}
start={chartTimeRange.from}
end={chartTimeRange.to}
compressed
commonlyUsedRanges={commonlyUsedRanges}
updateButtonProps={{
iconOnly: true,
fill: false,
}}
customQuickSelectRender={customQuickSelectRender}
customQuickSelectPanels={[
{
title: i18n.translate('eventAnnotationListing.timeField', {
defaultMessage: 'Time field',
}),
content: (
<EuiSelect
aria-label={i18n.translate('eventAnnotationListing.timeField', {
defaultMessage: 'Time field',
})}
options={timeFieldNames.map((name) => ({
text: name,
}))}
value={currentTimeFieldName}
onChange={(e) => setCurrentTimeFieldName(e.target.value)}
/>
),
},
]}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody
css={css`
.euiFlyoutBody__overflowContent {
height: 100%;
}
`}
>
{!showMissingDataViewPrompt ? (
<EuiFlexGroup
css={css`
height: 100%;
`}
direction="column"
justifyContent="center"
>
<EuiFlexItem grow={0}>
<div
css={css`
& > div {
height: 400px;
width: 100%;
}
`}
>
<LensEmbeddableComponent
data-test-subj="chart"
id="annotation-library-preview"
timeRange={chartTimeRange}
attributes={lensAttributes}
onBrushEnd={({ range }) =>
setChartTimeRange({
from: new Date(range[0]).toISOString(),
to: new Date(range[1]).toISOString(),
})
}
searchSessionId={searchSessionId}
/>
</div>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiFlexGroup
css={css`
height: 100%;
`}
direction="column"
justifyContent="center"
>
<EuiFlexItem>
<EuiEmptyPrompt
iconType="error"
color="danger"
data-test-subj="missingDataViewPrompt"
title={
<h4>
<FormattedMessage
id="eventAnnotationListing.groupPreview.missingDataViewTitle"
defaultMessage="Select a valid data view"
/>
</h4>
}
body={
<p>
<FormattedMessage
id="eventAnnotationListing.groupPreview.missingDataViewDescription"
defaultMessage="The previously selected data view no longer exists. Please select a valid data view in order to preview and use this annotation group."
/>
</p>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlyoutBody>
</>
);
};

View file

@ -0,0 +1,9 @@
/*
* 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 { GroupEditorFlyout } from './group_editor_flyout';

View file

@ -0,0 +1,153 @@
/*
* 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 { EventAnnotationGroupConfig } from '@kbn/event-annotation-common';
import { FieldBasedIndexPatternColumn, TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { XYPersistedByValueAnnotationLayerConfig } from '@kbn/lens-plugin/public/async_services';
export const DATA_LAYER_ID = 'data-layer-id';
export const DATE_HISTOGRAM_COLUMN_ID = 'date-histogram-column-id';
const ANNOTATION_LAYER_ID = 'annotation-layer-id';
export const getLensAttributes = (group: EventAnnotationGroupConfig, timeField: string) =>
({
title: 'Line visualization with annotation layer', // TODO - should this be translated?
description: '',
visualizationType: 'lnsXY',
type: 'lens',
state: {
visualization: {
legend: {
isVisible: true,
position: 'right',
},
valueLabels: 'hide',
fittingFunction: 'None',
axisTitlesVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
labelsOrientation: {
x: 0,
yLeft: 0,
yRight: 0,
},
gridlinesVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
preferredSeriesType: 'line',
layers: [
{
layerId: DATA_LAYER_ID,
accessors: ['a7264a99-cd42-4b3f-855f-05364df71a71'],
position: 'top',
seriesType: 'line',
showGridlines: false,
layerType: 'data',
xAccessor: [DATE_HISTOGRAM_COLUMN_ID],
},
{
layerId: ANNOTATION_LAYER_ID,
layerType: 'annotations',
persistanceType: 'byValue',
...group,
} as XYPersistedByValueAnnotationLayerConfig,
],
},
query: {
query: '',
language: 'kuery',
},
filters: [],
datasourceStates: {
formBased: {
layers: {
[DATA_LAYER_ID]: {
columns: {
[DATE_HISTOGRAM_COLUMN_ID]: {
label: 'timestamp',
dataType: 'date',
operationType: 'date_histogram',
sourceField: timeField,
isBucketed: true,
scale: 'interval',
params: {
interval: 'auto',
includeEmptyRows: true,
dropPartials: false,
},
} as FieldBasedIndexPatternColumn,
'a7264a99-cd42-4b3f-855f-05364df71a71': {
label: 'Count of records',
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
params: {
emptyAsNull: true,
},
} as FieldBasedIndexPatternColumn,
},
columnOrder: [DATE_HISTOGRAM_COLUMN_ID, 'a7264a99-cd42-4b3f-855f-05364df71a71'],
incompleteColumns: {},
sampling: 1,
},
},
},
textBased: {
layers: {},
},
},
...(group.dataViewSpec
? {
internalReferences: [
{
type: 'index-pattern',
id: group.dataViewSpec.id!,
name: `indexpattern-datasource-layer-${DATA_LAYER_ID}`,
},
{
type: 'index-pattern',
id: group.dataViewSpec.id!,
name: `xy-visualization-layer-${ANNOTATION_LAYER_ID}`,
},
],
adHocDataViews: {
[group.dataViewSpec.id!]: group.dataViewSpec,
},
}
: { internalReferences: [], adHocDataViews: {} }),
},
references: group.dataViewSpec
? []
: [
{
type: 'index-pattern',
id: group.indexPatternId,
name: `indexpattern-datasource-layer-${DATA_LAYER_ID}`,
},
],
} as TypedLensByValueInput['attributes']);
export const getCurrentTimeField = (attributes: TypedLensByValueInput['attributes']) => {
return (
attributes.state.datasourceStates.formBased.layers[DATA_LAYER_ID].columns[
DATE_HISTOGRAM_COLUMN_ID
] as FieldBasedIndexPatternColumn
).sourceField;
};

View file

@ -16,7 +16,7 @@ import {
TableListViewTable,
type UserContentCommonSchema,
} from '@kbn/content-management-table-list-view-table';
import type { EventAnnotationServiceType } from '../types';
import type { EventAnnotationServiceType } from '@kbn/event-annotation-components/types';
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { shallow, ShallowWrapper } from 'enzyme';
import {
@ -31,6 +31,7 @@ import { DataView } from '@kbn/data-views-plugin/common';
import { QueryInputServices } from '@kbn/visualization-ui-components';
import { toastsServiceMock } from '@kbn/core-notifications-browser-mocks/src/toasts_service.mock';
import { IToasts } from '@kbn/core-notifications-browser';
import { ISessionService } from '@kbn/data-plugin/public';
describe('annotation list view', () => {
const adHocDVId = 'ad-hoc';
@ -51,6 +52,7 @@ describe('annotation list view', () => {
let wrapper: ShallowWrapper<typeof EventAnnotationGroupTableList>;
let mockEventAnnotationService: EventAnnotationServiceType;
let mockToasts: IToasts;
const searchSessionStartMethod = jest.fn<string, []>(() => 'some-session-id');
beforeEach(() => {
mockEventAnnotationService = {
@ -95,6 +97,10 @@ describe('annotation list view', () => {
queryInputServices={{} as QueryInputServices}
toasts={mockToasts}
navigateToLens={() => {}}
LensEmbeddableComponent={() => <div />}
sessionService={
{ start: searchSessionStartMethod } as Partial<ISessionService> as ISessionService
}
/>
);
});
@ -190,6 +196,7 @@ describe('annotation list view', () => {
expect(mockEventAnnotationService.loadAnnotationGroup).toHaveBeenCalledWith('1234');
expect(wrapper.find(GroupEditorFlyout).exists()).toBeTruthy();
expect(wrapper.find(GroupEditorFlyout).prop('searchSessionId')).toBe('some-session-id');
const updatedGroup = { ...group, tags: ['my-new-tag'] };
@ -210,6 +217,24 @@ describe('annotation list view', () => {
); // (should refresh list)
});
it('refreshes the search session', async () => {
act(() => {
wrapper.find(TableListViewTable).prop('editItem')!({
id: '1234',
} as UserContentCommonSchema);
});
// wait one tick to give promise time to settle
await new Promise((resolve) => setTimeout(resolve, 0));
expect(wrapper.find(GroupEditorFlyout).prop('searchSessionId')).toBe('some-session-id');
searchSessionStartMethod.mockReturnValue('new-session-id');
wrapper.find(GroupEditorFlyout).prop('refreshSearchSession')();
expect(wrapper.find(GroupEditorFlyout).prop('searchSessionId')).toBe('new-session-id');
});
it('opens editor when title is clicked', async () => {
act(() => {
wrapper.find(TableListViewTable).prop('onClickTitle')!({

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { TableListViewTable } from '@kbn/content-management-table-list-view-table';
import type { TableListTabParentProps } from '@kbn/content-management-tabbed-table-list-view';
import { i18n } from '@kbn/i18n';
@ -16,13 +16,16 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug
import { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
import type { QueryInputServices } from '@kbn/visualization-ui-components';
import { IToasts } from '@kbn/core-notifications-browser';
import { EuiButton, EuiEmptyPrompt, EuiTitle } from '@elastic/eui';
import { EuiButton, EuiEmptyPrompt, EuiIcon, EuiText, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { EmbeddableComponent as LensEmbeddableComponent } from '@kbn/lens-plugin/public';
import type {
EventAnnotationGroupConfig,
EventAnnotationGroupContent,
} from '@kbn/event-annotation-common';
import type { EventAnnotationServiceType } from '../types';
import { ISessionService, UI_SETTINGS } from '@kbn/data-plugin/public';
import { EventAnnotationServiceType } from '@kbn/event-annotation-components';
import { css } from '@emotion/react';
import { GroupEditorFlyout } from './group_editor_flyout';
export const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit';
@ -35,7 +38,7 @@ const getCustomColumn = (dataViews: DataView[]) => {
return {
field: 'dataView',
name: i18n.translate('eventAnnotationComponents.tableList.dataView', {
name: i18n.translate('eventAnnotationListing.tableList.dataView', {
defaultMessage: 'Data view',
}),
sortable: false,
@ -44,7 +47,24 @@ const getCustomColumn = (dataViews: DataView[]) => {
<div>
{record.attributes.dataViewSpec
? record.attributes.dataViewSpec.name
: dataViewNameMap[record.attributes.indexPatternId]}
: dataViewNameMap[record.attributes.indexPatternId] ?? (
<EuiText size="s" color={'danger'}>
<FormattedMessage
id="eventAnnotationListing.tableList.dataView.missing"
defaultMessage="{errorIcon} No longer exists"
values={{
errorIcon: (
<EuiIcon
type="error"
css={css`
margin-top: -3px;
`}
/>
),
}}
/>
</EuiText>
)}
</div>
),
};
@ -53,6 +73,7 @@ const getCustomColumn = (dataViews: DataView[]) => {
export const EventAnnotationGroupTableList = ({
uiSettings,
eventAnnotationService,
sessionService,
visualizeCapabilities,
savedObjectsTagging,
parentProps,
@ -61,9 +82,11 @@ export const EventAnnotationGroupTableList = ({
queryInputServices,
toasts,
navigateToLens,
LensEmbeddableComponent,
}: {
uiSettings: IUiSettingsClient;
eventAnnotationService: EventAnnotationServiceType;
sessionService: ISessionService;
visualizeCapabilities: Record<string, boolean | Record<string, boolean>>;
savedObjectsTagging: SavedObjectsTaggingApi;
parentProps: TableListTabParentProps;
@ -72,10 +95,23 @@ export const EventAnnotationGroupTableList = ({
queryInputServices: QueryInputServices;
toasts: IToasts;
navigateToLens: () => void;
LensEmbeddableComponent: LensEmbeddableComponent;
}) => {
const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING);
const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING);
const [searchSessionId, setSearchSessionId] = useState<string>(sessionService.start());
const refreshSearchSession = useCallback(() => {
setSearchSessionId(sessionService.start());
}, [sessionService]);
useEffect(() => {
return () => {
sessionService.clear();
};
}, [sessionService]);
const [refreshListBouncer, setRefreshListBouncer] = useState(false);
const refreshList = useCallback(() => {
@ -138,15 +174,20 @@ export const EventAnnotationGroupTableList = ({
savedObjectsTagging={savedObjectsTagging}
dataViews={dataViews}
createDataView={createDataView}
LensEmbeddableComponent={LensEmbeddableComponent}
queryInputServices={queryInputServices}
searchSessionId={searchSessionId}
refreshSearchSession={refreshSearchSession}
timePickerQuickRanges={uiSettings.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES)}
/>
) : undefined;
return (
<>
<TableListViewTable<EventAnnotationGroupContent>
id="eventAnnotation"
refreshListBouncer={refreshListBouncer}
tableCaption={i18n.translate('eventAnnotationComponents.tableList.listTitle', {
tableCaption={i18n.translate('eventAnnotationListing.tableList.listTitle', {
defaultMessage: 'Annotation Library',
})}
findItems={fetchItems}
@ -162,11 +203,12 @@ export const EventAnnotationGroupTableList = ({
customTableColumn={getCustomColumn(dataViews)}
emptyPrompt={
<EuiEmptyPrompt
color="transparent"
title={
<EuiTitle>
<h2>
<FormattedMessage
id="eventAnnotationComponents.tableList.emptyPrompt.title"
id="eventAnnotationListing.tableList.emptyPrompt.title"
defaultMessage="Create your first annotation in Lens"
/>
</h2>
@ -175,27 +217,26 @@ export const EventAnnotationGroupTableList = ({
body={
<p>
<FormattedMessage
id="eventAnnotationComponents.tableList.emptyPrompt.body"
defaultMessage="You can create and save annotations for use across multiple visualization in the
Lens visualization editor."
id="eventAnnotationListing.tableList.emptyPrompt.body"
defaultMessage="You can create and save annotations for use across multiple visualizations in the Lens editor."
/>
</p>
}
actions={
<EuiButton onClick={navigateToLens}>
<FormattedMessage
id="eventAnnotationComponents.tableList.emptyPrompt.cta"
defaultMessage="Create new annotation in Lens"
id="eventAnnotationListing.tableList.emptyPrompt.cta"
defaultMessage="Create annotation in Lens"
/>
</EuiButton>
}
iconType="flag"
/>
}
entityName={i18n.translate('eventAnnotationComponents.tableList.entityName', {
entityName={i18n.translate('eventAnnotationListing.tableList.entityName', {
defaultMessage: 'annotation group',
})}
entityNamePlural={i18n.translate('eventAnnotationComponents.tableList.entityNamePlural', {
entityNamePlural={i18n.translate('eventAnnotationListing.tableList.entityNamePlural', {
defaultMessage: 'annotation groups',
})}
onClickTitle={editItem}

View file

@ -0,0 +1,9 @@
/*
* 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 EVENT_ANNOTATION_APP_NAME = 'annotations';

View file

@ -16,8 +16,10 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
import type { QueryInputServices } from '@kbn/visualization-ui-components';
import { RootDragDropProvider } from '@kbn/dom-drag-drop';
import { EventAnnotationGroupTableList } from '@kbn/event-annotation-components';
import type { EventAnnotationServiceType } from '.';
import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public';
import type { EmbeddableComponent as LensEmbeddableComponent } from '@kbn/lens-plugin/public';
import { ISessionService } from '@kbn/data-plugin/public';
import { EventAnnotationGroupTableList } from './components/table_list';
export interface EventAnnotationListingPageServices {
core: CoreStart;
@ -27,6 +29,8 @@ export interface EventAnnotationListingPageServices {
dataViews: DataView[];
createDataView: (spec: DataViewSpec) => Promise<DataView>;
queryInputServices: QueryInputServices;
LensEmbeddableComponent: LensEmbeddableComponent;
sessionService: ISessionService;
}
export const getTableList = (
@ -54,6 +58,8 @@ export const getTableList = (
createDataView={services.createDataView}
queryInputServices={services.queryInputServices}
navigateToLens={() => services.core.application.navigateToApp('lens')}
LensEmbeddableComponent={services.LensEmbeddableComponent}
sessionService={services.sessionService}
/>
</TableListViewKibanaProvider>
</RootDragDropProvider>

View file

@ -0,0 +1,26 @@
/*
* 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 { EventAnnotationListingPlugin } from './plugin';
export const plugin = () => new EventAnnotationListingPlugin();
export type {
EventAnnotationListingPluginSetup as eventAnnotationListingPluginSetup,
EventAnnotationListingPluginStart as eventAnnotationListingPluginStart,
} from './plugin';
export {
defaultAnnotationColor,
defaultAnnotationRangeColor,
isRangeAnnotationConfig,
isManualPointAnnotationConfig,
isQueryAnnotationConfig,
} from '@kbn/event-annotation-common';
export {
AnnotationEditorControls,
annotationsIconSet,
getAnnotationAccessor,
} from '@kbn/event-annotation-components';

View file

@ -0,0 +1,101 @@
/*
* 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 { Plugin, CoreSetup, CoreStart } from '@kbn/core/public';
import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public';
import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public/types';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { VisualizationsSetup } from '@kbn/visualizations-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { i18n } from '@kbn/i18n';
import { EventAnnotationPluginStart } from '@kbn/event-annotation-plugin/public';
import type { LensPublicStart } from '@kbn/lens-plugin/public';
import type { EventAnnotationListingPageServices } from './get_table_list';
export interface EventAnnotationListingStartDependencies {
savedObjectsManagement: SavedObjectsManagementPluginStart;
eventAnnotation: EventAnnotationPluginStart;
data: DataPublicPluginStart;
savedObjectsTagging: SavedObjectTaggingPluginStart;
presentationUtil: PresentationUtilPluginStart;
dataViews: DataViewsPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
contentManagement: ContentManagementPublicStart;
lens: LensPublicStart;
}
interface SetupDependencies {
visualizations: VisualizationsSetup;
}
/** @public */
export type EventAnnotationListingPluginStart = void;
export type EventAnnotationListingPluginSetup = void;
/** @public */
export class EventAnnotationListingPlugin
implements
Plugin<
EventAnnotationListingPluginSetup,
EventAnnotationListingPluginStart,
SetupDependencies,
EventAnnotationListingStartDependencies
>
{
public setup(
core: CoreSetup<EventAnnotationListingStartDependencies>,
dependencies: SetupDependencies
) {
dependencies.visualizations.listingViewRegistry.add({
title: i18n.translate('eventAnnotationListing.listingViewTitle', {
defaultMessage: 'Annotation groups',
}),
id: 'annotations',
getTableList: async (props) => {
const [coreStart, pluginsStart] = await core.getStartServices();
const eventAnnotationService = await pluginsStart.eventAnnotation.getService();
const ids = await pluginsStart.dataViews.getIds();
const dataViews = await Promise.all(ids.map((id) => pluginsStart.dataViews.get(id)));
const services: EventAnnotationListingPageServices = {
core: coreStart,
LensEmbeddableComponent: pluginsStart.lens.EmbeddableComponent,
savedObjectsTagging: pluginsStart.savedObjectsTagging,
eventAnnotationService,
PresentationUtilContextProvider: pluginsStart.presentationUtil.ContextProvider,
dataViews,
createDataView: pluginsStart.dataViews.create.bind(pluginsStart.dataViews),
sessionService: pluginsStart.data.search.session,
queryInputServices: {
http: coreStart.http,
docLinks: coreStart.docLinks,
notifications: coreStart.notifications,
uiSettings: coreStart.uiSettings,
dataViews: pluginsStart.dataViews,
unifiedSearch: pluginsStart.unifiedSearch,
data: pluginsStart.data,
storage: new Storage(localStorage),
},
};
const { getTableList } = await import('./get_table_list');
return getTableList(props, services);
},
});
}
public start(core: CoreStart, plugins: object): void {
// nothing to do here
}
}

View file

@ -0,0 +1,48 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
},
"include": [
"common/**/*",
"public/**/*",
"server/**/*"
],
"kbn_references": [
"@kbn/core",
"@kbn/data-plugin",
"@kbn/i18n",
"@kbn/data-views-plugin",
"@kbn/saved-objects-management-plugin",
"@kbn/saved-objects-tagging-plugin",
"@kbn/presentation-util-plugin",
"@kbn/visualizations-plugin",
"@kbn/data-views-plugin",
"@kbn/visualization-ui-components",
"@kbn/dom-drag-drop",
"@kbn/i18n-react",
"@kbn/saved-objects-tagging-oss-plugin",
"@kbn/kibana-react-plugin",
"@kbn/core-lifecycle-browser",
"@kbn/kibana-utils-plugin",
"@kbn/unified-search-plugin",
"@kbn/content-management-table-list-view-table",
"@kbn/content-management-tabbed-table-list-view",
"@kbn/content-management-plugin",
"@kbn/event-annotation-plugin",
"@kbn/event-annotation-components",
"@kbn/event-annotation-common",
"@kbn/lens-plugin",
"@kbn/ui-theme",
"@kbn/test-jest-helpers",
"@kbn/expressions-plugin",
"@kbn/es-query",
"@kbn/core-ui-settings-browser",
"@kbn/core-notifications-browser-mocks",
"@kbn/core-notifications-browser",
"@kbn/core-saved-objects-api-browser"
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,182 @@
/*
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['visualize', 'annotationEditor']);
const listingTable = getService('listingTable');
const kibanaServer = getService('kibanaServer');
const find = getService('find');
const retry = getService('retry');
const log = getService('log');
describe('annotation listing page', function () {
before(async function () {
await kibanaServer.importExport.load(
'test/functional/fixtures/kbn_archiver/annotation_listing_page_search'
);
// we need to test the missing data view scenario, so delete one of them
// (can't just omit it from the archive because Kibana won't import with broken references)
log.info(`deleting one data view to replicate missing data view scenario...`);
await kibanaServer.request({
method: 'DELETE',
path: `/api/data_views/data_view/data-view-to-delete`,
});
await PageObjects.visualize.gotoVisualizationLandingPage();
await PageObjects.visualize.selectAnnotationsTab();
});
after(async function () {
log.info(`unloading annotations and data views`);
await kibanaServer.importExport.unload(
'test/functional/fixtures/kbn_archiver/annotation_listing_page_search'
);
});
describe('search', function () {
afterEach(async function () {
await listingTable.clearSearchFilter();
});
describe('by text', () => {
it('matches on the first word', async function () {
await listingTable.searchForItemWithName('search');
await listingTable.expectItemsCount('eventAnnotation', 1);
});
it('matches the second word', async function () {
await listingTable.searchForItemWithName('for');
await listingTable.expectItemsCount('eventAnnotation', 1);
});
it('matches the second word prefix', async function () {
await listingTable.searchForItemWithName('fo');
await listingTable.expectItemsCount('eventAnnotation', 1);
});
it('does not match mid word', async function () {
await listingTable.searchForItemWithName('earc');
// custom timeout so this test moves faster
await listingTable.expectItemsCount('eventAnnotation', 0, 1000);
});
it('is case insensitive', async function () {
await listingTable.searchForItemWithName('SEARCH');
await listingTable.expectItemsCount('eventAnnotation', 1);
});
it('is using AND operator', async function () {
await listingTable.searchForItemWithName('search banana');
// custom timeout so this test moves faster
await listingTable.expectItemsCount('eventAnnotation', 0, 1000);
});
it('matches on description', async function () {
await listingTable.searchForItemWithName('i have a description');
await listingTable.expectItemsCount('eventAnnotation', 1);
});
});
describe('by tag', () => {
it('filters by tag', async () => {
await listingTable.selectFilterTags('tag');
await listingTable.expectItemsCount('eventAnnotation', 7);
});
});
});
describe('delete', function () {
it('deletes some groups', async function () {
await listingTable.deleteItem('to delete 1');
await listingTable.deleteItem('to delete 2');
await listingTable.searchForItemWithName('to delete');
await listingTable.expectItemsCount('eventAnnotation', 0, 1000);
await listingTable.clearSearchFilter();
});
});
describe('edit', function () {
it('edits group metadata', async function () {
await listingTable.clickItemLink('eventAnnotation', 'group 3');
await PageObjects.annotationEditor.editGroupMetadata({
title: 'edited title',
description: 'edited description',
});
await PageObjects.annotationEditor.saveGroup();
await listingTable.searchForItemWithName('edited title');
await listingTable.expectItemsCount('eventAnnotation', 1);
await listingTable.searchForItemWithName('edited description');
await listingTable.expectItemsCount('eventAnnotation', 1);
});
describe('individual annotations', () => {
it('edits an existing annotation', async function () {
await listingTable.clickItemLink('eventAnnotation', 'edited title');
expect(await PageObjects.annotationEditor.getAnnotationCount()).to.be(1);
await PageObjects.annotationEditor.openAnnotation();
await PageObjects.annotationEditor.configureAnnotation({
query: 'my query',
lineThickness: 5,
color: '#FF0000',
});
});
it('adds a new annotation', async function () {
await PageObjects.annotationEditor.addAnnotation({
query: 'other query',
lineThickness: 3,
color: '#00FF00',
});
retry.try(async () => {
expect(await PageObjects.annotationEditor.getAnnotationCount()).to.be(2);
});
});
it('removes an annotation', async function () {
await PageObjects.annotationEditor.removeAnnotation();
await retry.try(async () => {
expect(await PageObjects.annotationEditor.getAnnotationCount()).to.be(1);
});
await PageObjects.annotationEditor.saveGroup();
await listingTable.clearSearchFilter();
});
});
describe.skip('data view switching', () => {
it('recovers from missing data view', async () => {
await listingTable.clickItemLink('eventAnnotation', 'missing data view');
await retry.try(async () => {
expect(await PageObjects.annotationEditor.showingMissingDataViewPrompt()).to.be(true);
});
await retry.try(async () => {
await PageObjects.annotationEditor.editGroupMetadata({
dataView: 'logs*',
});
expect(await PageObjects.annotationEditor.showingMissingDataViewPrompt()).to.be(false);
expect(await find.byCssSelector('canvas')).to.be.ok();
});
await PageObjects.annotationEditor.saveGroup();
});
it('recovers from missing field in data view', () => {});
});
});
});
}

View file

@ -30,5 +30,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_visualize_listing'));
loadTestFile(require.resolve('./_add_to_dashboard.ts'));
loadTestFile(require.resolve('./_pie_chart'));
loadTestFile(require.resolve('./_annotation_listing'));
});
}

View file

@ -0,0 +1,832 @@
{
"attributes": {
"fieldAttrs": "{}",
"fieldFormatMap": "{}",
"fields": "[]",
"name": "logs*",
"runtimeFieldMap": "{}",
"sourceFilters": "[]",
"timeFieldName": "@timestamp",
"title": "logs*",
"typeMeta": "{}"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-07T17:23:20.906Z",
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"managed": false,
"references": [],
"type": "index-pattern",
"typeMigrationVersion": "8.0.0",
"updated_at": "2023-09-11T15:50:59.227Z",
"version": "WzIyNywxXQ=="
}
{
"attributes": {
"fieldAttrs": "{}",
"fieldFormatMap": "{\"hour_of_day\":{}}",
"fields": "[]",
"name": "To Delete!",
"runtimeFieldMap": "{\"hour_of_day\":{\"type\":\"long\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getHour());\"}}}",
"sourceFilters": "[]",
"timeFieldName": "timestamp",
"title": "kibana_sample_data_logs",
"typeMeta": "{}"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-07T17:23:20.906Z",
"id": "data-view-to-delete",
"managed": false,
"references": [],
"type": "index-pattern",
"typeMigrationVersion": "8.0.0",
"updated_at": "2023-09-11T15:50:59.227Z",
"version": "WzIyNywxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 19"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T14:00:20.704Z",
"id": "b6071e00-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T14:00:20.704Z",
"version": "WzMxNywxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "i have a description",
"ignoreGlobalFilters": true,
"title": "group 21"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T14:00:12.994Z",
"id": "b16eaa20-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T14:00:12.994Z",
"version": "WzMxNiwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "search for me"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T14:01:40.768Z",
"id": "e5bfc2f0-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T14:02:00.597Z",
"version": "WzMyMCwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "to delete 1"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:53.977Z",
"id": "a618e690-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T14:00:06.429Z",
"version": "WzMxNSwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "to delete 2"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:47.961Z",
"id": "a282ee90-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:47.961Z",
"version": "WzMxMiwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 16"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:42.118Z",
"id": "9f075c60-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:42.118Z",
"version": "WzMxMSwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "missing data view"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:36.447Z",
"id": "9ba608f0-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "data-view-to-delete",
"name": "event-annotation-group_dataView-ref-data-view-to-delete",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:36.447Z",
"version": "WzMxMCwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 14"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:30.997Z",
"id": "98666e50-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:30.997Z",
"version": "WzMwOSwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 13"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:25.669Z",
"id": "95397150-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:25.669Z",
"version": "WzMwOCwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 12"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:20.653Z",
"id": "923c0fd0-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:20.653Z",
"version": "WzMwNywxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 11"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:14.944Z",
"id": "8ed4f000-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:14.944Z",
"version": "WzMwNiwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 10"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:09.600Z",
"id": "8ba58200-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:09.600Z",
"version": "WzMwNSwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 9"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:59:02.338Z",
"id": "87514310-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:59:02.338Z",
"version": "WzMwNCwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 8"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:58:55.464Z",
"id": "83388680-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:58:55.464Z",
"version": "WzMwMywxXQ=="
}
{
"attributes": {
"color": "#dac7c4",
"description": "a tag to filter by",
"name": "tag"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-08T18:40:42.018Z",
"id": "36a8f020-4e77-11ee-bb97-116581699678",
"managed": false,
"references": [],
"type": "tag",
"typeMigrationVersion": "8.0.0",
"updated_at": "2023-09-08T18:40:42.018Z",
"version": "WzI3NDUsMV0="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 7"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:58:46.671Z",
"id": "7dfad1f0-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
},
{
"id": "36a8f020-4e77-11ee-bb97-116581699678",
"name": "36a8f020-4e77-11ee-bb97-116581699678",
"type": "tag"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:58:46.671Z",
"version": "WzMwMiwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 6"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:58:37.886Z",
"id": "78be55e0-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
},
{
"id": "36a8f020-4e77-11ee-bb97-116581699678",
"name": "36a8f020-4e77-11ee-bb97-116581699678",
"type": "tag"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:58:37.886Z",
"version": "WzMwMSwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 5"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:58:32.312Z",
"id": "756bcf80-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
},
{
"id": "36a8f020-4e77-11ee-bb97-116581699678",
"name": "36a8f020-4e77-11ee-bb97-116581699678",
"type": "tag"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:58:32.312Z",
"version": "WzMwMCwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 4"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:58:26.947Z",
"id": "72392d30-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
},
{
"id": "36a8f020-4e77-11ee-bb97-116581699678",
"name": "36a8f020-4e77-11ee-bb97-116581699678",
"type": "tag"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:58:26.947Z",
"version": "WzI5OSwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 3"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:58:21.136Z",
"id": "6ec27d00-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
},
{
"id": "36a8f020-4e77-11ee-bb97-116581699678",
"name": "36a8f020-4e77-11ee-bb97-116581699678",
"type": "tag"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:58:21.136Z",
"version": "WzI5OCwxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 2"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:58:15.691Z",
"id": "6b83a5b0-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
},
{
"id": "36a8f020-4e77-11ee-bb97-116581699678",
"name": "36a8f020-4e77-11ee-bb97-116581699678",
"type": "tag"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:58:15.691Z",
"version": "WzI5NywxXQ=="
}
{
"attributes": {
"annotations": [
{
"icon": "triangle",
"id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843",
"key": {
"timestamp": "2023-09-13T16:30:00.000Z",
"type": "point_in_time"
},
"label": "Event",
"type": "manual"
}
],
"dataViewSpec": null,
"description": "",
"ignoreGlobalFilters": true,
"title": "group 1"
},
"coreMigrationVersion": "8.8.0",
"created_at": "2023-09-12T13:58:07.740Z",
"id": "66c66bc0-5174-11ee-a5c4-7dce2e3293a7",
"managed": false,
"references": [
{
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
"name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247",
"type": "index-pattern"
},
{
"id": "36a8f020-4e77-11ee-bb97-116581699678",
"name": "36a8f020-4e77-11ee-bb97-116581699678",
"type": "tag"
}
],
"type": "event-annotation-group",
"updated_at": "2023-09-12T13:58:07.740Z",
"version": "WzI5NiwxXQ=="
}

View file

@ -0,0 +1,84 @@
/*
* 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 { FtrService } from '../ftr_provider_context';
export class AnnotationEditorPageObject extends FtrService {
private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly retry = this.ctx.getService('retry');
/**
* Fills out group metadata
*/
public async editGroupMetadata(metadata: {
title?: string;
description?: string;
dataView?: string;
}) {
if (metadata.title) {
await this.testSubjects.setValue('annotationGroupTitle', metadata.title);
}
if (metadata.description) {
await this.testSubjects.setValue('annotationGroupDescription', metadata.description);
}
if (metadata.dataView) {
await this.testSubjects.setValue('annotationDataViewSelection', metadata.dataView);
}
}
public async saveGroup() {
await this.testSubjects.click('saveAnnotationGroup');
}
public async getAnnotationCount() {
const triggers = await this.testSubjects.findAll('lns-dimensionTrigger');
return triggers.length;
}
public async openAnnotation() {
await this.testSubjects.click('lns-dimensionTrigger');
}
public async configureAnnotation(config: {
query: string;
lineThickness: number;
color: string;
}) {
await this.testSubjects.click('lnsXY_annotation_query');
const queryInput = await this.testSubjects.find('annotation-query-based-query-input');
await queryInput.type(config.query);
await this.testSubjects.setValue('lnsXYThickness', '' + config.lineThickness);
await this.testSubjects.setValue(
'euiColorPickerAnchor indexPattern-dimension-colorPicker',
config.color
);
await this.retry.waitFor('annotation editor UI to close', async () => {
await this.testSubjects.click('backToGroupSettings');
return !(await this.testSubjects.exists('backToGroupSettings'));
});
}
public async addAnnotation(config: { query: string; lineThickness: number; color: string }) {
await this.testSubjects.click('addAnnotation');
await this.configureAnnotation(config);
}
public async removeAnnotation() {
await this.testSubjects.click('indexPattern-dimension-remove');
}
public async showingMissingDataViewPrompt() {
return await this.testSubjects.exists('missingDataViewPrompt');
}
}

View file

@ -34,8 +34,10 @@ import { DashboardPageControls } from './dashboard_page_controls';
import { UnifiedSearchPageObject } from './unified_search_page';
import { UnifiedFieldListPageObject } from './unified_field_list';
import { FilesManagementPageObject } from './files_management';
import { AnnotationEditorPageObject } from './annotation_library_editor_page';
export const pageObjects = {
annotationEditor: AnnotationEditorPageObject,
common: CommonPageObject,
console: ConsolePageObject,
context: ContextPageObject,

View file

@ -10,7 +10,12 @@ import expect from '@kbn/expect';
import { FtrService } from '../ftr_provider_context';
type AppName = keyof typeof PREFIX_MAP;
const PREFIX_MAP = { visualize: 'vis', dashboard: 'dashboard', map: 'map' };
const PREFIX_MAP = {
visualize: 'vis',
dashboard: 'dashboard',
map: 'map',
eventAnnotation: 'eventAnnotation',
};
export class ListingTableService extends FtrService {
private readonly testSubjects = this.ctx.getService('testSubjects');
@ -186,10 +191,11 @@ export class ListingTableService extends FtrService {
/**
* Returns items count on landing page
*/
public async expectItemsCount(appName: AppName, count: number) {
public async expectItemsCount(appName: AppName, count: number, findTimeout?: number) {
await this.retry.try(async () => {
const elements = await this.find.allByCssSelector(
`[data-test-subj^="${PREFIX_MAP[appName]}ListingTitleLink"]`
`[data-test-subj^="${PREFIX_MAP[appName]}ListingTitleLink"]`,
findTimeout ?? 10000
);
expect(elements.length).to.equal(count);
});

View file

@ -722,6 +722,8 @@
"@kbn/event-annotation-common/*": ["packages/kbn-event-annotation-common/*"],
"@kbn/event-annotation-components": ["packages/kbn-event-annotation-components"],
"@kbn/event-annotation-components/*": ["packages/kbn-event-annotation-components/*"],
"@kbn/event-annotation-listing-plugin": ["src/plugins/event_annotation_listing"],
"@kbn/event-annotation-listing-plugin/*": ["src/plugins/event_annotation_listing/*"],
"@kbn/event-annotation-plugin": ["src/plugins/event_annotation"],
"@kbn/event-annotation-plugin/*": ["src/plugins/event_annotation/*"],
"@kbn/event-log-fixture-plugin": ["x-pack/test/plugin_api_integration/plugins/event_log"],

View file

@ -461,7 +461,6 @@ export class Embeddable
this.expressionRenderer = deps.expressionRenderer;
this.initializeSavedVis(initialInput)
.then(() => {
this.loadUserMessages();
this.reload();
})
.catch((e) => this.onFatalError(e));
@ -595,17 +594,6 @@ export class Embeddable
private fullAttributes: LensSavedObjectAttributes | undefined;
public getUserMessages: UserMessagesGetter = (locationId, filters) => {
return filterAndSortUserMessages(
[...this._userMessages, ...Object.values(this.additionalUserMessages)],
locationId,
filters ?? {}
);
};
private _userMessages: UserMessage[] = [];
// loads all available user messages
private loadUserMessages() {
const userMessages: UserMessage[] = [];
userMessages.push(
@ -672,8 +660,12 @@ export class Embeddable
}) ?? [])
);
this._userMessages = userMessages;
}
return filterAndSortUserMessages(
[...userMessages, ...Object.values(this.additionalUserMessages)],
locationId,
filters ?? {}
);
};
private additionalUserMessages: Record<string, UserMessage> = {};
@ -908,12 +900,7 @@ export class Embeddable
this.activeData = newActiveData;
// Refresh messanges if info type is found as with active data
// these messages can be enriched
if (this._userMessages.some(({ severity }) => severity === 'info')) {
this.loadUserMessages();
this.renderUserMessages();
}
this.renderUserMessages();
};
private onRender: ExpressionWrapperProps['onRender$'] = () => {
@ -992,6 +979,7 @@ export class Embeddable
return;
}
super.render(domNode as HTMLElement);
if (this.input.onLoad) {
this.input.onLoad(true);
}
@ -1291,7 +1279,6 @@ export class Embeddable
this.expression = ast;
this.loadUserMessages();
this.reload();
}
};

View file

@ -86,6 +86,8 @@ export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceI
showInspector?: boolean;
};
export type EmbeddableComponent = React.ComponentType<EmbeddableComponentProps>;
interface PluginsStartDependencies {
uiActions: UiActionsStart;
embeddable: EmbeddableStart;

View file

@ -9,6 +9,7 @@ import { LensPlugin } from './plugin';
export type {
EmbeddableComponentProps,
EmbeddableComponent,
TypedLensByValueInput,
} from './embeddable/embeddable_component';
export type {

View file

@ -113,10 +113,7 @@ import { visualizeDashboardVisualizePanelction } from './trigger_actions/dashboa
import type { LensEmbeddableInput } from './embeddable';
import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory';
import {
EmbeddableComponentProps,
getEmbeddableComponent,
} from './embeddable/embeddable_component';
import { EmbeddableComponent, getEmbeddableComponent } from './embeddable/embeddable_component';
import { getSaveModalComponent } from './app_plugin/shared/saved_modal_lazy';
import type { SaveModalContainerProps } from './app_plugin/save_modal_container';
@ -209,7 +206,7 @@ export interface LensPublicStart {
*
* @experimental
*/
EmbeddableComponent: React.ComponentType<EmbeddableComponentProps>;
EmbeddableComponent: EmbeddableComponent;
/**
* React component which can be used to embed a Lens Visualization Save Modal Component.
* See `x-pack/examples/embedded_lens_example` for exemplary usage.

View file

@ -7030,7 +7030,6 @@
"visualizationUiComponents.dimensionButtonIcon.colorIndicatorLabel": "Couleur de cette dimension : {hex}",
"visualizationUiComponents.queryInput.queryPlaceholderKql": "{example}",
"visualizationUiComponents.queryInput.queryPlaceholderLucene": "{example}",
"visualizationUiComponents.colorPicker.seriesColor.auto": "Auto",
"visualizationUiComponents.colorPicker.seriesColor.label": "Couleur de la série",
"visualizationUiComponents.colorPicker.tooltip.auto": "Lens choisit automatiquement des couleurs à votre place sauf si vous spécifiez une couleur personnalisée.",
"visualizationUiComponents.colorPicker.tooltip.custom": "Effacez la couleur personnalisée pour revenir au mode \"Auto\".",
@ -39430,7 +39429,6 @@
"eventAnnotation.group.args.annotationConfigs.ignoreGlobalFilters.help": "Basculer pour ignorer les filtres globaux pour l'annotation",
"eventAnnotation.group.args.annotationGroups": "Groupe d'annotations",
"eventAnnotation.group.description": "Groupe d'annotations d'événement",
"eventAnnotation.listingViewTitle": "Groupes d'annotations",
"eventAnnotation.manualAnnotation.args.color": "Couleur de la ligne",
"eventAnnotation.manualAnnotation.args.icon": "Icône facultative utilisée pour les lignes d'annotation",
"eventAnnotation.manualAnnotation.args.id": "ID de l'annotation",

View file

@ -7046,7 +7046,6 @@
"visualizationUiComponents.dimensionButtonIcon.colorIndicatorLabel": "このディメンションの色:{hex}",
"visualizationUiComponents.queryInput.queryPlaceholderKql": "{example}",
"visualizationUiComponents.queryInput.queryPlaceholderLucene": "{example}",
"visualizationUiComponents.colorPicker.seriesColor.auto": "自動",
"visualizationUiComponents.colorPicker.seriesColor.label": "系列色",
"visualizationUiComponents.colorPicker.tooltip.auto": "カスタム色を指定しない場合、Lensは自動的に色を選択します。",
"visualizationUiComponents.colorPicker.tooltip.custom": "[自動]モードに戻すには、カスタム色をオフにしてください。",
@ -39421,7 +39420,6 @@
"eventAnnotation.group.args.annotationConfigs.ignoreGlobalFilters.help": "注釈のグローバルフィルターを無視するスイッチ",
"eventAnnotation.group.args.annotationGroups": "注釈グループ",
"eventAnnotation.group.description": "イベント注釈グループ",
"eventAnnotation.listingViewTitle": "注釈グループ",
"eventAnnotation.manualAnnotation.args.color": "行の色",
"eventAnnotation.manualAnnotation.args.icon": "注釈行で使用される任意のアイコン",
"eventAnnotation.manualAnnotation.args.id": "注釈のID",

View file

@ -7045,7 +7045,6 @@
"visualizationUiComponents.dimensionButtonIcon.colorIndicatorLabel": "此维度的颜色:{hex}",
"visualizationUiComponents.queryInput.queryPlaceholderKql": "{example}",
"visualizationUiComponents.queryInput.queryPlaceholderLucene": "{example}",
"visualizationUiComponents.colorPicker.seriesColor.auto": "自动",
"visualizationUiComponents.colorPicker.seriesColor.label": "系列颜色",
"visualizationUiComponents.colorPicker.tooltip.auto": "Lens 自动为您选取颜色,除非您指定定制颜色。",
"visualizationUiComponents.colorPicker.tooltip.custom": "清除定制颜色以返回到“自动”模式。",
@ -39415,7 +39414,6 @@
"eventAnnotation.group.args.annotationConfigs.ignoreGlobalFilters.help": "进行切换以忽略标注的全局筛选",
"eventAnnotation.group.args.annotationGroups": "标注组",
"eventAnnotation.group.description": "事件标注组",
"eventAnnotation.listingViewTitle": "标注组",
"eventAnnotation.manualAnnotation.args.color": "线条的颜色",
"eventAnnotation.manualAnnotation.args.icon": "用于标注线条的可选图标",
"eventAnnotation.manualAnnotation.args.id": "标注的 ID",

View file

@ -4393,6 +4393,10 @@
version "0.0.0"
uid ""
"@kbn/event-annotation-listing-plugin@link:src/plugins/event_annotation_listing":
version "0.0.0"
uid ""
"@kbn/event-annotation-plugin@link:src/plugins/event_annotation":
version "0.0.0"
uid ""