diff --git a/packages/core/capabilities/core-capabilities-browser-mocks/src/capabilities_service.mock.ts b/packages/core/capabilities/core-capabilities-browser-mocks/src/capabilities_service.mock.ts index fa1636b57ded..0f2d12390a6a 100644 --- a/packages/core/capabilities/core-capabilities-browser-mocks/src/capabilities_service.mock.ts +++ b/packages/core/capabilities/core-capabilities-browser-mocks/src/capabilities_service.mock.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { deepFreeze } from '@kbn/std'; import type { PublicMethodsOf } from '@kbn/utility-types'; import type { CapabilitiesStart, @@ -14,11 +13,11 @@ import type { } from '@kbn/core-capabilities-browser-internal'; const createStartContractMock = (): jest.Mocked => ({ - capabilities: deepFreeze({ + capabilities: { catalogue: {}, management: {}, navLinks: {}, - }), + }, }); const createMock = (): jest.Mocked> => ({ diff --git a/packages/core/capabilities/core-capabilities-browser-mocks/tsconfig.json b/packages/core/capabilities/core-capabilities-browser-mocks/tsconfig.json index 3ef89ddcbb46..531350d61892 100644 --- a/packages/core/capabilities/core-capabilities-browser-mocks/tsconfig.json +++ b/packages/core/capabilities/core-capabilities-browser-mocks/tsconfig.json @@ -12,7 +12,6 @@ "**/*.tsx", ], "kbn_references": [ - "@kbn/std", "@kbn/utility-types", "@kbn/core-capabilities-browser-internal" ], diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 60a3e7bcd264..819647823279 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -931,6 +931,17 @@ } } }, + "event-annotation-group": { + "dynamic": false, + "properties": { + "title": { + "type": "text" + }, + "description": { + "type": "text" + } + } + }, "visualization": { "dynamic": false, "properties": { diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 422130eca560..cad710f8b64e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -39,7 +39,7 @@ pageLoadAssetSize: embeddableEnhanced: 22107 enterpriseSearch: 35741 esUiShared: 326654 - eventAnnotation: 20500 + eventAnnotation: 22000 exploratoryView: 74673 expressionError: 22127 expressionGauge: 25000 diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index cc2ef92e5d6d..8534c6da57a9 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -86,6 +86,7 @@ describe('checking migration metadata changes on all registered SO types', () => "enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d", "epm-packages": "2449bb565f987eff70b1b39578bb17e90c404c6e", "epm-packages-assets": "7a3e58efd9a14191d0d1a00b8aaed30a145fd0b1", + "event-annotation-group": "715ba867d8c68f3c9438052210ea1c30a9362582", "event_loop_delays_daily": "01b967e8e043801357503de09199dfa3853bab88", "exception-list": "4aebc4e61fb5d608cae48eaeb0977e8db21c61a4", "exception-list-agnostic": "6d3262d58eee28ac381ec9654f93126a58be6f5d", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/dot_kibana_split.test.ts index a5a10cd05e57..3eaca3a8e553 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/dot_kibana_split.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/dot_kibana_split.test.ts @@ -201,6 +201,7 @@ describe('split .kibana index into multiple system indices', () => { "enterprise_search_telemetry", "epm-packages", "epm-packages-assets", + "event-annotation-group", "event_loop_delays_daily", "exception-list", "exception-list-agnostic", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index f528d6af100e..ed6cd9713678 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -43,6 +43,7 @@ const previouslyRegisteredTypes = [ 'csp-rule-template', 'csp_rule', 'dashboard', + 'event-annotation-group', 'endpoint:user-artifact', 'endpoint:user-artifact-manifest', 'enterprise_search_telemetry', diff --git a/src/plugins/chart_expressions/expression_xy/public/plugin.ts b/src/plugins/chart_expressions/expression_xy/public/plugin.ts index 9c76a0d59395..c81837789642 100755 --- a/src/plugins/chart_expressions/expression_xy/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_xy/public/plugin.ts @@ -12,7 +12,7 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { ChartsPluginStart } from '@kbn/charts-plugin/public'; import { CoreSetup, CoreStart, IUiSettingsClient } from '@kbn/core/public'; -import { EventAnnotationPluginSetup } from '@kbn/event-annotation-plugin/public'; +import type { EventAnnotationPluginStart } from '@kbn/event-annotation-plugin/public'; import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { ExpressionXyPluginSetup, ExpressionXyPluginStart, SetupDeps } from './types'; import { @@ -37,7 +37,7 @@ export interface XYPluginStartDependencies { data: DataPublicPluginStart; fieldFormats: FieldFormatsStart; charts: ChartsPluginStart; - eventAnnotation: EventAnnotationPluginSetup; + eventAnnotation: EventAnnotationPluginStart; usageCollection?: UsageCollectionStart; } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/__snapshots__/save_modal.test.js.snap b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/__snapshots__/save_modal.test.js.snap index dbf3cb7ab824..05a8cc296400 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/__snapshots__/save_modal.test.js.snap +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/__snapshots__/save_modal.test.js.snap @@ -2,32 +2,13 @@ exports[`renders DashboardSaveModal 1`] = ` - - } - labelType="label" - > - - } showCopyOnSave={true} - showDescription={false} + showDescription={true} title="dash title" /> `; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/save_modal.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/save_modal.tsx index 2f2254e054c2..737b8eac640f 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/save_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/save_modal.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui'; +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; import { SavedObjectSaveModal } from '@kbn/saved-objects-plugin/public'; import type { DashboardSaveOptions } from '../../../types'; @@ -39,14 +39,12 @@ interface Props { } interface State { - description: string; tags: string[]; timeRestore: boolean; } export class DashboardSaveModal extends React.Component { state: State = { - description: this.props.description, timeRestore: this.props.timeRestore, tags: this.props.tags ?? [], }; @@ -57,18 +55,20 @@ export class DashboardSaveModal extends React.Component { saveDashboard = ({ newTitle, + newDescription, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate, }: { newTitle: string; + newDescription: string; newCopyOnSave: boolean; isTitleDuplicateConfirmed: boolean; onTitleDuplicate: () => void; }) => { this.props.onSave({ newTitle, - newDescription: this.state.description, + newDescription, newCopyOnSave, newTimeRestore: this.state.timeRestore, isTitleDuplicateConfirmed, @@ -77,12 +77,6 @@ export class DashboardSaveModal extends React.Component { }); }; - onDescriptionChange = (event: any) => { - this.setState({ - description: event.target.value, - }); - }; - onTimeRestoreChange = (event: any) => { this.setState({ timeRestore: event.target.checked, @@ -102,26 +96,12 @@ export class DashboardSaveModal extends React.Component { tags, }); }} + markOptional /> ) : undefined; return ( - - } - > - - - {tagSelector} { onSave={this.saveDashboard} onClose={this.props.onClose} title={this.props.title} + description={this.props.description} + showDescription showCopyOnSave={this.props.showCopyOnSave} initialCopyOnSave={this.props.showCopyOnSave} objectType={i18n.translate('dashboard.topNav.saveModal.objectType', { defaultMessage: 'dashboard', })} options={this.renderDashboardSaveOptions()} - showDescription={false} /> ); } diff --git a/src/plugins/event_annotation/common/constants.ts b/src/plugins/event_annotation/common/constants.ts index 3f3f9877b978..04255cee00c2 100644 --- a/src/plugins/event_annotation/common/constants.ts +++ b/src/plugins/event_annotation/common/constants.ts @@ -23,3 +23,5 @@ export const AvailableAnnotationIcons = { TAG: 'tag', TRIANGLE: 'triangle', } as const; + +export const EVENT_ANNOTATION_GROUP_TYPE = 'event-annotation-group'; diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index 779d0e13e781..f7a62d4f3918 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -33,4 +33,7 @@ export type { QueryPointEventAnnotationConfig, AvailableAnnotationIcon, EventAnnotationOutput, + EventAnnotationGroupAttributes, } from './types'; + +export { EVENT_ANNOTATION_GROUP_TYPE } from './constants'; diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 5f869cbee552..b3e704ef647e 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { KibanaQueryOutput } from '@kbn/data-plugin/common'; +import { DataViewSpec, KibanaQueryOutput } from '@kbn/data-plugin/common'; import { DatatableColumn } from '@kbn/expressions-plugin/common'; import { $Values } from '@kbn/utility-types'; import { AvailableAnnotationIcons } from './constants'; @@ -82,10 +82,23 @@ export type EventAnnotationConfig = | RangeEventAnnotationConfig | QueryPointEventAnnotationConfig; +export interface EventAnnotationGroupAttributes { + title: string; + description: string; + tags: string[]; + ignoreGlobalFilters: boolean; + annotations: EventAnnotationConfig[]; + dataViewSpec?: DataViewSpec; +} + export interface EventAnnotationGroupConfig { annotations: EventAnnotationConfig[]; indexPatternId: string; - ignoreGlobalFilters?: boolean; + ignoreGlobalFilters: boolean; + title: string; + description: string; + tags: string[]; + dataViewSpec?: DataViewSpec; } export type EventAnnotationArgs = diff --git a/src/plugins/event_annotation/kibana.jsonc b/src/plugins/event_annotation/kibana.jsonc index f4f2a4791cfb..b4df5edf135a 100644 --- a/src/plugins/event_annotation/kibana.jsonc +++ b/src/plugins/event_annotation/kibana.jsonc @@ -9,7 +9,12 @@ "browser": true, "requiredPlugins": [ "expressions", - "data" + "savedObjectsManagement", + "data", + ], + "requiredBundles": [ + "savedObjectsFinder", + "dataViews" ], "extraPublicDirs": [ "common" diff --git a/src/plugins/event_annotation/public/components/event_annotation_group_saved_object_finder.tsx b/src/plugins/event_annotation/public/components/event_annotation_group_saved_object_finder.tsx new file mode 100644 index 000000000000..34b1ce7eb069 --- /dev/null +++ b/src/plugins/event_annotation/public/components/event_annotation_group_saved_object_finder.tsx @@ -0,0 +1,133 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from '@kbn/core/public'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import type { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; +import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; +import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import { + EuiButton, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { EVENT_ANNOTATION_GROUP_TYPE } from '../../common'; + +export const EventAnnotationGroupSavedObjectFinder = ({ + uiSettings, + http, + savedObjectsManagement, + fixedPageSize = 10, + checkHasAnnotationGroups, + onChoose, + onCreateNew, +}: { + uiSettings: IUiSettingsClient; + http: CoreStart['http']; + savedObjectsManagement: SavedObjectsManagementPluginStart; + fixedPageSize?: number; + checkHasAnnotationGroups: () => Promise; + onChoose: (value: { + id: string; + type: string; + fullName: string; + savedObject: SavedObjectCommon; + }) => void; + onCreateNew: () => void; +}) => { + const [hasAnnotationGroups, setHasAnnotationGroups] = useState(); + + useEffect(() => { + checkHasAnnotationGroups().then(setHasAnnotationGroups); + }, [checkHasAnnotationGroups]); + + return hasAnnotationGroups === undefined ? ( + + + + + + ) : hasAnnotationGroups === false ? ( + + + + + } + body={ + +

+ +

+
+ } + actions={ + onCreateNew()} size="s"> + + + } + /> +
+ ) : ( + { + onChoose({ id, type, fullName, savedObject }); + }} + showFilter={false} + noItemsMessage={ + + } + savedObjectMetaData={savedObjectMetaData} + services={{ + uiSettings, + http, + savedObjectsManagement, + }} + /> + ); +}; + +const savedObjectMetaData = [ + { + type: EVENT_ANNOTATION_GROUP_TYPE, + getIconForSavedObject: () => 'annotation', + name: i18n.translate('eventAnnotation.eventAnnotationGroup.metadata.name', { + defaultMessage: 'Annotations Groups', + }), + includeFields: ['*'], + }, +]; diff --git a/src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap b/src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap new file mode 100644 index 000000000000..73073c0e7ea1 --- /dev/null +++ b/src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Event Annotation Service loadAnnotationGroup should properly load an annotation group with a multiple annotation 1`] = ` +Object { + "annotations": undefined, + "dataViewSpec": undefined, + "description": undefined, + "ignoreGlobalFilters": undefined, + "indexPatternId": "ipid", + "tags": undefined, + "title": "groupTitle", +} +`; diff --git a/src/plugins/event_annotation/public/event_annotation_service/index.tsx b/src/plugins/event_annotation/public/event_annotation_service/index.tsx index e967a7cb0f0a..18ef89681d62 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/index.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/index.tsx @@ -6,14 +6,28 @@ * Side Public License, v 1. */ +import { CoreStart } from '@kbn/core/public'; +import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { EventAnnotationServiceType } from './types'; export class EventAnnotationService { private eventAnnotationService?: EventAnnotationServiceType; + + private core: CoreStart; + private savedObjectsManagement: SavedObjectsManagementPluginStart; + + constructor(core: CoreStart, savedObjectsManagement: SavedObjectsManagementPluginStart) { + this.core = core; + this.savedObjectsManagement = savedObjectsManagement; + } + public async getService() { if (!this.eventAnnotationService) { const { getEventAnnotationService } = await import('./service'); - this.eventAnnotationService = getEventAnnotationService(); + this.eventAnnotationService = getEventAnnotationService( + this.core, + this.savedObjectsManagement + ); } return this.eventAnnotationService; } diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index 5df25bc69d30..c131ca288a88 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -6,14 +6,150 @@ * Side Public License, v 1. */ +import { CoreStart, SimpleSavedObject } from '@kbn/core/public'; +import { coreMock } from '@kbn/core/public/mocks'; +import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import { EventAnnotationConfig, EventAnnotationGroupAttributes } from '../../common'; import { getEventAnnotationService } from './service'; import { EventAnnotationServiceType } from './types'; +type AnnotationGroupSavedObject = SimpleSavedObject; + +const annotationGroupResolveMocks: Record = { + nonExistingGroup: { + attributes: {} as EventAnnotationGroupAttributes, + references: [], + id: 'nonExistingGroup', + error: { + error: 'Saved object not found', + statusCode: 404, + message: 'Not found', + }, + } as Partial as AnnotationGroupSavedObject, + noAnnotations: { + attributes: { + title: 'groupTitle', + description: '', + tags: [], + ignoreGlobalFilters: false, + annotations: [], + }, + type: 'event-annotation-group', + references: [ + { + id: 'ipid', + name: 'ipid', + type: 'index-pattern', + }, + ], + } as Partial as AnnotationGroupSavedObject, + multiAnnotations: { + attributes: { + title: 'groupTitle', + }, + id: 'multiAnnotations', + type: 'event-annotation-group', + references: [ + { + id: 'ipid', + name: 'ipid', + type: 'index-pattern', + }, + ], + } as Partial as AnnotationGroupSavedObject, + withAdHocDataView: { + attributes: { + title: 'groupTitle', + dataViewSpec: { + id: 'my-id', + }, + } as Partial, + id: 'multiAnnotations', + type: 'event-annotation-group', + references: [], + } as Partial as AnnotationGroupSavedObject, +}; + +const annotationResolveMocks = { + nonExistingGroup: { savedObjects: [] }, + noAnnotations: { savedObjects: [] }, + multiAnnotations: { + savedObjects: [ + { + id: 'annotation1', + attributes: { + id: 'annotation1', + type: 'manual', + key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' }, + label: 'Event', + icon: 'triangle' as const, + color: 'red', + lineStyle: 'dashed' as const, + lineWidth: 3, + } as EventAnnotationConfig, + type: 'event-annotation', + references: [ + { + id: 'multiAnnotations', + name: 'event_annotation_group_ref', + type: 'event-annotation-group', + }, + ], + }, + { + id: 'annotation2', + attributes: { + id: 'ann2', + label: 'Query based event', + icon: 'triangle', + color: 'red', + type: 'query', + timeField: 'timestamp', + key: { + type: 'point_in_time', + }, + lineStyle: 'dashed', + lineWidth: 3, + filter: { type: 'kibana_query', query: '', language: 'kuery' }, + } as EventAnnotationConfig, + type: 'event-annotation', + references: [ + { + id: 'multiAnnotations', + name: 'event_annotation_group_ref', + type: 'event-annotation-group', + }, + ], + }, + ], + }, +}; + +let core: CoreStart; + describe('Event Annotation Service', () => { let eventAnnotationService: EventAnnotationServiceType; - beforeAll(() => { - eventAnnotationService = getEventAnnotationService(); + beforeEach(() => { + core = coreMock.createStart(); + (core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + return annotationGroupResolveMocks.multiAnnotations; + }); + (core.savedObjects.client.get as jest.Mock).mockImplementation((_type, id) => { + const typedId = id as keyof typeof annotationGroupResolveMocks; + return annotationGroupResolveMocks[typedId]; + }); + (core.savedObjects.client.bulkCreate as jest.Mock).mockImplementation(() => { + return annotationResolveMocks.multiAnnotations; + }); + eventAnnotationService = getEventAnnotationService( + core, + {} as SavedObjectsManagementPluginStart + ); }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('toExpression', () => { it('should work for an empty list', () => { expect(eventAnnotationService.toExpression([])).toEqual([]); @@ -318,4 +454,190 @@ describe('Event Annotation Service', () => { } ); }); + describe('loadAnnotationGroup', () => { + it('should throw error when loading group doesnt exist', async () => { + expect(() => eventAnnotationService.loadAnnotationGroup('nonExistingGroup')).rejects + .toMatchInlineSnapshot(` + Object { + "error": "Saved object not found", + "message": "Not found", + "statusCode": 404, + } + `); + }); + it('should properly load an annotation group with no annotation', async () => { + expect(await eventAnnotationService.loadAnnotationGroup('noAnnotations')) + .toMatchInlineSnapshot(` + Object { + "annotations": Array [], + "dataViewSpec": undefined, + "description": "", + "ignoreGlobalFilters": false, + "indexPatternId": "ipid", + "tags": Array [], + "title": "groupTitle", + } + `); + }); + it('should properly load an annotation group with a multiple annotation', async () => { + expect( + await eventAnnotationService.loadAnnotationGroup('multiAnnotations') + ).toMatchSnapshot(); + }); + it('populates id if group has ad-hoc data view', async () => { + const group = await eventAnnotationService.loadAnnotationGroup('withAdHocDataView'); + + expect(group.indexPatternId).toBe(group.dataViewSpec?.id); + }); + }); + // describe.skip('deleteAnnotationGroup', () => { + // it('deletes annotation group along with annotations that reference them', async () => { + // await eventAnnotationService.deleteAnnotationGroup('multiAnnotations'); + // expect(core.savedObjects.client.bulkDelete).toHaveBeenCalledWith([ + // { id: 'multiAnnotations', type: 'event-annotation-group' }, + // { id: 'annotation1', type: 'event-annotation' }, + // { id: 'annotation2', type: 'event-annotation' }, + // ]); + // }); + // }); + describe('createAnnotationGroup', () => { + it('creates annotation group along with annotations', async () => { + const annotations = [ + annotationResolveMocks.multiAnnotations.savedObjects[0].attributes, + annotationResolveMocks.multiAnnotations.savedObjects[1].attributes, + ]; + await eventAnnotationService.createAnnotationGroup({ + title: 'newGroupTitle', + description: 'my description', + tags: ['my', 'many', 'tags'], + indexPatternId: 'ipid', + ignoreGlobalFilters: false, + annotations, + }); + expect(core.savedObjects.client.create).toHaveBeenCalledWith( + 'event-annotation-group', + { + title: 'newGroupTitle', + description: 'my description', + tags: ['my', 'many', 'tags'], + ignoreGlobalFilters: false, + dataViewSpec: null, + annotations, + }, + { + references: [ + { + id: 'ipid', + name: 'event-annotation-group_dataView-ref-ipid', + type: 'index-pattern', + }, + ], + } + ); + }); + }); + describe('updateAnnotationGroup', () => { + it('updates annotation group attributes', async () => { + await eventAnnotationService.updateAnnotationGroup( + { + title: 'newTitle', + description: '', + tags: [], + indexPatternId: 'newId', + annotations: [], + ignoreGlobalFilters: false, + }, + 'multiAnnotations' + ); + expect(core.savedObjects.client.update).toHaveBeenCalledWith( + 'event-annotation-group', + 'multiAnnotations', + { + title: 'newTitle', + description: '', + tags: [], + annotations: [], + dataViewSpec: null, + ignoreGlobalFilters: false, + }, + { + references: [ + { + id: 'newId', + name: 'event-annotation-group_dataView-ref-newId', + type: 'index-pattern', + }, + ], + } + ); + }); + }); + // describe.skip('updateAnnotations', () => { + // const upsert = [ + // { + // id: 'annotation2', + // label: 'Query based event', + // icon: 'triangle', + // color: 'red', + // type: 'query', + // timeField: 'timestamp', + // key: { + // type: 'point_in_time', + // }, + // lineStyle: 'dashed', + // lineWidth: 3, + // filter: { type: 'kibana_query', query: '', language: 'kuery' }, + // }, + // { + // id: 'annotation4', + // label: 'Query based event', + // type: 'query', + // timeField: 'timestamp', + // key: { + // type: 'point_in_time', + // }, + // filter: { type: 'kibana_query', query: '', language: 'kuery' }, + // }, + // ] as EventAnnotationConfig[]; + // it('updates annotations - deletes annotations', async () => { + // await eventAnnotationService.updateAnnotations('multiAnnotations', { + // delete: ['annotation1', 'annotation2'], + // }); + // expect(core.savedObjects.client.bulkDelete).toHaveBeenCalledWith([ + // { id: 'annotation1', type: 'event-annotation' }, + // { id: 'annotation2', type: 'event-annotation' }, + // ]); + // }); + // it('updates annotations - inserts new annotations', async () => { + // await eventAnnotationService.updateAnnotations('multiAnnotations', { upsert }); + // expect(core.savedObjects.client.bulkCreate).toHaveBeenCalledWith([ + // { + // id: 'annotation2', + // type: 'event-annotation', + // attributes: upsert[0], + // overwrite: true, + // references: [ + // { + // id: 'multiAnnotations', + // name: 'event-annotation-group-ref-annotation2', + // type: 'event-annotation-group', + // }, + // ], + // }, + // { + // id: 'annotation4', + // type: 'event-annotation', + // attributes: upsert[1], + // overwrite: true, + // references: [ + // { + // id: 'multiAnnotations', + // name: 'event-annotation-group-ref-annotation4', + // type: 'event-annotation-group', + // }, + // ], + // }, + // ]); + // }); + // }); }); diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index 1b2bdbc9611c..a5ac2e265b0f 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -6,10 +6,19 @@ * Side Public License, v 1. */ +import React from 'react'; import { partition } from 'lodash'; import { queryToAst } from '@kbn/data-plugin/common'; import { ExpressionAstExpression } from '@kbn/expressions-plugin/common'; -import { EventAnnotationConfig } from '../../common'; +import { CoreStart, SavedObjectReference, SavedObjectsClientContract } from '@kbn/core/public'; +import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; +import { + EventAnnotationConfig, + EventAnnotationGroupAttributes, + EventAnnotationGroupConfig, + EVENT_ANNOTATION_GROUP_TYPE, +} from '../../common'; import { EventAnnotationServiceType } from './types'; import { defaultAnnotationColor, @@ -18,108 +27,138 @@ import { isRangeAnnotationConfig, isQueryAnnotationConfig, } from './helpers'; +import { EventAnnotationGroupSavedObjectFinder } from '../components/event_annotation_group_saved_object_finder'; export function hasIcon(icon: string | undefined): icon is string { return icon != null && icon !== 'empty'; } -export function getEventAnnotationService(): EventAnnotationServiceType { - const annotationsToExpression = (annotations: EventAnnotationConfig[]) => { - const [queryBasedAnnotations, manualBasedAnnotations] = partition( - annotations, - isQueryAnnotationConfig +export function getEventAnnotationService( + core: CoreStart, + savedObjectsManagement: SavedObjectsManagementPluginStart +): EventAnnotationServiceType { + const client: SavedObjectsClientContract = core.savedObjects.client; + + const loadAnnotationGroup = async ( + savedObjectId: string + ): Promise => { + const savedObject = await client.get( + EVENT_ANNOTATION_GROUP_TYPE, + savedObjectId ); - const expressions = []; - - for (const annotation of manualBasedAnnotations) { - if (isRangeAnnotationConfig(annotation)) { - const { label, color, key, outside, id } = annotation; - const { timestamp: time, endTimestamp: endTime } = key; - expressions.push({ - type: 'expression' as const, - chain: [ - { - type: 'function' as const, - function: 'manual_range_event_annotation', - arguments: { - id: [id], - time: [time], - endTime: [endTime], - label: [label || defaultAnnotationLabel], - color: [color || defaultAnnotationRangeColor], - outside: [Boolean(outside)], - isHidden: [Boolean(annotation.isHidden)], - }, - }, - ], - }); - } else { - const { label, color, lineStyle, lineWidth, icon, key, textVisibility, id } = annotation; - expressions.push({ - type: 'expression' as const, - chain: [ - { - type: 'function' as const, - function: 'manual_point_event_annotation', - arguments: { - id: [id], - time: [key.timestamp], - label: [label || defaultAnnotationLabel], - color: [color || defaultAnnotationColor], - lineWidth: [lineWidth || 1], - lineStyle: [lineStyle || 'solid'], - icon: hasIcon(icon) ? [icon] : ['triangle'], - textVisibility: [textVisibility || false], - isHidden: [Boolean(annotation.isHidden)], - }, - }, - ], - }); - } + if (savedObject.error) { + throw savedObject.error; } - for (const annotation of queryBasedAnnotations) { - const { - id, - label, - color, - lineStyle, - lineWidth, - icon, - timeField, - textVisibility, - textField, - filter, - extraFields, - } = annotation; - expressions.push({ - type: 'expression' as const, - chain: [ - { - type: 'function' as const, - function: 'query_point_event_annotation', - arguments: { - id: [id], - timeField: timeField ? [timeField] : [], - label: [label || defaultAnnotationLabel], - color: [color || defaultAnnotationColor], - lineWidth: [lineWidth || 1], - lineStyle: [lineStyle || 'solid'], - icon: hasIcon(icon) ? [icon] : ['triangle'], - textVisibility: [textVisibility || false], - textField: textVisibility && textField ? [textField] : [], - filter: filter ? [queryToAst(filter)] : [], - extraFields: extraFields || [], - isHidden: [Boolean(annotation.isHidden)], - }, - }, - ], - }); - } - return expressions; + const adHocDataViewSpec = savedObject.attributes.dataViewSpec + ? DataViewPersistableStateService.inject( + savedObject.attributes.dataViewSpec, + savedObject.references + ) + : undefined; + + return { + title: savedObject.attributes.title, + description: savedObject.attributes.description, + tags: savedObject.attributes.tags, + ignoreGlobalFilters: savedObject.attributes.ignoreGlobalFilters, + indexPatternId: adHocDataViewSpec + ? adHocDataViewSpec.id! + : savedObject.references.find((ref) => ref.type === 'index-pattern')?.id!, + annotations: savedObject.attributes.annotations, + dataViewSpec: adHocDataViewSpec, + }; }; + + const extractDataViewInformation = (group: EventAnnotationGroupConfig) => { + let { dataViewSpec = null } = group; + + let references: SavedObjectReference[]; + + if (dataViewSpec) { + if (!dataViewSpec.id) + throw new Error( + 'tried to create annotation group with a data view spec that did not include an ID!' + ); + + const { state, references: refsFromDataView } = + DataViewPersistableStateService.extract(dataViewSpec); + dataViewSpec = state; + references = refsFromDataView; + } else { + references = [ + { + type: 'index-pattern', + id: group.indexPatternId, + name: `event-annotation-group_dataView-ref-${group.indexPatternId}`, + }, + ]; + } + + return { references, dataViewSpec }; + }; + + const createAnnotationGroup = async ( + group: EventAnnotationGroupConfig + ): Promise<{ id: string }> => { + const { references, dataViewSpec } = extractDataViewInformation(group); + const { title, description, tags, ignoreGlobalFilters, annotations } = group; + + const groupSavedObjectId = ( + await client.create( + EVENT_ANNOTATION_GROUP_TYPE, + { title, description, tags, ignoreGlobalFilters, annotations, dataViewSpec }, + { + references, + } + ) + ).id; + + return { id: groupSavedObjectId }; + }; + + const updateAnnotationGroup = async ( + group: EventAnnotationGroupConfig, + annotationGroupId: string + ): Promise => { + const { references, dataViewSpec } = extractDataViewInformation(group); + const { title, description, tags, ignoreGlobalFilters, annotations } = group; + + await client.update( + EVENT_ANNOTATION_GROUP_TYPE, + annotationGroupId, + { title, description, tags, ignoreGlobalFilters, annotations, dataViewSpec }, + { + references, + } + ); + }; + + const checkHasAnnotationGroups = async (): Promise => { + const response = await client.find({ + type: EVENT_ANNOTATION_GROUP_TYPE, + perPage: 0, + }); + + return response.total > 0; + }; + return { + loadAnnotationGroup, + updateAnnotationGroup, + createAnnotationGroup, + renderEventAnnotationGroupSavedObjectFinder: (props) => { + return ( + + ); + }, toExpression: annotationsToExpression, toFetchExpression: ({ interval, groups }) => { if (groups.length === 0) { @@ -177,3 +216,99 @@ export function getEventAnnotationService(): EventAnnotationServiceType { }, }; } + +const annotationsToExpression = (annotations: EventAnnotationConfig[]) => { + const [queryBasedAnnotations, manualBasedAnnotations] = partition( + annotations, + isQueryAnnotationConfig + ); + + const expressions = []; + + for (const annotation of manualBasedAnnotations) { + if (isRangeAnnotationConfig(annotation)) { + const { label, color, key, outside, id } = annotation; + const { timestamp: time, endTimestamp: endTime } = key; + expressions.push({ + type: 'expression' as const, + chain: [ + { + type: 'function' as const, + function: 'manual_range_event_annotation', + arguments: { + id: [id], + time: [time], + endTime: [endTime], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationRangeColor], + outside: [Boolean(outside)], + isHidden: [Boolean(annotation.isHidden)], + }, + }, + ], + }); + } else { + const { label, color, lineStyle, lineWidth, icon, key, textVisibility, id } = annotation; + expressions.push({ + type: 'expression' as const, + chain: [ + { + type: 'function' as const, + function: 'manual_point_event_annotation', + arguments: { + id: [id], + time: [key.timestamp], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationColor], + lineWidth: [lineWidth || 1], + lineStyle: [lineStyle || 'solid'], + icon: hasIcon(icon) ? [icon] : ['triangle'], + textVisibility: [textVisibility || false], + isHidden: [Boolean(annotation.isHidden)], + }, + }, + ], + }); + } + } + + for (const annotation of queryBasedAnnotations) { + const { + id, + label, + color, + lineStyle, + lineWidth, + icon, + timeField, + textVisibility, + textField, + filter, + extraFields, + } = annotation; + expressions.push({ + type: 'expression' as const, + chain: [ + { + type: 'function' as const, + function: 'query_point_event_annotation', + arguments: { + id: [id], + timeField: timeField ? [timeField] : [], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationColor], + lineWidth: [lineWidth || 1], + lineStyle: [lineStyle || 'solid'], + icon: hasIcon(icon) ? [icon] : ['triangle'], + textVisibility: [textVisibility || false], + textField: textVisibility && textField ? [textField] : [], + filter: filter ? [queryToAst(filter)] : [], + extraFields: extraFields || [], + isHidden: [Boolean(annotation.isHidden)], + }, + }, + ], + }); + } + return expressions; +}; diff --git a/src/plugins/event_annotation/public/event_annotation_service/types.ts b/src/plugins/event_annotation/public/event_annotation_service/types.ts index cf3d759b7a32..603cc20c34bc 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/types.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/types.ts @@ -7,12 +7,31 @@ */ import { ExpressionAstExpression } from '@kbn/expressions-plugin/common/ast'; +import type { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; import { EventAnnotationConfig, EventAnnotationGroupConfig } from '../../common'; export interface EventAnnotationServiceType { + loadAnnotationGroup: (savedObjectId: string) => Promise; + createAnnotationGroup: (group: EventAnnotationGroupConfig) => Promise<{ id: string }>; + updateAnnotationGroup: ( + group: EventAnnotationGroupConfig, + savedObjectId: string + ) => Promise; toExpression: (props: EventAnnotationConfig[]) => ExpressionAstExpression[]; toFetchExpression: (props: { interval: string; - groups: EventAnnotationGroupConfig[]; + groups: Array< + Pick + >; }) => ExpressionAstExpression[]; + renderEventAnnotationGroupSavedObjectFinder: (props: { + fixedPageSize?: number; + onChoose: (value: { + id: string; + type: string; + fullName: string; + savedObject: SavedObjectCommon; + }) => void; + onCreateNew: () => void; + }) => JSX.Element; } diff --git a/src/plugins/event_annotation/public/mocks.ts b/src/plugins/event_annotation/public/mocks.ts index e78d4e8f75de..100b5d3f1c3e 100644 --- a/src/plugins/event_annotation/public/mocks.ts +++ b/src/plugins/event_annotation/public/mocks.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ +import { coreMock } from '@kbn/core/public/mocks'; +import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { getEventAnnotationService } from './event_annotation_service/service'; // not really mocking but avoiding async loading -export const eventAnnotationServiceMock = getEventAnnotationService(); +export const eventAnnotationServiceMock = getEventAnnotationService( + coreMock.createStart(), + {} as SavedObjectsManagementPluginStart +); diff --git a/src/plugins/event_annotation/public/plugin.ts b/src/plugins/event_annotation/public/plugin.ts index 47df586e061d..4d390f308a47 100644 --- a/src/plugins/event_annotation/public/plugin.ts +++ b/src/plugins/event_annotation/public/plugin.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { Plugin, CoreSetup, CoreStart, IUiSettingsClient } from '@kbn/core/public'; +import { Plugin, CoreSetup, CoreStart } from '@kbn/core/public'; import { ExpressionsSetup } from '@kbn/expressions-plugin/public'; +import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { EventAnnotationService } from './event_annotation_service'; import { @@ -19,8 +20,8 @@ import { import { getFetchEventAnnotations } from './fetch_event_annotations'; export interface EventAnnotationStartDependencies { + savedObjectsManagement: SavedObjectsManagementPluginStart; data: DataPublicPluginStart; - uiSettings: IUiSettingsClient; } interface SetupDependencies { @@ -29,14 +30,12 @@ interface SetupDependencies { /** @public */ export type EventAnnotationPluginStart = EventAnnotationService; -export type EventAnnotationPluginSetup = EventAnnotationService; +export type EventAnnotationPluginSetup = void; /** @public */ export class EventAnnotationPlugin implements Plugin { - private readonly eventAnnotationService = new EventAnnotationService(); - public setup( core: CoreSetup, dependencies: SetupDependencies @@ -48,13 +47,12 @@ export class EventAnnotationPlugin dependencies.expressions.registerFunction( getFetchEventAnnotations({ getStartServices: core.getStartServices }) ); - return this.eventAnnotationService; } public start( core: CoreStart, startDependencies: EventAnnotationStartDependencies ): EventAnnotationService { - return this.eventAnnotationService; + return new EventAnnotationService(core, startDependencies.savedObjectsManagement); } } diff --git a/src/plugins/event_annotation/server/plugin.ts b/src/plugins/event_annotation/server/plugin.ts index 4d59f0e6800d..0ae55744016e 100644 --- a/src/plugins/event_annotation/server/plugin.ts +++ b/src/plugins/event_annotation/server/plugin.ts @@ -15,6 +15,7 @@ import { manualRangeEventAnnotation, queryPointEventAnnotation, } from '../common'; +import { setupSavedObjects } from './saved_objects'; // import { getFetchEventAnnotations } from './fetch_event_annotations'; interface SetupDependencies { @@ -33,9 +34,8 @@ export class EventAnnotationServerPlugin implements Plugin { dependencies.expressions.registerFunction(manualRangeEventAnnotation); dependencies.expressions.registerFunction(queryPointEventAnnotation); dependencies.expressions.registerFunction(eventAnnotationGroup); - // dependencies.expressions.registerFunction( - // getFetchEventAnnotations({ getStartServices: core.getStartServices }) - // ); + + setupSavedObjects(core); return {}; } diff --git a/src/plugins/event_annotation/server/saved_objects.ts b/src/plugins/event_annotation/server/saved_objects.ts new file mode 100644 index 000000000000..768def6b27f7 --- /dev/null +++ b/src/plugins/event_annotation/server/saved_objects.ts @@ -0,0 +1,50 @@ +/* + * 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 { ANALYTICS_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import { + CoreSetup, + mergeSavedObjectMigrationMaps, + SavedObjectMigrationMap, +} from '@kbn/core/server'; + +import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; +import { EVENT_ANNOTATION_GROUP_TYPE } from '../common/constants'; +import { EventAnnotationGroupAttributes } from '../common/types'; + +export function setupSavedObjects(coreSetup: CoreSetup) { + coreSetup.savedObjects.registerType({ + name: EVENT_ANNOTATION_GROUP_TYPE, + indexPattern: ANALYTICS_SAVED_OBJECT_INDEX, + hidden: false, + namespaceType: 'multiple', + management: { + icon: 'flag', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle: (obj: { attributes: EventAnnotationGroupAttributes }) => obj.attributes.title, + }, + migrations: () => { + const dataViewMigrations = DataViewPersistableStateService.getAllMigrations(); + return mergeSavedObjectMigrationMaps(eventAnnotationGroupMigrations, dataViewMigrations); + }, + mappings: { + dynamic: false, + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + }, + }, + }); +} + +const eventAnnotationGroupMigrations: SavedObjectMigrationMap = {}; diff --git a/src/plugins/event_annotation/tsconfig.json b/src/plugins/event_annotation/tsconfig.json index 0b5e98d364fe..562bf05259c4 100644 --- a/src/plugins/event_annotation/tsconfig.json +++ b/src/plugins/event_annotation/tsconfig.json @@ -19,6 +19,10 @@ "@kbn/core-ui-settings-browser", "@kbn/datemath", "@kbn/ui-theme", + "@kbn/saved-objects-finder-plugin", + "@kbn/saved-objects-management-plugin", + "@kbn/i18n-react", + "@kbn/core-saved-objects-server" ], "exclude": [ "target/**/*", diff --git a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap index 0f2367910cef..70ba2d5619d0 100644 --- a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -62,17 +62,45 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` values={Object {}} /> } + labelAppend={ + + + + } labelType="label" >
- + + } + labelAppend={ + + + + } labelType="label" >
- + + } + labelAppend={ + + + + } labelType="label" > - + + } + labelAppend={ + + + + } labelType="label" > @@ -383,7 +477,22 @@ exports[`SavedObjectSaveModal should render matching snapshot when given options - + + hasTitleDuplicate: false, isLoading: false, visualizationDescription: this.props.description ? this.props.description : '', + hasAttemptedSubmit: false, }; public render() { - const { isTitleDuplicateConfirmed, hasTitleDuplicate, title } = this.state; + const { isTitleDuplicateConfirmed, hasTitleDuplicate, title, hasAttemptedSubmit } = this.state; const duplicateWarningId = generateId(); const hasColumns = !!this.props.rightOptions; @@ -101,7 +105,10 @@ export class SavedObjectSaveModal extends React.Component data-test-subj="savedObjectTitle" value={title} onChange={this.onTitleChange} - isInvalid={(!isTitleDuplicateConfirmed && hasTitleDuplicate) || title.length === 0} + isInvalid={ + hasAttemptedSubmit && + ((!isTitleDuplicateConfirmed && hasTitleDuplicate) || title.length === 0) + } aria-describedby={this.state.hasTitleDuplicate ? duplicateWarningId : undefined} /> @@ -135,11 +142,15 @@ export class SavedObjectSaveModal extends React.Component > - + {this.props.customModalTitle ? ( + this.props.customModalTitle + ) : ( + + )} @@ -153,11 +164,15 @@ export class SavedObjectSaveModal extends React.Component )} {formBody} - {this.renderCopyOnSave()} - + + {this.renderCopyOnSave()} return ( + + + } label={ } > private onFormSubmit = (event: React.FormEvent) => { event.preventDefault(); - this.saveSavedObject(); + + const { hasAttemptedSubmit, title } = this.state; + + if (!hasAttemptedSubmit) { + this.setState({ hasAttemptedSubmit: true }); + } + + const isValid = this.props.isValid !== undefined ? this.props.isValid : true; + + if (title.length !== 0 && isValid) { + this.saveSavedObject(); + } }; private renderConfirmButton = () => { - const { isLoading, title } = this.state; + const { isLoading } = this.state; let confirmLabel: string | React.ReactNode = i18n.translate( 'savedObjects.saveModal.saveButtonLabel', @@ -269,14 +301,11 @@ export class SavedObjectSaveModal extends React.Component confirmLabel = this.props.confirmButtonLabel; } - const isValid = this.props.isValid !== undefined ? this.props.isValid : true; - return ( @@ -327,21 +356,18 @@ export class SavedObjectSaveModal extends React.Component } return ( - <> - - - } - /> - + + } + /> ); }; } diff --git a/src/plugins/saved_objects_tagging_oss/public/api.ts b/src/plugins/saved_objects_tagging_oss/public/api.ts index 54afed5d6203..50508b4f5a0c 100644 --- a/src/plugins/saved_objects_tagging_oss/public/api.ts +++ b/src/plugins/saved_objects_tagging_oss/public/api.ts @@ -268,6 +268,11 @@ export type SavedObjectSaveModalTagSelectorComponentProps = EuiComboBoxProps< * tags selection callback */ onTagsSelected: (ids: string[]) => void; + + /** + * Add "Optional" to the label + */ + markOptional?: boolean; }; /** diff --git a/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx b/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx index 250f6b40c1e5..9bc781f46408 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx +++ b/src/plugins/visualizations/public/visualize_app/utils/get_top_nav_config.tsx @@ -544,6 +544,7 @@ export const getTopNavConfig = ( onTagsSelected={(newSelection) => { selectedTags = newSelection; }} + markOptional /> ); } diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index be870ce24e12..bcfbc8caa9ce 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -16,6 +16,7 @@ interface VisualizeSaveModalArgs { redirectToOrigin?: boolean; addToDashboard?: boolean; dashboardId?: string; + description?: string; } type DashboardPickerOption = @@ -393,10 +394,20 @@ export class VisualizePageObject extends FtrService { public async setSaveModalValues( vizName: string, - { saveAsNew, redirectToOrigin, addToDashboard, dashboardId }: VisualizeSaveModalArgs = {} + { + saveAsNew, + redirectToOrigin, + addToDashboard, + dashboardId, + description, + }: VisualizeSaveModalArgs = {} ) { await this.testSubjects.setValue('savedObjectTitle', vizName); + if (description) { + await this.testSubjects.setValue('viewDescription', description); + } + const saveAsNewCheckboxExists = await this.testSubjects.exists('saveAsNewCheckbox'); if (saveAsNewCheckboxExists) { const state = saveAsNew ? 'check' : 'uncheck'; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index b4b45209aef4..58d422b05adb 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -113,6 +113,7 @@ export function App({ isLoading, isSaveable, visualization, + annotationGroups, } = useLensSelector((state) => state.lens); const selectorDependencies = useMemo( @@ -180,7 +181,9 @@ export function App({ persistedDoc, lastKnownDoc, data.query.filterManager.inject.bind(data.query.filterManager), - datasourceMap + datasourceMap, + visualizationMap, + annotationGroups ) && (isSaveable || persistedDoc) ) { @@ -209,6 +212,8 @@ export function App({ application.capabilities.visualize.save, data.query.filterManager, datasourceMap, + visualizationMap, + annotationGroups, ]); const getLegacyUrlConflictCallout = useCallback(() => { @@ -374,7 +379,14 @@ export function App({ initialDocFromContext, persistedDoc, ].map((refDoc) => - isLensEqual(refDoc, lastKnownDoc, data.query.filterManager.inject, datasourceMap) + isLensEqual( + refDoc, + lastKnownDoc, + data.query.filterManager.inject, + datasourceMap, + visualizationMap, + annotationGroups + ) ); if (initialDocFromContextUnchanged || currentDocHasBeenSavedInLens) { onAppLeave((actions) => { @@ -386,6 +398,7 @@ export function App({ } } }, [ + annotationGroups, application, data.query.filterManager.inject, datasourceMap, @@ -394,6 +407,7 @@ export function App({ lastKnownDoc, onAppLeave, persistedDoc, + visualizationMap, ]); const navigateToVizEditor = useCallback(() => { @@ -422,7 +436,6 @@ export function App({ dataViews, uiActions, core: { http, notifications, uiSettings }, - data, contextDataViewSpec: (initialContext as VisualizeFieldContext | undefined)?.dataViewSpec, updateIndexPatterns: (newIndexPatternsState, options) => { dispatch(updateIndexPatterns(newIndexPatternsState)); @@ -437,7 +450,7 @@ export function App({ } }, }), - [dataViews, uiActions, http, notifications, uiSettings, data, initialContext, dispatch] + [dataViews, uiActions, http, notifications, uiSettings, initialContext, dispatch] ); const onTextBasedSavedAndExit = useCallback(async ({ onSave, onCancel }) => { @@ -597,7 +610,9 @@ export function App({ persistedDoc, lastKnownDoc, data.query.filterManager.inject.bind(data.query.filterManager), - datasourceMap + datasourceMap, + visualizationMap, + annotationGroups ) } goBackToOriginatingApp={goBackToOriginatingApp} diff --git a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts index 8bd1e6e980bb..babde51e39f2 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts +++ b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.test.ts @@ -8,11 +8,19 @@ import { Filter, FilterStateStore } from '@kbn/es-query'; import { isLensEqual } from './lens_document_equality'; import { Document } from '../persistence/saved_object_store'; -import { Datasource, DatasourceMap } from '../types'; +import { + AnnotationGroups, + Datasource, + DatasourceMap, + Visualization, + VisualizationMap, +} from '../types'; + +const visualizationType = 'lnsSomeVis'; const defaultDoc: Document = { title: 'some-title', - visualizationType: 'lnsXY', + visualizationType, state: { query: { query: '', @@ -53,23 +61,67 @@ describe('lens document equality', () => { ); let mockDatasourceMap: DatasourceMap; + let mockVisualizationMap: VisualizationMap; + let mockAnnotationGroups: AnnotationGroups; beforeEach(() => { mockDatasourceMap = { - indexpattern: { isEqual: jest.fn(() => true) }, - } as unknown as DatasourceMap; + indexpattern: { isEqual: jest.fn(() => true) } as Partial as Datasource, + }; + + mockVisualizationMap = { + [visualizationType]: { + isEqual: jest.fn(() => true), + } as Partial as Visualization, + }; + + mockAnnotationGroups = {}; }); it('returns true when documents are equal', () => { expect( - isLensEqual(defaultDoc, defaultDoc, mockInjectFilterReferences, mockDatasourceMap) + isLensEqual( + defaultDoc, + defaultDoc, + mockInjectFilterReferences, + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups + ) ).toBeTruthy(); }); it('handles undefined documents', () => { - expect(isLensEqual(undefined, undefined, mockInjectFilterReferences, {})).toBeTruthy(); - expect(isLensEqual(undefined, {} as Document, mockInjectFilterReferences, {})).toBeFalsy(); - expect(isLensEqual({} as Document, undefined, mockInjectFilterReferences, {})).toBeFalsy(); + expect( + isLensEqual( + undefined, + undefined, + mockInjectFilterReferences, + {}, + mockVisualizationMap, + mockAnnotationGroups + ) + ).toBeTruthy(); + expect( + isLensEqual( + undefined, + {} as Document, + mockInjectFilterReferences, + {}, + mockVisualizationMap, + mockAnnotationGroups + ) + ).toBeFalsy(); + expect( + isLensEqual( + {} as Document, + undefined, + mockInjectFilterReferences, + {}, + mockVisualizationMap, + mockAnnotationGroups + ) + ).toBeFalsy(); }); it('should compare visualization type', () => { @@ -78,7 +130,9 @@ describe('lens document equality', () => { defaultDoc, { ...defaultDoc, visualizationType: 'other-type' }, mockInjectFilterReferences, - mockDatasourceMap + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups ) ).toBeFalsy(); }); @@ -98,26 +152,9 @@ describe('lens document equality', () => { }, }, mockInjectFilterReferences, - mockDatasourceMap - ) - ).toBeFalsy(); - }); - - it('should compare the visualization state', () => { - expect( - isLensEqual( - defaultDoc, - { - ...defaultDoc, - state: { - ...defaultDoc.state, - visualization: { - some: 'other-props', - }, - }, - }, - mockInjectFilterReferences, - mockDatasourceMap + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups ) ).toBeFalsy(); }); @@ -139,7 +176,9 @@ describe('lens document equality', () => { }, }, mockInjectFilterReferences, - { ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource } + { ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource }, + mockVisualizationMap, + mockAnnotationGroups ) ).toBeFalsy(); @@ -167,7 +206,9 @@ describe('lens document equality', () => { }, }, mockInjectFilterReferences, - { ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource } + { ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource }, + mockVisualizationMap, + mockAnnotationGroups ) ).toBeTruthy(); }); @@ -176,7 +217,67 @@ describe('lens document equality', () => { // datasource's isEqual returns false (mockDatasourceMap.indexpattern.isEqual as jest.Mock).mockReturnValue(false); expect( - isLensEqual(defaultDoc, defaultDoc, mockInjectFilterReferences, mockDatasourceMap) + isLensEqual( + defaultDoc, + defaultDoc, + mockInjectFilterReferences, + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups + ) + ).toBeFalsy(); + }); + }); + + describe('comparing the visualizations', () => { + it('delegates to visualization class if visualization.isEqual available', () => { + expect( + isLensEqual( + defaultDoc, + defaultDoc, + mockInjectFilterReferences, + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups + ) + ).toBeTruthy(); + + expect(mockVisualizationMap[visualizationType].isEqual).toHaveBeenCalled(); + + (mockVisualizationMap[visualizationType].isEqual as jest.Mock).mockReturnValue(false); + + expect( + isLensEqual( + defaultDoc, + defaultDoc, + mockInjectFilterReferences, + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups + ) + ).toBeFalsy(); + }); + + it('direct comparison if no isEqual implementation', () => { + delete mockVisualizationMap[visualizationType].isEqual; + + expect( + isLensEqual( + defaultDoc, + { + ...defaultDoc, + state: { + ...defaultDoc.state, + visualization: { + some: 'other-props', + }, + }, + }, + mockInjectFilterReferences, + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups + ) ).toBeFalsy(); }); }); @@ -197,7 +298,9 @@ describe('lens document equality', () => { defaultDoc, { ...defaultDoc, state: { ...defaultDoc.state, filters: filtersWithPinned } }, mockInjectFilterReferences, - mockDatasourceMap + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups ) ).toBeTruthy(); }); @@ -221,7 +324,9 @@ describe('lens document equality', () => { }, }, mockInjectFilterReferences, - mockDatasourceMap + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups ) ).toBeTruthy(); }); @@ -241,7 +346,9 @@ describe('lens document equality', () => { }, }, mockInjectFilterReferences, - mockDatasourceMap + mockDatasourceMap, + mockVisualizationMap, + mockAnnotationGroups ) ).toBeTruthy(); }); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts index dae571acf10e..60316802ca5e 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts +++ b/x-pack/plugins/lens/public/app_plugin/lens_document_equality.ts @@ -8,7 +8,7 @@ import { isEqual, intersection, union } from 'lodash'; import { FilterManager } from '@kbn/data-plugin/public'; import { Document } from '../persistence/saved_object_store'; -import { DatasourceMap } from '../types'; +import { AnnotationGroups, DatasourceMap, VisualizationMap } from '../types'; import { removePinnedFilters } from './save_modal_container'; const removeNonSerializable = (obj: Parameters[0]) => @@ -18,7 +18,9 @@ export const isLensEqual = ( doc1In: Document | undefined, doc2In: Document | undefined, injectFilterReferences: FilterManager['inject'], - datasourceMap: DatasourceMap + datasourceMap: DatasourceMap, + visualizationMap: VisualizationMap, + annotationGroups: AnnotationGroups ) => { if (doc1In === undefined || doc2In === undefined) { return doc1In === doc2In; @@ -36,7 +38,23 @@ export const isLensEqual = ( return false; } - if (!isEqual(doc1.state.visualization, doc2.state.visualization)) { + const isEqualFromVis = visualizationMap[doc1.visualizationType]?.isEqual; + const visualizationStateIsEqual = isEqualFromVis + ? (() => { + try { + return isEqualFromVis( + doc1.state.visualization, + doc1.references, + doc2.state.visualization, + doc2.references, + annotationGroups + ); + } catch (err) { + return false; + } + })() + : isEqual(doc1.state.visualization, doc2.state.visualization); + if (!visualizationStateIsEqual) { return false; } diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index da790f334f10..9b77ad9d589f 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -96,6 +96,7 @@ export async function getLensServices( inspector, navigation, embeddable, + eventAnnotation, savedObjectsTagging, usageCollection, fieldFormats, @@ -107,6 +108,7 @@ export async function getLensServices( const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(); const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); + const eventAnnotationService = await eventAnnotation.getService(); return { data, @@ -118,6 +120,7 @@ export async function getLensServices( usageCollection, savedObjectsTagging, attributeService, + eventAnnotationService, executionContext: coreStart.executionContext, http: coreStart.http, uiActions: startDependencies.uiActions, diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts index a181cea79458..5b6712ed2386 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts @@ -25,7 +25,10 @@ import { showMemoizedErrorNotification } from '../lens_ui_errors'; import { TableInspectorAdapter } from '../editor_frame_service/types'; import { Datasource, DatasourcePublicAPI, IndexPatternMap } from '../types'; import { Visualization } from '..'; -import { getLayerType } from '../editor_frame_service/editor_frame/config_panel/add_layer'; + +function getLayerType(visualization: Visualization, state: unknown, layerId: string) { + return visualization.getLayerType(layerId, state) || LayerTypes.DATA; +} /** * Joins a series of queries. diff --git a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx index 761f46cbbd2c..39c2f55bdd67 100644 --- a/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx +++ b/x-pack/plugins/lens/public/app_plugin/tags_saved_object_save_modal_dashboard_wrapper.tsx @@ -43,6 +43,7 @@ export const TagEnhancedSavedObjectSaveModalDashboard: FC< ) : undefined, [savedObjectsTagging, initialTags] diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index a1b68fb1b433..142d24f64546 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -45,6 +45,7 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import type { SharePluginStart } from '@kbn/share-plugin/public'; +import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import type { SettingsStart } from '@kbn/core-ui-settings-browser'; import type { DatasourceMap, @@ -148,6 +149,7 @@ export interface LensAppServices { dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; data: DataPublicPluginStart; + eventAnnotationService: EventAnnotationServiceType; inspector: LensInspector; uiSettings: IUiSettingsClient; settings: SettingsStart; diff --git a/x-pack/plugins/lens/public/data_views_service/service.ts b/x-pack/plugins/lens/public/data_views_service/service.ts index 5192de1d2385..217d621b0ee9 100644 --- a/x-pack/plugins/lens/public/data_views_service/service.ts +++ b/x-pack/plugins/lens/public/data_views_service/service.ts @@ -8,20 +8,18 @@ import type { DataViewsContract, DataView, DataViewSpec } from '@kbn/data-views-plugin/public'; import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { ActionExecutionContext, UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { UPDATE_FILTER_REFERENCES_ACTION, UPDATE_FILTER_REFERENCES_TRIGGER, } from '@kbn/unified-search-plugin/public'; -import type { IndexPattern, IndexPatternMap, IndexPatternRef } from '../types'; -import { ensureIndexPattern, loadIndexPatternRefs, loadIndexPatterns } from './loader'; +import type { IndexPattern, IndexPatternMap } from '../types'; +import { ensureIndexPattern, loadIndexPatterns } from './loader'; import type { DataViewsState } from '../state_management'; import { generateId } from '../id_generator'; export interface IndexPatternServiceProps { core: Pick; - data: DataPublicPluginStart; dataViews: DataViewsContract; uiActions: UiActionsStart; contextDataViewSpec?: DataViewSpec; @@ -54,10 +52,7 @@ export interface IndexPatternServiceAPI { cache: IndexPatternMap; onIndexPatternRefresh?: () => void; }) => Promise; - /** - * Load indexPatternRefs with title and ids - */ - loadIndexPatternRefs: (options: { isFullEditor: boolean }) => Promise; + /** * Ensure an indexPattern is loaded in the cache, usually used in conjuction with a indexPattern change action. */ @@ -81,16 +76,15 @@ export interface IndexPatternServiceAPI { ) => void; } -export function createIndexPatternService({ +export const createIndexPatternService = ({ core, dataViews, - data, updateIndexPatterns, replaceIndexPattern, uiActions, contextDataViewSpec, -}: IndexPatternServiceProps): IndexPatternServiceAPI { - const onChangeError = (err: Error) => +}: IndexPatternServiceProps): IndexPatternServiceAPI => { + const showLoadingDataViewError = (err: Error) => core.notifications.toasts.addError(err, { title: i18n.translate('xpack.lens.indexPattern.dataViewLoadError', { defaultMessage: 'Error loading data view', @@ -131,9 +125,7 @@ export function createIndexPatternService({ } as ActionExecutionContext); }, ensureIndexPattern: (args) => - ensureIndexPattern({ onError: onChangeError, dataViews, ...args }), - loadIndexPatternRefs: async ({ isFullEditor }) => - isFullEditor ? loadIndexPatternRefs(dataViews) : [], + ensureIndexPattern({ onError: showLoadingDataViewError, dataViews, ...args }), getDefaultIndex: () => core.uiSettings.get('defaultIndex'), }; -} +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx deleted file mode 100644 index 299f2b81fc5b..000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/add_layer.tsx +++ /dev/null @@ -1,158 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useMemo } from 'react'; -import { - EuiToolTip, - EuiButton, - EuiPopover, - EuiIcon, - EuiContextMenu, - EuiBadge, - EuiFlexItem, - EuiFlexGroup, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { LayerTypes } from '@kbn/expression-xy-plugin/public'; -import type { LayerType } from '../../../../common/types'; -import type { FramePublicAPI, Visualization } from '../../../types'; - -interface AddLayerButtonProps { - visualization: Visualization; - visualizationState: unknown; - onAddLayerClick: (layerType: LayerType) => void; - layersMeta: Pick; -} - -export function getLayerType(visualization: Visualization, state: unknown, layerId: string) { - return visualization.getLayerType(layerId, state) || LayerTypes.DATA; -} - -export function AddLayerButton({ - visualization, - visualizationState, - onAddLayerClick, - layersMeta, -}: AddLayerButtonProps) { - const [showLayersChoice, toggleLayersChoice] = useState(false); - - const supportedLayers = useMemo(() => { - if (!visualization.appendLayer || !visualizationState) { - return null; - } - return visualization - .getSupportedLayers?.(visualizationState, layersMeta) - ?.filter(({ canAddViaMenu: hideFromMenu }) => !hideFromMenu); - }, [visualization, visualizationState, layersMeta]); - - if (supportedLayers == null || !supportedLayers.length) { - return null; - } - if (supportedLayers.length === 1) { - return ( - - onAddLayerClick(supportedLayers[0].type)} - iconType="layers" - > - {i18n.translate('xpack.lens.configPanel.addLayerButton', { - defaultMessage: 'Add layer', - })} - - - ); - } - return ( - toggleLayersChoice(!showLayersChoice)} - iconType="layers" - > - {i18n.translate('xpack.lens.configPanel.addLayerButton', { - defaultMessage: 'Add layer', - })} - - } - isOpen={showLayersChoice} - closePopover={() => toggleLayersChoice(false)} - panelPaddingSize="none" - > - { - return { - toolTipContent, - disabled, - name: - type === LayerTypes.ANNOTATIONS ? ( - - - {label} - - - - - {i18n.translate('xpack.lens.configPanel.experimentalLabel', { - defaultMessage: 'Technical preview', - })} - - - - ) : ( - {label} - ), - className: 'lnsLayerAddButton', - icon: icon && , - ['data-test-subj']: `lnsLayerAddButton-${type}`, - onClick: () => { - onAddLayerClick(type); - toggleLayersChoice(false); - }, - }; - }), - }, - ]} - /> - - ); -} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx index fe7df1983f3c..b1451a3fba8b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils.tsx @@ -35,9 +35,9 @@ export function shouldRemoveSource(source: DragDropIdentifier, dropType: DropTyp ); } -export function onDropForVisualization( +export function onDropForVisualization( props: OnVisDropProps, - activeVisualization: Visualization + activeVisualization: Visualization ) { const { prevState, target, frame, source, group } = props; const { layerId, columnId, groupId } = target; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index 184e7d572429..78f7246c52e6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -16,6 +16,7 @@ import { mockStoreDeps, MountStoreProps, } from '../../../mocks'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { Visualization } from '../../../types'; import { LayerPanels } from './config_panel'; import { LayerPanel } from './layer_panel'; @@ -25,11 +26,11 @@ import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { generateId } from '../../../id_generator'; import { mountWithProvider } from '../../../mocks'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; -import type { LayerType } from '../../../../common/types'; import { ReactWrapper } from 'enzyme'; import { addLayer } from '../../../state_management'; -import { AddLayerButton } from './add_layer'; import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock'; +import { AddLayerButton } from '../../../visualizations/xy/add_layer'; +import { LayerType } from '@kbn/visualizations-plugin/common'; jest.mock('../../../id_generator'); @@ -43,6 +44,11 @@ jest.mock('@kbn/kibana-utils-plugin/public', () => { }; }); +const addNewLayer = (instance: ReactWrapper, type: LayerType = LayerTypes.REFERENCELINE) => + act(() => { + instance.find(`button[data-test-subj="${type}"]`).first().simulate('click'); + }); + const waitMs = (time: number) => new Promise((r) => setTimeout(r, time)); let container: HTMLDivElement | undefined; @@ -117,7 +123,21 @@ describe('ConfigPanel', () => { activeVisualization: { ...visualizationMap.testVis, getLayerIds: () => Object.keys(frame.datasourceLayers), - } as unknown as Visualization, + getAddLayerButtonComponent: (props) => { + return ( + <> +