From af65e139f7a2a79125b6e53d268de572e72b72ab Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Mon, 16 Nov 2020 12:17:25 -0500 Subject: [PATCH 01/99] [Enterprise Search] Added a shouldShowActiveForSubroutes option (#83338) --- .../shared/layout/side_nav.test.tsx | 26 +++++++++++++++++++ .../applications/shared/layout/side_nav.tsx | 9 +++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx index e3e9872f892a4..9eaa2ba4c4d6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx @@ -105,6 +105,32 @@ describe('SideNavLink', () => { expect(wrapper.find('.enterpriseSearchNavLinks__subNav')).toHaveLength(1); expect(wrapper.find('[data-test-subj="subNav"]')).toHaveLength(1); }); + + describe('shouldShowActiveForSubroutes', () => { + it("won't set an active class when route is a subroute of 'to'", () => { + (useLocation as jest.Mock).mockImplementationOnce(() => ({ pathname: '/documents/1234' })); + + const wrapper = shallow( + + Link + + ); + + expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(0); + }); + + it('sets an active class if the current path is a subRoute of "to", and shouldShowActiveForSubroutes is true', () => { + (useLocation as jest.Mock).mockImplementationOnce(() => ({ pathname: '/documents/1234' })); + + const wrapper = shallow( + + Link + + ); + + expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1); + }); + }); }); describe('SideNavItem', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx index 6c4e1d084c16d..c75a48d5af41d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -63,6 +63,7 @@ export const SideNav: React.FC = ({ product, children }) => { interface SideNavLinkProps { to: string; + shouldShowActiveForSubroutes?: boolean; isExternal?: boolean; className?: string; isRoot?: boolean; @@ -70,8 +71,9 @@ interface SideNavLinkProps { } export const SideNavLink: React.FC = ({ - isExternal, to, + shouldShowActiveForSubroutes = false, + isExternal, children, className, isRoot, @@ -82,7 +84,10 @@ export const SideNavLink: React.FC = ({ const { pathname } = useLocation(); const currentPath = stripTrailingSlash(pathname); - const isActive = currentPath === to || (isRoot && currentPath === ''); + const isActive = + currentPath === to || + (shouldShowActiveForSubroutes && currentPath.startsWith(to)) || + (isRoot && currentPath === ''); const classes = classNames('enterpriseSearchNavLinks__item', className, { 'enterpriseSearchNavLinks__item--isActive': !isExternal && isActive, // eslint-disable-line @typescript-eslint/naming-convention From 7b49658cd2b6f6a9d68ba46a595f2411244a68b9 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Mon, 16 Nov 2020 17:27:59 +0000 Subject: [PATCH 02/99] [SecuritySolution] override timerange for prebuilt templates (#82468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * override timerange for prebuilt templates * add unit test * add unit tests * make sure it is template * check timelineType * overwrite prebuilt template's timerange * update mock path * override with relative timerange * Update x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts Co-authored-by: Patryk Kopyciński * review Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Patryk Kopyciński --- .../common/utils/default_date_settings.ts | 4 +- .../open_timeline/__mocks__/index.ts | 420 ++++++++++++++++++ .../components/open_timeline/helpers.test.ts | 408 ++++++++++++++++- .../components/open_timeline/helpers.ts | 36 +- .../timelines/store/timeline/helpers.ts | 13 + .../timelines/store/timeline/reducer.test.ts | 33 ++ .../routes/__mocks__/create_timelines.ts | 217 +++++++++ .../routes/utils/create_timelines.test.ts | 166 +++++++ .../timeline/routes/utils/create_timelines.ts | 8 +- 9 files changed, 1299 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/open_timeline/__mocks__/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/create_timelines.ts create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.test.ts diff --git a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts index 148143bb00bea..94545424512bc 100644 --- a/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts +++ b/x-pack/plugins/security_solution/public/common/utils/default_date_settings.ts @@ -34,8 +34,8 @@ export type DefaultIntervalSetting = DefaultInterval | null | undefined; // Defaults for if everything fails including dateMath.parse(DEFAULT_FROM) or dateMath.parse(DEFAULT_TO) // These should not really be hit unless we are in an extreme buggy state. -const DEFAULT_FROM_MOMENT = moment().subtract(24, 'hours'); -const DEFAULT_TO_MOMENT = moment(); +export const DEFAULT_FROM_MOMENT = moment().subtract(24, 'hours'); +export const DEFAULT_TO_MOMENT = moment(); /** * Retrieves timeRange settings to populate filters diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/__mocks__/index.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/__mocks__/index.ts new file mode 100644 index 0000000000000..ce4b0f09e5c95 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/__mocks__/index.ts @@ -0,0 +1,420 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; + +export const mockTimeline = { + data: { + getOneTimeline: { + savedObjectId: 'eb2781c0-1df5-11eb-8589-2f13958b79f7', + columns: [ + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: '@timestamp', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'message', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'event.category', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'event.action', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'host.name', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'source.ip', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'destination.ip', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'user.name', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + ], + dataProviders: [], + dateRange: { + start: '2020-11-01T14:30:59.935Z', + end: '2020-11-03T14:31:11.417Z', + __typename: 'DateRangePickerResult', + }, + description: '', + eventType: 'all', + eventIdToNoteIds: [], + excludedRowRendererIds: [], + favorite: [], + filters: [], + kqlMode: 'filter', + kqlQuery: { filterQuery: null, __typename: 'SerializedFilterQueryResult' }, + indexNames: [ + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + '.siem-signals-angelachuang-default', + ], + notes: [], + noteIds: [], + pinnedEventIds: [], + pinnedEventsSaveObject: [], + status: TimelineStatus.active, + title: 'my timeline', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + __typename: 'SortTimelineResult', + }, + created: 1604497127973, + createdBy: 'elastic', + updated: 1604500278364, + updatedBy: 'elastic', + version: 'WzQ4NSwxXQ==', + __typename: 'TimelineResult', + }, + }, + loading: false, + networkStatus: 7, + stale: false, +}; + +export const mockTemplate = { + data: { + getOneTimeline: { + savedObjectId: '0c70a200-1de0-11eb-885c-6fc13fca1850', + columns: [ + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: '@timestamp', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'signal.rule.description', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'event.action', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'process.name', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: 'The working directory of the process.', + example: '/home/alice', + indexes: null, + id: 'process.working_directory', + name: null, + searchable: null, + type: 'string', + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: + 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', + example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + indexes: null, + id: 'process.args', + name: null, + searchable: null, + type: 'string', + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: null, + category: null, + columnHeaderType: 'not-filtered', + description: null, + example: null, + indexes: null, + id: 'process.pid', + name: null, + searchable: null, + type: null, + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: 'Absolute path to the process executable.', + example: '/usr/bin/ssh', + indexes: null, + id: 'process.parent.executable', + name: null, + searchable: null, + type: 'string', + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: + 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', + example: '["ssh","-l","user","10.0.0.16"]', + indexes: null, + id: 'process.parent.args', + name: null, + searchable: null, + type: 'string', + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: 'Process id.', + example: '4242', + indexes: null, + id: 'process.parent.pid', + name: null, + searchable: null, + type: 'number', + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: true, + category: 'user', + columnHeaderType: 'not-filtered', + description: 'Short name or login of the user.', + example: 'albert', + indexes: null, + id: 'user.name', + name: null, + searchable: null, + type: 'string', + __typename: 'ColumnHeaderResult', + }, + { + aggregatable: true, + category: 'host', + columnHeaderType: 'not-filtered', + description: + 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', + example: null, + indexes: null, + id: 'host.name', + name: null, + searchable: null, + type: 'string', + __typename: 'ColumnHeaderResult', + }, + ], + dataProviders: [ + { + id: 'timeline-1-8622010a-61fb-490d-b162-beac9c36a853', + name: '{process.name}', + enabled: true, + excluded: false, + kqlQuery: '', + type: 'template', + queryMatch: { + field: 'process.name', + displayField: null, + value: '{process.name}', + displayValue: null, + operator: ':', + __typename: 'QueryMatchResult', + }, + and: [], + __typename: 'DataProviderResult', + }, + { + id: 'timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568', + name: '{event.type}', + enabled: true, + excluded: false, + kqlQuery: '', + type: 'template', + queryMatch: { + field: 'event.type', + displayField: null, + value: '{event.type}', + displayValue: null, + operator: ':*', + __typename: 'QueryMatchResult', + }, + and: [], + __typename: 'DataProviderResult', + }, + ], + dateRange: { + start: '2020-10-27T14:22:11.809Z', + end: '2020-11-03T14:22:11.809Z', + __typename: 'DateRangePickerResult', + }, + description: '', + eventType: 'all', + eventIdToNoteIds: [], + excludedRowRendererIds: [], + favorite: [], + filters: [], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { kind: 'kuery', expression: '', __typename: 'KueryFilterQueryResult' }, + serializedQuery: '', + __typename: 'SerializedKueryQueryResult', + }, + __typename: 'SerializedFilterQueryResult', + }, + indexNames: [], + notes: [], + noteIds: [], + pinnedEventIds: [], + pinnedEventsSaveObject: [], + status: TimelineStatus.immutable, + title: 'Generic Process Timeline', + timelineType: 'template', + templateTimelineId: 'cd55e52b-7bce-4887-88e2-f1ece4c75447', + templateTimelineVersion: 1, + savedQueryId: null, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + __typename: 'SortTimelineResult', + }, + created: 1604413368243, + createdBy: 'angela', + updated: 1604413368243, + updatedBy: 'angela', + version: 'WzQwMywxXQ==', + __typename: 'TimelineResult', + }, + }, + loading: false, + networkStatus: 7, + stale: false, +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index c89114ec77138..921527a0079e3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep, omit } from 'lodash/fp'; +import { cloneDeep, getOr, omit } from 'lodash/fp'; import { Dispatch } from 'redux'; +import ApolloClient from 'apollo-client'; import { mockTimelineResults, @@ -30,6 +31,9 @@ import { isUntitled, omitTypenameInTimeline, dispatchUpdateTimeline, + queryTimelineById, + QueryTimelineById, + formatTimelineResultToModel, } from './helpers'; import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; import { KueryFilterQueryKind } from '../../../common/store/model'; @@ -37,6 +41,10 @@ import { Note } from '../../../common/lib/note'; import moment from 'moment'; import sinon from 'sinon'; import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { + mockTimeline as mockSelectedTimeline, + mockTemplate as mockSelectedTemplate, +} from './__mocks__'; jest.mock('../../../common/store/inputs/actions'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); @@ -49,6 +57,15 @@ jest.mock('uuid', () => { }; }); +jest.mock('../../../common/utils/default_date_settings', () => { + const actual = jest.requireActual('../../../common/utils/default_date_settings'); + return { + ...actual, + DEFAULT_FROM_MOMENT: new Date('2020-10-27T11:37:31.655Z'), + DEFAULT_TO_MOMENT: new Date('2020-10-28T11:37:31.655Z'), + }; +}); + describe('helpers', () => { let mockResults: OpenTimelineResult[]; @@ -903,6 +920,395 @@ describe('helpers', () => { id: 'savedObject-1', }); }); + + test('should override timerange if given an Elastic prebuilt template', () => { + const timeline = { + savedObjectId: 'savedObject-1', + title: 'Awesome Timeline', + version: '1', + status: TimelineStatus.immutable, + timelineType: TimelineType.template, + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.template); + expect(newTimeline).toEqual({ + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [], + dateRange: { end: '2020-10-28T11:37:31.655Z', start: '2020-10-27T11:37:31.655Z' }, + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + eventType: 'all', + excludedRowRendererIds: [], + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'savedObject-1', + indexNames: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'savedObject-1', + selectedEventIds: {}, + show: false, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + status: TimelineStatus.immutable, + title: 'Awesome Timeline', + timelineType: TimelineType.template, + templateTimelineId: null, + templateTimelineVersion: null, + version: '1', + width: 1100, + }); + }); + + test('should not override timerange if given a custom template or timeline', () => { + const timeline = { + savedObjectId: 'savedObject-1', + title: 'Awesome Timeline', + version: '1', + status: TimelineStatus.active, + timelineType: TimelineType.default, + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false, TimelineType.default); + expect(newTimeline).toEqual({ + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [], + dateRange: { end: '2020-07-08T08:20:18.966Z', start: '2020-07-07T08:20:18.966Z' }, + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + eventType: 'all', + excludedRowRendererIds: [], + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'savedObject-1', + indexNames: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'savedObject-1', + selectedEventIds: {}, + show: false, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + status: TimelineStatus.active, + title: 'Awesome Timeline', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + version: '1', + width: 1100, + }); + }); + }); + + describe('queryTimelineById', () => { + describe('open a timeline', () => { + const updateIsLoading = jest.fn(); + const selectedTimeline = { + ...mockSelectedTimeline, + }; + const apolloClient = { + query: (jest.fn().mockResolvedValue(selectedTimeline) as unknown) as ApolloClient<{}>, + }; + const onOpenTimeline = jest.fn(); + const args = { + apolloClient, + duplicate: false, + graphEventId: '', + timelineId: '', + timelineType: TimelineType.default, + onOpenTimeline, + openTimeline: true, + updateIsLoading, + updateTimeline: jest.fn(), + }; + + beforeAll(async () => { + await queryTimelineById<{}>((args as unknown) as QueryTimelineById<{}>); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('dispatch updateIsLoading to true', () => { + expect(updateIsLoading.mock.calls[0][0]).toEqual({ + id: TimelineId.active, + isLoading: true, + }); + }); + + test('get timeline by Id', () => { + expect(apolloClient.query).toHaveBeenCalled(); + }); + + test('Do not override daterange if TimelineStatus is active', () => { + const { timeline } = formatTimelineResultToModel( + omitTypenameInTimeline(getOr({}, 'data.getOneTimeline', selectedTimeline)), + args.duplicate, + args.timelineType + ); + expect(onOpenTimeline).toBeCalledWith({ + ...timeline, + }); + }); + + test('dispatch updateIsLoading to false', () => { + expect(updateIsLoading.mock.calls[1][0]).toEqual({ + id: TimelineId.active, + isLoading: false, + }); + }); + }); + + describe('update a timeline', () => { + const updateIsLoading = jest.fn(); + const updateTimeline = jest.fn(); + const selectedTimeline = { ...mockSelectedTimeline }; + const apolloClient = { + query: (jest.fn().mockResolvedValue(selectedTimeline) as unknown) as ApolloClient<{}>, + }; + const args = { + apolloClient, + duplicate: false, + graphEventId: '', + timelineId: '', + timelineType: TimelineType.default, + openTimeline: true, + updateIsLoading, + updateTimeline, + }; + + beforeAll(async () => { + await queryTimelineById<{}>((args as unknown) as QueryTimelineById<{}>); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('dispatch updateIsLoading to true', () => { + expect(updateIsLoading.mock.calls[0][0]).toEqual({ + id: TimelineId.active, + isLoading: true, + }); + }); + + test('get timeline by Id', () => { + expect(apolloClient.query).toHaveBeenCalled(); + }); + + test('should not override daterange if TimelineStatus is active', () => { + const { timeline } = formatTimelineResultToModel( + omitTypenameInTimeline(getOr({}, 'data.getOneTimeline', selectedTimeline)), + args.duplicate, + args.timelineType + ); + expect(updateTimeline).toBeCalledWith({ + timeline: { + ...timeline, + graphEventId: '', + show: true, + dateRange: { + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', + }, + }, + duplicate: false, + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', + notes: [], + id: TimelineId.active, + }); + }); + + test('dispatch updateIsLoading to false', () => { + expect(updateIsLoading.mock.calls[1][0]).toEqual({ + id: TimelineId.active, + isLoading: false, + }); + }); + }); + + describe('open an immutable template', () => { + const updateIsLoading = jest.fn(); + const template = { ...mockSelectedTemplate }; + const apolloClient = { + query: (jest.fn().mockResolvedValue(template) as unknown) as ApolloClient<{}>, + }; + const onOpenTimeline = jest.fn(); + const args = { + apolloClient, + duplicate: false, + graphEventId: '', + timelineId: '', + timelineType: TimelineType.template, + onOpenTimeline, + openTimeline: true, + updateIsLoading, + updateTimeline: jest.fn(), + }; + + beforeAll(async () => { + await queryTimelineById<{}>((args as unknown) as QueryTimelineById<{}>); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('dispatch updateIsLoading to true', () => { + expect(updateIsLoading.mock.calls[0][0]).toEqual({ + id: TimelineId.active, + isLoading: true, + }); + }); + + test('get timeline by Id', () => { + expect(apolloClient.query).toHaveBeenCalled(); + }); + + test('override daterange if TimelineStatus is immutable', () => { + const { timeline } = formatTimelineResultToModel( + omitTypenameInTimeline(getOr({}, 'data.getOneTimeline', template)), + args.duplicate, + args.timelineType + ); + expect(onOpenTimeline).toBeCalledWith({ + ...timeline, + dateRange: { + end: '2020-10-28T11:37:31.655Z', + start: '2020-10-27T11:37:31.655Z', + }, + }); + }); + + test('dispatch updateIsLoading to false', () => { + expect(updateIsLoading.mock.calls[1][0]).toEqual({ + id: TimelineId.active, + isLoading: false, + }); + }); + }); }); describe('omitTypenameInTimeline', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 4c3be81a4992a..a0090baeb9923 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -33,7 +33,10 @@ import { addNotes as dispatchAddNotes, updateNote as dispatchUpdateNote, } from '../../../common/store/app/actions'; -import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; +import { + setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker, + setRelativeRangeDatePicker as dispatchSetRelativeRangeDatePicker, +} from '../../../common/store/inputs/actions'; import { setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, applyKqlFilterQuery as dispatchApplyKqlFilterQuery, @@ -58,6 +61,10 @@ import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { sourcererActions } from '../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { + DEFAULT_FROM_MOMENT, + DEFAULT_TO_MOMENT, +} from '../../../common/utils/default_date_settings'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -252,6 +259,14 @@ export const defaultTimelineToTimelineModel = ( const timelineEntries = { ...timeline, columns: timeline.columns != null ? timeline.columns.map(setTimelineColumn) : defaultHeaders, + dateRange: + timeline.status === TimelineStatus.immutable && + timeline.timelineType === TimelineType.template + ? { + start: DEFAULT_FROM_MOMENT.toISOString(), + end: DEFAULT_TO_MOMENT.toISOString(), + } + : timeline.dateRange, dataProviders: getDataProviders(duplicate, timeline.dataProviders, timelineType), eventIdToNoteIds: setEventIdToNoteIds(duplicate, timeline.eventIdToNoteIds), filters: timeline.filters != null ? timeline.filters.map(setTimelineFilters) : [], @@ -340,6 +355,7 @@ export const queryTimelineById = ({ duplicate, timelineType ); + if (onOpenTimeline != null) { onOpenTimeline(timeline); } else if (updateTimeline) { @@ -356,6 +372,7 @@ export const queryTimelineById = ({ ...timeline, graphEventId, show: openTimeline, + dateRange: { start: from, end: to }, }, to, })(); @@ -384,7 +401,22 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli eventType: timeline.eventType, }) ); - dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); + if ( + timeline.status === TimelineStatus.immutable && + timeline.timelineType === TimelineType.template + ) { + dispatch( + dispatchSetRelativeRangeDatePicker({ + id: 'timeline', + fromStr: 'now-24h', + toStr: 'now', + from: DEFAULT_FROM_MOMENT.toISOString(), + to: DEFAULT_TO_MOMENT.toISOString(), + }) + ); + } else { + dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); + } dispatch(dispatchAddTimeline({ id, timeline, savedTimeline: duplicate })); if ( timeline.kqlQuery != null && diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index d4e807b4a9a07..9a0bf5ec4a940 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -26,6 +26,7 @@ import { TimelineTypeLiteral, TimelineType, RowRendererId, + TimelineStatus, TimelineId, } from '../../../../common/types/timeline'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; @@ -33,6 +34,10 @@ import { normalizeTimeRange } from '../../../common/components/url_state/normali import { timelineDefaults } from './defaults'; import { ColumnHeaderOptions, KqlMode, TimelineModel } from './model'; import { TimelineById } from './types'; +import { + DEFAULT_FROM_MOMENT, + DEFAULT_TO_MOMENT, +} from '../../../common/utils/default_date_settings'; import { activeTimeline } from '../../containers/active_timeline_context'; export const isNotNull = (value: T | null): value is T => value !== null; @@ -144,6 +149,14 @@ export const addTimelineToStore = ({ [id]: { ...timeline, isLoading: timelineById[id].isLoading, + dateRange: + timeline.status === TimelineStatus.immutable && + timeline.timelineType === TimelineType.template + ? { + start: DEFAULT_FROM_MOMENT.toISOString(), + end: DEFAULT_TO_MOMENT.toISOString(), + } + : timeline.dateRange, }, }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index c2f43625ab464..7bd86cd7e2452 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -48,6 +48,14 @@ import { timelineDefaults } from './defaults'; import { TimelineById } from './types'; jest.mock('../../../common/components/url_state/normalize_time_range.ts'); +jest.mock('../../../common/utils/default_date_settings', () => { + const actual = jest.requireActual('../../../common/utils/default_date_settings'); + return { + ...actual, + DEFAULT_FROM_MOMENT: new Date('2020-10-27T11:37:31.655Z'), + DEFAULT_TO_MOMENT: new Date('2020-10-28T11:37:31.655Z'), + }; +}); const basicDataProvider: DataProvider = { and: [], @@ -141,6 +149,31 @@ describe('Timeline', () => { }, }); }); + + test('should override timerange if adding an immutable template', () => { + const update = addTimelineToStore({ + id: 'foo', + timeline: { + ...basicTimeline, + status: TimelineStatus.immutable, + timelineType: TimelineType.template, + }, + timelineById: timelineByIdMock, + }); + + expect(update).toEqual({ + foo: { + ...basicTimeline, + status: TimelineStatus.immutable, + timelineType: TimelineType.template, + dateRange: { + start: '2020-10-27T11:37:31.655Z', + end: '2020-10-28T11:37:31.655Z', + }, + show: true, + }, + }); + }); }); describe('#addNewTimeline', () => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/create_timelines.ts new file mode 100644 index 0000000000000..e8242d9691032 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/create_timelines.ts @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockTemplate = { + columns: [ + { + columnHeaderType: 'not-filtered', + indexes: null, + id: '@timestamp', + name: null, + searchable: null, + }, + { + columnHeaderType: 'not-filtered', + indexes: null, + id: 'signal.rule.description', + name: null, + searchable: null, + }, + { + columnHeaderType: 'not-filtered', + indexes: null, + id: 'event.action', + name: null, + searchable: null, + }, + { + columnHeaderType: 'not-filtered', + indexes: null, + id: 'process.name', + name: null, + searchable: null, + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: 'The working directory of the process.', + example: '/home/alice', + indexes: null, + id: 'process.working_directory', + name: null, + searchable: null, + type: 'string', + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: + 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', + example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + indexes: null, + id: 'process.args', + name: null, + searchable: null, + type: 'string', + }, + { + columnHeaderType: 'not-filtered', + indexes: null, + id: 'process.pid', + name: null, + searchable: null, + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: 'Absolute path to the process executable.', + example: '/usr/bin/ssh', + indexes: null, + id: 'process.parent.executable', + name: null, + searchable: null, + type: 'string', + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: + 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', + example: '["ssh","-l","user","10.0.0.16"]', + indexes: null, + id: 'process.parent.args', + name: null, + searchable: null, + type: 'string', + }, + { + aggregatable: true, + category: 'process', + columnHeaderType: 'not-filtered', + description: 'Process id.', + example: '4242', + indexes: null, + id: 'process.parent.pid', + name: null, + searchable: null, + type: 'number', + }, + { + aggregatable: true, + category: 'user', + columnHeaderType: 'not-filtered', + description: 'Short name or login of the user.', + example: 'albert', + indexes: null, + id: 'user.name', + name: null, + searchable: null, + type: 'string', + }, + { + aggregatable: true, + category: 'host', + columnHeaderType: 'not-filtered', + description: + 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', + indexes: null, + id: 'host.name', + name: null, + searchable: null, + type: 'string', + }, + ], + dataProviders: [ + { + id: 'timeline-1-8622010a-61fb-490d-b162-beac9c36a853', + name: '{process.name}', + enabled: true, + excluded: false, + kqlQuery: '', + type: 'template', + queryMatch: { + field: 'process.name', + displayField: null, + value: '{process.name}', + displayValue: null, + operator: ':', + }, + and: [], + }, + { + id: 'timeline-1-4685da24-35c1-43f3-892d-1f926dbf5568', + name: '{event.type}', + enabled: true, + excluded: false, + kqlQuery: '', + type: 'template', + queryMatch: { + field: 'event.type', + displayField: null, + value: '{event.type}', + displayValue: null, + operator: ':*', + }, + and: [], + }, + ], + description: '', + eventType: 'all', + excludedRowRendererIds: [], + filters: [], + kqlMode: 'filter', + kqlQuery: { filterQuery: { kuery: { kind: 'kuery', expression: '' }, serializedQuery: '' } }, + indexNames: [], + title: 'Generic Process Timeline - Duplicate - Duplicate', + timelineType: 'template', + templateTimelineVersion: null, + templateTimelineId: null, + dateRange: { start: '2020-10-01T11:37:31.655Z', end: '2020-10-02T11:37:31.655Z' }, + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + status: 'active', +}; + +export const mockTimeline = { + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp' }, + { columnHeaderType: 'not-filtered', id: 'message' }, + { columnHeaderType: 'not-filtered', id: 'event.category' }, + { columnHeaderType: 'not-filtered', id: 'event.action' }, + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'source.ip' }, + { columnHeaderType: 'not-filtered', id: 'destination.ip' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + ], + dataProviders: [], + description: '', + eventType: 'all', + excludedRowRendererIds: [], + filters: [], + kqlMode: 'filter', + kqlQuery: { filterQuery: null }, + indexNames: [ + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + '.siem-signals-angelachuang-default', + ], + title: 'my timeline', + timelineType: 'default', + templateTimelineVersion: null, + templateTimelineId: null, + dateRange: { start: '2020-11-03T13:34:40.339Z', end: '2020-11-04T13:34:40.339Z' }, + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + status: 'draft', +}; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.test.ts new file mode 100644 index 0000000000000..933e71cc10255 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as module from './create_timelines'; +import { persistTimeline } from '../../saved_object'; +import { persistPinnedEventOnTimeline } from '../../../pinned_event/saved_object'; +import { persistNote, getNote } from '../../../note/saved_object'; +import { FrameworkRequest } from '../../../framework'; +import { SavedTimeline } from '../../../../../common/types/timeline'; +import { mockTemplate, mockTimeline } from '../__mocks__/create_timelines'; + +const frameworkRequest = {} as FrameworkRequest; +const template = { ...mockTemplate } as SavedTimeline; +const timeline = { ...mockTimeline } as SavedTimeline; +const timelineSavedObjectId = null; +const timelineVersion = null; +const pinnedEventIds = ['123']; +const notes = [ + { noteId: 'abc', note: 'new note', timelineId: '', created: 1603885051655, createdBy: 'elastic' }, +]; +const existingNoteIds = undefined; +const isImmutable = true; +const newTimelineSavedObjectId = 'eb2781c0-1df5-11eb-8589-2f13958b79f7'; + +jest.mock('moment', () => { + const mockMoment = { + toISOString: jest + .fn() + .mockReturnValueOnce('2020-11-03T11:37:31.655Z') + .mockReturnValue('2020-11-04T11:37:31.655Z'), + subtract: jest.fn(), + }; + mockMoment.subtract.mockReturnValue(mockMoment); + return jest.fn().mockReturnValue(mockMoment); +}); + +jest.mock('../../saved_object', () => ({ + persistTimeline: jest.fn().mockResolvedValue({ + timeline: { + savedObjectId: 'eb2781c0-1df5-11eb-8589-2f13958b79f7', + version: 'xJs23==', + }, + }), +})); + +jest.mock('../../../pinned_event/saved_object', () => ({ + persistPinnedEventOnTimeline: jest.fn(), +})); + +jest.mock('../../../note/saved_object', () => ({ + getNote: jest.fn(), + persistNote: jest.fn(), +})); + +describe('createTimelines', () => { + describe('create timelines', () => { + beforeAll(async () => { + await module.createTimelines({ + frameworkRequest, + timeline, + timelineSavedObjectId, + timelineVersion, + pinnedEventIds, + notes, + existingNoteIds, + isImmutable: false, + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('respect input timerange - start', () => { + expect((persistTimeline as jest.Mock).mock.calls[0][3].dateRange.start).toEqual( + '2020-11-03T13:34:40.339Z' + ); + }); + + test('respect input timerange - end', () => { + expect((persistTimeline as jest.Mock).mock.calls[0][3].dateRange.end).toEqual( + '2020-11-04T13:34:40.339Z' + ); + }); + + test('savePinnedEvents', () => { + expect((persistPinnedEventOnTimeline as jest.Mock).mock.calls[0][2]).toEqual('123'); + }); + + test('saveNotes', () => { + expect((persistNote as jest.Mock).mock.calls[0][3]).toEqual({ + eventId: undefined, + note: 'new note', + timelineId: newTimelineSavedObjectId, + }); + }); + }); + + describe('create immutable templates', () => { + beforeAll(async () => { + (getNote as jest.Mock).mockReturnValue({ + ...notes[0], + }); + await module.createTimelines({ + frameworkRequest, + timeline: template, + timelineSavedObjectId, + timelineVersion, + pinnedEventIds, + notes, + existingNoteIds, + isImmutable, + overrideNotesOwner: false, + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + test('override timerange - start', () => { + expect((persistTimeline as jest.Mock).mock.calls[0][3].dateRange.start).toEqual( + '2020-11-03T11:37:31.655Z' + ); + }); + + test('override timerange - end', () => { + expect((persistTimeline as jest.Mock).mock.calls[0][3].dateRange.end).toEqual( + '2020-11-04T11:37:31.655Z' + ); + }); + }); + + describe('create custom templates', () => { + beforeAll(async () => { + await module.createTimelines({ + frameworkRequest, + timeline: template, + timelineSavedObjectId, + timelineVersion, + pinnedEventIds, + notes, + existingNoteIds, + isImmutable: false, + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('respect input timerange - start', () => { + expect((persistTimeline as jest.Mock).mock.calls[0][3].dateRange.start).toEqual( + '2020-10-01T11:37:31.655Z' + ); + }); + + test('respect input timerange - end', () => { + expect((persistTimeline as jest.Mock).mock.calls[0][3].dateRange.end).toEqual( + '2020-10-02T11:37:31.655Z' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts index dc0caaf67d738..83f97ddb01eaa 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts @@ -5,6 +5,7 @@ */ import { isEmpty } from 'lodash/fp'; +import moment from 'moment'; import * as timelineLib from '../../saved_object'; import * as pinnedEventLib from '../../../pinned_event/saved_object'; import * as noteLib from '../../../note/saved_object'; @@ -128,15 +129,20 @@ export const createTimelines = async ({ isImmutable, overrideNotesOwner = true, }: CreateTimelineProps): Promise => { + const timerangeStart = isImmutable + ? moment().subtract(24, 'hours').toISOString() + : timeline.dateRange?.start; + const timerangeEnd = isImmutable ? moment().toISOString() : timeline.dateRange?.end; const responseTimeline = await saveTimelines( frameworkRequest, - timeline, + { ...timeline, dateRange: { start: timerangeStart, end: timerangeEnd } }, timelineSavedObjectId, timelineVersion, isImmutable ); const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId; const newTimelineVersion = responseTimeline.timeline.version; + let myPromises: unknown[] = []; if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) { myPromises = [ From 90729c6e19e1fc73bb0cebdb62c76d48145e8c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 16 Nov 2020 18:37:49 +0100 Subject: [PATCH 03/99] Remove dependency of tests on strict SyntaxKind values (#83440) --- .../__fixture__/all_extracted_collectors.ts | 37 +++ ...exed_interface_with_not_matching_schema.ts | 18 +- .../extract_collectors.test.ts.snap | 295 ------------------ .../tools/check_collector__integrity.test.ts | 4 +- .../src/tools/extract_collectors.test.ts | 3 +- 5 files changed, 49 insertions(+), 308 deletions(-) create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/all_extracted_collectors.ts delete mode 100644 packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/all_extracted_collectors.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/all_extracted_collectors.ts new file mode 100644 index 0000000000000..f531608dda50a --- /dev/null +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/all_extracted_collectors.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { parsedExternallyDefinedCollector } from './parsed_externally_defined_collector'; +import { parsedImportedSchemaCollector } from './parsed_imported_schema'; +import { parsedImportedUsageInterface } from './parsed_imported_usage_interface'; +import { parsedIndexedInterfaceWithNoMatchingSchema } from './parsed_indexed_interface_with_not_matching_schema'; +import { parsedNestedCollector } from './parsed_nested_collector'; +import { parsedSchemaDefinedWithSpreadsCollector } from './parsed_schema_defined_with_spreads_collector'; +import { parsedWorkingCollector } from './parsed_working_collector'; +import { ParsedUsageCollection } from '../ts_parser'; + +export const allExtractedCollectors: ParsedUsageCollection[] = [ + ...parsedExternallyDefinedCollector, + ...parsedImportedSchemaCollector, + ...parsedImportedUsageInterface, + parsedIndexedInterfaceWithNoMatchingSchema, + parsedNestedCollector, + parsedSchemaDefinedWithSpreadsCollector, + parsedWorkingCollector, +]; diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts index 109fc045b6ee0..572684fbe83fb 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_indexed_interface_with_not_matching_schema.ts @@ -36,16 +36,14 @@ export const parsedIndexedInterfaceWithNoMatchingSchema: ParsedUsageCollection = fetch: { typeName: 'Usage', typeDescriptor: { - '': { - '@@INDEX@@': { - count_1: { - kind: SyntaxKind.NumberKeyword, - type: 'NumberKeyword', - }, - count_2: { - kind: SyntaxKind.NumberKeyword, - type: 'NumberKeyword', - }, + '@@INDEX@@': { + count_1: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + count_2: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', }, }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap deleted file mode 100644 index fe589be7993d0..0000000000000 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ /dev/null @@ -1,295 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractCollectors extracts collectors given rc file 1`] = ` -Array [ - Array [ - "src/fixtures/telemetry_collectors/externally_defined_collector.ts", - Object { - "collectorName": "from_variable_collector", - "fetch": Object { - "typeDescriptor": Object { - "locale": Object { - "kind": 146, - "type": "StringKeyword", - }, - }, - "typeName": "Usage", - }, - "schema": Object { - "value": Object { - "locale": Object { - "type": "keyword", - }, - }, - }, - }, - ], - Array [ - "src/fixtures/telemetry_collectors/externally_defined_collector.ts", - Object { - "collectorName": "from_fn_collector", - "fetch": Object { - "typeDescriptor": Object { - "locale": Object { - "kind": 146, - "type": "StringKeyword", - }, - }, - "typeName": "Usage", - }, - "schema": Object { - "value": Object { - "locale": Object { - "type": "keyword", - }, - }, - }, - }, - ], - Array [ - "src/fixtures/telemetry_collectors/imported_schema.ts", - Object { - "collectorName": "with_imported_schema", - "fetch": Object { - "typeDescriptor": Object { - "locale": Object { - "kind": 146, - "type": "StringKeyword", - }, - }, - "typeName": "Usage", - }, - "schema": Object { - "value": Object { - "locale": Object { - "type": "keyword", - }, - }, - }, - }, - ], - Array [ - "src/fixtures/telemetry_collectors/imported_usage_interface.ts", - Object { - "collectorName": "imported_usage_interface_collector", - "fetch": Object { - "typeDescriptor": Object { - "locale": Object { - "kind": 146, - "type": "StringKeyword", - }, - }, - "typeName": "Usage", - }, - "schema": Object { - "value": Object { - "locale": Object { - "type": "keyword", - }, - }, - }, - }, - ], - Array [ - "src/fixtures/telemetry_collectors/indexed_interface_with_not_matching_schema.ts", - Object { - "collectorName": "indexed_interface_with_not_matching_schema", - "fetch": Object { - "typeDescriptor": Object { - "@@INDEX@@": Object { - "count_1": Object { - "kind": 143, - "type": "NumberKeyword", - }, - "count_2": Object { - "kind": 143, - "type": "NumberKeyword", - }, - }, - }, - "typeName": "Usage", - }, - "schema": Object { - "value": Object { - "something": Object { - "count_1": Object { - "type": "long", - }, - }, - }, - }, - }, - ], - Array [ - "src/fixtures/telemetry_collectors/nested_collector.ts", - Object { - "collectorName": "my_nested_collector", - "fetch": Object { - "typeDescriptor": Object { - "locale": Object { - "kind": 146, - "type": "StringKeyword", - }, - }, - "typeName": "Usage", - }, - "schema": Object { - "value": Object { - "locale": Object { - "type": "keyword", - }, - }, - }, - }, - ], - Array [ - "src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts", - Object { - "collectorName": "schema_defined_with_spreads", - "fetch": Object { - "typeDescriptor": Object { - "flat": Object { - "kind": 146, - "type": "StringKeyword", - }, - "my_objects": Object { - "total": Object { - "kind": 143, - "type": "NumberKeyword", - }, - "type": Object { - "kind": 131, - "type": "BooleanKeyword", - }, - }, - "my_str": Object { - "kind": 146, - "type": "StringKeyword", - }, - }, - "typeName": "Usage", - }, - "schema": Object { - "value": Object { - "flat": Object { - "type": "keyword", - }, - "my_objects": Object { - "total": Object { - "type": "long", - }, - "type": Object { - "type": "boolean", - }, - }, - "my_str": Object { - "type": "text", - }, - }, - }, - }, - ], - Array [ - "src/fixtures/telemetry_collectors/working_collector.ts", - Object { - "collectorName": "my_working_collector", - "fetch": Object { - "typeDescriptor": Object { - "flat": Object { - "kind": 146, - "type": "StringKeyword", - }, - "my_array": Object { - "items": Object { - "total": Object { - "kind": 143, - "type": "NumberKeyword", - }, - "type": Object { - "kind": 131, - "type": "BooleanKeyword", - }, - }, - }, - "my_index_signature_prop": Object { - "@@INDEX@@": Object { - "kind": 143, - "type": "NumberKeyword", - }, - }, - "my_objects": Object { - "total": Object { - "kind": 143, - "type": "NumberKeyword", - }, - "type": Object { - "kind": 131, - "type": "BooleanKeyword", - }, - }, - "my_str": Object { - "kind": 146, - "type": "StringKeyword", - }, - "my_str_array": Object { - "items": Object { - "kind": 146, - "type": "StringKeyword", - }, - }, - }, - "typeName": "Usage", - }, - "schema": Object { - "value": Object { - "flat": Object { - "type": "keyword", - }, - "my_array": Object { - "items": Object { - "total": Object { - "type": "long", - }, - "type": Object { - "type": "boolean", - }, - }, - "type": "array", - }, - "my_index_signature_prop": Object { - "avg": Object { - "type": "float", - }, - "count": Object { - "type": "long", - }, - "max": Object { - "type": "long", - }, - "min": Object { - "type": "long", - }, - }, - "my_objects": Object { - "total": Object { - "type": "long", - }, - "type": Object { - "type": "boolean", - }, - }, - "my_str": Object { - "type": "text", - }, - "my_str_array": Object { - "items": Object { - "type": "keyword", - }, - "type": "array", - }, - }, - }, - }, - ], -] -`; diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts index b6ea9d49cf6d0..b4e934746dc45 100644 --- a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts @@ -90,10 +90,10 @@ describe('checkCompatibleTypeDescriptor', () => { expect(incompatibles).toHaveLength(1); const { diff, message } = incompatibles[0]; // eslint-disable-next-line @typescript-eslint/naming-convention - expect(diff).toEqual({ '.@@INDEX@@.count_2.kind': 'number' }); + expect(diff).toEqual({ '@@INDEX@@.count_2.kind': 'number' }); expect(message).toHaveLength(1); expect(message).toEqual([ - 'incompatible Type key (Usage..@@INDEX@@.count_2): expected (undefined) got ("number").', + 'incompatible Type key (Usage.@@INDEX@@.count_2): expected (undefined) got ("number").', ]); }); diff --git a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts index b03db75b219f6..9f1a1a2052791 100644 --- a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts @@ -21,6 +21,7 @@ import * as ts from 'typescript'; import * as path from 'path'; import { extractCollectors, getProgramPaths } from './extract_collectors'; import { parseTelemetryRC } from './config'; +import { allExtractedCollectors } from './__fixture__/all_extracted_collectors'; describe('extractCollectors', () => { it('extracts collectors given rc file', async () => { @@ -35,6 +36,6 @@ describe('extractCollectors', () => { const results = [...extractCollectors(programPaths, tsConfig)]; expect(results).toHaveLength(8); - expect(results).toMatchSnapshot(); + expect(results).toStrictEqual(allExtractedCollectors); }); }); From fb6d1367a791464f765ef5a9a034e6301cf01206 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 16 Nov 2020 13:06:33 -0500 Subject: [PATCH 04/99] [Index Templates] Add test for legacy templates (#83346) --- .../client_integration/helpers/constants.ts | 7 ++ .../client_integration/helpers/index.ts | 2 + .../template_edit.test.tsx | 67 ++++++++++++++++++- 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts new file mode 100644 index 0000000000000..14ef9760d7a05 --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const BRANCH = '8.x'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts index d2d7eb0165d30..65af41033561e 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/index.ts @@ -11,3 +11,5 @@ export { nextTick, getRandomString, findTestSubject, TestBed } from '@kbn/test/j export { setupEnvironment, WithAppDependencies, services } from './setup_environment'; export { TestSubjects } from './test_subjects'; + +export { BRANCH } from './constants'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index 37d489b6afe72..6ba2454025beb 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import * as fixtures from '../../../test/fixtures'; -import { setupEnvironment } from '../helpers'; +import { setupEnvironment, BRANCH } from '../helpers'; import { TEMPLATE_NAME, SETTINGS, ALIASES, MAPPINGS as DEFAULT_MAPPING } from './constants'; import { setup } from './template_edit.helpers'; @@ -224,4 +224,69 @@ describe('', () => { }); }); }); + + // @ts-expect-error + if (BRANCH === '7.x') { + describe('legacy index templates', () => { + const legacyTemplateToEdit = fixtures.getTemplate({ + name: 'legacy_index_template', + indexPatterns: ['indexPattern1'], + isLegacy: true, + template: { + mappings: { + my_mapping_type: {}, + }, + }, + }); + + beforeAll(() => { + httpRequestsMockHelpers.setLoadTemplateResponse(legacyTemplateToEdit); + }); + + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + it('persists mappings type', async () => { + const { actions } = testBed; + // Logistics + await actions.completeStepOne(); + // Note: "step 2" (component templates) doesn't exist for legacy templates + // Index settings + await actions.completeStepThree(); + // Mappings + await actions.completeStepFour(); + // Aliases + await actions.completeStepFive(); + + // Submit the form + await act(async () => { + actions.clickNextButton(); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + const { version, template, name, indexPatterns, _kbnMeta, order } = legacyTemplateToEdit; + + const expected = { + name, + indexPatterns, + version, + order, + template: { + aliases: undefined, + mappings: template!.mappings, + settings: undefined, + }, + _kbnMeta, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + }); + } }); From 849fbcda6d382970af2bfcf7c3e1b57cdd2d73ee Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Mon, 16 Nov 2020 13:41:04 -0500 Subject: [PATCH 05/99] [Security Solution]: add policy summary api (#81686) [Security Solution]: add policy summary api --- .../common/endpoint/constants.ts | 4 + .../common/endpoint/schema/policy.ts | 7 + .../common/endpoint/types/index.ts | 11 + .../endpoint/routes/policy/handlers.test.ts | 227 ++++++++++++++---- .../server/endpoint/routes/policy/handlers.ts | 40 ++- .../server/endpoint/routes/policy/index.ts | 22 +- .../server/endpoint/routes/policy/service.ts | 72 +++++- 7 files changed, 331 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 4cfa9347b2b58..8e19c2e8f219d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -18,3 +18,7 @@ export const TRUSTED_APPS_SUPPORTED_OS_TYPES: readonly string[] = ['macos', 'win export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}'; + +export const BASE_POLICY_RESPONSE_ROUTE = `/api/endpoint/policy_response`; +export const BASE_POLICY_ROUTE = `/api/endpoint/policy`; +export const AGENT_POLICY_SUMMARY_ROUTE = `${BASE_POLICY_ROUTE}/summaries`; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts b/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts index 35ba1266066e9..7ab8fbc8a9c8e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts @@ -10,3 +10,10 @@ export const GetPolicyResponseSchema = { agentId: schema.string(), }), }; + +export const GetAgentPolicySummaryRequestSchema = { + query: schema.object({ + package_name: schema.string(), + policy_id: schema.nullable(schema.string()), + }), +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 673d04c856935..66ba15431e603 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -1102,3 +1102,14 @@ export interface HostPolicyResponse { export interface GetHostPolicyResponse { policy_response: HostPolicyResponse; } + +/** + * REST API response for retrieving agent summary + */ +export interface GetAgentSummaryResponse { + summary_response: { + package: string; + policy_id?: string; + versions_count: { [key: string]: number }; + }; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 7dddc357fe53d..009ce043db85e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -5,10 +5,11 @@ */ import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { + createMockAgentService, createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; -import { getHostPolicyResponseHandler } from './handlers'; +import { getHostPolicyResponseHandler, getAgentPolicySummaryHandler } from './handlers'; import { ILegacyScopedClusterClient, KibanaResponseFactory, @@ -24,6 +25,8 @@ import { SearchResponse } from 'elasticsearch'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; +import { Agent } from '../../../../../fleet/common/types/models'; +import { AgentService } from '../../../../../fleet/server/services'; describe('test policy response handler', () => { let endpointAppContextService: EndpointAppContextService; @@ -31,64 +34,198 @@ describe('test policy response handler', () => { let mockSavedObjectClient: jest.Mocked; let mockResponse: jest.Mocked; - beforeEach(() => { - mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - mockSavedObjectClient = savedObjectsClientMock.create(); - mockResponse = httpServerMock.createResponseFactory(); - endpointAppContextService = new EndpointAppContextService(); - endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); - }); + describe('test policy response handler', () => { + beforeEach(() => { + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockSavedObjectClient = savedObjectsClientMock.create(); + mockResponse = httpServerMock.createResponseFactory(); + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); + }); - afterEach(() => endpointAppContextService.stop()); + afterEach(() => endpointAppContextService.stop()); - it('should return the latest policy response for a host', async () => { - const response = createSearchResponse(new EndpointDocGenerator().generatePolicyResponse()); - const hostPolicyResponseHandler = getHostPolicyResponseHandler({ - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - }); + it('should return the latest policy response for a host', async () => { + const response = createSearchResponse(new EndpointDocGenerator().generatePolicyResponse()); + const hostPolicyResponseHandler = getHostPolicyResponseHandler({ + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); - const mockRequest = httpServerMock.createKibanaRequest({ - params: { agentId: 'id' }, + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + const mockRequest = httpServerMock.createKibanaRequest({ + params: { agentId: 'id' }, + }); + + await hostPolicyResponseHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockResponse.ok).toBeCalled(); + const result = mockResponse.ok.mock.calls[0][0]?.body as GetHostPolicyResponse; + expect(result.policy_response.agent.id).toEqual(response.hits.hits[0]._source.agent.id); }); - await hostPolicyResponseHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); + it('should return not found when there is no response policy for host', async () => { + const hostPolicyResponseHandler = getHostPolicyResponseHandler({ + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + }); + + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse()) + ); - expect(mockResponse.ok).toBeCalled(); - const result = mockResponse.ok.mock.calls[0][0]?.body as GetHostPolicyResponse; - expect(result.policy_response.agent.id).toEqual(response.hits.hits[0]._source.agent.id); + const mockRequest = httpServerMock.createKibanaRequest({ + params: { agentId: 'id' }, + }); + + await hostPolicyResponseHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockResponse.notFound).toBeCalled(); + const message = mockResponse.notFound.mock.calls[0][0]?.body; + expect(message).toEqual('Policy Response Not Found'); + }); }); - it('should return not found when there is no response policy for host', async () => { - const hostPolicyResponseHandler = getHostPolicyResponseHandler({ - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), + describe('test agent policy summary handler', () => { + let mockAgentService: jest.Mocked; + + let agentListResult: { + agents: Agent[]; + total: number; + page: number; + perPage: number; + }; + + let emptyAgentListResult: { + agents: Agent[]; + total: number; + page: number; + perPage: number; + }; + + beforeEach(() => { + mockScopedClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + mockSavedObjectClient = savedObjectsClientMock.create(); + mockResponse = httpServerMock.createResponseFactory(); + endpointAppContextService = new EndpointAppContextService(); + mockAgentService = createMockAgentService(); + emptyAgentListResult = { + agents: [], + total: 2, + page: 1, + perPage: 1, + }; + + agentListResult = { + agents: [ + ({ + local_metadata: { + elastic: { + agent: { + version: '8.0.0', + }, + }, + }, + } as unknown) as Agent, + ({ + local_metadata: { + elastic: { + agent: { + version: '8.0.0', + }, + }, + }, + } as unknown) as Agent, + ({ + local_metadata: { + elastic: { + agent: { + version: '8.1.0', + }, + }, + }, + } as unknown) as Agent, + ], + total: 2, + page: 1, + perPage: 1, + }; + endpointAppContextService.start({ + ...createMockEndpointAppContextServiceStartContract(), + ...{ agentService: mockAgentService }, + }); }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse()) - ); + afterEach(() => endpointAppContextService.stop()); + + it('should return the summary of all the agent with the given policy name', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => Promise.resolve(agentListResult)) + .mockImplementationOnce(() => Promise.resolve(emptyAgentListResult)); - const mockRequest = httpServerMock.createKibanaRequest({ - params: { agentId: 'id' }, + const policySummarysHandler = getAgentPolicySummaryHandler({ + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + }); + + const mockRequest = httpServerMock.createKibanaRequest({ + query: { policy_id: '41a1b470-221b-11eb-8fba-fb9c0d46ace3', package_name: 'endpoint' }, + }); + + await policySummarysHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]?.body).toEqual({ + summary_response: { + policy_id: '41a1b470-221b-11eb-8fba-fb9c0d46ace3', + package: 'endpoint', + versions_count: { '8.0.0': 2, '8.1.0': 1 }, + }, + }); }); - await hostPolicyResponseHandler( - createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), - mockRequest, - mockResponse - ); + it('should return the agent summary', async () => { + mockAgentService.listAgents + .mockImplementationOnce(() => Promise.resolve(agentListResult)) + .mockImplementationOnce(() => Promise.resolve(emptyAgentListResult)); + + const agentPolicySummaryHandler = getAgentPolicySummaryHandler({ + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + }); - expect(mockResponse.notFound).toBeCalled(); - const message = mockResponse.notFound.mock.calls[0][0]?.body; - expect(message).toEqual('Policy Response Not Found'); + const mockRequest = httpServerMock.createKibanaRequest({ + query: { package_name: 'endpoint' }, + }); + + await agentPolicySummaryHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]?.body).toEqual({ + summary_response: { + package: 'endpoint', + versions_count: { '8.0.0': 2, '8.1.0': 1 }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts index f3a7b08a4cd44..728e3279c52a4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts @@ -6,9 +6,13 @@ import { RequestHandler } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { policyIndexPattern } from '../../../../common/endpoint/constants'; -import { GetPolicyResponseSchema } from '../../../../common/endpoint/schema/policy'; +import { + GetPolicyResponseSchema, + GetAgentPolicySummaryRequestSchema, +} from '../../../../common/endpoint/schema/policy'; import { EndpointAppContext } from '../../types'; -import { getPolicyResponseByAgentId } from './service'; +import { getAgentPolicySummary, getPolicyResponseByAgentId } from './service'; +import { GetAgentSummaryResponse } from '../../../../common/endpoint/types'; export const getHostPolicyResponseHandler = function ( endpointAppContext: EndpointAppContext @@ -31,3 +35,35 @@ export const getHostPolicyResponseHandler = function ( } }; }; + +export const getAgentPolicySummaryHandler = function ( + endpointAppContext: EndpointAppContext +): RequestHandler, undefined> { + return async (context, request, response) => { + try { + const result = await getAgentPolicySummary( + endpointAppContext, + context.core.savedObjects.client, + request.query.package_name, + request.query?.policy_id || undefined + ); + const responseBody = { + package: request.query.package_name, + versions_count: { ...result }, + }; + + const body: GetAgentSummaryResponse = { + summary_response: request.query?.policy_id + ? { ...responseBody, ...{ policy_id: request.query?.policy_id } } + : responseBody, + }; + + return response.ok({ + body, + }); + } catch (err) { + endpointAppContext.logFactory.get('metadata').error(JSON.stringify(err, null, 2)); + return response.internalError({ body: err }); + } + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts index 5993b0b0e752e..a924095b95be1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts @@ -6,10 +6,15 @@ import { IRouter } from 'kibana/server'; import { EndpointAppContext } from '../../types'; -import { GetPolicyResponseSchema } from '../../../../common/endpoint/schema/policy'; -import { getHostPolicyResponseHandler } from './handlers'; - -export const BASE_POLICY_RESPONSE_ROUTE = `/api/endpoint/policy_response`; +import { + GetPolicyResponseSchema, + GetAgentPolicySummaryRequestSchema, +} from '../../../../common/endpoint/schema/policy'; +import { getHostPolicyResponseHandler, getAgentPolicySummaryHandler } from './handlers'; +import { + AGENT_POLICY_SUMMARY_ROUTE, + BASE_POLICY_RESPONSE_ROUTE, +} from '../../../../common/endpoint/constants'; export const INITIAL_POLICY_ID = '00000000-0000-0000-0000-000000000000'; @@ -22,4 +27,13 @@ export function registerPolicyRoutes(router: IRouter, endpointAppContext: Endpoi }, getHostPolicyResponseHandler(endpointAppContext) ); + + router.get( + { + path: AGENT_POLICY_SUMMARY_ROUTE, + validate: GetAgentPolicySummaryRequestSchema, + options: { authRequired: true }, + }, + getAgentPolicySummaryHandler(endpointAppContext) + ); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts index 0019c97a6cced..e670ca6e20cb2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts @@ -5,9 +5,12 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { INITIAL_POLICY_ID } from './index'; +import { Agent } from '../../../../../fleet/common/types/models'; +import { EndpointAppContext } from '../../types'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../../../fleet/common/constants'; export function getESQueryPolicyResponseByAgentID(agentID: string, index: string) { return { @@ -57,3 +60,70 @@ export async function getPolicyResponseByAgentId( policy_response: response.hits.hits[0]._source, }; } + +const transformAgentVersionMap = (versionMap: Map): { [key: string]: number } => { + const data: { [key: string]: number } = {}; + versionMap.forEach((value, key) => { + data[key] = value; + }); + return data; +}; + +export async function getAgentPolicySummary( + endpointAppContext: EndpointAppContext, + soClient: SavedObjectsClientContract, + packageName: string, + policyId?: string, + pageSize: number = 1000 +): Promise<{ [key: string]: number }> { + const agentQuery = `${AGENT_SAVED_OBJECT_TYPE}.packages:"${packageName}"`; + if (policyId) { + return transformAgentVersionMap( + await agentVersionsMap( + endpointAppContext, + soClient, + `${agentQuery} AND ${AGENT_SAVED_OBJECT_TYPE}.policy_id:${policyId}`, + pageSize + ) + ); + } + + return transformAgentVersionMap( + await agentVersionsMap(endpointAppContext, soClient, agentQuery, pageSize) + ); +} + +export async function agentVersionsMap( + endpointAppContext: EndpointAppContext, + soClient: SavedObjectsClientContract, + kqlQuery: string, + pageSize: number = 1000 +): Promise> { + const searchOptions = (pageNum: number) => { + return { + page: pageNum, + perPage: pageSize, + showInactive: false, + kuery: kqlQuery, + }; + }; + + let page = 1; + const result: Map = new Map(); + let hasMore = true; + while (hasMore) { + const queryResult = await endpointAppContext.service + .getAgentService()! + .listAgents(soClient, searchOptions(page++)); + queryResult.agents.forEach((agent: Agent) => { + const agentVersion = agent.local_metadata?.elastic?.agent?.version; + if (result.has(agentVersion)) { + result.set(agentVersion, result.get(agentVersion)! + 1); + } else { + result.set(agentVersion, 1); + } + }); + hasMore = queryResult.agents.length > 0; + } + return result; +} From bc3bb2afa8c3536f301030030633b3890c33a82c Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 16 Nov 2020 11:33:30 -0800 Subject: [PATCH 06/99] [App Search] Engine Overview server route & Logic file (#83353) * Add overview server route * Add EngineOverviewLogic * tfw when you forget index.ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../engine_overview_logic.test.ts | 163 ++++++++++++++++++ .../engine_overview/engine_overview_logic.ts | 130 ++++++++++++++ .../components/engine_overview/index.ts | 7 + .../server/routes/app_search/engines.test.ts | 26 ++- .../server/routes/app_search/engines.ts | 15 ++ 5 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts new file mode 100644 index 0000000000000..d35bde20f4f1e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { mockHttpValues } from '../../../__mocks__'; +jest.mock('../../../shared/http', () => ({ + HttpLogic: { values: mockHttpValues }, +})); +const { http } = mockHttpValues; + +jest.mock('../../../shared/flash_messages', () => ({ + flashAPIErrors: jest.fn(), +})); +import { flashAPIErrors } from '../../../shared/flash_messages'; + +jest.mock('../engine', () => ({ + EngineLogic: { values: { engineName: 'some-engine' } }, +})); + +import { EngineOverviewLogic } from './'; + +describe('EngineOverviewLogic', () => { + const mockEngineMetrics = { + apiLogsUnavailable: true, + documentCount: 10, + startDate: '1970-01-30', + endDate: '1970-01-31', + operationsPerDay: [0, 0, 0, 0, 0, 0, 0], + queriesPerDay: [0, 0, 0, 0, 0, 25, 50], + totalClicks: 50, + totalQueries: 75, + }; + + const DEFAULT_VALUES = { + dataLoading: true, + apiLogsUnavailable: false, + documentCount: 0, + startDate: '', + endDate: '', + operationsPerDay: [], + queriesPerDay: [], + totalClicks: 0, + totalQueries: 0, + timeoutId: null, + }; + + const mount = () => { + resetContext({}); + EngineOverviewLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(EngineOverviewLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setPolledData', () => { + it('should set all received data as top-level values and set dataLoading to false', () => { + mount(); + EngineOverviewLogic.actions.setPolledData(mockEngineMetrics); + + expect(EngineOverviewLogic.values).toEqual({ + ...DEFAULT_VALUES, + ...mockEngineMetrics, + dataLoading: false, + }); + }); + }); + + describe('setTimeoutId', () => { + describe('timeoutId', () => { + it('should be set to the provided value', () => { + mount(); + EngineOverviewLogic.actions.setTimeoutId(123); + + expect(EngineOverviewLogic.values).toEqual({ + ...DEFAULT_VALUES, + timeoutId: 123, + }); + }); + }); + }); + + describe('pollForOverviewMetrics', () => { + it('fetches data and calls onPollingSuccess', async () => { + mount(); + jest.spyOn(EngineOverviewLogic.actions, 'onPollingSuccess'); + const promise = Promise.resolve(mockEngineMetrics); + http.get.mockReturnValueOnce(promise); + + EngineOverviewLogic.actions.pollForOverviewMetrics(); + await promise; + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/overview'); + expect(EngineOverviewLogic.actions.onPollingSuccess).toHaveBeenCalledWith( + mockEngineMetrics + ); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occurred'); + http.get.mockReturnValue(promise); + + try { + EngineOverviewLogic.actions.pollForOverviewMetrics(); + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); + }); + }); + + describe('onPollingSuccess', () => { + it('starts a polling timeout and sets data', async () => { + mount(); + jest.useFakeTimers(); + jest.spyOn(EngineOverviewLogic.actions, 'setTimeoutId'); + jest.spyOn(EngineOverviewLogic.actions, 'setPolledData'); + + EngineOverviewLogic.actions.onPollingSuccess(mockEngineMetrics); + + expect(setTimeout).toHaveBeenCalledWith( + EngineOverviewLogic.actions.pollForOverviewMetrics, + 5000 + ); + expect(EngineOverviewLogic.actions.setTimeoutId).toHaveBeenCalledWith(expect.any(Number)); + expect(EngineOverviewLogic.actions.setPolledData).toHaveBeenCalledWith(mockEngineMetrics); + }); + }); + }); + + describe('unmount', () => { + let unmount: Function; + + beforeEach(() => { + jest.useFakeTimers(); + resetContext({}); + unmount = EngineOverviewLogic.mount(); + }); + + it('clears existing polling timeouts on unmount', () => { + EngineOverviewLogic.actions.setTimeoutId(123); + unmount(); + expect(clearTimeout).toHaveBeenCalled(); + }); + + it("does not clear timeout if one hasn't been set", () => { + unmount(); + expect(clearTimeout).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts new file mode 100644 index 0000000000000..3fc7ce8083e03 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { EngineLogic } from '../engine'; + +const POLLING_DURATION = 5000; + +interface EngineOverviewApiData { + apiLogsUnavailable: boolean; + documentCount: number; + startDate: string; + endDate: string; + operationsPerDay: number[]; + queriesPerDay: number[]; + totalClicks: number; + totalQueries: number; +} +interface EngineOverviewValues extends EngineOverviewApiData { + dataLoading: boolean; + timeoutId: number | null; +} + +interface EngineOverviewActions { + setPolledData(engineMetrics: EngineOverviewApiData): EngineOverviewApiData; + setTimeoutId(timeoutId: number): { timeoutId: number }; + pollForOverviewMetrics(): void; + onPollingSuccess(engineMetrics: EngineOverviewApiData): EngineOverviewApiData; +} + +export const EngineOverviewLogic = kea>({ + path: ['enterprise_search', 'app_search', 'engine_overview_logic'], + actions: () => ({ + setPolledData: (engineMetrics) => engineMetrics, + setTimeoutId: (timeoutId) => ({ timeoutId }), + pollForOverviewMetrics: true, + onPollingSuccess: (engineMetrics) => engineMetrics, + }), + reducers: () => ({ + dataLoading: [ + true, + { + setPolledData: () => false, + }, + ], + apiLogsUnavailable: [ + false, + { + setPolledData: (_, { apiLogsUnavailable }) => apiLogsUnavailable, + }, + ], + startDate: [ + '', + { + setPolledData: (_, { startDate }) => startDate, + }, + ], + endDate: [ + '', + { + setPolledData: (_, { endDate }) => endDate, + }, + ], + queriesPerDay: [ + [], + { + setPolledData: (_, { queriesPerDay }) => queriesPerDay, + }, + ], + operationsPerDay: [ + [], + { + setPolledData: (_, { operationsPerDay }) => operationsPerDay, + }, + ], + totalQueries: [ + 0, + { + setPolledData: (_, { totalQueries }) => totalQueries, + }, + ], + totalClicks: [ + 0, + { + setPolledData: (_, { totalClicks }) => totalClicks, + }, + ], + documentCount: [ + 0, + { + setPolledData: (_, { documentCount }) => documentCount, + }, + ], + timeoutId: [ + null, + { + setTimeoutId: (_, { timeoutId }) => timeoutId, + }, + ], + }), + listeners: ({ actions }) => ({ + pollForOverviewMetrics: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.get(`/api/app_search/engines/${engineName}/overview`); + actions.onPollingSuccess(response); + } catch (e) { + flashAPIErrors(e); + } + }, + onPollingSuccess: (engineMetrics) => { + const timeoutId = window.setTimeout(actions.pollForOverviewMetrics, POLLING_DURATION); + actions.setTimeoutId(timeoutId); + actions.setPolledData(engineMetrics); + }, + }), + events: ({ values }) => ({ + beforeUnmount() { + if (values.timeoutId !== null) clearTimeout(values.timeoutId); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts new file mode 100644 index 0000000000000..fcd92ba6a338c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EngineOverviewLogic } from './engine_overview_logic'; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index b7009c1b76fbc..ed6847a029100 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -116,7 +116,6 @@ describe('engine routes', () => { mockRouter = new MockRouter({ method: 'get', path: '/api/app_search/engines/{name}', - payload: 'params', }); registerEnginesRoutes({ @@ -133,4 +132,29 @@ describe('engine routes', () => { }); }); }); + + describe('GET /api/app_search/engines/{name}/overview', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{name}/overview', + }); + + registerEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ params: { name: 'some-engine' } }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/some-engine/overview_metrics', + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index 2c4e235556ae3..f9169d8795f4b 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -60,4 +60,19 @@ export function registerEnginesRoutes({ })(context, request, response); } ); + router.get( + { + path: '/api/app_search/engines/{name}/overview', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/${request.params.name}/overview_metrics`, + })(context, request, response); + } + ); } From fe3357927241bc6e95650df38dee735df952dedd Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 16 Nov 2020 14:50:20 -0500 Subject: [PATCH 07/99] Add support for licensed sub feature privileges (#80905) --- .../security/feature-registration.asciidoc | 27 +- x-pack/plugins/features/common/sub_feature.ts | 8 + .../__snapshots__/oss_features.test.ts.snap | 498 ++++++++++++++++- .../features/server/feature_registry.test.ts | 201 +++++++ .../features/server/feature_registry.ts | 28 +- .../plugins/features/server/feature_schema.ts | 29 +- .../features/server/oss_features.test.ts | 25 +- .../features/server/routes/index.test.ts | 102 +++- .../plugins/features/server/routes/index.ts | 14 +- .../security/common/licensing/index.mock.ts | 1 + .../common/licensing/license_service.ts | 5 +- .../roles/__fixtures__/kibana_privileges.ts | 1 + .../elasticsearch_privileges.test.tsx.snap | 1 + .../feature_table/sub_feature_form.test.tsx | 62 +++ .../kibana/feature_table/sub_feature_form.tsx | 10 +- .../plugins/security/public/plugin.test.tsx | 2 + .../feature_privilege_iterator.test.ts | 137 +++++ .../feature_privilege_iterator.ts | 12 +- .../sub_feature_privilege_iterator.ts | 9 +- .../privileges/privileges.test.ts | 515 ++++++++++++++++++ .../authorization/privileges/privileges.ts | 8 +- x-pack/plugins/security/server/plugin.test.ts | 1 + 22 files changed, 1641 insertions(+), 55 deletions(-) diff --git a/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc index 8c80c2e5f2ffb..4e0c220477faf 100644 --- a/docs/developer/architecture/security/feature-registration.asciidoc +++ b/docs/developer/architecture/security/feature-registration.asciidoc @@ -198,7 +198,10 @@ server.route({ === Example 3: Discover Discover takes advantage of subfeature privileges to allow fine-grained access control. In this example, -a single "Create Short URLs" subfeature privilege is defined, which allows users to grant access to this feature without having to grant the `all` privilege to Discover. In other words, you can grant `read` access to Discover, and also grant the ability to create short URLs. +two subfeature privileges are defined: "Create Short URLs", and "Generate PDF Reports". These allow users to grant access to this feature without having to grant the `all` privilege to Discover. In other words, you can grant `read` access to Discover, and also grant the ability to create short URLs or generate PDF reports. + +Notice the "Generate PDF Reports" subfeature privilege has an additional `minimumPrivilege` option. Kibana will only offer this subfeature privilege if the +license requirement is satisfied. ["source","javascript"] ----------- @@ -259,6 +262,28 @@ public setup(core, { features }) { }, ], }, + { + groupType: 'independent', + privileges: [ + { + id: 'pdf_generate', + name: i18n.translate( + 'xpack.features.ossFeatures.discoverGeneratePDFReportsPrivilegeName', + { + defaultMessage: 'Generate PDF Reports', + } + ), + minimumLicense: 'platinum', + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + api: ['generatePDFReports'], + ui: ['generatePDFReports'], + }, + ], + }, ], }, ], diff --git a/x-pack/plugins/features/common/sub_feature.ts b/x-pack/plugins/features/common/sub_feature.ts index 0651bad883ea5..f791db6154731 100644 --- a/x-pack/plugins/features/common/sub_feature.ts +++ b/x-pack/plugins/features/common/sub_feature.ts @@ -5,6 +5,7 @@ */ import { RecursiveReadonly } from '@kbn/utility-types'; +import { LicenseType } from '../../licensing/common/types'; import { FeatureKibanaPrivileges } from './feature_kibana_privileges'; /** @@ -68,6 +69,13 @@ export interface SubFeaturePrivilegeConfig * `read` is also included in `all` automatically. */ includeIn: 'all' | 'read' | 'none'; + + /** + * The minimum supported license level for this sub-feature privilege. + * If no license level is supplied, then this privilege will be available for all licences + * that are valid for the overall feature. + */ + minimumLicense?: LicenseType; } export class SubFeature { diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index f616daebf662a..e18acbfea8f48 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`buildOSSFeatures returns the advancedSettings feature augmented with appropriate sub feature privileges 1`] = ` +exports[`buildOSSFeatures with a basic license returns the advancedSettings feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { "privilege": Object { @@ -51,7 +51,7 @@ Array [ ] `; -exports[`buildOSSFeatures returns the dashboard feature augmented with appropriate sub feature privileges 1`] = ` +exports[`buildOSSFeatures with a basic license returns the dashboard feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { "privilege": Object { @@ -128,7 +128,7 @@ Array [ ] `; -exports[`buildOSSFeatures returns the dev_tools feature augmented with appropriate sub feature privileges 1`] = ` +exports[`buildOSSFeatures with a basic license returns the dev_tools feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { "privilege": Object { @@ -182,7 +182,7 @@ Array [ ] `; -exports[`buildOSSFeatures returns the discover feature augmented with appropriate sub feature privileges 1`] = ` +exports[`buildOSSFeatures with a basic license returns the discover feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { "privilege": Object { @@ -243,7 +243,7 @@ Array [ ] `; -exports[`buildOSSFeatures returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = ` +exports[`buildOSSFeatures with a basic license returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { "privilege": Object { @@ -296,7 +296,7 @@ Array [ ] `; -exports[`buildOSSFeatures returns the savedObjectsManagement feature augmented with appropriate sub feature privileges 1`] = ` +exports[`buildOSSFeatures with a basic license returns the savedObjectsManagement feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { "privilege": Object { @@ -363,7 +363,7 @@ Array [ ] `; -exports[`buildOSSFeatures returns the timelion feature augmented with appropriate sub feature privileges 1`] = ` +exports[`buildOSSFeatures with a basic license returns the timelion feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { "privilege": Object { @@ -411,7 +411,489 @@ Array [ ] `; -exports[`buildOSSFeatures returns the visualize feature augmented with appropriate sub feature privileges 1`] = ` +exports[`buildOSSFeatures with a basic license returns the visualize feature augmented with appropriate sub feature privileges 1`] = ` +Array [ + Object { + "privilege": Object { + "alerting": Object { + "all": Array [], + "read": Array [], + }, + "api": Array [], + "app": Array [ + "visualize", + "lens", + "kibana", + ], + "catalogue": Array [ + "visualize", + ], + "management": Object {}, + "savedObject": Object { + "all": Array [ + "visualization", + "query", + "lens", + "url", + ], + "read": Array [ + "index-pattern", + "search", + "tag", + ], + }, + "ui": Array [ + "show", + "delete", + "save", + "saveQuery", + "createShortUrl", + ], + }, + "privilegeId": "all", + }, + Object { + "privilege": Object { + "app": Array [ + "visualize", + "lens", + "kibana", + ], + "catalogue": Array [ + "visualize", + ], + "savedObject": Object { + "all": Array [], + "read": Array [ + "index-pattern", + "search", + "visualization", + "query", + "lens", + "tag", + ], + }, + "ui": Array [ + "show", + ], + }, + "privilegeId": "read", + }, +] +`; + +exports[`buildOSSFeatures with a enterprise license returns the advancedSettings feature augmented with appropriate sub feature privileges 1`] = ` +Array [ + Object { + "privilege": Object { + "app": Array [ + "kibana", + ], + "catalogue": Array [ + "advanced_settings", + ], + "management": Object { + "kibana": Array [ + "settings", + ], + }, + "savedObject": Object { + "all": Array [ + "config", + ], + "read": Array [], + }, + "ui": Array [ + "save", + ], + }, + "privilegeId": "all", + }, + Object { + "privilege": Object { + "app": Array [ + "kibana", + ], + "catalogue": Array [ + "advanced_settings", + ], + "management": Object { + "kibana": Array [ + "settings", + ], + }, + "savedObject": Object { + "all": Array [], + "read": Array [], + }, + "ui": Array [], + }, + "privilegeId": "read", + }, +] +`; + +exports[`buildOSSFeatures with a enterprise license returns the dashboard feature augmented with appropriate sub feature privileges 1`] = ` +Array [ + Object { + "privilege": Object { + "alerting": Object { + "all": Array [], + "read": Array [], + }, + "api": Array [], + "app": Array [ + "dashboards", + "kibana", + ], + "catalogue": Array [ + "dashboard", + ], + "management": Object {}, + "savedObject": Object { + "all": Array [ + "dashboard", + "query", + "url", + ], + "read": Array [ + "index-pattern", + "search", + "visualization", + "timelion-sheet", + "canvas-workpad", + "lens", + "map", + "tag", + ], + }, + "ui": Array [ + "createNew", + "show", + "showWriteControls", + "saveQuery", + "createShortUrl", + ], + }, + "privilegeId": "all", + }, + Object { + "privilege": Object { + "app": Array [ + "dashboards", + "kibana", + ], + "catalogue": Array [ + "dashboard", + ], + "savedObject": Object { + "all": Array [], + "read": Array [ + "index-pattern", + "search", + "visualization", + "timelion-sheet", + "canvas-workpad", + "lens", + "map", + "dashboard", + "query", + "tag", + ], + }, + "ui": Array [ + "show", + ], + }, + "privilegeId": "read", + }, +] +`; + +exports[`buildOSSFeatures with a enterprise license returns the dev_tools feature augmented with appropriate sub feature privileges 1`] = ` +Array [ + Object { + "privilege": Object { + "api": Array [ + "console", + ], + "app": Array [ + "dev_tools", + "kibana", + ], + "catalogue": Array [ + "console", + "searchprofiler", + "grokdebugger", + ], + "savedObject": Object { + "all": Array [], + "read": Array [], + }, + "ui": Array [ + "show", + "save", + ], + }, + "privilegeId": "all", + }, + Object { + "privilege": Object { + "api": Array [ + "console", + ], + "app": Array [ + "dev_tools", + "kibana", + ], + "catalogue": Array [ + "console", + "searchprofiler", + "grokdebugger", + ], + "savedObject": Object { + "all": Array [], + "read": Array [], + }, + "ui": Array [ + "show", + ], + }, + "privilegeId": "read", + }, +] +`; + +exports[`buildOSSFeatures with a enterprise license returns the discover feature augmented with appropriate sub feature privileges 1`] = ` +Array [ + Object { + "privilege": Object { + "alerting": Object { + "all": Array [], + "read": Array [], + }, + "api": Array [], + "app": Array [ + "discover", + "kibana", + ], + "catalogue": Array [ + "discover", + ], + "management": Object {}, + "savedObject": Object { + "all": Array [ + "search", + "query", + "index-pattern", + "url", + ], + "read": Array [], + }, + "ui": Array [ + "show", + "save", + "saveQuery", + "createShortUrl", + ], + }, + "privilegeId": "all", + }, + Object { + "privilege": Object { + "app": Array [ + "discover", + "kibana", + ], + "catalogue": Array [ + "discover", + ], + "savedObject": Object { + "all": Array [], + "read": Array [ + "index-pattern", + "search", + "query", + ], + }, + "ui": Array [ + "show", + ], + }, + "privilegeId": "read", + }, +] +`; + +exports[`buildOSSFeatures with a enterprise license returns the indexPatterns feature augmented with appropriate sub feature privileges 1`] = ` +Array [ + Object { + "privilege": Object { + "app": Array [ + "kibana", + ], + "catalogue": Array [ + "indexPatterns", + ], + "management": Object { + "kibana": Array [ + "indexPatterns", + ], + }, + "savedObject": Object { + "all": Array [ + "index-pattern", + ], + "read": Array [], + }, + "ui": Array [ + "save", + ], + }, + "privilegeId": "all", + }, + Object { + "privilege": Object { + "app": Array [ + "kibana", + ], + "catalogue": Array [ + "indexPatterns", + ], + "management": Object { + "kibana": Array [ + "indexPatterns", + ], + }, + "savedObject": Object { + "all": Array [], + "read": Array [ + "index-pattern", + ], + }, + "ui": Array [], + }, + "privilegeId": "read", + }, +] +`; + +exports[`buildOSSFeatures with a enterprise license returns the savedObjectsManagement feature augmented with appropriate sub feature privileges 1`] = ` +Array [ + Object { + "privilege": Object { + "api": Array [ + "copySavedObjectsToSpaces", + ], + "app": Array [ + "kibana", + ], + "catalogue": Array [ + "saved_objects", + ], + "management": Object { + "kibana": Array [ + "objects", + ], + }, + "savedObject": Object { + "all": Array [ + "foo", + "bar", + ], + "read": Array [], + }, + "ui": Array [ + "read", + "edit", + "delete", + "copyIntoSpace", + "shareIntoSpace", + ], + }, + "privilegeId": "all", + }, + Object { + "privilege": Object { + "api": Array [ + "copySavedObjectsToSpaces", + ], + "app": Array [ + "kibana", + ], + "catalogue": Array [ + "saved_objects", + ], + "management": Object { + "kibana": Array [ + "objects", + ], + }, + "savedObject": Object { + "all": Array [], + "read": Array [ + "foo", + "bar", + ], + }, + "ui": Array [ + "read", + ], + }, + "privilegeId": "read", + }, +] +`; + +exports[`buildOSSFeatures with a enterprise license returns the timelion feature augmented with appropriate sub feature privileges 1`] = ` +Array [ + Object { + "privilege": Object { + "app": Array [ + "timelion", + "kibana", + ], + "catalogue": Array [ + "timelion", + ], + "savedObject": Object { + "all": Array [ + "timelion-sheet", + ], + "read": Array [ + "index-pattern", + ], + }, + "ui": Array [ + "save", + ], + }, + "privilegeId": "all", + }, + Object { + "privilege": Object { + "app": Array [ + "timelion", + "kibana", + ], + "catalogue": Array [ + "timelion", + ], + "savedObject": Object { + "all": Array [], + "read": Array [ + "index-pattern", + "timelion-sheet", + ], + }, + "ui": Array [], + }, + "privilegeId": "read", + }, +] +`; + +exports[`buildOSSFeatures with a enterprise license returns the visualize feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { "privilege": Object { diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index fda72e4536939..6deb7cd968490 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -6,6 +6,7 @@ import { FeatureRegistry } from './feature_registry'; import { ElasticsearchFeatureConfig, KibanaFeatureConfig } from '../common'; +import { licensingMock } from '../../licensing/server/mocks'; describe('FeatureRegistry', () => { describe('Kibana Features', () => { @@ -1280,6 +1281,123 @@ describe('FeatureRegistry', () => { ); }); + it('allows independent sub-feature privileges to register a minimumLicense', () => { + const feature1: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + subFeatures: [ + { + name: 'foo', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'foo', + name: 'foo', + minimumLicense: 'platinum', + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }, + ], + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature1); + }); + + it('prevents mutually exclusive sub-feature privileges from registering a minimumLicense', () => { + const feature1: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + subFeatures: [ + { + name: 'foo', + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'foo', + name: 'foo', + minimumLicense: 'platinum', + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + { + id: 'bar', + name: 'Bar', + minimumLicense: 'platinum', + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + ], + }, + ], + }, + ], + }; + + const featureRegistry = new FeatureRegistry(); + expect(() => { + featureRegistry.registerKibanaFeature(feature1); + }).toThrowErrorMatchingInlineSnapshot( + `"child \\"subFeatures\\" fails because [\\"subFeatures\\" at position 0 fails because [child \\"privilegeGroups\\" fails because [\\"privilegeGroups\\" at position 0 fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"minimumLicense\\" fails because [\\"minimumLicense\\" is not allowed]]]]]]]"` + ); + }); + it('cannot register feature after getAll has been called', () => { const feature1: KibanaFeatureConfig = { id: 'test-feature', @@ -1305,6 +1423,89 @@ describe('FeatureRegistry', () => { `"Features are locked, can't register new features. Attempt to register test-feature-2 failed."` ); }); + describe('#getAllKibanaFeatures', () => { + const features: KibanaFeatureConfig[] = [ + { + id: 'gold-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + minimumLicense: 'gold', + privileges: null, + }, + { + id: 'unlicensed-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: null, + }, + { + id: 'with-sub-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: { savedObject: { all: [], read: [] }, ui: [] }, + read: { savedObject: { all: [], read: [] }, ui: [] }, + }, + minimumLicense: 'platinum', + subFeatures: [ + { + name: 'licensed-sub-feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'sub-feature', + includeIn: 'all', + minimumLicense: 'enterprise', + name: 'sub feature', + savedObject: { all: [], read: [] }, + ui: [], + }, + ], + }, + ], + }, + ], + }, + ]; + + const registry = new FeatureRegistry(); + features.forEach((f) => registry.registerKibanaFeature(f)); + + it('returns all features and sub-feature privileges by default', () => { + const result = registry.getAllKibanaFeatures(); + expect(result).toHaveLength(3); + const [, , withSubFeature] = result; + expect(withSubFeature.subFeatures).toHaveLength(1); + expect(withSubFeature.subFeatures[0].privilegeGroups).toHaveLength(1); + expect(withSubFeature.subFeatures[0].privilegeGroups[0].privileges).toHaveLength(1); + }); + + it('returns features which are satisfied by the current license', () => { + const license = licensingMock.createLicense({ license: { type: 'gold' } }); + const result = registry.getAllKibanaFeatures(license); + expect(result).toHaveLength(2); + const ids = result.map((f) => f.id); + expect(ids).toEqual(['gold-feature', 'unlicensed-feature']); + }); + + it('filters out sub-feature privileges which do not match the current license', () => { + const license = licensingMock.createLicense({ license: { type: 'platinum' } }); + const result = registry.getAllKibanaFeatures(license); + expect(result).toHaveLength(3); + const ids = result.map((f) => f.id); + expect(ids).toEqual(['gold-feature', 'unlicensed-feature', 'with-sub-feature']); + + const [, , withSubFeature] = result; + expect(withSubFeature.subFeatures).toHaveLength(1); + expect(withSubFeature.subFeatures[0].privilegeGroups).toHaveLength(1); + expect(withSubFeature.subFeatures[0].privilegeGroups[0].privileges).toHaveLength(0); + }); + }); }); describe('Elasticsearch Features', () => { diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index e9e556ba22fd2..cdceb5a2d1c77 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -5,6 +5,7 @@ */ import { cloneDeep, uniq } from 'lodash'; +import { ILicense } from '../../licensing/server'; import { KibanaFeatureConfig, KibanaFeature, @@ -55,11 +56,30 @@ export class FeatureRegistry { this.esFeatures[feature.id] = featureCopy; } - public getAllKibanaFeatures(): KibanaFeature[] { + public getAllKibanaFeatures(license?: ILicense, ignoreLicense = false): KibanaFeature[] { this.locked = true; - return Object.values(this.kibanaFeatures).map( - (featureConfig) => new KibanaFeature(featureConfig) - ); + let features = Object.values(this.kibanaFeatures); + + const performLicenseCheck = license && !ignoreLicense; + + if (performLicenseCheck) { + features = features.filter((feature) => { + const filter = !feature.minimumLicense || license!.hasAtLeast(feature.minimumLicense); + if (!filter) return false; + + feature.subFeatures?.forEach((subFeature) => { + subFeature.privilegeGroups.forEach((group) => { + group.privileges = group.privileges.filter( + (privilege) => + !privilege.minimumLicense || license!.hasAtLeast(privilege.minimumLicense) + ); + }); + }); + + return true; + }); + } + return features.map((featureConfig) => new KibanaFeature(featureConfig)); } public getAllElasticsearchFeatures(): ElasticsearchFeature[] { diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 78ffcdb087360..3d8b649802168 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -21,6 +21,11 @@ const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/; const reservedFeaturePrrivilegePartRegex = /^(?!reserved_)[a-zA-Z0-9_-]+$/; export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/; +const validLicenses = ['basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial']; +// sub-feature privileges are only available with a `gold` license or better, so restricting sub-feature privileges +// for `gold` or below doesn't make a whole lot of sense. +const validSubFeaturePrivilegeLicenses = ['platinum', 'enterprise', 'trial']; + const managementSchema = Joi.object().pattern( managementSectionIdRegex, Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)) @@ -53,10 +58,11 @@ const kibanaPrivilegeSchema = Joi.object({ ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), }); -const kibanaSubFeaturePrivilegeSchema = Joi.object({ +const kibanaIndependentSubFeaturePrivilegeSchema = Joi.object({ id: Joi.string().regex(subFeaturePrivilegePartRegex).required(), name: Joi.string().required(), includeIn: Joi.string().allow('all', 'read', 'none').required(), + minimumLicense: Joi.string().valid(...validSubFeaturePrivilegeLicenses), management: managementSchema, catalogue: catalogueSchema, alerting: Joi.object({ @@ -72,12 +78,22 @@ const kibanaSubFeaturePrivilegeSchema = Joi.object({ ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), }); +const kibanaMutuallyExclusiveSubFeaturePrivilegeSchema = kibanaIndependentSubFeaturePrivilegeSchema.keys( + { + minimumLicense: Joi.forbidden(), + } +); + const kibanaSubFeatureSchema = Joi.object({ name: Joi.string().required(), privilegeGroups: Joi.array().items( Joi.object({ groupType: Joi.string().valid('mutually_exclusive', 'independent').required(), - privileges: Joi.array().items(kibanaSubFeaturePrivilegeSchema).min(1), + privileges: Joi.when('groupType', { + is: 'mutually_exclusive', + then: Joi.array().items(kibanaMutuallyExclusiveSubFeaturePrivilegeSchema).min(1), + otherwise: Joi.array().items(kibanaIndependentSubFeaturePrivilegeSchema).min(1), + }), }) ), }); @@ -91,14 +107,7 @@ const kibanaFeatureSchema = Joi.object({ category: appCategorySchema, order: Joi.number(), excludeFromBasePrivileges: Joi.boolean(), - minimumLicense: Joi.string().valid( - 'basic', - 'standard', - 'gold', - 'platinum', - 'enterprise', - 'trial' - ), + minimumLicense: Joi.string().valid(...validLicenses), app: Joi.array().items(Joi.string()).required(), management: managementSchema, catalogue: catalogueSchema, diff --git a/x-pack/plugins/features/server/oss_features.test.ts b/x-pack/plugins/features/server/oss_features.test.ts index 961656aba8bfd..a22e95105ba05 100644 --- a/x-pack/plugins/features/server/oss_features.test.ts +++ b/x-pack/plugins/features/server/oss_features.test.ts @@ -7,6 +7,7 @@ import { buildOSSFeatures } from './oss_features'; import { featurePrivilegeIterator } from '../../security/server/authorization'; import { KibanaFeature } from '.'; +import { LicenseType } from '../../licensing/server'; describe('buildOSSFeatures', () => { it('returns features including timelion', () => { @@ -46,14 +47,22 @@ Array [ const features = buildOSSFeatures({ savedObjectTypes: ['foo', 'bar'], includeTimelion: true }); features.forEach((featureConfig) => { - it(`returns the ${featureConfig.id} feature augmented with appropriate sub feature privileges`, () => { - const privileges = []; - for (const featurePrivilege of featurePrivilegeIterator(new KibanaFeature(featureConfig), { - augmentWithSubFeaturePrivileges: true, - })) { - privileges.push(featurePrivilege); - } - expect(privileges).toMatchSnapshot(); + (['enterprise', 'basic'] as LicenseType[]).forEach((licenseType) => { + describe(`with a ${licenseType} license`, () => { + it(`returns the ${featureConfig.id} feature augmented with appropriate sub feature privileges`, () => { + const privileges = []; + for (const featurePrivilege of featurePrivilegeIterator( + new KibanaFeature(featureConfig), + { + augmentWithSubFeaturePrivileges: true, + licenseType, + } + )) { + privileges.push(featurePrivilege); + } + expect(privileges).toMatchSnapshot(); + }); + }); }); }); }); diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts index 7080f18906146..a3a038533777c 100644 --- a/x-pack/plugins/features/server/routes/index.test.ts +++ b/x-pack/plugins/features/server/routes/index.test.ts @@ -11,15 +11,78 @@ import { httpServerMock, httpServiceMock, coreMock } from '../../../../../src/co import { LicenseType } from '../../../licensing/server/'; import { licensingMock } from '../../../licensing/server/mocks'; import { RequestHandler } from '../../../../../src/core/server'; -import { KibanaFeatureConfig } from '../../common'; +import { FeatureKibanaPrivileges, KibanaFeatureConfig, SubFeatureConfig } from '../../common'; -function createContextMock(licenseType: LicenseType = 'gold') { +function createContextMock(licenseType: LicenseType = 'platinum') { return { core: coreMock.createRequestHandlerContext(), licensing: licensingMock.createRequestHandlerContext({ license: { type: licenseType } }), }; } +function createPrivilege(partial: Partial = {}): FeatureKibanaPrivileges { + return { + savedObject: { + all: [], + read: [], + }, + ui: [], + ...partial, + }; +} + +function getExpectedSubFeatures(licenseType: LicenseType = 'platinum'): SubFeatureConfig[] { + return [ + { + name: 'basicFeature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'basicSub1', + name: 'basic sub 1', + includeIn: 'all', + ...createPrivilege(), + }, + ], + }, + ], + }, + { + name: 'platinumFeature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: + licenseType !== 'basic' + ? [ + { + id: 'platinumFeatureSub1', + name: 'platinum sub 1', + includeIn: 'all', + minimumLicense: 'platinum', + ...createPrivilege(), + }, + ] + : [], + }, + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'platinumFeatureMutExSub1', + name: 'platinum sub 1', + includeIn: 'all', + ...createPrivilege(), + }, + ], + }, + ], + }, + ]; +} + describe('GET /api/features', () => { let routeHandler: RequestHandler; beforeEach(() => { @@ -29,7 +92,11 @@ describe('GET /api/features', () => { name: 'Feature 1', app: [], category: { id: 'foo', label: 'foo' }, - privileges: null, + privileges: { + all: createPrivilege(), + read: createPrivilege(), + }, + subFeatures: getExpectedSubFeatures(), }); featureRegistry.registerKibanaFeature({ @@ -76,7 +143,12 @@ describe('GET /api/features', () => { const [call] = mockResponse.ok.mock.calls; const body = call[0]!.body as KibanaFeatureConfig[]; - const features = body.map((feature) => ({ id: feature.id, order: feature.order })); + const features = body.map((feature) => ({ + id: feature.id, + order: feature.order, + subFeatures: feature.subFeatures, + })); + expect(features).toEqual([ { id: 'feature_3', @@ -89,6 +161,7 @@ describe('GET /api/features', () => { { id: 'feature_1', order: undefined, + subFeatures: getExpectedSubFeatures(), }, { id: 'licensed_feature', @@ -105,7 +178,11 @@ describe('GET /api/features', () => { const [call] = mockResponse.ok.mock.calls; const body = call[0]!.body as KibanaFeatureConfig[]; - const features = body.map((feature) => ({ id: feature.id, order: feature.order })); + const features = body.map((feature) => ({ + id: feature.id, + order: feature.order, + subFeatures: feature.subFeatures, + })); expect(features).toEqual([ { @@ -119,6 +196,7 @@ describe('GET /api/features', () => { { id: 'feature_1', order: undefined, + subFeatures: getExpectedSubFeatures('basic'), }, ]); }); @@ -135,7 +213,11 @@ describe('GET /api/features', () => { const [call] = mockResponse.ok.mock.calls; const body = call[0]!.body as KibanaFeatureConfig[]; - const features = body.map((feature) => ({ id: feature.id, order: feature.order })); + const features = body.map((feature) => ({ + id: feature.id, + order: feature.order, + subFeatures: feature.subFeatures, + })); expect(features).toEqual([ { @@ -149,6 +231,7 @@ describe('GET /api/features', () => { { id: 'feature_1', order: undefined, + subFeatures: getExpectedSubFeatures('basic'), }, ]); }); @@ -165,7 +248,11 @@ describe('GET /api/features', () => { const [call] = mockResponse.ok.mock.calls; const body = call[0]!.body as KibanaFeatureConfig[]; - const features = body.map((feature) => ({ id: feature.id, order: feature.order })); + const features = body.map((feature) => ({ + id: feature.id, + order: feature.order, + subFeatures: feature.subFeatures, + })); expect(features).toEqual([ { @@ -179,6 +266,7 @@ describe('GET /api/features', () => { { id: 'feature_1', order: undefined, + subFeatures: getExpectedSubFeatures(), }, { id: 'licensed_feature', diff --git a/x-pack/plugins/features/server/routes/index.ts b/x-pack/plugins/features/server/routes/index.ts index 1b0cd20775352..b2bfa8b0296b7 100644 --- a/x-pack/plugins/features/server/routes/index.ts +++ b/x-pack/plugins/features/server/routes/index.ts @@ -26,17 +26,15 @@ export function defineRoutes({ router, featureRegistry }: RouteDefinitionParams) }, }, (context, request, response) => { - const allFeatures = featureRegistry.getAllKibanaFeatures(); + const currentLicense = context.licensing!.license; + + const allFeatures = featureRegistry.getAllKibanaFeatures( + currentLicense, + request.query.ignoreValidLicenses + ); return response.ok({ body: allFeatures - .filter( - (feature) => - request.query.ignoreValidLicenses || - !feature.minimumLicense || - (context.licensing!.license && - context.licensing!.license.hasAtLeast(feature.minimumLicense)) - ) .sort( (f1, f2) => (f1.order ?? Number.MAX_SAFE_INTEGER) - (f2.order ?? Number.MAX_SAFE_INTEGER) diff --git a/x-pack/plugins/security/common/licensing/index.mock.ts b/x-pack/plugins/security/common/licensing/index.mock.ts index 87225f479ceed..df7d8cd7b416b 100644 --- a/x-pack/plugins/security/common/licensing/index.mock.ts +++ b/x-pack/plugins/security/common/licensing/index.mock.ts @@ -11,6 +11,7 @@ export const licenseMock = { create: (): jest.Mocked => ({ isLicenseAvailable: jest.fn(), isEnabled: jest.fn().mockReturnValue(true), + getType: jest.fn().mockReturnValue('basic'), getFeatures: jest.fn(), features$: of(), }), diff --git a/x-pack/plugins/security/common/licensing/license_service.ts b/x-pack/plugins/security/common/licensing/license_service.ts index 09b6ae95c282c..ca6366ef0bb8e 100644 --- a/x-pack/plugins/security/common/licensing/license_service.ts +++ b/x-pack/plugins/security/common/licensing/license_service.ts @@ -6,12 +6,13 @@ import { Observable, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ILicense } from '../../../licensing/common/types'; +import { ILicense, LicenseType } from '../../../licensing/common/types'; import { SecurityLicenseFeatures } from './license_features'; export interface SecurityLicense { isLicenseAvailable(): boolean; isEnabled(): boolean; + getType(): LicenseType | undefined; getFeatures(): SecurityLicenseFeatures; features$: Observable; } @@ -36,6 +37,8 @@ export class SecurityLicenseService { isEnabled: () => this.isSecurityEnabledFromRawLicense(rawLicense), + getType: () => rawLicense?.type, + getFeatures: () => this.calculateFeaturesFromRawLicense(rawLicense), features$: license$.pipe( diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts index 02a18039cee74..66e8eff323313 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts @@ -23,6 +23,7 @@ export const createRawKibanaPrivileges = ( const licensingService = { getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures), + getType: () => 'basic' as const, }; return privilegesFactory( diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index a2e46af19bf34..173ae4f081ed7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -183,6 +183,7 @@ exports[`it renders without crashing 1`] = ` "_subscribe": [Function], }, "getFeatures": [MockFunction], + "getType": [MockFunction], "isEnabled": [MockFunction], "isLicenseAvailable": [MockFunction], } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.test.tsx index eba94338d52d3..e08871d1a645c 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.test.tsx @@ -14,6 +14,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import { SubFeatureForm } from './sub_feature_form'; import { EuiCheckbox, EuiButtonGroup } from '@elastic/eui'; import { act } from '@testing-library/react'; +import { KibanaFeature } from '../../../../../../../../features/public'; // Note: these tests are not concerned with the proper display of privileges, // as that is verified by the feature_table and privilege_space_form tests. @@ -234,4 +235,65 @@ describe('SubFeatureForm', () => { expect(onChange).toHaveBeenCalledWith([]); }); + + it('does not render empty privilege groups', () => { + // privilege groups are filtered server-side to only include the + // sub-feature privileges that are allowed by the current license. + + const role = createRole([ + { + base: [], + feature: { + with_sub_features: ['cool_all'], + }, + spaces: [], + }, + ]); + const feature = new KibanaFeature({ + id: 'test_feature', + name: 'test feature', + category: { id: 'test', label: 'test' }, + app: [], + privileges: { + all: { + savedObject: { all: [], read: [] }, + ui: [], + }, + read: { + savedObject: { all: [], read: [] }, + ui: [], + }, + }, + subFeatures: [ + { + name: 'subFeature1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [], + }, + ], + }, + ], + }); + const subFeature1 = new SecuredSubFeature(feature.toRaw().subFeatures![0]); + const kibanaPrivileges = createKibanaPrivileges([feature]); + const calculator = new PrivilegeFormCalculator(kibanaPrivileges, role); + + const onChange = jest.fn(); + + const wrapper = mountWithIntl( + + ); + + expect(wrapper.children()).toMatchInlineSnapshot(`null`); + }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx index 41e15387f9c47..9ffb50066a58e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/sub_feature_form.tsx @@ -27,12 +27,20 @@ interface Props { } export const SubFeatureForm = (props: Props) => { + const groupsWithPrivileges = props.subFeature + .getPrivilegeGroups() + .filter((group) => group.privileges.length > 0); + + if (groupsWithPrivileges.length === 0) { + return null; + } + return ( {props.subFeature.name} - {props.subFeature.getPrivilegeGroups().map(renderPrivilegeGroup)} + {groupsWithPrivileges.map(renderPrivilegeGroup)} ); diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 6f5a2a031a7b2..d16662922f696 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -45,6 +45,7 @@ describe('Security Plugin', () => { license: { isLicenseAvailable: expect.any(Function), isEnabled: expect.any(Function), + getType: expect.any(Function), getFeatures: expect.any(Function), features$: expect.any(Observable), }, @@ -73,6 +74,7 @@ describe('Security Plugin', () => { license: { isLicenseAvailable: expect.any(Function), isEnabled: expect.any(Function), + getType: expect.any(Function), getFeatures: expect.any(Function), features$: expect.any(Observable), }, diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts index bdf2c87f40f0b..4f545fd5d17a5 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -20,6 +20,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', }) ); @@ -72,6 +73,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', }) ); @@ -164,6 +166,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', predicate: (privilegeId) => privilegeId === 'all', }) ); @@ -270,6 +273,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: false, + licenseType: 'basic', }) ); @@ -394,6 +398,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', }) ); @@ -519,6 +524,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', }) ); @@ -645,6 +651,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', }) ); @@ -771,6 +778,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', }) ); @@ -818,6 +826,133 @@ describe('featurePrivilegeIterator', () => { ]); }); + it('excludes sub feature privileges when the minimum license is not met', () => { + const feature = new KibanaFeature({ + id: 'foo', + name: 'foo', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: { + api: ['all-api', 'read-api'], + app: ['foo'], + catalogue: ['foo-catalogue'], + management: { + section: ['foo-management'], + }, + savedObject: { + all: ['all-type'], + read: ['read-type'], + }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, + ui: ['ui-action'], + }, + read: { + api: ['read-api'], + app: ['foo'], + catalogue: ['foo-catalogue'], + management: { + section: ['foo-management'], + }, + savedObject: { + all: [], + read: ['read-type'], + }, + alerting: { + read: ['alerting-read-type'], + }, + ui: ['ui-action'], + }, + }, + subFeatures: [ + { + name: 'sub feature 1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'sub-feature-priv-1', + name: 'first sub feature privilege', + includeIn: 'all', + minimumLicense: 'gold', + api: ['sub-feature-api'], + app: ['sub-app'], + catalogue: ['sub-catalogue'], + management: { + section: ['other-sub-management'], + kibana: ['sub-management'], + }, + savedObject: { + all: ['all-sub-type'], + read: ['read-sub-type'], + }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, + ui: ['ui-sub-type'], + }, + ], + }, + ], + }, + ], + }); + + const actualPrivileges = Array.from( + featurePrivilegeIterator(feature, { + augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', + }) + ); + + expect(actualPrivileges).toEqual([ + { + privilegeId: 'all', + privilege: { + api: ['all-api', 'read-api'], + app: ['foo'], + catalogue: ['foo-catalogue'], + management: { + section: ['foo-management'], + }, + savedObject: { + all: ['all-type'], + read: ['read-type'], + }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, + ui: ['ui-action'], + }, + }, + { + privilegeId: 'read', + privilege: { + api: ['read-api'], + app: ['foo'], + catalogue: ['foo-catalogue'], + management: { + section: ['foo-management'], + }, + savedObject: { + all: [], + read: ['read-type'], + }, + alerting: { + read: ['alerting-read-type'], + }, + ui: ['ui-action'], + }, + }, + ]); + }); + it(`can augment primary feature privileges even if they don't specify their own`, () => { const feature = new KibanaFeature({ id: 'foo', @@ -878,6 +1013,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', }) ); @@ -995,6 +1131,7 @@ describe('featurePrivilegeIterator', () => { const actualPrivileges = Array.from( featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType: 'basic', }) ); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts index dba33f7a4f360..47216baea2025 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts @@ -5,11 +5,13 @@ */ import _ from 'lodash'; +import { LicenseType } from '../../../../../licensing/server'; import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { subFeaturePrivilegeIterator } from './sub_feature_privilege_iterator'; interface IteratorOptions { augmentWithSubFeaturePrivileges: boolean; + licenseType: LicenseType; predicate?: (privilegeId: string, privilege: FeatureKibanaPrivileges) => boolean; } @@ -25,7 +27,10 @@ export function* featurePrivilegeIterator( } if (options.augmentWithSubFeaturePrivileges) { - yield { privilegeId, privilege: mergeWithSubFeatures(privilegeId, privilege, feature) }; + yield { + privilegeId, + privilege: mergeWithSubFeatures(privilegeId, privilege, feature, options.licenseType), + }; } else { yield { privilegeId, privilege }; } @@ -35,10 +40,11 @@ export function* featurePrivilegeIterator( function mergeWithSubFeatures( privilegeId: string, privilege: FeatureKibanaPrivileges, - feature: KibanaFeature + feature: KibanaFeature, + licenseType: LicenseType ) { const mergedConfig = _.cloneDeep(privilege); - for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature)) { + for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature, licenseType)) { if (subFeaturePrivilege.includeIn !== 'read' && subFeaturePrivilege.includeIn !== privilegeId) { continue; } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts index d54b6d458d913..3a282eb8279f0 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LicenseType } from '../../../../../licensing/server'; + import { KibanaFeature, SubFeaturePrivilegeConfig } from '../../../../../features/common'; export function* subFeaturePrivilegeIterator( - feature: KibanaFeature + feature: KibanaFeature, + licenseType: LicenseType ): IterableIterator { for (const subFeature of feature.subFeatures) { for (const group of subFeature.privilegeGroups) { - yield* group.privileges; + yield* group.privileges.filter( + (privilege) => !privilege.minimumLicense || privilege.minimumLicense <= licenseType + ); } } } diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index c7b015b001ccf..d9b712025c064 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -48,6 +48,7 @@ describe('features', () => { const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesService, mockLicenseService); @@ -89,6 +90,7 @@ describe('features', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -177,6 +179,7 @@ describe('features', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -245,6 +248,7 @@ describe('features', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -368,6 +372,7 @@ describe('features', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -443,6 +448,7 @@ describe('features', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -508,6 +514,7 @@ describe('features', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -574,6 +581,7 @@ describe('features', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -633,6 +641,7 @@ describe('reserved', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -671,6 +680,7 @@ describe('reserved', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -737,6 +747,7 @@ describe('reserved', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -800,6 +811,7 @@ describe('subFeatures', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -928,6 +940,7 @@ describe('subFeatures', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -1135,6 +1148,7 @@ describe('subFeatures', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -1278,6 +1292,7 @@ describe('subFeatures', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -1446,6 +1461,7 @@ describe('subFeatures', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -1576,6 +1592,7 @@ describe('subFeatures', () => { }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: false }), + getType: jest.fn().mockReturnValue('basic'), }; const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); @@ -1705,4 +1722,502 @@ describe('subFeatures', () => { ]); }); }); + + describe(`when license allows subfeatures, but not a specific sub feature`, () => { + test(`should create minimal privileges, but not augment the primary feature privileges or create the disallowed sub-feature privileges`, () => { + const features: KibanaFeature[] = [ + new KibanaFeature({ + id: 'foo', + name: 'Foo KibanaFeature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + }, + subFeatures: [ + { + name: 'subFeature1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'subFeaturePriv1', + name: 'sub feature priv 1', + includeIn: 'read', + savedObject: { + all: ['all-sub-feature-type'], + read: ['read-sub-feature-type'], + }, + ui: ['sub-feature-ui'], + }, + ], + }, + { + groupType: 'independent', + privileges: [ + { + id: 'licensedSubFeaturePriv', + name: 'licensed sub feature priv', + includeIn: 'read', + minimumLicense: 'platinum', + savedObject: { + all: ['all-licensed-sub-feature-type'], + read: ['read-licensed-sub-feature-type'], + }, + ui: ['licensed-sub-feature-ui'], + }, + ], + }, + ], + }, + ], + }), + ]; + + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), + }; + const mockLicenseService = { + getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('gold'), + }; + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); + + const actual = privileges.get(); + expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`); + expect(actual.features).not.toHaveProperty(`foo.licensedSubFeaturePriv`); + + expect(actual.features).toHaveProperty(`foo.all`, [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + ]); + + expect(actual.features).toHaveProperty(`foo.minimal_all`, [ + actions.login, + actions.version, + actions.ui.get('foo', 'foo'), + ]); + + expect(actual.features).toHaveProperty(`foo.read`, [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + ]); + + expect(actual.features).toHaveProperty(`foo.minimal_read`, [ + actions.login, + actions.version, + actions.ui.get('foo', 'foo'), + ]); + + expect(actual).toHaveProperty('global.all', [ + actions.login, + actions.version, + actions.api.get('features'), + actions.space.manage, + actions.ui.get('spaces', 'manage'), + actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + ]); + expect(actual).toHaveProperty('global.read', [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + ]); + + expect(actual).toHaveProperty('space.all', [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + ]); + expect(actual).toHaveProperty('space.read', [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + ]); + }); + }); + + describe(`when license allows subfeatures, but and a licensed sub feature`, () => { + test(`should create minimal privileges, augment the primary feature privileges, and create the licensed sub-feature privileges`, () => { + const features: KibanaFeature[] = [ + new KibanaFeature({ + id: 'foo', + name: 'Foo KibanaFeature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + }, + subFeatures: [ + { + name: 'subFeature1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'subFeaturePriv1', + name: 'sub feature priv 1', + includeIn: 'read', + savedObject: { + all: ['all-sub-feature-type'], + read: ['read-sub-feature-type'], + }, + ui: ['sub-feature-ui'], + }, + ], + }, + { + groupType: 'independent', + privileges: [ + { + id: 'licensedSubFeaturePriv', + name: 'licensed sub feature priv', + includeIn: 'read', + minimumLicense: 'platinum', + savedObject: { + all: ['all-licensed-sub-feature-type'], + read: ['read-licensed-sub-feature-type'], + }, + ui: ['licensed-sub-feature-ui'], + }, + ], + }, + ], + }, + ], + }), + ]; + + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), + }; + const mockLicenseService = { + getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), + getType: jest.fn().mockReturnValue('platinum'), + }; + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); + + const actual = privileges.get(); + expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`); + expect(actual.features).toHaveProperty(`foo.licensedSubFeaturePriv`); + + expect(actual.features).toHaveProperty(`foo.all`, [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'find'), + actions.savedObject.get('all-licensed-sub-feature-type', 'create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'delete'), + actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + actions.ui.get('foo', 'licensed-sub-feature-ui'), + ]); + + expect(actual.features).toHaveProperty(`foo.minimal_all`, [ + actions.login, + actions.version, + actions.ui.get('foo', 'foo'), + ]); + + expect(actual.features).toHaveProperty(`foo.read`, [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'find'), + actions.savedObject.get('all-licensed-sub-feature-type', 'create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'delete'), + actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + actions.ui.get('foo', 'licensed-sub-feature-ui'), + ]); + + expect(actual.features).toHaveProperty(`foo.minimal_read`, [ + actions.login, + actions.version, + actions.ui.get('foo', 'foo'), + ]); + + expect(actual).toHaveProperty('global.all', [ + actions.login, + actions.version, + actions.api.get('features'), + actions.space.manage, + actions.ui.get('spaces', 'manage'), + actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'find'), + actions.savedObject.get('all-licensed-sub-feature-type', 'create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'delete'), + actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + actions.ui.get('foo', 'licensed-sub-feature-ui'), + ]); + expect(actual).toHaveProperty('global.read', [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'find'), + actions.savedObject.get('all-licensed-sub-feature-type', 'create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'delete'), + actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + actions.ui.get('foo', 'licensed-sub-feature-ui'), + ]); + + expect(actual).toHaveProperty('space.all', [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'find'), + actions.savedObject.get('all-licensed-sub-feature-type', 'create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'delete'), + actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + actions.ui.get('foo', 'licensed-sub-feature-ui'), + ]); + expect(actual).toHaveProperty('space.read', [ + actions.login, + actions.version, + actions.savedObject.get('all-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-sub-feature-type', 'get'), + actions.savedObject.get('all-sub-feature-type', 'find'), + actions.savedObject.get('all-sub-feature-type', 'create'), + actions.savedObject.get('all-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-sub-feature-type', 'update'), + actions.savedObject.get('all-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-sub-feature-type', 'delete'), + actions.savedObject.get('all-sub-feature-type', 'share_to_space'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'get'), + actions.savedObject.get('all-licensed-sub-feature-type', 'find'), + actions.savedObject.get('all-licensed-sub-feature-type', 'create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_create'), + actions.savedObject.get('all-licensed-sub-feature-type', 'update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'bulk_update'), + actions.savedObject.get('all-licensed-sub-feature-type', 'delete'), + actions.savedObject.get('all-licensed-sub-feature-type', 'share_to_space'), + actions.savedObject.get('read-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-sub-feature-type', 'get'), + actions.savedObject.get('read-sub-feature-type', 'find'), + actions.savedObject.get('read-licensed-sub-feature-type', 'bulk_get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'get'), + actions.savedObject.get('read-licensed-sub-feature-type', 'find'), + actions.ui.get('foo', 'foo'), + actions.ui.get('foo', 'sub-feature-ui'), + actions.ui.get('foo', 'licensed-sub-feature-ui'), + ]); + }); + }); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index 24b46222e7f35..1b2b0cd8a0a52 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -25,7 +25,7 @@ export interface PrivilegesService { export function privilegesFactory( actions: Actions, featuresService: FeaturesPluginSetup, - licenseService: Pick + licenseService: Pick ) { const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions); @@ -33,6 +33,7 @@ export function privilegesFactory( get() { const features = featuresService.getKibanaFeatures(); const { allowSubFeaturePrivileges } = licenseService.getFeatures(); + const licenseType = licenseService.getType()!; const basePrivilegeFeatures = features.filter( (feature) => !feature.excludeFromBasePrivileges ); @@ -43,6 +44,7 @@ export function privilegesFactory( basePrivilegeFeatures.forEach((feature) => { for (const { privilegeId, privilege } of featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType, predicate: (pId, featurePrivilege) => !featurePrivilege.excludeFromBasePrivileges, })) { const privilegeActions = featurePrivilegeBuilder.getActions(privilege, feature); @@ -61,6 +63,7 @@ export function privilegesFactory( featurePrivileges[feature.id] = {}; for (const featurePrivilege of featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: true, + licenseType, })) { featurePrivileges[feature.id][featurePrivilege.privilegeId] = [ actions.login, @@ -72,6 +75,7 @@ export function privilegesFactory( if (allowSubFeaturePrivileges && feature.subFeatures?.length > 0) { for (const featurePrivilege of featurePrivilegeIterator(feature, { augmentWithSubFeaturePrivileges: false, + licenseType, })) { featurePrivileges[feature.id][`minimal_${featurePrivilege.privilegeId}`] = [ actions.login, @@ -80,7 +84,7 @@ export function privilegesFactory( ]; } - for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature)) { + for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature, licenseType)) { featurePrivileges[feature.id][subFeaturePrivilege.id] = [ actions.login, actions.version, diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 9b08ba8c275fd..cf9a30b0b3857 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -110,6 +110,7 @@ describe('Security Plugin', () => { }, }, "getFeatures": [Function], + "getType": [Function], "isEnabled": [Function], "isLicenseAvailable": [Function], }, From 32c3676b3d22bc5e13d84b3dae15a60191dadea4 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 16 Nov 2020 14:54:33 -0500 Subject: [PATCH 08/99] [Maps] Split out agg-descriptors (#83294) Splits the AggDescriptor into multiple types, to better distinguish Counts from aggs with fields. Corresponding split in agg-classes. --- .../source_descriptor_types.ts | 22 ++- .../maps/common/migrations/join_agg_key.ts | 2 +- .../classes/fields/agg/agg_field.test.ts | 60 ++++++ .../public/classes/fields/agg/agg_field.ts | 78 ++++++++ .../classes/fields/agg/agg_field_types.ts | 22 +++ .../fields/agg/count_agg_field.test.ts | 45 +++++ .../classes/fields/agg/count_agg_field.ts | 100 ++++++++++ .../classes/fields/agg/es_agg_factory.test.ts | 51 ++++++ .../classes/fields/agg/es_agg_factory.ts | 51 ++++++ .../maps/public/classes/fields/agg/index.ts | 8 + .../{ => agg}/top_term_percentage_field.ts | 9 +- .../classes/fields/es_agg_field.test.ts | 80 -------- .../public/classes/fields/es_agg_field.ts | 171 ------------------ .../create_choropleth_layer_descriptor.ts | 6 +- .../create_region_map_layer_descriptor.ts | 2 +- .../create_tile_map_layer_descriptor.ts | 2 +- .../maps/public/classes/layers/layer.tsx | 4 +- .../observability/create_layer_descriptor.ts | 4 +- .../es_agg_source/es_agg_source.test.ts | 2 +- .../sources/es_agg_source/es_agg_source.ts | 3 +- .../metrics_editor/metric_editor.tsx | 29 +-- 21 files changed, 465 insertions(+), 286 deletions(-) create mode 100644 x-pack/plugins/maps/public/classes/fields/agg/agg_field.test.ts create mode 100644 x-pack/plugins/maps/public/classes/fields/agg/agg_field.ts create mode 100644 x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts create mode 100644 x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.test.ts create mode 100644 x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts create mode 100644 x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.test.ts create mode 100644 x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts create mode 100644 x-pack/plugins/maps/public/classes/fields/agg/index.ts rename x-pack/plugins/maps/public/classes/fields/{ => agg}/top_term_percentage_field.ts (86%) delete mode 100644 x-pack/plugins/maps/public/classes/fields/es_agg_field.test.ts delete mode 100644 x-pack/plugins/maps/public/classes/fields/es_agg_field.ts diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index a6afbe4d55f9b..c11ee59768a91 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -40,12 +40,28 @@ export type AbstractESSourceDescriptor = AbstractSourceDescriptor & { applyGlobalTime: boolean; }; -export type AggDescriptor = { - field?: string; // count aggregation does not require field. All other aggregation types do - label?: string; +type AbstractAggDescriptor = { type: AGG_TYPE; + label?: string; +}; + +export type CountAggDescriptor = AbstractAggDescriptor & { + type: AGG_TYPE.COUNT; }; +export type FieldedAggDescriptor = AbstractAggDescriptor & { + type: + | AGG_TYPE.UNIQUE_COUNT + | AGG_TYPE.MAX + | AGG_TYPE.MIN + | AGG_TYPE.SUM + | AGG_TYPE.AVG + | AGG_TYPE.TERMS; + field?: string; +}; + +export type AggDescriptor = CountAggDescriptor | FieldedAggDescriptor; + export type AbstractESAggSourceDescriptor = AbstractESSourceDescriptor & { metrics: AggDescriptor[]; }; diff --git a/x-pack/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.ts index 4dc70d3c0fa22..b961637218dd6 100644 --- a/x-pack/plugins/maps/common/migrations/join_agg_key.ts +++ b/x-pack/plugins/maps/common/migrations/join_agg_key.ts @@ -82,7 +82,7 @@ export function migrateJoinAggKey({ _.get(joinDescriptor, 'right.metrics', []).forEach((aggDescriptor: AggDescriptor) => { const legacyAggKey = getLegacyAggKey({ aggType: aggDescriptor.type, - aggFieldName: aggDescriptor.field, + aggFieldName: 'field' in aggDescriptor ? aggDescriptor.field : undefined, indexPatternTitle: _.get(joinDescriptor, 'right.indexPatternTitle', ''), termFieldName: _.get(joinDescriptor, 'right.term', ''), }); diff --git a/x-pack/plugins/maps/public/classes/fields/agg/agg_field.test.ts b/x-pack/plugins/maps/public/classes/fields/agg/agg_field.test.ts new file mode 100644 index 0000000000000..49a599326d54c --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/agg/agg_field.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AggField } from './agg_field'; +import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; +import { IESAggSource } from '../../sources/es_agg_source'; +import { IIndexPattern } from 'src/plugins/data/public'; + +const mockIndexPattern = { + title: 'wildIndex', + fields: [ + { + name: 'foo*', + }, + ], +} as IIndexPattern; + +const mockEsAggSource = { + getAggKey: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_key'; + }, + getAggLabel: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_label'; + }, + getIndexPattern: async () => { + return mockIndexPattern; + }, +} as IESAggSource; + +const defaultParams = { + label: 'my agg field', + source: mockEsAggSource, + origin: FIELD_ORIGIN.SOURCE, +}; + +describe('supportsFieldMeta', () => { + test('Non-counting aggregations should support field meta', () => { + const avgMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.AVG }); + expect(avgMetric.supportsFieldMeta()).toBe(true); + const maxMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.MAX }); + expect(maxMetric.supportsFieldMeta()).toBe(true); + const minMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.MIN }); + expect(minMetric.supportsFieldMeta()).toBe(true); + const termsMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.TERMS }); + expect(termsMetric.supportsFieldMeta()).toBe(true); + }); + + test('Counting aggregations should not support field meta', () => { + const sumMetric = new AggField({ ...defaultParams, aggType: AGG_TYPE.SUM }); + expect(sumMetric.supportsFieldMeta()).toBe(false); + const uniqueCountMetric = new AggField({ + ...defaultParams, + aggType: AGG_TYPE.UNIQUE_COUNT, + }); + expect(uniqueCountMetric.supportsFieldMeta()).toBe(false); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/fields/agg/agg_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/agg_field.ts new file mode 100644 index 0000000000000..31595327a64b8 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/agg/agg_field.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndexPattern } from 'src/plugins/data/public'; +import { AGG_TYPE } from '../../../../common/constants'; +import { CountAggField } from './count_agg_field'; +import { isMetricCountable } from '../../util/is_metric_countable'; +import { CountAggFieldParams } from './agg_field_types'; +import { addFieldToDSL, getField } from '../../../../common/elasticsearch_util'; +import { IField } from '../field'; + +const TERMS_AGG_SHARD_SIZE = 5; + +export interface AggFieldParams extends CountAggFieldParams { + esDocField?: IField; + aggType: AGG_TYPE; +} + +export class AggField extends CountAggField { + private readonly _esDocField?: IField; + private readonly _aggType: AGG_TYPE; + + constructor(params: AggFieldParams) { + super(params); + this._esDocField = params.esDocField; + this._aggType = params.aggType; + } + + isValid(): boolean { + return !!this._esDocField; + } + + supportsFieldMeta(): boolean { + // count and sum aggregations are not within field bounds so they do not support field meta. + return !isMetricCountable(this._getAggType()); + } + + canValueBeFormatted(): boolean { + return this._getAggType() !== AGG_TYPE.UNIQUE_COUNT; + } + + _getAggType(): AGG_TYPE { + return this._aggType; + } + + getValueAggDsl(indexPattern: IndexPattern): unknown { + const field = getField(indexPattern, this.getRootName()); + const aggType = this._getAggType(); + const aggBody = aggType === AGG_TYPE.TERMS ? { size: 1, shard_size: TERMS_AGG_SHARD_SIZE } : {}; + return { + [aggType]: addFieldToDSL(aggBody, field), + }; + } + + getRootName(): string { + return this._esDocField ? this._esDocField.getName() : ''; + } + + async getDataType(): Promise { + return this._getAggType() === AGG_TYPE.TERMS ? 'string' : 'number'; + } + + getBucketCount(): number { + // terms aggregation increases the overall number of buckets per split bucket + return this._getAggType() === AGG_TYPE.TERMS ? TERMS_AGG_SHARD_SIZE : 0; + } + + async getOrdinalFieldMetaRequest(): Promise { + return this._esDocField ? await this._esDocField.getOrdinalFieldMetaRequest() : null; + } + + async getCategoricalFieldMetaRequest(size: number): Promise { + return this._esDocField ? await this._esDocField.getCategoricalFieldMetaRequest(size) : null; + } +} diff --git a/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts b/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts new file mode 100644 index 0000000000000..74f03a3f31909 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IField } from '../field'; +import { IndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { IESAggSource } from '../../sources/es_agg_source'; +import { FIELD_ORIGIN } from '../../../../common/constants'; + +export interface IESAggField extends IField { + getValueAggDsl(indexPattern: IndexPattern): unknown | null; + getBucketCount(): number; +} + +export interface CountAggFieldParams { + label?: string; + source: IESAggSource; + origin: FIELD_ORIGIN; + canReadFromGeoJson?: boolean; +} diff --git a/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.test.ts b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.test.ts new file mode 100644 index 0000000000000..a313b59643c34 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CountAggField } from './count_agg_field'; +import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; +import { IESAggSource } from '../../sources/es_agg_source'; +import { IIndexPattern } from 'src/plugins/data/public'; + +const mockIndexPattern = { + title: 'wildIndex', + fields: [ + { + name: 'foo*', + }, + ], +} as IIndexPattern; + +const mockEsAggSource = { + getAggKey: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_key'; + }, + getAggLabel: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_label'; + }, + getIndexPattern: async () => { + return mockIndexPattern; + }, +} as IESAggSource; + +const defaultParams = { + label: 'my agg field', + source: mockEsAggSource, + aggType: AGG_TYPE.COUNT, + origin: FIELD_ORIGIN.SOURCE, +}; + +describe('supportsFieldMeta', () => { + test('Counting aggregations should not support field meta', () => { + const countMetric = new CountAggField({ ...defaultParams }); + expect(countMetric.supportsFieldMeta()).toBe(false); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts new file mode 100644 index 0000000000000..a4562c91e92a6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndexPattern } from 'src/plugins/data/public'; +import { IESAggSource } from '../../sources/es_agg_source'; +import { IVectorSource } from '../../sources/vector_source'; +import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; +import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; +import { ESAggTooltipProperty } from '../../tooltips/es_agg_tooltip_property'; +import { IESAggField, CountAggFieldParams } from './agg_field_types'; + +// Agg without field. Essentially a count-aggregation. +export class CountAggField implements IESAggField { + private readonly _source: IESAggSource; + private readonly _origin: FIELD_ORIGIN; + private readonly _label?: string; + private readonly _canReadFromGeoJson: boolean; + + constructor({ label, source, origin, canReadFromGeoJson = true }: CountAggFieldParams) { + this._source = source; + this._origin = origin; + this._label = label; + this._canReadFromGeoJson = canReadFromGeoJson; + } + + _getAggType(): AGG_TYPE { + return AGG_TYPE.COUNT; + } + + getSource(): IVectorSource { + return this._source; + } + + getOrigin(): FIELD_ORIGIN { + return this._origin; + } + + getName(): string { + return this._source.getAggKey(this._getAggType(), this.getRootName()); + } + + getRootName(): string { + return ''; + } + + async getLabel(): Promise { + return this._label + ? this._label + : this._source.getAggLabel(this._getAggType(), this.getRootName()); + } + + isValid(): boolean { + return true; + } + + async getDataType(): Promise { + return 'number'; + } + + async createTooltipProperty(value: string | string[] | undefined): Promise { + const indexPattern = await this._source.getIndexPattern(); + const tooltipProperty = new TooltipProperty(this.getName(), await this.getLabel(), value); + return new ESAggTooltipProperty(tooltipProperty, indexPattern, this, this._getAggType()); + } + + getValueAggDsl(indexPattern: IndexPattern): unknown | null { + return null; + } + + supportsFieldMeta(): boolean { + return false; + } + + getBucketCount() { + return 0; + } + + canValueBeFormatted(): boolean { + return false; + } + + async getOrdinalFieldMetaRequest(): Promise { + return null; + } + + async getCategoricalFieldMetaRequest(size: number): Promise { + return null; + } + + supportsAutoDomain(): boolean { + return this._canReadFromGeoJson ? true : this.supportsFieldMeta(); + } + + canReadFromGeoJson(): boolean { + return this._canReadFromGeoJson; + } +} diff --git a/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.test.ts b/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.test.ts new file mode 100644 index 0000000000000..cb0bb51e374fb --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { esAggFieldsFactory } from './es_agg_factory'; +import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; +import { IESAggSource } from '../../sources/es_agg_source'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; + +const mockIndexPattern = { + title: 'wildIndex', + fields: [ + { + name: 'foo*', + }, + ], +} as IIndexPattern; + +const mockEsAggSource = { + getAggKey: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_key'; + }, + getAggLabel: (aggType: AGG_TYPE, fieldName: string) => { + return 'agg_label'; + }, + getIndexPattern: async () => { + return mockIndexPattern; + }, +} as IESAggSource; + +describe('esAggFieldsFactory', () => { + test('Should only create top terms field when term field is not provided', () => { + const fields = esAggFieldsFactory( + { type: AGG_TYPE.TERMS }, + mockEsAggSource, + FIELD_ORIGIN.SOURCE + ); + expect(fields.length).toBe(1); + }); + + test('Should create top terms and top terms percentage fields', () => { + const fields = esAggFieldsFactory( + { type: AGG_TYPE.TERMS, field: 'myField' }, + mockEsAggSource, + FIELD_ORIGIN.SOURCE + ); + expect(fields.length).toBe(2); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts b/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts new file mode 100644 index 0000000000000..a734432d03ca2 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AggDescriptor } from '../../../../common/descriptor_types'; +import { IESAggSource } from '../../sources/es_agg_source'; +import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; +import { ESDocField } from '../es_doc_field'; +import { TopTermPercentageField } from './top_term_percentage_field'; +import { CountAggField } from './count_agg_field'; +import { IESAggField } from './agg_field_types'; +import { AggField } from './agg_field'; + +export function esAggFieldsFactory( + aggDescriptor: AggDescriptor, + source: IESAggSource, + origin: FIELD_ORIGIN, + canReadFromGeoJson: boolean = true +): IESAggField[] { + let aggField; + if (aggDescriptor.type === AGG_TYPE.COUNT) { + aggField = new CountAggField({ + label: aggDescriptor.label, + source, + origin, + canReadFromGeoJson, + }); + } else { + aggField = new AggField({ + label: aggDescriptor.label, + esDocField: + 'field' in aggDescriptor && aggDescriptor.field + ? new ESDocField({ fieldName: aggDescriptor.field, source, origin }) + : undefined, + aggType: aggDescriptor.type, + source, + origin, + canReadFromGeoJson, + }); + } + + const aggFields: IESAggField[] = [aggField]; + + if ('field' in aggDescriptor && aggDescriptor.type === AGG_TYPE.TERMS) { + aggFields.push(new TopTermPercentageField(aggField, canReadFromGeoJson)); + } + + return aggFields; +} diff --git a/x-pack/plugins/maps/public/classes/fields/agg/index.ts b/x-pack/plugins/maps/public/classes/fields/agg/index.ts new file mode 100644 index 0000000000000..e6aeb7ba9e7b1 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/fields/agg/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { esAggFieldsFactory } from './es_agg_factory'; +export { IESAggField } from './agg_field_types'; diff --git a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts similarity index 86% rename from x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts rename to x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts index 50db04d08b2aa..e3d62afaca921 100644 --- a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IESAggField } from './es_agg_field'; -import { IVectorSource } from '../sources/vector_source'; -import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property'; -import { TOP_TERM_PERCENTAGE_SUFFIX } from '../../../common/constants'; -import { FIELD_ORIGIN } from '../../../common/constants'; +import { IESAggField } from './agg_field_types'; +import { IVectorSource } from '../../sources/vector_source'; +import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; +import { TOP_TERM_PERCENTAGE_SUFFIX, FIELD_ORIGIN } from '../../../../common/constants'; export class TopTermPercentageField implements IESAggField { private readonly _topTermAggField: IESAggField; diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.test.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.test.ts deleted file mode 100644 index 7a65b5f9f6b46..0000000000000 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.test.ts +++ /dev/null @@ -1,80 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ESAggField, esAggFieldsFactory } from './es_agg_field'; -import { AGG_TYPE, FIELD_ORIGIN } from '../../../common/constants'; -import { IESAggSource } from '../sources/es_agg_source'; -import { IIndexPattern } from 'src/plugins/data/public'; - -const mockIndexPattern = { - title: 'wildIndex', - fields: [ - { - name: 'foo*', - }, - ], -} as IIndexPattern; - -const mockEsAggSource = { - getAggKey: (aggType: AGG_TYPE, fieldName: string) => { - return 'agg_key'; - }, - getAggLabel: (aggType: AGG_TYPE, fieldName: string) => { - return 'agg_label'; - }, - getIndexPattern: async () => { - return mockIndexPattern; - }, -} as IESAggSource; - -const defaultParams = { - label: 'my agg field', - source: mockEsAggSource, - aggType: AGG_TYPE.COUNT, - origin: FIELD_ORIGIN.SOURCE, -}; - -describe('supportsFieldMeta', () => { - test('Non-counting aggregations should support field meta', () => { - const avgMetric = new ESAggField({ ...defaultParams, aggType: AGG_TYPE.AVG }); - expect(avgMetric.supportsFieldMeta()).toBe(true); - const maxMetric = new ESAggField({ ...defaultParams, aggType: AGG_TYPE.MAX }); - expect(maxMetric.supportsFieldMeta()).toBe(true); - const minMetric = new ESAggField({ ...defaultParams, aggType: AGG_TYPE.MIN }); - expect(minMetric.supportsFieldMeta()).toBe(true); - const termsMetric = new ESAggField({ ...defaultParams, aggType: AGG_TYPE.TERMS }); - expect(termsMetric.supportsFieldMeta()).toBe(true); - }); - - test('Counting aggregations should not support field meta', () => { - const countMetric = new ESAggField({ ...defaultParams, aggType: AGG_TYPE.COUNT }); - expect(countMetric.supportsFieldMeta()).toBe(false); - const sumMetric = new ESAggField({ ...defaultParams, aggType: AGG_TYPE.SUM }); - expect(sumMetric.supportsFieldMeta()).toBe(false); - const uniqueCountMetric = new ESAggField({ ...defaultParams, aggType: AGG_TYPE.UNIQUE_COUNT }); - expect(uniqueCountMetric.supportsFieldMeta()).toBe(false); - }); -}); - -describe('esAggFieldsFactory', () => { - test('Should only create top terms field when term field is not provided', () => { - const fields = esAggFieldsFactory( - { type: AGG_TYPE.TERMS }, - mockEsAggSource, - FIELD_ORIGIN.SOURCE - ); - expect(fields.length).toBe(1); - }); - - test('Should create top terms and top terms percentage fields', () => { - const fields = esAggFieldsFactory( - { type: AGG_TYPE.TERMS, field: 'myField' }, - mockEsAggSource, - FIELD_ORIGIN.SOURCE - ); - expect(fields.length).toBe(2); - }); -}); diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts deleted file mode 100644 index 8cff98205186f..0000000000000 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ /dev/null @@ -1,171 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { IndexPattern } from 'src/plugins/data/public'; -import { IField } from './field'; -import { AggDescriptor } from '../../../common/descriptor_types'; -import { IESAggSource } from '../sources/es_agg_source'; -import { IVectorSource } from '../sources/vector_source'; -import { ESDocField } from './es_doc_field'; -import { AGG_TYPE, FIELD_ORIGIN } from '../../../common/constants'; -import { isMetricCountable } from '../util/is_metric_countable'; -import { getField, addFieldToDSL } from '../../../common/elasticsearch_util'; -import { TopTermPercentageField } from './top_term_percentage_field'; -import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property'; -import { ESAggTooltipProperty } from '../tooltips/es_agg_tooltip_property'; - -const TERMS_AGG_SHARD_SIZE = 5; - -export interface IESAggField extends IField { - getValueAggDsl(indexPattern: IndexPattern): unknown | null; - getBucketCount(): number; -} - -export class ESAggField implements IESAggField { - private readonly _source: IESAggSource; - private readonly _origin: FIELD_ORIGIN; - private readonly _label?: string; - private readonly _aggType: AGG_TYPE; - private readonly _esDocField?: IField | undefined; - private readonly _canReadFromGeoJson: boolean; - - constructor({ - label, - source, - aggType, - esDocField, - origin, - canReadFromGeoJson = true, - }: { - label?: string; - source: IESAggSource; - aggType: AGG_TYPE; - esDocField?: IField; - origin: FIELD_ORIGIN; - canReadFromGeoJson?: boolean; - }) { - this._source = source; - this._origin = origin; - this._label = label; - this._aggType = aggType; - this._esDocField = esDocField; - this._canReadFromGeoJson = canReadFromGeoJson; - } - - getSource(): IVectorSource { - return this._source; - } - - getOrigin(): FIELD_ORIGIN { - return this._origin; - } - - getName(): string { - return this._source.getAggKey(this.getAggType(), this.getRootName()); - } - - getRootName(): string { - return this._getESDocFieldName(); - } - - async getLabel(): Promise { - return this._label - ? this._label - : this._source.getAggLabel(this.getAggType(), this.getRootName()); - } - - getAggType(): AGG_TYPE { - return this._aggType; - } - - isValid(): boolean { - return this.getAggType() === AGG_TYPE.COUNT ? true : !!this._esDocField; - } - - async getDataType(): Promise { - return this.getAggType() === AGG_TYPE.TERMS ? 'string' : 'number'; - } - - _getESDocFieldName(): string { - return this._esDocField ? this._esDocField.getName() : ''; - } - - async createTooltipProperty(value: string | string[] | undefined): Promise { - const indexPattern = await this._source.getIndexPattern(); - const tooltipProperty = new TooltipProperty(this.getName(), await this.getLabel(), value); - return new ESAggTooltipProperty(tooltipProperty, indexPattern, this, this.getAggType()); - } - - getValueAggDsl(indexPattern: IndexPattern): unknown | null { - if (this.getAggType() === AGG_TYPE.COUNT) { - return null; - } - - const field = getField(indexPattern, this.getRootName()); - const aggType = this.getAggType(); - const aggBody = aggType === AGG_TYPE.TERMS ? { size: 1, shard_size: TERMS_AGG_SHARD_SIZE } : {}; - return { - [aggType]: addFieldToDSL(aggBody, field), - }; - } - - getBucketCount(): number { - // terms aggregation increases the overall number of buckets per split bucket - return this.getAggType() === AGG_TYPE.TERMS ? TERMS_AGG_SHARD_SIZE : 0; - } - - supportsFieldMeta(): boolean { - // count and sum aggregations are not within field bounds so they do not support field meta. - return !isMetricCountable(this.getAggType()); - } - - canValueBeFormatted(): boolean { - // Do not use field formatters for counting metrics - return ![AGG_TYPE.COUNT, AGG_TYPE.UNIQUE_COUNT].includes(this.getAggType()); - } - - async getOrdinalFieldMetaRequest(): Promise { - return this._esDocField ? this._esDocField.getOrdinalFieldMetaRequest() : null; - } - - async getCategoricalFieldMetaRequest(size: number): Promise { - return this._esDocField ? this._esDocField.getCategoricalFieldMetaRequest(size) : null; - } - - supportsAutoDomain(): boolean { - return this._canReadFromGeoJson ? true : this.supportsFieldMeta(); - } - - canReadFromGeoJson(): boolean { - return this._canReadFromGeoJson; - } -} - -export function esAggFieldsFactory( - aggDescriptor: AggDescriptor, - source: IESAggSource, - origin: FIELD_ORIGIN, - canReadFromGeoJson: boolean = true -): IESAggField[] { - const aggField = new ESAggField({ - label: aggDescriptor.label, - esDocField: aggDescriptor.field - ? new ESDocField({ fieldName: aggDescriptor.field, source, origin }) - : undefined, - aggType: aggDescriptor.type, - source, - origin, - canReadFromGeoJson, - }); - - const aggFields: IESAggField[] = [aggField]; - - if (aggDescriptor.field && aggDescriptor.type === AGG_TYPE.TERMS) { - aggFields.push(new TopTermPercentageField(aggField, canReadFromGeoJson)); - } - - return aggFields; -} diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts index 43d1d39c170c0..cdfe60946f5f9 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts @@ -16,8 +16,8 @@ import { } from '../../../../common/constants'; import { getJoinAggKey } from '../../../../common/get_agg_key'; import { - AggDescriptor, ColorDynamicOptions, + CountAggDescriptor, EMSFileSourceDescriptor, ESSearchSourceDescriptor, } from '../../../../common/descriptor_types'; @@ -43,11 +43,11 @@ function createChoroplethLayerDescriptor({ rightIndexPatternTitle: string; rightTermField: string; }) { - const metricsDescriptor: AggDescriptor = { type: AGG_TYPE.COUNT }; + const metricsDescriptor: CountAggDescriptor = { type: AGG_TYPE.COUNT }; const joinId = uuid(); const joinKey = getJoinAggKey({ aggType: metricsDescriptor.type, - aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '', + aggFieldName: '', rightSourceId: joinId, }); return VectorLayer.createDescriptor({ diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts index 8830831b8b656..6f9bb686459b5 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -69,7 +69,7 @@ export function createRegionMapLayerDescriptor({ const joinId = uuid(); const joinKey = getJoinAggKey({ aggType: metricsDescriptor.type, - aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '', + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', rightSourceId: joinId, }); const colorPallette = NUMERICAL_COLOR_PALETTES.find((pallette) => { diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts index 05a8620e436d5..5b89373f2db48 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts @@ -103,7 +103,7 @@ export function createTileMapLayerDescriptor({ const metricSourceKey = getSourceAggKey({ aggType: metricsDescriptor.type, - aggFieldName: metricsDescriptor.field, + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', }); const metricStyleField = { name: metricSourceKey, diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 7c76df7f6e877..b982e6452e8cb 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -173,12 +173,12 @@ export class AbstractLayer implements ILayer { metrics.forEach((metricsDescriptor: AggDescriptor) => { const originalJoinKey = getJoinAggKey({ aggType: metricsDescriptor.type, - aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '', + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', rightSourceId: originalJoinId, }); const newJoinKey = getJoinAggKey({ aggType: metricsDescriptor.type, - aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '', + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', rightSourceId: joinDescriptor.right.id!, }); diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts index 5dbf07ed2a535..dea551866f4a9 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts @@ -161,7 +161,7 @@ export function createLayerDescriptor({ const joinId = uuid(); const joinKey = getJoinAggKey({ aggType: metricsDescriptor.type, - aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '', + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : '', rightSourceId: joinId, }); return VectorLayer.createDescriptor({ @@ -219,7 +219,7 @@ export function createLayerDescriptor({ const metricSourceKey = getSourceAggKey({ aggType: metricsDescriptor.type, - aggFieldName: metricsDescriptor.field, + aggFieldName: 'field' in metricsDescriptor ? metricsDescriptor.field : undefined, }); const metricStyleField = { name: metricSourceKey, diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts index d31e8366e4ef4..a731fcee3f6f5 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts @@ -6,7 +6,7 @@ import { AbstractESAggSource } from '../es_agg_source'; import { IField } from '../../fields/field'; -import { IESAggField } from '../../fields/es_agg_field'; +import { IESAggField } from '../../fields/agg'; import _ from 'lodash'; import { AGG_TYPE } from '../../../../common/constants'; import { AggDescriptor } from '../../../../common/descriptor_types'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts index dc95632032fa9..b88ae9a4727a8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts @@ -9,9 +9,8 @@ import { Adapters } from 'src/plugins/inspector/public'; import { GeoJsonProperties } from 'geojson'; import { IESSource } from '../es_source'; import { AbstractESSource } from '../es_source'; -import { esAggFieldsFactory } from '../../fields/es_agg_field'; +import { esAggFieldsFactory, IESAggField } from '../../fields/agg'; import { AGG_TYPE, COUNT_PROP_LABEL, FIELD_ORIGIN } from '../../../../common/constants'; -import { IESAggField } from '../../fields/es_agg_field'; import { getSourceAggKey } from '../../../../common/get_agg_key'; import { AbstractESAggSourceDescriptor, AggDescriptor } from '../../../../common/descriptor_types'; import { IndexPattern } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx index 543d144efdcc7..61d8a143549e1 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx @@ -57,30 +57,31 @@ export function MetricEditor({ if (!metricAggregationType) { return; } - const newMetricProps = { - ...metric, + + const descriptor = { type: metricAggregationType, + label: metric.label, }; - // unset field when new agg type does not support currently selected field. - if (metric.field && metricAggregationType !== AGG_TYPE.COUNT) { - const fieldsForNewAggType = filterFieldsForAgg(fields, metricAggregationType); - const found = fieldsForNewAggType.find((field) => { - return field.name === metric.field; - }); - if (!found) { - newMetricProps.field = undefined; - } + if (metricAggregationType === AGG_TYPE.COUNT || !('field' in metric) || !metric.field) { + onChange(descriptor); + return; } - onChange(newMetricProps); + const fieldsForNewAggType = filterFieldsForAgg(fields, metricAggregationType); + const found = fieldsForNewAggType.find((field) => field.name === metric.field); + onChange({ + ...descriptor, + field: found ? metric.field : undefined, + }); }; const onFieldChange = (fieldName?: string) => { - if (!fieldName) { + if (!fieldName || metric.type === AGG_TYPE.COUNT) { return; } onChange({ - ...metric, + label: metric.label, + type: metric.type, field: fieldName, }); }; From dac35cfcfe565d063c6cd09a3ebf3b6f6cc36024 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 16 Nov 2020 14:44:43 -0600 Subject: [PATCH 09/99] [deb/rpm] Create PID folder during installation (#83351) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../build/tasks/os_packages/package_scripts/post_install.sh | 1 + src/dev/build/tasks/os_packages/run_fpm.ts | 4 ++++ .../service_templates/systemd/run/kibana/.gitempty | 0 3 files changed, 5 insertions(+) create mode 100644 src/dev/build/tasks/os_packages/service_templates/systemd/run/kibana/.gitempty diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh index 728278dae746b..6eb111e066c83 100644 --- a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh +++ b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh @@ -17,6 +17,7 @@ set_chmod() { set_chown() { chown <%= user %>:<%= group %> <%= logDir %> + chown <%= user %>:<%= group %> <%= pidDir %> chown -R <%= user %>:<%= group %> <%= dataDir %> chown -R root:<%= group %> ${KBN_PATH_CONF} } diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index f16eaea1daa2f..7dff592eb9b83 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -112,6 +112,8 @@ export async function runFpm( '--template-value', `logDir=/var/log/kibana`, '--template-value', + `pidDir=/run/kibana`, + '--template-value', `envFile=/etc/default/kibana`, // config and data directories are copied to /usr/share and /var/lib // below, so exclude them from the main package source located in @@ -120,6 +122,8 @@ export async function runFpm( `usr/share/kibana/config`, '--exclude', `usr/share/kibana/data`, + '--exclude', + 'run/kibana/.gitempty', // flags specific to the package we are building, supplied by tasks below ...pkgSpecificFlags, diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/run/kibana/.gitempty b/src/dev/build/tasks/os_packages/service_templates/systemd/run/kibana/.gitempty new file mode 100644 index 0000000000000..e69de29bb2d1d From c6e984d7b22fbce4c560e7dc8876cee3ca734d43 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 16 Nov 2020 15:05:52 -0600 Subject: [PATCH 10/99] [Search] Fixes EQL search strategy (#83064) * Ensure that data is not lost when parsing EQL responses The shared search utilities expect that response data exists in the response's body field. However, in an EQL response this information also exists as a sibling to the body field, and so we must normalize this data into the body before we can leverage these utilities with EQL queries. * Remove unused EQL parameters These were previously needed to work around an index resolution but, but this has since been resolved upstream in elasticsearch via elastic/elasticsearch#63573. * Allow custom test subj for Preview Histogram to propagate to DOM Previously, custom preview histograms were passing a data-test-subj prop to our general histogram, but the general histogram did not know/care about this prop and it did not become a data property on the underlying DOM element. While most of our tests leveraged enzyme, they could still query by this react prop and everything worked as expected. However, now that we want to exercise this behavior in cypress, we need something to propagate to the DOM so that we can determine which histogram has rendered, so the prop has been updated to be `dataTestSubj`, which then becomes a data-test-subj on the histogram's panel. Tests have been updated accordingly. * Exercise Query Preview during EQL rule creation * Asserts that the preview displays a histogram * Asserts that no error toast is displayed * Add integration tests around EQL sequence signal generation * Clearer assertion * Simplify test assertion * Fix typings These were updated on an upstream refactor. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../search/es_search/es_search_rxjs_utils.ts | 12 +- .../server/search/eql_search_strategy.test.ts | 17 +++ .../server/search/eql_search_strategy.ts | 7 +- .../cypress/screens/create_new_rule.ts | 6 + .../cypress/screens/shared.ts | 9 ++ .../cypress/tasks/create_new_rule.ts | 10 +- .../public/common/hooks/eql/api.ts | 3 +- .../common/hooks/eql/use_eql_preview.ts | 2 - .../query_preview/custom_histogram.test.tsx | 52 +++---- .../rules/query_preview/custom_histogram.tsx | 2 +- .../query_preview/eql_histogram.test.tsx | 6 +- .../rules/query_preview/eql_histogram.tsx | 2 +- .../rules/query_preview/histogram.tsx | 4 +- .../rules/query_preview/index.test.tsx | 46 +++---- .../components/rules/query_preview/index.tsx | 3 - .../threshold_histogram.test.tsx | 2 +- .../query_preview/threshold_histogram.tsx | 2 +- .../tests/generating_signals.ts | 129 +++++++++++++++++- 18 files changed, 245 insertions(+), 69 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/screens/shared.ts diff --git a/x-pack/plugins/data_enhanced/common/search/es_search/es_search_rxjs_utils.ts b/x-pack/plugins/data_enhanced/common/search/es_search/es_search_rxjs_utils.ts index f38bec6d1f59f..8b25a59ed857a 100644 --- a/x-pack/plugins/data_enhanced/common/search/es_search/es_search_rxjs_utils.ts +++ b/x-pack/plugins/data_enhanced/common/search/es_search/es_search_rxjs_utils.ts @@ -5,7 +5,8 @@ */ import { of, merge, timer, throwError } from 'rxjs'; -import { takeWhile, switchMap, expand, mergeMap, tap } from 'rxjs/operators'; +import { map, takeWhile, switchMap, expand, mergeMap, tap } from 'rxjs/operators'; +import { ApiResponse } from '@elastic/elasticsearch'; import { doSearch, @@ -35,6 +36,15 @@ export const doPartialSearch = ( takeWhile((response) => !isCompleteResponse(response), true) ); +export const normalizeEqlResponse = () => + map((eqlResponse) => ({ + ...eqlResponse, + body: { + ...eqlResponse.body, + ...eqlResponse, + }, + })); + export const throwOnEsError = () => mergeMap((r: IKibanaSearchResponse) => isErrorResponse(r) ? merge(of(r), throwError(new AbortError())) : of(r) diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts index 88aaee8eb7da2..cd94d91db8c5e 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts @@ -22,6 +22,8 @@ const getMockEqlResponse = () => ({ sequences: [], }, }, + meta: {}, + statusCode: 200, }); describe('EQL search strategy', () => { @@ -193,5 +195,20 @@ describe('EQL search strategy', () => { expect(requestOptions).toEqual(expect.objectContaining({ ignore: [400] })); }); }); + + describe('response', () => { + it('contains a rawResponse field containing the full search response', async () => { + const eqlSearch = await eqlSearchStrategyProvider(mockLogger); + const response = await eqlSearch + .search({ id: 'my-search-id', options: { ignore: [400] } }, {}, mockDeps) + .toPromise(); + + expect(response).toEqual( + expect.objectContaining({ + rawResponse: expect.objectContaining(getMockEqlResponse()), + }) + ); + }); + }); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts index a75f2617a9bf3..7b3d0db450b04 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts @@ -8,7 +8,10 @@ import type { Logger } from 'kibana/server'; import type { ApiResponse } from '@elastic/elasticsearch'; import { search } from '../../../../../src/plugins/data/server'; -import { doPartialSearch } from '../../common/search/es_search/es_search_rxjs_utils'; +import { + doPartialSearch, + normalizeEqlResponse, +} from '../../common/search/es_search/es_search_rxjs_utils'; import { getAsyncOptions, getDefaultSearchParams } from './get_default_search_params'; import type { ISearchStrategy, IEsRawSearchResponse } from '../../../../../src/plugins/data/server'; @@ -64,7 +67,7 @@ export const eqlSearchStrategyProvider = ( (response) => response.body.id, request.id, options - ).pipe(utils.toKibanaSearchResponse()); + ).pipe(normalizeEqlResponse(), utils.toKibanaSearchResponse()); }, }; }; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index 5e7dce6966195..618ddbad9f44a 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -50,6 +50,10 @@ export const EQL_TYPE = '[data-test-subj="eqlRuleType"]'; export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; +export const EQL_QUERY_PREVIEW_HISTOGRAM = '[data-test-subj="queryPreviewEqlHistogram"]'; + +export const EQL_QUERY_VALIDATION_SPINNER = '[data-test-subj="eql-validation-loading"]'; + export const IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK = '[data-test-subj="importQueryFromSavedTimeline"]'; @@ -80,6 +84,8 @@ export const MITRE_TACTIC_DROPDOWN = '[data-test-subj="mitreTactic"]'; export const MITRE_TECHNIQUES_INPUT = '[data-test-subj="mitreTechniques"] [data-test-subj="comboBoxSearchInput"]'; +export const QUERY_PREVIEW_BUTTON = '[data-test-subj="queryPreviewButton"]'; + export const REFERENCE_URLS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleReferenceUrls"] input'; diff --git a/x-pack/plugins/security_solution/cypress/screens/shared.ts b/x-pack/plugins/security_solution/cypress/screens/shared.ts new file mode 100644 index 0000000000000..ccfe0f97c732c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/shared.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const NOTIFICATION_TOASTS = '[data-test-subj="globalToastList"]'; + +export const TOAST_ERROR_CLASS = 'euiToast--danger'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 5b2c365dfd8c3..251a7ccc4b9c9 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -61,7 +61,11 @@ import { THRESHOLD_TYPE, EQL_TYPE, EQL_QUERY_INPUT, + QUERY_PREVIEW_BUTTON, + EQL_QUERY_PREVIEW_HISTOGRAM, + EQL_QUERY_VALIDATION_SPINNER, } from '../screens/create_new_rule'; +import { NOTIFICATION_TOASTS, TOAST_ERROR_CLASS } from '../screens/shared'; import { TIMELINE } from '../screens/timelines'; import { refreshPage } from './security_header'; @@ -225,8 +229,12 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { cy.get(EQL_QUERY_INPUT).type(rule.customQuery); - cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + cy.get(EQL_QUERY_VALIDATION_SPINNER).should('not.exist'); + cy.get(QUERY_PREVIEW_BUTTON).should('not.be.disabled').click({ force: true }); + cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); + cy.get(NOTIFICATION_TOASTS).children().should('not.have.class', TOAST_ERROR_CLASS); // asserts no error toast on page + cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(EQL_QUERY_INPUT).should('not.exist'); }; diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts index fb2e484c0e3f1..5b78a0e6ae582 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts @@ -32,8 +32,7 @@ export const validateEql = async ({ const { rawResponse: response } = await data.search .search( { - // @ts-expect-error allow_no_indices is missing on EqlSearch - params: { allow_no_indices: true, index: index.join(), body: { query, size: 0 } }, + params: { index: index.join(), body: { query, size: 0 } }, options: { ignore: [400] }, }, { diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts index 1f4424a4f28b8..f259db6e932bf 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts @@ -74,8 +74,6 @@ export const useEqlPreview = (): [ .search>>( { params: { - // @ts-expect-error allow_no_indices is missing on EqlSearch - allow_no_indices: true, index: index.join(), body: { filter: { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx index 3dc3213d65314..8c1a59f912f94 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx @@ -51,7 +51,7 @@ describe('PreviewCustomQueryHistogram', () => { expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); expect( - wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') + wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING); }); @@ -78,32 +78,32 @@ describe('PreviewCustomQueryHistogram', () => { expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); expect( - wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') + wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); - expect( - wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).props().data - ).toEqual([ - { - key: 'hits', - value: [ - { - g: 'All others', - x: 1602247050000, - y: 2314, - }, - { - g: 'All others', - x: 1602247162500, - y: 3471, - }, - { - g: 'All others', - x: 1602247275000, - y: 3369, - }, - ], - }, - ]); + expect(wrapper.find('[dataTestSubj="queryPreviewCustomHistogram"]').at(0).props().data).toEqual( + [ + { + key: 'hits', + value: [ + { + g: 'All others', + x: 1602247050000, + y: 2314, + }, + { + g: 'All others', + x: 1602247162500, + y: 3471, + }, + { + g: 'All others', + x: 1602247275000, + y: 3369, + }, + ], + }, + ] + ); }); test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx index 77b6fbb938e20..42243bca0fca5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx @@ -69,7 +69,7 @@ export const PreviewCustomQueryHistogram = ({ subtitle={subtitle} disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER} isLoading={isLoading} - data-test-subj="queryPreviewCustomHistogram" + dataTestSubj="queryPreviewCustomHistogram" /> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx index 3e7807f423be9..c39d14f6257b7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx @@ -51,7 +51,7 @@ describe('PreviewEqlQueryHistogram', () => { expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); expect( - wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') + wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') ).toEqual(i18n.QUERY_PREVIEW_SUBTITLE_LOADING); }); @@ -78,9 +78,9 @@ describe('PreviewEqlQueryHistogram', () => { expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); expect( - wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') + wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); - expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).props().data).toEqual([ + expect(wrapper.find('[dataTestSubj="queryPreviewEqlHistogram"]').at(0).props().data).toEqual([ { key: 'hits', value: [ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx index ed1fd5b7367d4..88fe228a364ab 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx @@ -66,7 +66,7 @@ export const PreviewEqlQueryHistogram = ({ subtitle={subtitle} disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER_EQL} isLoading={isLoading} - data-test-subj="queryPreviewEqlHistogram" + dataTestSubj="queryPreviewEqlHistogram" /> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx index 2c43dac7b6bce..a7357672b486f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx @@ -21,6 +21,7 @@ const LoadingChart = styled(EuiLoadingChart)` interface PreviewHistogramProps { id: string; data: ChartSeriesData[]; + dataTestSubj?: string; barConfig: ChartSeriesConfigs; title: string; subtitle: string; @@ -31,6 +32,7 @@ interface PreviewHistogramProps { export const PreviewHistogram = ({ id, data, + dataTestSubj, barConfig, title, subtitle, @@ -39,7 +41,7 @@ export const PreviewHistogram = ({ }: PreviewHistogramProps) => { return ( <> - + diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx index 26891dae1752a..3b6d3bca67574 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx @@ -79,9 +79,9 @@ describe('PreviewQuery', () => { expect( wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled ).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); }); test('it renders preview button disabled if "isDisabled" is true', () => { @@ -146,9 +146,9 @@ describe('PreviewQuery', () => { const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); }); test('it renders noise warning when rule type is query, timeframe is last hour and hit average is greater than 1/hour', async () => { @@ -209,9 +209,9 @@ describe('PreviewQuery', () => { const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); }); test('it renders eql histogram when preview button clicked and rule type is eql', () => { @@ -236,9 +236,9 @@ describe('PreviewQuery', () => { const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeTruthy(); }); test('it renders noise warning when rule type is eql, timeframe is last hour and hit average is greater than 1/hour', async () => { @@ -314,9 +314,9 @@ describe('PreviewQuery', () => { expect(mockCalls.length).toEqual(1); expect(wrapper.find('[data-test-subj="previewQueryWarning"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); }); test('it renders noise warning when rule type is threshold, and threshold field is defined, timeframe is last hour and hit average is greater than 1/hour', async () => { @@ -380,9 +380,9 @@ describe('PreviewQuery', () => { const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); }); test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => { @@ -407,9 +407,9 @@ describe('PreviewQuery', () => { const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; expect(mockCalls.length).toEqual(1); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').exists()).toBeFalsy(); }); test('it hides histogram when timeframe changes', () => { @@ -431,13 +431,13 @@ describe('PreviewQuery', () => { wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeTruthy(); wrapper .find('[data-test-subj="queryPreviewTimeframeSelect"] select') .at(0) .simulate('change', { target: { value: 'd' } }); - expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx index 6669ea6d97969..52f19704ed917 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx @@ -312,7 +312,6 @@ export const PreviewQuery = ({ inspect={inspect} refetch={refetch} isLoading={isMatrixHistogramLoading} - data-test-subj="previewNonEqlQueryHistogram" /> )} {ruleType === 'threshold' && thresholdFieldExists && showHistogram && ( @@ -321,7 +320,6 @@ export const PreviewQuery = ({ buckets={buckets} inspect={inspect} refetch={refetch} - data-test-subj="previewThresholdQueryHistogram" /> )} {ruleType === 'eql' && showHistogram && ( @@ -333,7 +331,6 @@ export const PreviewQuery = ({ inspect={eqlQueryInspect} refetch={eqlQueryRefetch} isLoading={eqlQueryLoading} - data-test-subj="previewEqlQueryHistogram" /> )} {showHistogram && diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx index 8a0cfef1b6256..44655a91075a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx @@ -68,7 +68,7 @@ describe('PreviewThresholdQueryHistogram', () => { expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); expect( - wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').at(0).props().data + wrapper.find('[dataTestSubj="thresholdQueryPreviewHistogram"]').at(0).props().data ).toEqual([ { key: 'hits', diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx index a102c567a98e8..360b834bb7722 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx @@ -75,7 +75,7 @@ export const PreviewThresholdQueryHistogram = ({ subtitle={subtitle} disclaimer={i18n.QUERY_PREVIEW_DISCLAIMER} isLoading={isLoading} - data-test-subj="thresholdQueryPreviewHistogram" + dataTestSubj="thresholdQueryPreviewHistogram" /> ); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 0ba2abb466f7b..f76bdb4ebc718 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -6,7 +6,10 @@ import expect from '@kbn/expect'; -import { QueryCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { + EqlCreateSchema, + QueryCreateSchema, +} from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { DEFAULT_SIGNALS_INDEX } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -191,6 +194,130 @@ export default ({ getService }: FtrProviderContext) => { }, }); }); + + describe('EQL Rules', () => { + it('generates signals from EQL sequences in the expected form', async () => { + const rule: EqlCreateSchema = { + ...getSimpleRule(), + from: '1900-01-01T00:00:00.000Z', + rule_id: 'eql-rule', + type: 'eql', + language: 'eql', + query: 'sequence by host.name [any where true] [any where true]', + }; + await createRule(supertest, rule); + await waitForSignalsToBePresent(supertest, 1); + const signals = await getSignalsByRuleIds(supertest, ['eql-rule']); + const signal = signals.hits.hits[0]._source.signal; + + expect(signal).eql({ + rule: signal.rule, + group: signal.group, + original_time: signal.original_time, + status: 'open', + depth: 1, + ancestors: [ + { + depth: 0, + id: 'UBXOBmkBR346wHgnLP8T', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + ], + original_event: { + action: 'boot', + dataset: 'login', + kind: 'event', + module: 'system', + origin: '/var/log/wtmp', + }, + parent: { + depth: 0, + id: 'UBXOBmkBR346wHgnLP8T', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + parents: [ + { + depth: 0, + id: 'UBXOBmkBR346wHgnLP8T', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + ], + }); + }); + + it('generates building block signals from EQL sequences in the expected form', async () => { + const rule: EqlCreateSchema = { + ...getSimpleRule(), + from: '1900-01-01T00:00:00.000Z', + rule_id: 'eql-rule', + type: 'eql', + language: 'eql', + query: 'sequence by host.name [any where true] [any where true]', + }; + await createRule(supertest, rule); + await waitForSignalsToBePresent(supertest, 1); + const signalsOpen = await getSignalsByRuleIds(supertest, ['eql-rule']); + const sequenceSignal = signalsOpen.hits.hits.find( + (signal) => signal._source.signal.depth === 2 + ); + const signal = sequenceSignal!._source.signal; + const eventIds = signal.parents.map((event) => event.id); + + expect(signal).eql({ + status: 'open', + depth: 2, + group: signal.group, + rule: signal.rule, + ancestors: [ + { + depth: 0, + id: 'UBXOBmkBR346wHgnLP8T', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + { + depth: 1, + id: eventIds[0], + index: '.siem-signals-default', + rule: signal.rule.id, + type: 'signal', + }, + { + depth: 0, + id: 'URXOBmkBR346wHgnLP8T', + index: 'auditbeat-8.0.0-2019.02.19-000001', + type: 'event', + }, + { + depth: 1, + id: eventIds[1], + index: '.siem-signals-default', + rule: signal.rule.id, + type: 'signal', + }, + ], + parents: [ + { + depth: 1, + id: eventIds[0], + index: '.siem-signals-default', + rule: signal.rule.id, + type: 'signal', + }, + { + depth: 1, + id: eventIds[1], + index: '.siem-signals-default', + rule: signal.rule.id, + type: 'signal', + }, + ], + }); + }); + }); }); /** From 50efaecf70ff8f18e7ffcf0b0bc6246eb50c2980 Mon Sep 17 00:00:00 2001 From: DeDe Morton Date: Mon, 16 Nov 2020 13:25:15 -0800 Subject: [PATCH 11/99] Fix links to Fleet docs (#83358) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/getting-started/quick-start-guide.asciidoc | 2 +- docs/setup/connect-to-elasticsearch.asciidoc | 2 +- .../public/applications/fleet/components/alpha_flyout.tsx | 2 +- .../fleet/components/enrollment_instructions/manual/index.tsx | 2 +- .../applications/fleet/sections/agents/setup_page/index.tsx | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index f239b7ae6ca88..ccb6e931d69e3 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -140,4 +140,4 @@ For more information, refer to <>. If you are you ready to add your own data, refer to <>. -If you want to ingest your data, refer to {ingest-guide}/ingest-management-getting-started.html[Quick start: Get logs and metrics into the Elastic Stack]. +If you want to ingest your data, refer to {ingest-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 03a728a15351e..0af953ec2cb09 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -37,7 +37,7 @@ ship with dashboards and visualizations, so you can quickly get insights into your data. To get started, refer to -{ingest-guide}/ingest-management-getting-started.html[Quick start: Get logs and metrics into the Elastic Stack]. +{ingest-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. [role="screenshot"] image::images/add-data-fleet.png[Add data using Fleet] diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/alpha_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/alpha_flyout.tsx index a6a162dbdeafa..6f986ced6cc62 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/alpha_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/alpha_flyout.tsx @@ -48,7 +48,7 @@ export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { values={{ docsLink: ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx index 343ddfab23c71..d0145601b8a0b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/enrollment_instructions/manual/index.tsx @@ -78,7 +78,7 @@ export const ManualInstructions: React.FunctionComponent = ({ @@ -262,7 +262,7 @@ xpack.encryptedSavedObjects.encryptionKey: "something_at_least_32_characters"`} values={{ link: ( From 2286c50dc5e8b8b448afc09978fa608829c32054 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Mon, 16 Nov 2020 15:53:04 -0600 Subject: [PATCH 12/99] [Workplace Search] Migrate sourceData from ent-search (#83459) * Add types and missing routes Salesforce Sandbox routes had been added since the routes were migrated. * Migrate source_data This is a direct port from `ent-search` with no changes other than linting. * Adds i18n to strings in sourceData Shared items were extracted to constants. Others were done inline * Fix typo * Another typo * Rename ONE_DIVE * Rename SERVICE_NOW * Remove redundant variable * Rename SHARE_POINT * Update routes to remove hyphens --- .../workplace_search/constants.ts | 190 +++++ .../applications/workplace_search/routes.ts | 20 +- .../applications/workplace_search/types.ts | 10 + .../views/content_sources/source_data.tsx | 743 ++++++++++++++++++ 4 files changed, 954 insertions(+), 9 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 3b911b87dea12..4e093f472d562 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -54,4 +54,194 @@ export const SOURCE_STATUSES = { ALWAYS_SYNCED: 'always_synced', }; +export const SOURCE_NAMES = { + CONFLUENCE: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.confluence', + { defaultMessage: 'Confluence' } + ), + CONFLUENCE_SERVER: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.confluenceServer', + { defaultMessage: 'Confluence (Server)' } + ), + DROPBOX: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.dropbox', { + defaultMessage: 'Dropbox', + }), + GITHUB: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.github', { + defaultMessage: 'GitHub', + }), + GITHUB_ENTERPRISE: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.githubEnterprise', + { defaultMessage: 'GitHub Enterprise Server' } + ), + GMAIL: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.gmail', { + defaultMessage: 'Gmail', + }), + GOOGLE_DRIVE: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.googleDrive', + { defaultMessage: 'Google Drive' } + ), + JIRA: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.jira', { + defaultMessage: 'Jira', + }), + JIRA_SERVER: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.jiraServer', + { defaultMessage: 'Jira (Server)' } + ), + ONEDRIVE: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.oneDrive', { + defaultMessage: 'OneDrive', + }), + SALESFORCE: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.salesforce', + { defaultMessage: 'Salesforce' } + ), + SALESFORCE_SANDBOX: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.salesforceSandbox', + { defaultMessage: 'Salesforce Sandbox' } + ), + SERVICENOW: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.serviceNow', + { defaultMessage: 'ServiceNow' } + ), + SHAREPOINT: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.sharePoint', + { defaultMessage: 'SharePoint Online' } + ), + SLACK: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.slack', { + defaultMessage: 'Slack', + }), + ZENDESK: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.zendesk', { + defaultMessage: 'Zendesk', + }), + CUSTOM: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.custom', { + defaultMessage: 'Custom API Source', + }), +}; + +export const SOURCE_OBJ_TYPES = { + PAGES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.pages', { + defaultMessage: 'Pages', + }), + ATTACHMENTS: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.attachments', + { defaultMessage: 'Attachments' } + ), + BLOG_POSTS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.blogPosts', { + defaultMessage: 'Blog Posts', + }), + SITES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.sites', { + defaultMessage: 'Sites', + }), + SPACES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.spaces', { + defaultMessage: 'Spaces', + }), + ALL_FILES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.allFiles', { + defaultMessage: + 'All Files (including images, PDFs, spreadsheets, textual documents, presentations)', + }), + ALL_STORED_FILES: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.allStoredFiles', + { + defaultMessage: + 'All Stored Files (including images, videos, PDFs, spreadsheets, textual documents, presentations)', + } + ), + G_SUITE_FILES: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.gSuiteFiles', + { + defaultMessage: 'Google G Suite Documents (Docs, Sheets, Slides)', + } + ), + EPICS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.epics', { + defaultMessage: 'Epics', + }), + PROJECTS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.projects', { + defaultMessage: 'Projects', + }), + TASKS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.tasks', { + defaultMessage: 'Tasks', + }), + STORIES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.stories', { + defaultMessage: 'Stories', + }), + BUGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.bugs', { + defaultMessage: 'Bugs', + }), + ISSUES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.issues', { + defaultMessage: 'Issues', + }), + PULL_REQUESTS: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.pullRequests', + { + defaultMessage: 'Pull Requests', + } + ), + REPOSITORY_LIST: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.repositoryList', + { + defaultMessage: 'Repository List', + } + ), + EMAILS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.emails', { + defaultMessage: 'Emails', + }), + CONTACTS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.contacts', { + defaultMessage: 'Contacts', + }), + OPPORTUNITIES: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.opportunities', + { + defaultMessage: 'Opportunities', + } + ), + LEADS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.leads', { + defaultMessage: 'Leads', + }), + ACCOUNTS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.accounts', { + defaultMessage: 'Accounts', + }), + CAMPAIGNS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.campaigns', { + defaultMessage: 'Campaigns', + }), + USERS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.users', { + defaultMessage: 'Users', + }), + INCIDENTS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.incidents', { + defaultMessage: 'Incidents', + }), + ITEMS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.items', { + defaultMessage: 'Items', + }), + ARTICLES: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.articles', { + defaultMessage: 'Articles', + }), + TICKETS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.objTypes.tickets', { + defaultMessage: 'Tickets', + }), + PUBLIC_MESSAGES: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.publicMessages', + { + defaultMessage: 'Public channel messages', + } + ), + PRIVATE_MESSAGES: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.privateMessages', + { + defaultMessage: 'Private channel messages in which you are an active participant', + } + ), + DIRECT_MESSAGES: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.objTypes.directMessages', + { + defaultMessage: 'Direct messages', + } + ), +}; + +export const GITHUB_LINK_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.applicationLinkTitles.github', + { + defaultMessage: 'GitHub Developer Portal', + } +); + export const CUSTOM_SERVICE_TYPE = 'custom'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index fb47b0a090f56..6099a42e6d7cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -30,10 +30,10 @@ export const GMAIL_DOCS_URL = `${DOCS_PREFIX}/workplace-search-gmail-connector.h export const GOOGLE_DRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-google-drive-connector.html`; export const JIRA_DOCS_URL = `${DOCS_PREFIX}/workplace-search-jira-cloud-connector.html`; export const JIRA_SERVER_DOCS_URL = `${DOCS_PREFIX}/workplace-search-jira-server-connector.html`; -export const ONE_DRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-onedrive-connector.html`; +export const ONEDRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-onedrive-connector.html`; export const SALESFORCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-salesforce-connector.html`; -export const SERVICE_NOW_DOCS_URL = `${DOCS_PREFIX}/workplace-search-servicenow-connector.html`; -export const SHARE_POINT_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sharepoint-online-connector.html`; +export const SERVICENOW_DOCS_URL = `${DOCS_PREFIX}/workplace-search-servicenow-connector.html`; +export const SHAREPOINT_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sharepoint-online-connector.html`; export const SLACK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-slack-connector.html`; export const ZENDESK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-zendesk-connector.html`; export const CUSTOM_SOURCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-api-sources.html`; @@ -68,10 +68,11 @@ export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google-drive`; export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira-cloud`; export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira-server`; -export const ADD_ONE_DRIVE_PATH = `${SOURCES_PATH}/add/one-drive`; +export const ADD_ONEDRIVE_PATH = `${SOURCES_PATH}/add/onedrive`; export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`; -export const ADD_SERVICE_NOW_PATH = `${SOURCES_PATH}/add/service-now`; -export const ADD_SHARE_POINT_PATH = `${SOURCES_PATH}/add/share-point`; +export const ADD_SALESFORCE_SANDBOX_PATH = `${SOURCES_PATH}/add/salesforce-sandbox`; +export const ADD_SERVICENOW_PATH = `${SOURCES_PATH}/add/servicenow`; +export const ADD_SHAREPOINT_PATH = `${SOURCES_PATH}/add/sharepoint`; export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`; export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`; export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`; @@ -101,10 +102,11 @@ export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`; export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google-drive/edit`; export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira-cloud/edit`; export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira-server/edit`; -export const EDIT_ONE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/one-drive/edit`; +export const EDIT_ONEDRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/onedrive/edit`; export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`; -export const EDIT_SERVICE_NOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/service-now/edit`; -export const EDIT_SHARE_POINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/share-point/edit`; +export const EDIT_SALESFORCE_SANDBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce-sandbox/edit`; +export const EDIT_SERVICENOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/servicenow/edit`; +export const EDIT_SHAREPOINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/sharepoint/edit`; export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 8f44fcacc17f6..f09160d513344 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -57,3 +57,13 @@ export interface ContentSourceDetails extends ContentSource { export interface SourcePriority { [id: string]: number; } + +export enum FeatureIds { + SyncFrequency = 'SyncFrequency', + SyncedItems = 'SyncedItems', + SearchableContent = 'SearchableContent', + Remote = 'Remote', + Private = 'Private', + GlobalAccessPermissions = 'GlobalAccessPermissions', + DocumentLevelPermissions = 'DocumentLevelPermissions', +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx new file mode 100644 index 0000000000000..d04b2cb16d308 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -0,0 +1,743 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { + ADD_CONFLUENCE_PATH, + ADD_CONFLUENCE_SERVER_PATH, + ADD_DROPBOX_PATH, + ADD_GITHUB_ENTERPRISE_PATH, + ADD_GITHUB_PATH, + ADD_GMAIL_PATH, + ADD_GOOGLE_DRIVE_PATH, + ADD_JIRA_PATH, + ADD_JIRA_SERVER_PATH, + ADD_ONEDRIVE_PATH, + ADD_SALESFORCE_PATH, + ADD_SALESFORCE_SANDBOX_PATH, + ADD_SERVICENOW_PATH, + ADD_SHAREPOINT_PATH, + ADD_SLACK_PATH, + ADD_ZENDESK_PATH, + ADD_CUSTOM_PATH, + EDIT_CONFLUENCE_PATH, + EDIT_CONFLUENCE_SERVER_PATH, + EDIT_DROPBOX_PATH, + EDIT_GITHUB_ENTERPRISE_PATH, + EDIT_GITHUB_PATH, + EDIT_GMAIL_PATH, + EDIT_GOOGLE_DRIVE_PATH, + EDIT_JIRA_PATH, + EDIT_JIRA_SERVER_PATH, + EDIT_ONEDRIVE_PATH, + EDIT_SALESFORCE_PATH, + EDIT_SALESFORCE_SANDBOX_PATH, + EDIT_SERVICENOW_PATH, + EDIT_SHAREPOINT_PATH, + EDIT_SLACK_PATH, + EDIT_ZENDESK_PATH, + EDIT_CUSTOM_PATH, + CONFLUENCE_DOCS_URL, + CONFLUENCE_SERVER_DOCS_URL, + GITHUB_ENTERPRISE_DOCS_URL, + DROPBOX_DOCS_URL, + GITHUB_DOCS_URL, + GMAIL_DOCS_URL, + GOOGLE_DRIVE_DOCS_URL, + JIRA_DOCS_URL, + JIRA_SERVER_DOCS_URL, + ONEDRIVE_DOCS_URL, + SALESFORCE_DOCS_URL, + SERVICENOW_DOCS_URL, + SHAREPOINT_DOCS_URL, + SLACK_DOCS_URL, + ZENDESK_DOCS_URL, + CUSTOM_SOURCE_DOCS_URL, +} from '../../routes'; + +import { FeatureIds } from '../../types'; + +import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; + +const connectStepDescription = { + attachments: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.connectStepDescription.attachments', + { + defaultMessage: + 'Content found within Attachments (PDFs, Microsoft Office Files, and other popular textual file formats) will be automatically indexed and searchable.', + } + ), + files: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.connectStepDescription.files', + { + defaultMessage: + 'Content found within PDFs, Microsoft Office Files, and other popular textual file formats will be automatically indexed and searchable.', + } + ), + empty: '', +}; + +export const staticSourceData = [ + { + name: SOURCE_NAMES.CONFLUENCE, + serviceType: 'confluence_cloud', + addPath: ADD_CONFLUENCE_PATH, + editPath: EDIT_CONFLUENCE_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: true, + documentationUrl: CONFLUENCE_DOCS_URL, + applicationPortalUrl: 'https://developer.atlassian.com/apps/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.confluence', + { + defaultMessage: + '{sourceName} is a team workspace, where knowledge and collaboration meet. Often used as an organizational wiki and intranet, it usually houses valuable information for staff across multiple areas of your business.', + values: { sourceName: SOURCE_NAMES.CONFLUENCE }, + } + ), + connectStepDescription: connectStepDescription.attachments, + objTypes: [ + SOURCE_OBJ_TYPES.PAGES, + SOURCE_OBJ_TYPES.ATTACHMENTS, + SOURCE_OBJ_TYPES.BLOG_POSTS, + SOURCE_OBJ_TYPES.SPACES, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.CONFLUENCE_SERVER, + serviceType: 'confluence_server', + addPath: ADD_CONFLUENCE_SERVER_PATH, + editPath: EDIT_CONFLUENCE_SERVER_PATH, + configuration: { + isPublicKey: true, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: CONFLUENCE_SERVER_DOCS_URL, + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.confluenceServer', + { + defaultMessage: + '{sourceName} is a team workspace, where knowledge and collaboration meet. Often used as an organizational wiki and intranet, it usually houses valuable information for staff across multiple areas of your business.', + values: { sourceName: SOURCE_NAMES.CONFLUENCE }, + } + ), + connectStepDescription: connectStepDescription.attachments, + objTypes: [ + SOURCE_OBJ_TYPES.PAGES, + SOURCE_OBJ_TYPES.ATTACHMENTS, + SOURCE_OBJ_TYPES.BLOG_POSTS, + SOURCE_OBJ_TYPES.SPACES, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.DROPBOX, + serviceType: 'dropbox', + addPath: ADD_DROPBOX_PATH, + editPath: EDIT_DROPBOX_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: DROPBOX_DOCS_URL, + applicationPortalUrl: 'https://www.dropbox.com/developers/apps', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.dropbox', + { + defaultMessage: + '{sourceName} is a cloud-based storage service for organizations of all sizes. Create, store, share and automatically synchronize documents across your desktop and web.', + values: { sourceName: SOURCE_NAMES.DROPBOX }, + } + ), + connectStepDescription: connectStepDescription.files, + objTypes: [SOURCE_OBJ_TYPES.ALL_FILES], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.GITHUB, + serviceType: 'github', + addPath: ADD_GITHUB_PATH, + editPath: EDIT_GITHUB_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + needsConfiguration: true, + documentationUrl: GITHUB_DOCS_URL, + applicationPortalUrl: 'https://github.com/settings/developers', + applicationLinkTitle: GITHUB_LINK_TITLE, + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.github', + { + defaultMessage: + '{sourceName} is a development platform, version control and collaboration platform for teams of all sizes. From open source to business, you can host and review code, manage projects, and build software across departments and continents.', + values: { sourceName: SOURCE_NAMES.GITHUB }, + } + ), + connectStepDescription: connectStepDescription.empty, + objTypes: [ + SOURCE_OBJ_TYPES.ISSUES, + SOURCE_OBJ_TYPES.PULL_REQUESTS, + SOURCE_OBJ_TYPES.REPOSITORY_LIST, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.GITHUB_ENTERPRISE, + serviceType: 'github_enterprise_server', + addPath: ADD_GITHUB_ENTERPRISE_PATH, + editPath: EDIT_GITHUB_ENTERPRISE_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsConfiguration: true, + needsBaseUrl: true, + baseUrlTitle: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.baseUrlTitles.github', + { + defaultMessage: 'GitHub Enterprise URL', + } + ), + documentationUrl: GITHUB_ENTERPRISE_DOCS_URL, + applicationPortalUrl: 'https://github.com/settings/developers', + applicationLinkTitle: GITHUB_LINK_TITLE, + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.githubEnterprise', + { + defaultMessage: + '{sourceName} is a development platform, version control and collaboration platform for teams of all sizes. From open source to business, you can host and review code, manage projects, and build software across departments and continents.', + values: { sourceName: SOURCE_NAMES.GITHUB_ENTERPRISE }, + } + ), + connectStepDescription: connectStepDescription.empty, + objTypes: [ + SOURCE_OBJ_TYPES.ISSUES, + SOURCE_OBJ_TYPES.PULL_REQUESTS, + SOURCE_OBJ_TYPES.REPOSITORY_LIST, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.GMAIL, + serviceType: 'gmail', + addPath: ADD_GMAIL_PATH, + editPath: EDIT_GMAIL_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: GMAIL_DOCS_URL, + applicationPortalUrl: 'https://console.developers.google.com/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.gmail', + { + defaultMessage: + '{sourceName} is a free email service developed by Google. It is fast, reliable, and trusted by millions of people and organizations around the world. Workplace Search brings all of your Gmail content into one relevant and ease-to-use search experience.', + values: { sourceName: SOURCE_NAMES.GMAIL }, + } + ), + connectStepDescription: connectStepDescription.empty, + objTypes: [SOURCE_OBJ_TYPES.EMAILS], + features: { + platinumPrivateContext: [FeatureIds.Remote, FeatureIds.Private, FeatureIds.SearchableContent], + }, + accountContextOnly: true, + }, + { + name: SOURCE_NAMES.GOOGLE_DRIVE, + serviceType: 'google_drive', + addPath: ADD_GOOGLE_DRIVE_PATH, + editPath: EDIT_GOOGLE_DRIVE_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: GOOGLE_DRIVE_DOCS_URL, + applicationPortalUrl: 'https://console.developers.google.com/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.googleDrive', + { + defaultMessage: + '{sourceName} is a cloud-based storage and collaboration service for organizations of all sizes, with a focus on G Suite document (Google Docs, Sheets, Slides, etc) storage and collaboration. Create, store, share and automatically synchronize documents across your desktop and web.', + values: { sourceName: SOURCE_NAMES.GOOGLE_DRIVE }, + } + ), + connectStepDescription: connectStepDescription.files, + objTypes: [SOURCE_OBJ_TYPES.G_SUITE_FILES, SOURCE_OBJ_TYPES.ALL_STORED_FILES], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + basicOrgContextExcludedFeatures: [FeatureIds.DocumentLevelPermissions], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.JIRA, + serviceType: 'jira_cloud', + addPath: ADD_JIRA_PATH, + editPath: EDIT_JIRA_PATH, + configuration: { + isPublicKey: true, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: JIRA_DOCS_URL, + applicationPortalUrl: '', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.jira', + { + defaultMessage: + '{sourceName} is an issue tracking product that provides bug tracking, workflow automation, and agile project management tools for teams of all sizes. ', + values: { sourceName: SOURCE_NAMES.JIRA }, + } + ), + connectStepDescription: connectStepDescription.files, + objTypes: [ + SOURCE_OBJ_TYPES.EPICS, + SOURCE_OBJ_TYPES.PROJECTS, + SOURCE_OBJ_TYPES.TASKS, + SOURCE_OBJ_TYPES.STORIES, + SOURCE_OBJ_TYPES.BUGS, + SOURCE_OBJ_TYPES.ATTACHMENTS, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.JIRA_SERVER, + serviceType: 'jira_server', + addPath: ADD_JIRA_SERVER_PATH, + editPath: EDIT_JIRA_SERVER_PATH, + configuration: { + isPublicKey: true, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: JIRA_SERVER_DOCS_URL, + applicationPortalUrl: '', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.jiraServer', + { + defaultMessage: + '{sourceName} is an issue tracking product that provides bug tracking, workflow automation, and agile project management tools for teams of all sizes. ', + values: { sourceName: SOURCE_NAMES.JIRA_SERVER }, + } + ), + connectStepDescription: connectStepDescription.files, + objTypes: [ + SOURCE_OBJ_TYPES.EPICS, + SOURCE_OBJ_TYPES.PROJECTS, + SOURCE_OBJ_TYPES.TASKS, + SOURCE_OBJ_TYPES.STORIES, + SOURCE_OBJ_TYPES.BUGS, + SOURCE_OBJ_TYPES.ATTACHMENTS, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.ONEDRIVE, + serviceType: 'one_drive', + addPath: ADD_ONEDRIVE_PATH, + editPath: EDIT_ONEDRIVE_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: ONEDRIVE_DOCS_URL, + applicationPortalUrl: 'https://portal.azure.com/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.oneDrive', + { + defaultMessage: + '{sourceName} is a cloud-based storage service for organizations of all sizes, with a focus on Office 365 document storage and collaboration. Create, store, share and automatically synchronize documents across your organization.', + values: { sourceName: SOURCE_NAMES.ONEDRIVE }, + } + ), + connectStepDescription: connectStepDescription.files, + objTypes: [SOURCE_OBJ_TYPES.ALL_FILES], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + basicOrgContextExcludedFeatures: [FeatureIds.DocumentLevelPermissions], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.SALESFORCE, + serviceType: 'salesforce', + addPath: ADD_SALESFORCE_PATH, + editPath: EDIT_SALESFORCE_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: SALESFORCE_DOCS_URL, + applicationPortalUrl: 'https://salesforce.com/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.salesforce', + { + defaultMessage: + '{sourceName} is a cloud-based customer relationship management (CRM) platform with a focus on customer service, marketing automation, analytics, and sales operation tooling.', + values: { sourceName: SOURCE_NAMES.SALESFORCE }, + } + ), + connectStepDescription: connectStepDescription.attachments, + objTypes: [ + SOURCE_OBJ_TYPES.CONTACTS, + SOURCE_OBJ_TYPES.OPPORTUNITIES, + SOURCE_OBJ_TYPES.LEADS, + SOURCE_OBJ_TYPES.ACCOUNTS, + SOURCE_OBJ_TYPES.CAMPAIGNS, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.SALESFORCE_SANDBOX, + serviceType: 'salesforce_sandbox', + addPath: ADD_SALESFORCE_SANDBOX_PATH, + editPath: EDIT_SALESFORCE_SANDBOX_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: SALESFORCE_DOCS_URL, + applicationPortalUrl: 'https://test.salesforce.com/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.salesforceSandbox', + { + defaultMessage: + '{sourceName} is a cloud-based customer relationship management (CRM) platform with a focus on customer service, marketing automation, analytics, and sales operation tooling.', + values: { sourceName: SOURCE_NAMES.SALESFORCE_SANDBOX }, + } + ), + connectStepDescription: connectStepDescription.attachments, + objTypes: [ + SOURCE_OBJ_TYPES.CONTACTS, + SOURCE_OBJ_TYPES.OPPORTUNITIES, + SOURCE_OBJ_TYPES.LEADS, + SOURCE_OBJ_TYPES.ACCOUNTS, + SOURCE_OBJ_TYPES.CAMPAIGNS, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.SERVICENOW, + serviceType: 'service_now', + addPath: ADD_SERVICENOW_PATH, + editPath: EDIT_SERVICENOW_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: false, + needsBaseUrl: true, + documentationUrl: SERVICENOW_DOCS_URL, + applicationPortalUrl: 'https://www.servicenow.com/my-account/sign-in.html', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.serviceNow', + { + defaultMessage: + '{sourceName} is a cloud-based IT Service Management (ITSM) platform focusing on workflow automation and internal organizational support.', + values: { sourceName: SOURCE_NAMES.SERVICENOW }, + } + ), + connectStepDescription: connectStepDescription.empty, + objTypes: [ + SOURCE_OBJ_TYPES.USERS, + SOURCE_OBJ_TYPES.INCIDENTS, + SOURCE_OBJ_TYPES.ITEMS, + SOURCE_OBJ_TYPES.ARTICLES, + ], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.SHAREPOINT, + serviceType: 'share_point', + addPath: ADD_SHAREPOINT_PATH, + editPath: EDIT_SHAREPOINT_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: SHAREPOINT_DOCS_URL, + applicationPortalUrl: 'https://portal.azure.com/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.sharePoint', + { + defaultMessage: + '{sourceName} is a cloud-based collaboration, knowledge management and storage platform for organizations of all sizes. Often used as a centralized content management system (CMS), SharePoint Online stores a wealth of information across departments and teams.', + values: { sourceName: SOURCE_NAMES.SHAREPOINT }, + } + ), + connectStepDescription: connectStepDescription.files, + objTypes: [SOURCE_OBJ_TYPES.SITES, SOURCE_OBJ_TYPES.ALL_FILES], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + basicOrgContextExcludedFeatures: [FeatureIds.DocumentLevelPermissions], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.SLACK, + serviceType: 'slack', + addPath: ADD_SLACK_PATH, + editPath: EDIT_SLACK_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: SLACK_DOCS_URL, + applicationPortalUrl: 'https://api.slack.com/apps/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.slack', + { + defaultMessage: + '{sourceName} is a communication tool that enables real-time collaboration and decision making. With {sourceName}, keep track of the work happening across teams, engage directly with your coworkers on ongoing projects and communicate with other organizations.', + values: { sourceName: SOURCE_NAMES.SLACK }, + } + ), + connectStepDescription: connectStepDescription.empty, + objTypes: [ + SOURCE_OBJ_TYPES.PUBLIC_MESSAGES, + SOURCE_OBJ_TYPES.PRIVATE_MESSAGES, + SOURCE_OBJ_TYPES.DIRECT_MESSAGES, + ], + features: { + platinumPrivateContext: [FeatureIds.Remote, FeatureIds.Private, FeatureIds.SearchableContent], + }, + accountContextOnly: true, + }, + { + name: SOURCE_NAMES.ZENDESK, + serviceType: 'zendesk', + addPath: ADD_ZENDESK_PATH, + editPath: EDIT_ZENDESK_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + needsSubdomain: true, + documentationUrl: ZENDESK_DOCS_URL, + applicationPortalUrl: 'https://www.zendesk.com/login/', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.zendesk', + { + defaultMessage: + '{sourceName} is cloud-based customer relationship management and customer support platform that provides tools for tracking, prioritizing, and solving customer support tickets.', + values: { sourceName: SOURCE_NAMES.ZENDESK }, + } + ), + connectStepDescription: connectStepDescription.empty, + objTypes: [SOURCE_OBJ_TYPES.TICKETS], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, + { + name: SOURCE_NAMES.CUSTOM, + serviceType: 'custom', + addPath: ADD_CUSTOM_PATH, + editPath: EDIT_CUSTOM_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: false, + needsBaseUrl: false, + helpText: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.helpText.custom', { + defaultMessage: + 'To create a Custom API Source, provide a human-readable and descriptive name. The name will appear as-is in the various search experiences and management interfaces.', + }), + documentationUrl: CUSTOM_SOURCE_DOCS_URL, + applicationPortalUrl: '', + }, + sourceDescription: '', + connectStepDescription: connectStepDescription.empty, + accountContextOnly: false, + }, +]; From 6a1cd730821d40e6424fcf7cb0543917e658205e Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 16 Nov 2020 21:56:41 +0000 Subject: [PATCH 13/99] [ML] Additional job spaces initialization (#83127) * [ML] Additional job spaces initialization * adding logs test * updating integrations * updating test text * fixing logs jobs error * fix bug with duplicate ids * updating initialization log text * fixing initialization text * adding metrics overrides Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../create_anomaly_detection_jobs.ts | 1 + .../plugins/ml/common/types/saved_objects.ts | 9 + x-pack/plugins/ml/server/lib/route_guard.ts | 5 + .../data_recognizer/data_recognizer.test.ts | 2 + .../models/data_recognizer/data_recognizer.ts | 28 ++- x-pack/plugins/ml/server/plugin.ts | 6 + x-pack/plugins/ml/server/routes/modules.ts | 229 +++++++++++------- .../ml/server/routes/schemas/modules.ts | 5 + .../ml/server/saved_objects/authorization.ts | 39 +++ .../saved_objects/initialization/index.ts | 7 + .../{ => initialization}/initialization.ts | 36 ++- .../initialization/space_overrides/index.ts | 7 + .../initialization/space_overrides/logs.ts | 54 +++++ .../initialization/space_overrides/metrics.ts | 61 +++++ .../space_overrides/space_overrides.test.ts | 72 ++++++ .../space_overrides/space_overrides.ts | 30 +++ .../plugins/ml/server/saved_objects/repair.ts | 34 +-- .../ml/server/saved_objects/service.ts | 48 ++-- .../shared_services/providers/modules.ts | 44 +++- .../server/shared_services/shared_services.ts | 18 +- .../common/components/ml_popover/api.ts | 1 + .../uptime/public/state/api/ml_anomaly.ts | 1 + 22 files changed, 583 insertions(+), 154 deletions(-) create mode 100644 x-pack/plugins/ml/server/saved_objects/authorization.ts create mode 100644 x-pack/plugins/ml/server/saved_objects/initialization/index.ts rename x-pack/plugins/ml/server/saved_objects/{ => initialization}/initialization.ts (78%) create mode 100644 x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/index.ts create mode 100644 x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/logs.ts create mode 100644 x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/metrics.ts create mode 100644 x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/space_overrides.test.ts create mode 100644 x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/space_overrides.ts diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 73e590064bac0..a10762622b2c6 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -77,6 +77,7 @@ async function createAnomalyDetectionJob({ prefix: `${APM_ML_JOB_GROUP}-${snakeCase(environment)}-${randomToken}-`, groups: [APM_ML_JOB_GROUP], indexPatternName, + applyToAllSpaces: true, query: { bool: { filter: [ diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index 6fd1b2cc997be..dde235476f1f9 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -6,3 +6,12 @@ export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export const ML_SAVED_OBJECT_TYPE = 'ml-job'; + +type Result = Record; + +export interface RepairSavedObjectResponse { + savedObjectsCreated: Result; + savedObjectsDeleted: Result; + datafeedsAdded: Result; + datafeedsRemoved: Result; +} diff --git a/x-pack/plugins/ml/server/lib/route_guard.ts b/x-pack/plugins/ml/server/lib/route_guard.ts index 390288ca197e9..68700048ce6e7 100644 --- a/x-pack/plugins/ml/server/lib/route_guard.ts +++ b/x-pack/plugins/ml/server/lib/route_guard.ts @@ -13,6 +13,7 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { SpacesPluginSetup } from '../../../spaces/server'; +import type { SecurityPluginSetup } from '../../../security/server'; import { jobSavedObjectServiceFactory, JobSavedObjectService } from '../saved_objects'; import { MlLicense } from '../../common/license'; @@ -36,6 +37,7 @@ export class RouteGuard { private _getMlSavedObjectClient: GetMlSavedObjectClient; private _getInternalSavedObjectClient: GetInternalSavedObjectClient; private _spacesPlugin: SpacesPluginSetup | undefined; + private _authorization: SecurityPluginSetup['authz'] | undefined; private _isMlReady: () => Promise; constructor( @@ -43,12 +45,14 @@ export class RouteGuard { getSavedObject: GetMlSavedObjectClient, getInternalSavedObject: GetInternalSavedObjectClient, spacesPlugin: SpacesPluginSetup | undefined, + authorization: SecurityPluginSetup['authz'] | undefined, isMlReady: () => Promise ) { this._mlLicense = mlLicense; this._getMlSavedObjectClient = getSavedObject; this._getInternalSavedObjectClient = getInternalSavedObject; this._spacesPlugin = spacesPlugin; + this._authorization = authorization; this._isMlReady = isMlReady; } @@ -81,6 +85,7 @@ export class RouteGuard { mlSavedObjectClient, internalSavedObjectsClient, this._spacesPlugin !== undefined, + this._authorization, this._isMlReady ); const client = context.core.elasticsearch.client; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts index af93c86978856..532a529db1cf2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts @@ -8,6 +8,7 @@ import { SavedObjectsClientContract, KibanaRequest, IScopedClusterClient } from import { Module } from '../../../common/types/modules'; import { DataRecognizer } from '../data_recognizer'; import type { MlClient } from '../../lib/ml_client'; +import { JobSavedObjectService } from '../../saved_objects'; const callAs = () => Promise.resolve({ body: {} }); @@ -26,6 +27,7 @@ describe('ML - data recognizer', () => { find: jest.fn(), bulkCreate: jest.fn(), } as unknown) as SavedObjectsClientContract, + {} as JobSavedObjectService, { headers: { authorization: '' } } as KibanaRequest ); diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 5afd00f259064..f875788d50c5e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -44,6 +44,7 @@ import { jobServiceProvider } from '../job_service'; import { resultsServiceProvider } from '../results_service'; import { JobExistResult, JobStat } from '../../../common/types/data_recognizer'; import { MlJobsStatsResponse } from '../job_service/jobs'; +import { JobSavedObjectService } from '../../saved_objects'; const ML_DIR = 'ml'; const KIBANA_DIR = 'kibana'; @@ -108,6 +109,8 @@ export class DataRecognizer { private _client: IScopedClusterClient; private _mlClient: MlClient; private _savedObjectsClient: SavedObjectsClientContract; + private _jobSavedObjectService: JobSavedObjectService; + private _request: KibanaRequest; private _authorizationHeader: object; private _modulesDir = `${__dirname}/modules`; @@ -127,11 +130,14 @@ export class DataRecognizer { mlClusterClient: IScopedClusterClient, mlClient: MlClient, savedObjectsClient: SavedObjectsClientContract, + jobSavedObjectService: JobSavedObjectService, request: KibanaRequest ) { this._client = mlClusterClient; this._mlClient = mlClient; this._savedObjectsClient = savedObjectsClient; + this._jobSavedObjectService = jobSavedObjectService; + this._request = request; this._authorizationHeader = getAuthorizationHeader(request); this._jobsService = jobServiceProvider(mlClusterClient, mlClient); this._resultsService = resultsServiceProvider(mlClient); @@ -394,7 +400,8 @@ export class DataRecognizer { end?: number, jobOverrides?: JobOverride | JobOverride[], datafeedOverrides?: DatafeedOverride | DatafeedOverride[], - estimateModelMemory: boolean = true + estimateModelMemory: boolean = true, + applyToAllSpaces: boolean = false ) { // load the config from disk const moduleConfig = await this.getModule(moduleId, jobPrefix); @@ -458,7 +465,7 @@ export class DataRecognizer { if (useDedicatedIndex === true) { moduleConfig.jobs.forEach((job) => (job.config.results_index_name = job.id)); } - saveResults.jobs = await this.saveJobs(moduleConfig.jobs); + saveResults.jobs = await this.saveJobs(moduleConfig.jobs, applyToAllSpaces); } // create the datafeeds @@ -699,8 +706,8 @@ export class DataRecognizer { // save the jobs. // if any fail (e.g. it already exists), catch the error and mark the result // as success: false - async saveJobs(jobs: ModuleJob[]): Promise { - return await Promise.all( + async saveJobs(jobs: ModuleJob[], applyToAllSpaces: boolean = false): Promise { + const resp = await Promise.all( jobs.map(async (job) => { const jobId = job.id; try { @@ -712,6 +719,19 @@ export class DataRecognizer { } }) ); + if (applyToAllSpaces === true) { + const canCreateGlobalJobs = await this._jobSavedObjectService.canCreateGlobalJobs( + this._request + ); + if (canCreateGlobalJobs === true) { + await this._jobSavedObjectService.assignJobsToSpaces( + 'anomaly-detector', + jobs.map((j) => j.id), + ['*'] + ); + } + } + return resp; } async saveJob(job: ModuleJob) { diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index f5e79427db616..669fc9a1d92e4 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -16,6 +16,7 @@ import { IClusterClient, SavedObjectsServiceStart, } from 'kibana/server'; +import type { SecurityPluginSetup } from '../../security/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { SpacesPluginSetup } from '../../spaces/server'; import { PluginsSetup, RouteInitialization } from './types'; @@ -68,6 +69,7 @@ export class MlServerPlugin implements Plugin; private setMlReady: () => void = () => {}; @@ -80,6 +82,7 @@ export class MlServerPlugin implements Plugin this.isMlReady ), mlLicense: this.mlLicense, @@ -185,6 +189,7 @@ export class MlServerPlugin implements Plugin this.clusterClient, () => getInternalSavedObjectsClient(), @@ -202,6 +207,7 @@ export class MlServerPlugin implements Plugin { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 95e1d85d793d1..0e5fde8a34c76 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -18,15 +18,23 @@ import { } from './schemas/modules'; import { RouteInitialization } from '../types'; import type { MlClient } from '../lib/ml_client'; +import type { JobSavedObjectService } from '../saved_objects'; function recognize( client: IScopedClusterClient, mlClient: MlClient, savedObjectsClient: SavedObjectsClientContract, + jobSavedObjectService: JobSavedObjectService, request: KibanaRequest, indexPatternTitle: string ) { - const dr = new DataRecognizer(client, mlClient, savedObjectsClient, request); + const dr = new DataRecognizer( + client, + mlClient, + savedObjectsClient, + jobSavedObjectService, + request + ); return dr.findMatches(indexPatternTitle); } @@ -34,10 +42,17 @@ function getModule( client: IScopedClusterClient, mlClient: MlClient, savedObjectsClient: SavedObjectsClientContract, + jobSavedObjectService: JobSavedObjectService, request: KibanaRequest, moduleId: string ) { - const dr = new DataRecognizer(client, mlClient, savedObjectsClient, request); + const dr = new DataRecognizer( + client, + mlClient, + savedObjectsClient, + jobSavedObjectService, + request + ); if (moduleId === undefined) { return dr.listModules(); } else { @@ -49,6 +64,7 @@ function setup( client: IScopedClusterClient, mlClient: MlClient, savedObjectsClient: SavedObjectsClientContract, + jobSavedObjectService: JobSavedObjectService, request: KibanaRequest, moduleId: string, prefix?: string, @@ -61,9 +77,16 @@ function setup( end?: number, jobOverrides?: JobOverride | JobOverride[], datafeedOverrides?: DatafeedOverride | DatafeedOverride[], - estimateModelMemory?: boolean + estimateModelMemory?: boolean, + applyToAllSpaces?: boolean ) { - const dr = new DataRecognizer(client, mlClient, savedObjectsClient, request); + const dr = new DataRecognizer( + client, + mlClient, + savedObjectsClient, + jobSavedObjectService, + request + ); return dr.setup( moduleId, prefix, @@ -76,7 +99,8 @@ function setup( end, jobOverrides, datafeedOverrides, - estimateModelMemory + estimateModelMemory, + applyToAllSpaces ); } @@ -84,10 +108,17 @@ function dataRecognizerJobsExist( client: IScopedClusterClient, mlClient: MlClient, savedObjectsClient: SavedObjectsClientContract, + jobSavedObjectService: JobSavedObjectService, request: KibanaRequest, moduleId: string ) { - const dr = new DataRecognizer(client, mlClient, savedObjectsClient, request); + const dr = new DataRecognizer( + client, + mlClient, + savedObjectsClient, + jobSavedObjectService, + request + ); return dr.dataRecognizerJobsExist(moduleId); } @@ -132,22 +163,25 @@ export function dataRecognizer({ router, routeGuard }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response, context }) => { - try { - const { indexPatternTitle } = request.params; - const results = await recognize( - client, - mlClient, - context.core.savedObjects.client, - request, - indexPatternTitle - ); + routeGuard.fullLicenseAPIGuard( + async ({ client, mlClient, request, response, context, jobSavedObjectService }) => { + try { + const { indexPatternTitle } = request.params; + const results = await recognize( + client, + mlClient, + context.core.savedObjects.client, + jobSavedObjectService, + request, + indexPatternTitle + ); - return response.ok({ body: results }); - } catch (e) { - return response.customError(wrapError(e)); + return response.ok({ body: results }); + } catch (e) { + return response.customError(wrapError(e)); + } } - }) + ) ); /** @@ -268,27 +302,30 @@ export function dataRecognizer({ router, routeGuard }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response, context }) => { - try { - let { moduleId } = request.params; - if (moduleId === '') { - // if the endpoint is called with a trailing / - // the moduleId will be an empty string. - moduleId = undefined; - } - const results = await getModule( - client, - mlClient, - context.core.savedObjects.client, - request, - moduleId - ); + routeGuard.fullLicenseAPIGuard( + async ({ client, mlClient, request, response, context, jobSavedObjectService }) => { + try { + let { moduleId } = request.params; + if (moduleId === '') { + // if the endpoint is called with a trailing / + // the moduleId will be an empty string. + moduleId = undefined; + } + const results = await getModule( + client, + mlClient, + context.core.savedObjects.client, + jobSavedObjectService, + request, + moduleId + ); - return response.ok({ body: results }); - } catch (e) { - return response.customError(wrapError(e)); + return response.ok({ body: results }); + } catch (e) { + return response.customError(wrapError(e)); + } } - }) + ) ); /** @@ -442,49 +479,53 @@ export function dataRecognizer({ router, routeGuard }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response, context }) => { - try { - const { moduleId } = request.params; - - const { - prefix, - groups, - indexPatternName, - query, - useDedicatedIndex, - startDatafeed, - start, - end, - jobOverrides, - datafeedOverrides, - estimateModelMemory, - } = request.body as TypeOf; + routeGuard.fullLicenseAPIGuard( + async ({ client, mlClient, request, response, context, jobSavedObjectService }) => { + try { + const { moduleId } = request.params; - const result = await setup( - client, - mlClient, + const { + prefix, + groups, + indexPatternName, + query, + useDedicatedIndex, + startDatafeed, + start, + end, + jobOverrides, + datafeedOverrides, + estimateModelMemory, + applyToAllSpaces, + } = request.body as TypeOf; - context.core.savedObjects.client, - request, - moduleId, - prefix, - groups, - indexPatternName, - query, - useDedicatedIndex, - startDatafeed, - start, - end, - jobOverrides, - datafeedOverrides, - estimateModelMemory - ); + const result = await setup( + client, + mlClient, + context.core.savedObjects.client, + jobSavedObjectService, + request, + moduleId, + prefix, + groups, + indexPatternName, + query, + useDedicatedIndex, + startDatafeed, + start, + end, + jobOverrides, + datafeedOverrides, + estimateModelMemory, + applyToAllSpaces + ); - return response.ok({ body: result }); - } catch (e) { - return response.customError(wrapError(e)); + return response.ok({ body: result }); + } catch (e) { + return response.customError(wrapError(e)); + } } - }) + ) ); /** @@ -549,22 +590,24 @@ export function dataRecognizer({ router, routeGuard }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response, context }) => { - try { - const { moduleId } = request.params; - const result = await dataRecognizerJobsExist( - client, - mlClient, + routeGuard.fullLicenseAPIGuard( + async ({ client, mlClient, request, response, context, jobSavedObjectService }) => { + try { + const { moduleId } = request.params; + const result = await dataRecognizerJobsExist( + client, + mlClient, + context.core.savedObjects.client, + jobSavedObjectService, + request, + moduleId + ); - context.core.savedObjects.client, - request, - moduleId - ); - - return response.ok({ body: result }); - } catch (e) { - return response.customError(wrapError(e)); + return response.ok({ body: result }); + } catch (e) { + return response.customError(wrapError(e)); + } } - }) + ) ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/modules.ts b/x-pack/plugins/ml/server/routes/schemas/modules.ts index e2b58cf2ce8f2..5fe621fee5ea1 100644 --- a/x-pack/plugins/ml/server/routes/schemas/modules.ts +++ b/x-pack/plugins/ml/server/routes/schemas/modules.ts @@ -69,6 +69,11 @@ export const setupModuleBodySchema = schema.object({ * should be made by checking the cardinality of fields in the job configurations (optional). */ estimateModelMemory: schema.maybe(schema.boolean()), + + /** + * Add each job created to the * space (optional) + */ + applyToAllSpaces: schema.maybe(schema.boolean()), }); export const optionalModuleIdParamSchema = schema.object({ diff --git a/x-pack/plugins/ml/server/saved_objects/authorization.ts b/x-pack/plugins/ml/server/saved_objects/authorization.ts new file mode 100644 index 0000000000000..815ff29ae010c --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/authorization.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; +import type { SecurityPluginSetup } from '../../../security/server'; + +export function authorizationProvider(authorization: SecurityPluginSetup['authz']) { + async function authorizationCheck(request: KibanaRequest) { + const checkPrivilegesWithRequest = authorization.checkPrivilegesWithRequest(request); + // Checking privileges "dynamically" will check against the current space, if spaces are enabled. + // If spaces are disabled, then this will check privileges globally instead. + // SO, if spaces are disabled, then you don't technically need to perform this check, but I included it here + // for completeness. + const checkPrivilegesDynamicallyWithRequest = authorization.checkPrivilegesDynamicallyWithRequest( + request + ); + const createMLJobAuthorizationAction = authorization.actions.savedObject.get( + 'ml-job', + 'create' + ); + const canCreateGlobally = ( + await checkPrivilegesWithRequest.globally({ + kibana: [createMLJobAuthorizationAction], + }) + ).hasAllRequested; + const canCreateAtSpace = ( + await checkPrivilegesDynamicallyWithRequest({ kibana: [createMLJobAuthorizationAction] }) + ).hasAllRequested; + return { + canCreateGlobally, + canCreateAtSpace, + }; + } + + return { authorizationCheck }; +} diff --git a/x-pack/plugins/ml/server/saved_objects/initialization/index.ts b/x-pack/plugins/ml/server/saved_objects/initialization/index.ts new file mode 100644 index 0000000000000..1438d55bfeced --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/initialization/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { jobSavedObjectsInitializationFactory } from './initialization'; diff --git a/x-pack/plugins/ml/server/saved_objects/initialization.ts b/x-pack/plugins/ml/server/saved_objects/initialization/initialization.ts similarity index 78% rename from x-pack/plugins/ml/server/saved_objects/initialization.ts rename to x-pack/plugins/ml/server/saved_objects/initialization/initialization.ts index a446433082ad7..5edf35c033177 100644 --- a/x-pack/plugins/ml/server/saved_objects/initialization.ts +++ b/x-pack/plugins/ml/server/saved_objects/initialization/initialization.ts @@ -5,11 +5,13 @@ */ import { IScopedClusterClient, CoreStart, SavedObjectsClientContract } from 'kibana/server'; -import { savedObjectClientsFactory } from './util'; -import { repairFactory } from './repair'; -import { jobSavedObjectServiceFactory, JobObject } from './service'; -import { mlLog } from '../lib/log'; -import { ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; +import { savedObjectClientsFactory } from '../util'; +import { repairFactory } from '../repair'; +import { jobSavedObjectServiceFactory, JobObject } from '../service'; +import { mlLog } from '../../lib/log'; +import { ML_SAVED_OBJECT_TYPE } from '../../../common/types/saved_objects'; +import { createJobSpaceOverrides } from './space_overrides'; +import type { SecurityPluginSetup } from '../../../../security/server'; /** * Creates initializeJobs function which is used to check whether @@ -17,7 +19,11 @@ import { ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; * * @param core: CoreStart */ -export function jobSavedObjectsInitializationFactory(core: CoreStart, spacesEnabled: boolean) { +export function jobSavedObjectsInitializationFactory( + core: CoreStart, + security: SecurityPluginSetup | undefined, + spacesEnabled: boolean +) { const client = (core.elasticsearch.client as unknown) as IScopedClusterClient; /** @@ -35,22 +41,26 @@ export function jobSavedObjectsInitializationFactory(core: CoreStart, spacesEnab return; } + if ((await _needsInitializing(savedObjectsClient)) === false) { + // ml job saved objects have already been initialized + return; + } + const jobSavedObjectService = jobSavedObjectServiceFactory( savedObjectsClient, savedObjectsClient, spacesEnabled, + security?.authz, () => Promise.resolve() // pretend isMlReady, to allow us to initialize the saved objects ); - if ((await _needsInitializing(savedObjectsClient)) === false) { - // ml job saved objects have already been initialized - return; - } - mlLog.info('Initializing job saved objects'); + // create space overrides for specific jobs + const jobSpaceOverrides = await createJobSpaceOverrides(client); + // initialize jobs const { initSavedObjects } = repairFactory(client, jobSavedObjectService); - const { jobs } = await initSavedObjects(); - mlLog.info(`${jobs.length} job saved objects initialized for * space`); + const { jobs } = await initSavedObjects(false, jobSpaceOverrides); + mlLog.info(`${jobs.length} job saved objects initialized`); } catch (error) { mlLog.error(`Error Initializing jobs ${JSON.stringify(error)}`); } diff --git a/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/index.ts b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/index.ts new file mode 100644 index 0000000000000..c6918bf905b4b --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createJobSpaceOverrides } from './space_overrides'; diff --git a/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/logs.ts b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/logs.ts new file mode 100644 index 0000000000000..913692d40bb9d --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/logs.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import RE2 from 're2'; +import { mlLog } from '../../../lib/log'; +import { Job } from '../../../../common/types/anomaly_detection_jobs'; + +const GROUP = 'logs-ui'; +const MODULE_PREFIX = 'kibana-logs-ui'; +const SOURCES = ['default', 'internal-stack-monitoring']; +const JOB_IDS = ['log-entry-rate', 'log-entry-categories-count']; + +// jobs created by the logs plugin will be in the logs-ui group +// they contain the a space name in the job id, and so the id can be parsed +// and the job assigned to the correct space. +export async function logJobsSpaces({ + asInternalUser, +}: IScopedClusterClient): Promise> { + try { + const { body } = await asInternalUser.ml.getJobs<{ jobs: Job[] }>({ + job_id: GROUP, + }); + if (body.jobs.length === 0) { + return []; + } + + const findLogJobSpace = findLogJobSpaceFactory(); + return body.jobs + .map((j) => ({ id: j.job_id, space: findLogJobSpace(j.job_id) })) + .filter((j) => j.space !== null) as Array<{ id: string; space: string }>; + } catch ({ body }) { + if (body.status !== 404) { + // 404s are expected if there are no logs-ui jobs + mlLog.error(`Error Initializing Logs job ${JSON.stringify(body)}`); + } + } + return []; +} + +function findLogJobSpaceFactory() { + const reg = new RE2(`${MODULE_PREFIX}-(.+)-(${SOURCES.join('|')})-(${JOB_IDS.join('|')})`); + + return (jobId: string) => { + const result = reg.exec(jobId); + if (result === null) { + return null; + } + return result[1] ?? null; + }; +} diff --git a/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/metrics.ts b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/metrics.ts new file mode 100644 index 0000000000000..c0a88567a0c1d --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/metrics.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import RE2 from 're2'; +import { mlLog } from '../../../lib/log'; +import { Job } from '../../../../common/types/anomaly_detection_jobs'; + +const GROUP = 'metrics'; +const MODULE_PREFIX = 'kibana-metrics-ui'; +const SOURCES = ['default', 'internal-stack-monitoring']; +const JOB_IDS = [ + 'k8s_memory_usage', + 'k8s_network_in', + 'k8s_network_out', + 'hosts_memory_usage', + 'hosts_network_in', + 'hosts_network_out', +]; + +// jobs created by the logs plugin will be in the metrics group +// they contain the a space name in the job id, and so the id can be parsed +// and the job assigned to the correct space. +export async function metricsJobsSpaces({ + asInternalUser, +}: IScopedClusterClient): Promise> { + try { + const { body } = await asInternalUser.ml.getJobs<{ jobs: Job[] }>({ + job_id: GROUP, + }); + if (body.jobs.length === 0) { + return []; + } + + const findMetricJobSpace = findMetricsJobSpaceFactory(); + return body.jobs + .map((j) => ({ id: j.job_id, space: findMetricJobSpace(j.job_id) })) + .filter((j) => j.space !== null) as Array<{ id: string; space: string }>; + } catch ({ body }) { + if (body.status !== 404) { + // 404s are expected if there are no metrics jobs + mlLog.error(`Error Initializing Metrics job ${JSON.stringify(body)}`); + } + } + return []; +} + +function findMetricsJobSpaceFactory() { + const reg = new RE2(`${MODULE_PREFIX}-(.+)-(${SOURCES.join('|')})-(${JOB_IDS.join('|')})`); + + return (jobId: string) => { + const result = reg.exec(jobId); + if (result === null) { + return null; + } + return result[1] ?? null; + }; +} diff --git a/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/space_overrides.test.ts b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/space_overrides.test.ts new file mode 100644 index 0000000000000..93d17f21a9c63 --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/space_overrides.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import { createJobSpaceOverrides } from './space_overrides'; + +const jobs = [ + { + job_id: 'kibana-logs-ui-default-default-log-entry-rate', + }, + { + job_id: 'kibana-logs-ui-other_space-default-log-entry-rate', + }, + { + job_id: 'kibana-logs-ui-other_space-default-log-entry-categories-count', + }, + { + job_id: 'kibana-logs-ui-other_space-internal-stack-monitoring-log-entry-rate', + }, + { + job_id: 'kibana-logs-ui-other_space-dinosaur-log-entry-rate', // shouldn't match + }, + { + job_id: 'kibana-logs-ui-other_space-default-dinosaur', // shouldn't match + }, + { + job_id: 'kibana-metrics-ui-default-default-k8s_memory_usage', + }, + { + job_id: 'kibana-metrics-ui-other_space-default-hosts_network_in', + }, +]; + +const result = { + overrides: { + 'anomaly-detector': { + 'kibana-logs-ui-default-default-log-entry-rate': ['default'], + 'kibana-logs-ui-other_space-default-log-entry-rate': ['other_space'], + 'kibana-logs-ui-other_space-default-log-entry-categories-count': ['other_space'], + 'kibana-logs-ui-other_space-internal-stack-monitoring-log-entry-rate': ['other_space'], + 'kibana-metrics-ui-default-default-k8s_memory_usage': ['default'], + 'kibana-metrics-ui-other_space-default-hosts_network_in': ['other_space'], + }, + 'data-frame-analytics': {}, + }, +}; + +const callAs = { + ml: { + getJobs: jest.fn(() => + Promise.resolve({ + body: { jobs }, + }) + ), + }, +}; + +const mlClusterClient = ({ + asInternalUser: callAs, +} as unknown) as IScopedClusterClient; + +describe('ML - job initialization', () => { + describe('createJobSpaceOverrides', () => { + it('should apply job overrides correctly', async () => { + const overrides = await createJobSpaceOverrides(mlClusterClient); + expect(overrides).toEqual(result); + }); + }); +}); diff --git a/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/space_overrides.ts b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/space_overrides.ts new file mode 100644 index 0000000000000..d8c713888051f --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/initialization/space_overrides/space_overrides.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import type { JobSpaceOverrides } from '../../repair'; +import { logJobsSpaces } from './logs'; +import { metricsJobsSpaces } from './metrics'; + +// create a list of jobs and specific spaces to place them in +// when the are being initialized. +export async function createJobSpaceOverrides( + clusterClient: IScopedClusterClient +): Promise { + const spaceOverrides: JobSpaceOverrides = { + overrides: { + 'anomaly-detector': {}, + 'data-frame-analytics': {}, + }, + }; + (await logJobsSpaces(clusterClient)).forEach( + (o) => (spaceOverrides.overrides['anomaly-detector'][o.id] = [o.space]) + ); + (await metricsJobsSpaces(clusterClient)).forEach( + (o) => (spaceOverrides.overrides['anomaly-detector'][o.id] = [o.space]) + ); + return spaceOverrides; +} diff --git a/x-pack/plugins/ml/server/saved_objects/repair.ts b/x-pack/plugins/ml/server/saved_objects/repair.ts index 9271032f83aec..1b0b4b2609a91 100644 --- a/x-pack/plugins/ml/server/saved_objects/repair.ts +++ b/x-pack/plugins/ml/server/saved_objects/repair.ts @@ -7,11 +7,17 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import type { JobObject, JobSavedObjectService } from './service'; -import { JobType } from '../../common/types/saved_objects'; +import { JobType, RepairSavedObjectResponse } from '../../common/types/saved_objects'; import { checksFactory } from './checks'; import { Datafeed } from '../../common/types/anomaly_detection_jobs'; +export interface JobSpaceOverrides { + overrides: { + [type in JobType]: { [jobId: string]: string[] }; + }; +} + export function repairFactory( client: IScopedClusterClient, jobSavedObjectService: JobSavedObjectService @@ -19,13 +25,7 @@ export function repairFactory( const { checkStatus } = checksFactory(client, jobSavedObjectService); async function repairJobs(simulate: boolean = false) { - type Result = Record; - const results: { - savedObjectsCreated: Result; - savedObjectsDeleted: Result; - datafeedsAdded: Result; - datafeedsRemoved: Result; - } = { + const results: RepairSavedObjectResponse = { savedObjectsCreated: {}, savedObjectsDeleted: {}, datafeedsAdded: {}, @@ -173,14 +173,14 @@ export function repairFactory( return results; } - async function initSavedObjects(simulate: boolean = false, namespaces: string[] = ['*']) { + async function initSavedObjects(simulate: boolean = false, spaceOverrides?: JobSpaceOverrides) { const results: { jobs: Array<{ id: string; type: string }>; success: boolean; error?: any } = { jobs: [], success: true, }; const status = await checkStatus(); - const jobs: JobObject[] = []; + const jobs: Array<{ job: JobObject; namespaces: string[] }> = []; const types: JobType[] = ['anomaly-detector', 'data-frame-analytics']; types.forEach((type) => { @@ -190,22 +190,28 @@ export function repairFactory( results.jobs.push({ id: job.jobId, type }); } else { jobs.push({ - job_id: job.jobId, - datafeed_id: job.datafeedId ?? null, - type, + job: { + job_id: job.jobId, + datafeed_id: job.datafeedId ?? null, + type, + }, + // allow some jobs to be assigned to specific spaces when initializing + namespaces: spaceOverrides?.overrides[type][job.jobId] ?? ['*'], }); } } }); }); + try { - const createResults = await jobSavedObjectService.bulkCreateJobs(jobs, namespaces); + const createResults = await jobSavedObjectService.bulkCreateJobs(jobs); createResults.saved_objects.forEach(({ attributes }) => { results.jobs.push({ id: attributes.job_id, type: attributes.type, }); }); + return { jobs: jobs.map((j) => j.job.job_id) }; } catch (error) { results.success = false; results.error = Boom.boomify(error).output; diff --git a/x-pack/plugins/ml/server/saved_objects/service.ts b/x-pack/plugins/ml/server/saved_objects/service.ts index a2453b9ab3fa1..1193dfde85f1c 100644 --- a/x-pack/plugins/ml/server/saved_objects/service.ts +++ b/x-pack/plugins/ml/server/saved_objects/service.ts @@ -5,9 +5,11 @@ */ import RE2 from 're2'; -import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/server'; +import { KibanaRequest, SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/server'; +import type { SecurityPluginSetup } from '../../../security/server'; import { JobType, ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; import { MLJobNotFound } from '../lib/ml_client'; +import { authorizationProvider } from './authorization'; export interface JobObject { job_id: string; @@ -22,6 +24,7 @@ export function jobSavedObjectServiceFactory( savedObjectsClient: SavedObjectsClientContract, internalSavedObjectsClient: SavedObjectsClientContract, spacesEnabled: boolean, + authorization: SecurityPluginSetup['authz'] | undefined, isMlReady: () => Promise ) { async function _getJobObjects( @@ -58,29 +61,33 @@ export function jobSavedObjectServiceFactory( async function _createJob(jobType: JobType, jobId: string, datafeedId?: string) { await isMlReady(); - await savedObjectsClient.create( - ML_SAVED_OBJECT_TYPE, - { - job_id: jobId, - datafeed_id: datafeedId ?? null, - type: jobType, - }, - { id: jobId, overwrite: true } - ); + const job: JobObject = { + job_id: jobId, + datafeed_id: datafeedId ?? null, + type: jobType, + }; + await savedObjectsClient.create(ML_SAVED_OBJECT_TYPE, job, { + id: savedObjectId(job), + overwrite: true, + }); } - async function _bulkCreateJobs(jobs: JobObject[], namespaces?: string[]) { + async function _bulkCreateJobs(jobs: Array<{ job: JobObject; namespaces: string[] }>) { await isMlReady(); return await savedObjectsClient.bulkCreate( jobs.map((j) => ({ type: ML_SAVED_OBJECT_TYPE, - id: j.job_id, - attributes: j, - initialNamespaces: namespaces, + id: savedObjectId(j.job), + attributes: j.job, + initialNamespaces: j.namespaces, })) ); } + function savedObjectId(job: JobObject) { + return `${job.type}-${job.job_id}`; + } + async function _deleteJob(jobType: JobType, jobId: string) { const jobs = await _getJobObjects(jobType, jobId); const job = jobs[0]; @@ -107,8 +114,8 @@ export function jobSavedObjectServiceFactory( await _deleteJob('data-frame-analytics', jobId); } - async function bulkCreateJobs(jobs: JobObject[], namespaces?: string[]) { - return await _bulkCreateJobs(jobs, namespaces); + async function bulkCreateJobs(jobs: Array<{ job: JobObject; namespaces: string[] }>) { + return await _bulkCreateJobs(jobs); } async function getAllJobObjects(jobType?: JobType, currentSpaceOnly: boolean = true) { @@ -279,6 +286,14 @@ export function jobSavedObjectServiceFactory( return results; } + async function canCreateGlobalJobs(request: KibanaRequest) { + if (authorization === undefined) { + return true; + } + const { authorizationCheck } = authorizationProvider(authorization); + return (await authorizationCheck(request)).canCreateGlobally; + } + return { getAllJobObjects, createAnomalyDetectionJob, @@ -295,6 +310,7 @@ export function jobSavedObjectServiceFactory( removeJobsFromSpaces, bulkCreateJobs, getAllJobObjectsForAllSpaces, + canCreateGlobalJobs, }; } diff --git a/x-pack/plugins/ml/server/shared_services/providers/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/modules.ts index ede92208902ae..05a7dba1973eb 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/modules.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/modules.ts @@ -10,6 +10,7 @@ import { DataRecognizer } from '../../models/data_recognizer'; import { GetGuards } from '../shared_services'; import { moduleIdParamSchema, setupModuleBodySchema } from '../../routes/schemas/modules'; import { MlClient } from '../../lib/ml_client'; +import { JobSavedObjectService } from '../../saved_objects'; export type ModuleSetupPayload = TypeOf & TypeOf; @@ -34,8 +35,14 @@ export function getModulesProvider(getGuards: GetGuards): ModulesProvider { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canGetJobs']) - .ok(async ({ scopedClient, mlClient }) => { - const dr = dataRecognizerFactory(scopedClient, mlClient, savedObjectsClient, request); + .ok(async ({ scopedClient, mlClient, jobSavedObjectService }) => { + const dr = dataRecognizerFactory( + scopedClient, + mlClient, + savedObjectsClient, + jobSavedObjectService, + request + ); return dr.findMatches(...args); }); }, @@ -43,8 +50,14 @@ export function getModulesProvider(getGuards: GetGuards): ModulesProvider { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canGetJobs']) - .ok(async ({ scopedClient, mlClient }) => { - const dr = dataRecognizerFactory(scopedClient, mlClient, savedObjectsClient, request); + .ok(async ({ scopedClient, mlClient, jobSavedObjectService }) => { + const dr = dataRecognizerFactory( + scopedClient, + mlClient, + savedObjectsClient, + jobSavedObjectService, + request + ); return dr.getModule(moduleId); }); }, @@ -52,8 +65,14 @@ export function getModulesProvider(getGuards: GetGuards): ModulesProvider { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canGetJobs']) - .ok(async ({ scopedClient, mlClient }) => { - const dr = dataRecognizerFactory(scopedClient, mlClient, savedObjectsClient, request); + .ok(async ({ scopedClient, mlClient, jobSavedObjectService }) => { + const dr = dataRecognizerFactory( + scopedClient, + mlClient, + savedObjectsClient, + jobSavedObjectService, + request + ); return dr.listModules(); }); }, @@ -61,8 +80,14 @@ export function getModulesProvider(getGuards: GetGuards): ModulesProvider { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canCreateJob']) - .ok(async ({ scopedClient, mlClient }) => { - const dr = dataRecognizerFactory(scopedClient, mlClient, savedObjectsClient, request); + .ok(async ({ scopedClient, mlClient, jobSavedObjectService }) => { + const dr = dataRecognizerFactory( + scopedClient, + mlClient, + savedObjectsClient, + jobSavedObjectService, + request + ); return dr.setup( payload.moduleId, payload.prefix, @@ -88,7 +113,8 @@ function dataRecognizerFactory( client: IScopedClusterClient, mlClient: MlClient, savedObjectsClient: SavedObjectsClientContract, + jobSavedObjectService: JobSavedObjectService, request: KibanaRequest ) { - return new DataRecognizer(client, mlClient, savedObjectsClient, request); + return new DataRecognizer(client, mlClient, savedObjectsClient, jobSavedObjectService, request); } diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index 85e24dd7f2819..dc7bc06fde7d5 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -12,7 +12,8 @@ import { SpacesPluginSetup } from '../../../spaces/server'; import { KibanaRequest } from '../../.././../../src/core/server/http'; import { MlLicense } from '../../common/license'; -import { CloudSetup } from '../../../cloud/server'; +import type { CloudSetup } from '../../../cloud/server'; +import type { SecurityPluginSetup } from '../../../security/server'; import { licenseChecks } from './license_checks'; import { MlSystemProvider, getMlSystemProvider } from './providers/system'; import { JobServiceProvider, getJobServiceProvider } from './providers/job_service'; @@ -26,7 +27,7 @@ import { ResolveMlCapabilities, MlCapabilitiesKey } from '../../common/types/cap import { hasMlCapabilitiesProvider, HasMlCapabilities } from '../lib/capabilities'; import { MLClusterClientUninitialized } from './errors'; import { MlClient, getMlClient } from '../lib/ml_client'; -import { jobSavedObjectServiceFactory } from '../saved_objects'; +import { jobSavedObjectServiceFactory, JobSavedObjectService } from '../saved_objects'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -53,6 +54,7 @@ export interface SharedServicesChecks { interface OkParams { scopedClient: IScopedClusterClient; mlClient: MlClient; + jobSavedObjectService: JobSavedObjectService; } type OkCallback = (okParams: OkParams) => any; @@ -61,6 +63,7 @@ export function createSharedServices( mlLicense: MlLicense, spacesPlugin: SpacesPluginSetup | undefined, cloud: CloudSetup, + authorization: SecurityPluginSetup['authz'] | undefined, resolveMlCapabilities: ResolveMlCapabilities, getClusterClient: () => IClusterClient | null, getInternalSavedObjectsClient: () => SavedObjectsClientContract | null, @@ -80,11 +83,14 @@ export function createSharedServices( getClusterClient, savedObjectsClient, internalSavedObjectsClient, + authorization, spacesPlugin !== undefined, isMlReady ); - const { hasMlCapabilities, scopedClient, mlClient } = getRequestItems(request); + const { hasMlCapabilities, scopedClient, mlClient, jobSavedObjectService } = getRequestItems( + request + ); const asyncGuards: Array> = []; const guards: Guards = { @@ -102,7 +108,7 @@ export function createSharedServices( }, async ok(callback: OkCallback) { await Promise.all(asyncGuards); - return callback({ scopedClient, mlClient }); + return callback({ scopedClient, mlClient, jobSavedObjectService }); }, }; return guards; @@ -122,6 +128,7 @@ function getRequestItemsProvider( getClusterClient: () => IClusterClient | null, savedObjectsClient: SavedObjectsClientContract, internalSavedObjectsClient: SavedObjectsClientContract, + authorization: SecurityPluginSetup['authz'] | undefined, spaceEnabled: boolean, isMlReady: () => Promise ) { @@ -138,6 +145,7 @@ function getRequestItemsProvider( savedObjectsClient, internalSavedObjectsClient, spaceEnabled, + authorization, isMlReady ); @@ -158,6 +166,6 @@ function getRequestItemsProvider( }; mlClient = getMlClient(scopedClient, jobSavedObjectService); } - return { hasMlCapabilities, scopedClient, mlClient }; + return { hasMlCapabilities, scopedClient, mlClient, jobSavedObjectService }; }; } diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts index dd0fb33fd2bc6..08850403f9994 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts @@ -83,6 +83,7 @@ export const setupMlJob = async ({ indexPatternName, startDatafeed: false, useDedicatedIndex: true, + applyToAllSpaces: true, }), asSystemRequest: true, } diff --git a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts index 1d25f35e8f38a..fa3d7ed834a9c 100644 --- a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts @@ -41,6 +41,7 @@ export const createMLJob = async ({ startDatafeed: true, start: moment().subtract(2, 'w').valueOf(), indexPatternName: heartbeatIndices, + applyToAllSpaces: true, query: { bool: { filter: [ From 422dd2b6d5b999fb0b2997a1dd06c8c9464b6f3f Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 16 Nov 2020 15:04:34 -0700 Subject: [PATCH 14/99] upgrade to lmdb-store 0.8 (#83479) Co-authored-by: spalger --- package.json | 2 +- packages/kbn-optimizer/src/node/cache.ts | 51 ++++++++++++------------ yarn.lock | 45 ++++++++++++++------- 3 files changed, 57 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index 1a19b322bf655..2a0c292435403 100644 --- a/package.json +++ b/package.json @@ -723,7 +723,7 @@ "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^0.6.10", + "lmdb-store": "^0.8.11", "load-grunt-config": "^3.0.1", "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", diff --git a/packages/kbn-optimizer/src/node/cache.ts b/packages/kbn-optimizer/src/node/cache.ts index 1ce3b9eeeafd0..dc96bf47fafcf 100644 --- a/packages/kbn-optimizer/src/node/cache.ts +++ b/packages/kbn-optimizer/src/node/cache.ts @@ -19,7 +19,6 @@ import Path from 'path'; -// @ts-expect-error no types available import * as LmdbStore from 'lmdb-store'; import { REPO_ROOT, UPSTREAM_BRANCH } from '@kbn/dev-utils'; @@ -37,25 +36,11 @@ const MINUTE = 1000 * 60; const HOUR = MINUTE * 60; const DAY = HOUR * 24; -interface Lmdb { - get(key: string): T | undefined; - put(key: string, value: T, version?: number, ifVersion?: number): Promise; - remove(key: string, ifVersion?: number): Promise; - openDB(options: { name: string; encoding: 'msgpack' | 'string' | 'json' | 'binary' }): Lmdb; - getRange(options?: { - start?: T; - end?: T; - reverse?: boolean; - limit?: number; - versions?: boolean; - }): Iterable<{ key: string; value: T }>; -} - export class Cache { - private readonly codes: Lmdb; - private readonly atimes: Lmdb; - private readonly mtimes: Lmdb; - private readonly sourceMaps: Lmdb; + private readonly codes: LmdbStore.RootDatabase; + private readonly atimes: LmdbStore.Database; + private readonly mtimes: LmdbStore.Database; + private readonly sourceMaps: LmdbStore.Database; private readonly prefix: string; constructor(config: { prefix: string }) { @@ -64,19 +49,23 @@ export class Cache { this.codes = LmdbStore.open({ name: 'codes', path: CACHE_DIR, + // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 maxReaders: 500, }); + // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 this.atimes = this.codes.openDB({ name: 'atimes', encoding: 'string', }); + // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 this.mtimes = this.codes.openDB({ name: 'mtimes', encoding: 'string', }); + // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 this.sourceMaps = this.codes.openDB({ name: 'sourceMaps', encoding: 'msgpack', @@ -92,7 +81,7 @@ export class Cache { } getMtime(path: string) { - return this.mtimes.get(this.getKey(path)); + return this.safeGet(this.mtimes, this.getKey(path)); } getCode(path: string) { @@ -103,11 +92,11 @@ export class Cache { // touched in a long time (currently 30 days) this.atimes.put(key, GLOBAL_ATIME).catch(reportError); - return this.codes.get(key); + return this.safeGet(this.codes, key); } getSourceMap(path: string) { - return this.sourceMaps.get(this.getKey(path)); + return this.safeGet(this.sourceMaps, this.getKey(path)); } update(path: string, file: { mtime: string; code: string; map: any }) { @@ -125,17 +114,27 @@ export class Cache { return `${this.prefix}${path}`; } + private safeGet(db: LmdbStore.Database, key: string) { + try { + return db.get(key) as V | undefined; + } catch (error) { + // get errors indicate that a key value is corrupt in some way, so remove it + db.removeSync(key); + } + } + private async pruneOldKeys() { try { const ATIME_LIMIT = Date.now() - 30 * DAY; const BATCH_SIZE = 1000; - const validKeys: string[] = []; - const invalidKeys: string[] = []; + const validKeys: LmdbStore.Key[] = []; + const invalidKeys: LmdbStore.Key[] = []; + // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 for (const { key, value } of this.atimes.getRange()) { - const atime = parseInt(value, 10); - if (atime < ATIME_LIMIT) { + const atime = parseInt(`${value}`, 10); + if (Number.isNaN(atime) || atime < ATIME_LIMIT) { invalidKeys.push(key); } else { validKeys.push(key); diff --git a/yarn.lock b/yarn.lock index 1b01574ba66e7..3497fdf83d7dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18793,17 +18793,29 @@ livereload-js@^2.3.0: resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c" integrity sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw== -lmdb-store@^0.6.10: - version "0.6.10" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.6.10.tgz#db8efde6e052aabd17ebc63c8a913e1f31694129" - integrity sha512-ZLvp3qbBQ5VlBmaWa4EUAPyYEZ8qdUHsW69HmxkDi84pFQ37WMxYhFaF/7PQkdtxS/vyiKkZigd9TFgHjek1Nw== +lmdb-store-0.9@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/lmdb-store-0.9/-/lmdb-store-0.9-0.7.2.tgz#45b907a46d0a676fee955629bd2f70f06efb26bb" + integrity sha512-/MO8G6p4l7ku1ltCCdE/2ZOtSQBSM0B02vIemMHjoKgjE/fooanJYXIFwtZYM5r/hBDxmO+L3q5ASAXbfHQ6pQ== dependencies: fs-extra "^9.0.1" - msgpackr "^0.5.0" + msgpackr "^0.5.3" nan "^2.14.1" node-gyp-build "^4.2.3" weak-lru-cache "^0.2.0" +lmdb-store@^0.8.11: + version "0.8.11" + resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.8.11.tgz#7f7c756a115ceab32c51c0948444bfd5d1716ab3" + integrity sha512-CFgxh2/TL1NXyJ8FQPXY50O/gADxih7Gx0RjKRScGlyxihqSLd/fGzMkbvDdeAOAS8bsnYpLojAMTSeNiwN8dQ== + dependencies: + fs-extra "^9.0.1" + lmdb-store-0.9 "0.7.2" + msgpackr "^0.5.4" + nan "^2.14.1" + node-gyp-build "^4.2.3" + weak-lru-cache "^0.3.9" + load-bmfont@^1.3.1, load-bmfont@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.4.0.tgz#75f17070b14a8c785fe7f5bee2e6fd4f98093b6b" @@ -20318,20 +20330,20 @@ ms@2.1.1, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -msgpackr-extract@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.4.tgz#8ee5e73d1135340e564c498e8c593134365eb060" - integrity sha512-d3+qwTJzgqqsq2L2sQuH0SoO4StvpUhMqMAKy6tMimn7XdBaRtDlquFzRJsp0iMGt2hnU4UOqD8Tz9mb0KglTA== +msgpackr-extract@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-0.3.5.tgz#0f206da058bd3dad0f8605d324de001a8f4de967" + integrity sha512-zHhstybu+m/j3H6CVBMcILVIzATK6dWRGtlePJjsnSAj8kLT5joMa9i0v21Uc80BPNDcwFsnG/dz2318tfI81w== dependencies: nan "^2.14.1" node-gyp-build "^4.2.3" -msgpackr@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-0.5.1.tgz#7eecbf342645b7718dd2e3386894368d06732b3f" - integrity sha512-nK2uJl67Q5KU3MWkYBUlYynqKS1UUzJ5M1h6TQejuJtJzD3hW2Suv2T1pf01E9lUEr93xaLokf/xC+jwBShMPQ== +msgpackr@^0.5.3, msgpackr@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-0.5.4.tgz#c21c03d5e132d2e54d0b9ced02a75b1f48413380" + integrity sha512-ILEWtIWwd5ESWHKoVjJ4GP7JWkpuAUJ20qi2j2qEC6twecBmK4E6YG3QW847OpmvdAhMJGq2LoDJRn/kNERTeQ== optionalDependencies: - msgpackr-extract "^0.3.4" + msgpackr-extract "^0.3.5" multicast-dns-service-types@^1.1.0: version "1.1.0" @@ -29129,6 +29141,11 @@ weak-lru-cache@^0.2.0: resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-0.2.0.tgz#447379ccff6dfda1b7a9566c9ef168260be859d1" integrity sha512-M1l5CzKvM7maa7tCbtL0NW6sOnp8gqup853+9Aq7GL0XNWKNnFOkeE3v3Z5X2IeMzedPwQyPbi4RlFvD6rxs7A== +weak-lru-cache@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-0.3.9.tgz#9e56920d4115e8542625d8ef8cc278cbd97f7624" + integrity sha512-WqAu3wzbHQvjSi/vgYhidZkf2p7L3Z8iDEIHnqvE31EQQa7Vh7PDOphrRJ1oxlW8JIjgr2HvMcRe9Q1GhW2NPw== + web-namespaces@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" From 859ba3efa3228afa0039760962eedfe777d72e51 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 16 Nov 2020 15:07:57 -0700 Subject: [PATCH 15/99] [kbn/plugin-helpers/build] copy the public assets of a plugin (#83458) Co-authored-by: spalger --- packages/kbn-plugin-helpers/src/cli.ts | 1 + .../src/integration_tests/build.test.ts | 3 +- .../kbn-plugin-helpers/src/tasks/index.ts | 1 + .../src/tasks/write_public_assets.ts | 45 +++++++++++++++++++ .../src/tasks/write_server_files.ts | 2 +- 5 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 packages/kbn-plugin-helpers/src/tasks/write_public_assets.ts diff --git a/packages/kbn-plugin-helpers/src/cli.ts b/packages/kbn-plugin-helpers/src/cli.ts index 21b6559f63650..81db3b0bbebc0 100644 --- a/packages/kbn-plugin-helpers/src/cli.ts +++ b/packages/kbn-plugin-helpers/src/cli.ts @@ -90,6 +90,7 @@ export function runCli() { await Tasks.initTargets(context); await Tasks.optimize(context); + await Tasks.writePublicAssets(context); await Tasks.writeServerFiles(context); await Tasks.yarnInstall(context); diff --git a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts index 5c1ecc4ee4ee4..a9f8ed045f6e3 100644 --- a/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts +++ b/packages/kbn-plugin-helpers/src/integration_tests/build.test.ts @@ -77,7 +77,8 @@ it('builds a generated plugin into a viable archive', async () => { │ info initialized, 0 bundles cached │ info starting worker [1 bundle] │ succ 1 bundles compiled successfully after + @@ -111,12 +105,15 @@ export function ServiceOverview({ - - + + {!isRumAgentName(agentName) && ( - + )} @@ -125,8 +122,8 @@ export function ServiceOverview({ - - + + @@ -175,8 +172,8 @@ export function ServiceOverview({ - - + + @@ -207,7 +204,7 @@ export function ServiceOverview({ - + diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx index 4c8d368811a0c..912490d866e88 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx @@ -4,27 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ErrorStatePrompt } from '../../../shared/ErrorStatePrompt'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; export function FetchWrapper({ - hasData, status, children, }: { - hasData: boolean; status: FETCH_STATUS; - children: React.ReactNode; + children: ReactNode; }) { if (status === FETCH_STATUS.FAILURE) { return ; } - if (!hasData && status !== FETCH_STATUS.SUCCESS) { - return ; - } - return <>{children}; } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index a5a002cf3aca4..34b934c41cca3 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -3,25 +3,27 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; -import { EuiTitle } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; +import { + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable } from '@elastic/eui'; -import { EuiBasicTableColumn } from '@elastic/eui'; +import React, { useState } from 'react'; import styled from 'styled-components'; -import { EuiToolTip } from '@elastic/eui'; import { asInteger } from '../../../../../common/utils/formatters'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; -import { TableLinkFlexItem } from '../table_link_flex_item'; -import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; import { px, truncate, unit } from '../../../../style/variables'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ServiceOverviewTable } from '../service_overview_table'; +import { TableLinkFlexItem } from '../table_link_flex_item'; import { FetchWrapper } from './fetch_wrapper'; interface Props { @@ -108,7 +110,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { render: (_, { last_seen: lastSeen }) => { return ; }, - width: px(unit * 8), + width: px(unit * 9), }, { field: 'occurrences', @@ -223,8 +225,8 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { - - + ` + height: ${tableHeight}px; + display: flex; + flex-direction: column; + + .euiBasicTable { + display: flex; + flex-direction: column; + flex-grow: 1; + + > :first-child { + flex-grow: 1; + } + } + + .euiTableRowCell { + visibility: ${({ isEmptyAndLoading }) => + isEmptyAndLoading ? 'hidden' : 'visible'}; + } +`; + +export function ServiceOverviewTable(props: EuiBasicTableProps) { + const { items, loading } = props; + const isEmptyAndLoading = !!(items.length === 0 && loading); + + return ( + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx index 507acc49d89db..b40df89a22c33 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx @@ -31,6 +31,7 @@ import { onBrushEnd } from '../helper/helper'; interface Props { id: string; fetchStatus: FETCH_STATUS; + height?: number; onToggleLegend?: LegendItemListener; timeseries: TimeSeries[]; /** @@ -44,10 +45,9 @@ interface Props { showAnnotations?: boolean; } -const XY_HEIGHT = unit * 16; - export function LineChart({ id, + height = unit * 16, fetchStatus, onToggleLegend, timeseries, @@ -88,7 +88,7 @@ export function LineChart({ ); return ( - + onBrushEnd({ x, history })} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 2743d12a3eb04..5b977b6991612 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -27,10 +27,14 @@ function yTickFormat(y?: number | null) { } interface Props { + height?: number; showAnnotations?: boolean; } -export function TransactionErrorRateChart({ showAnnotations = true }: Props) { +export function TransactionErrorRateChart({ + height, + showAnnotations = true, +}: Props) { const theme = useTheme(); const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); @@ -71,6 +75,7 @@ export function TransactionErrorRateChart({ showAnnotations = true }: Props) { Date: Mon, 16 Nov 2020 19:52:15 -0700 Subject: [PATCH 18/99] [Maps] saved object tagging (#83197) * add tag selector to save modal * save tag references onSave * populate tags when unwrapping attributes * tslint * update listing page to show tags * fix data-test-subj id in functional tests * i18n cleanup * tslint * remove unused import * use listingTable service for functional tests * tslint and fix mvt grid layer functional test * review feedback * add tags to all privileges and add test user to find, delete, get, get_all, and update tests * move functions to module scope Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../table_list_view/table_list_view.tsx | 5 +- .../functional/page_objects/dashboard_page.ts | 4 +- test/functional/services/listing_table.ts | 31 +- x-pack/plugins/maps/kibana.json | 2 +- x-pack/plugins/maps/public/kibana_services.ts | 1 + .../maps/public/map_attribute_service.ts | 32 +- x-pack/plugins/maps/public/plugin.ts | 2 + .../routes/list_page/maps_list_view.tsx | 572 ++++-------------- .../routes/map_page/saved_map/saved_map.ts | 34 +- .../public/routes/map_page/top_nav_config.tsx | 16 + x-pack/plugins/maps/server/plugin.ts | 4 +- .../translations/translations/ja-JP.json | 15 - .../translations/translations/zh-CN.json | 15 - .../functional/apps/maps/mvt_super_fine.js | 2 +- .../apps/maps/saved_object_management.js | 14 +- .../es_archives/maps/kibana/data.json | 2 +- .../test/functional/page_objects/gis_page.ts | 48 +- .../security_and_spaces/apis/_find.ts | 1 + .../security_and_spaces/apis/create.ts | 1 + .../security_and_spaces/apis/delete.ts | 1 + .../security_and_spaces/apis/get.ts | 1 + .../security_and_spaces/apis/get_all.ts | 1 + .../security_and_spaces/apis/update.ts | 1 + .../fixtures/es_archiver/maps/data.json | 210 +++++++ .../common/lib/authentication.ts | 18 + .../functional/tests/index.ts | 1 + .../functional/tests/maps_integration.ts | 139 +++++ 27 files changed, 603 insertions(+), 570 deletions(-) create mode 100644 x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json create mode 100644 x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 4f509876a75f4..75fd2c30a995a 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -57,7 +57,7 @@ export interface TableListViewProps { listingLimit: number; initialFilter: string; initialPageSize: number; - noItemsFragment: JSX.Element; + noItemsFragment?: JSX.Element; tableColumns: Array>; tableListTitle: string; toastNotifications: ToastsStart; @@ -73,7 +73,7 @@ export interface TableListViewProps { /** * Describes the content of the table. If not specified, the caption will be "This table contains {itemCount} rows." */ - tableCaption: string; + tableCaption?: string; searchFilters?: SearchFilterConfig[]; } @@ -445,6 +445,7 @@ class TableListView extends React.Component { const elements = await find.allByCssSelector( `[data-test-subj^="${prefixMap[appName]}ListingTitleLink"]` @@ -126,14 +123,8 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider /** * Searches for item on Landing page and retruns items count that match `ListingTitleLink-${name}` pattern - * @param appName 'visualize' | 'dashboard' - * @param name item name */ - public async searchAndExpectItemsCount( - appName: 'visualize' | 'dashboard', - name: string, - count: number - ) { + public async searchAndExpectItemsCount(appName: AppName, name: string, count: number) { await this.searchForItemWithName(name); await retry.try(async () => { const links = await testSubjects.findAll( @@ -165,10 +156,8 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider /** * Clicks item on Landing page by link name if it is present - * @param appName 'dashboard' | 'visualize' - * @param name item name */ - public async clickItemLink(appName: 'dashboard' | 'visualize', name: string) { + public async clickItemLink(appName: AppName, name: string) { await testSubjects.click( `${prefixMap[appName]}ListingTitleLink-${name.split(' ').join('-')}` ); @@ -204,6 +193,12 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider } }); } + + public async onListingPage(appName: AppName) { + return await testSubjects.exists(`${appName}LandingPage`, { + timeout: 5000, + }); + } } return new ListingTable(); diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 8983d4ab1a4da..e47968b027cc3 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -19,7 +19,7 @@ "savedObjects", "share" ], - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "savedObjectsTagging"], "ui": true, "server": true, "extraPublicDirs": ["common/constants"], diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 4dcc9193420c0..02b875257a5ac 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -48,6 +48,7 @@ export const getCoreI18n = () => coreStart.i18n; export const getSearchService = () => pluginsStart.data.search; export const getEmbeddableService = () => pluginsStart.embeddable; export const getNavigateToApp = () => coreStart.application.navigateToApp; +export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; // xpack.maps.* kibana.yml settings from this plugin let mapAppConfig: MapsConfigType; diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 0e3ef1b9ea518..9b2f3105f6870 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectReference } from 'src/core/types'; import { AttributeService } from '../../../../src/plugins/embeddable/public'; import { MapSavedObjectAttributes } from '../common/map_saved_object_type'; import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; @@ -14,11 +15,9 @@ import { getCoreOverlays, getEmbeddableService, getSavedObjectsClient } from './ import { extractReferences, injectReferences } from '../common/migrations/references'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; -export type MapAttributeService = AttributeService< - MapSavedObjectAttributes, - MapByValueInput, - MapByReferenceInput ->; +type MapDoc = MapSavedObjectAttributes & { references?: SavedObjectReference[] }; + +export type MapAttributeService = AttributeService; let mapAttributeService: MapAttributeService | null = null; @@ -28,30 +27,37 @@ export function getMapAttributeService(): MapAttributeService { } mapAttributeService = getEmbeddableService().getAttributeService< - MapSavedObjectAttributes, + MapDoc, MapByValueInput, MapByReferenceInput >(MAP_SAVED_OBJECT_TYPE, { - saveMethod: async (attributes: MapSavedObjectAttributes, savedObjectId?: string) => { - const { attributes: attributesWithExtractedReferences, references } = extractReferences({ - attributes, + saveMethod: async (attributes: MapDoc, savedObjectId?: string) => { + // AttributeService "attributes" contains "references" as a child. + // SavedObjectClient "attributes" uses "references" as a sibling. + // https://github.com/elastic/kibana/issues/83133 + const savedObjectClientReferences = attributes.references; + const savedObjectClientAttributes = { ...attributes }; + delete savedObjectClientAttributes.references; + const { attributes: updatedAttributes, references } = extractReferences({ + attributes: savedObjectClientAttributes, + references: savedObjectClientReferences, }); const savedObject = await (savedObjectId ? getSavedObjectsClient().update( MAP_SAVED_OBJECT_TYPE, savedObjectId, - attributesWithExtractedReferences, + updatedAttributes, { references } ) : getSavedObjectsClient().create( MAP_SAVED_OBJECT_TYPE, - attributesWithExtractedReferences, + updatedAttributes, { references } )); return { id: savedObject.id }; }, - unwrapMethod: async (savedObjectId: string): Promise => { + unwrapMethod: async (savedObjectId: string): Promise => { const savedObject = await getSavedObjectsClient().get( MAP_SAVED_OBJECT_TYPE, savedObjectId @@ -62,7 +68,7 @@ export function getMapAttributeService(): MapAttributeService { } const { attributes } = injectReferences(savedObject); - return attributes; + return { ...attributes, references: savedObject.references }; }, checkForDuplicateTitle: (props: OnSaveProps) => { return checkForDuplicateTitle( diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 3da346aaf4443..ecb647cbb61b2 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -63,6 +63,7 @@ import { setLicensingPluginStart, } from './licensed_features'; import { EMSSettings } from '../common/ems_settings'; +import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -86,6 +87,7 @@ export interface MapsPluginStartDependencies { visualizations: VisualizationsStart; savedObjects: SavedObjectsStart; dashboard: DashboardStart; + savedObjectsTagging?: SavedObjectTaggingPluginStart; } /** diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index 97ed3d428d341..a579e3f122cc6 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -5,45 +5,68 @@ */ import React, { MouseEvent } from 'react'; -import _ from 'lodash'; -import { - EuiTitle, - EuiFieldSearch, - EuiBasicTable, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiLink, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiSpacer, - EuiOverlayMask, - EuiConfirmModal, - EuiCallOut, -} from '@elastic/eui'; +import { SavedObjectReference } from 'src/core/types'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Direction } from '@elastic/eui'; -import { - CriteriaWithPagination, - EuiBasicTableColumn, -} from '@elastic/eui/src/components/basic_table/basic_table'; -import { EuiTableSortingType } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; +import { TableListView } from '../../../../../../src/plugins/kibana_react/public'; import { goToSpecifiedPath } from '../../render_app'; import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { getMapsCapabilities, - getUiSettings, getToasts, getCoreChrome, getNavigateToApp, getSavedObjectsClient, + getSavedObjectsTagging, + getSavedObjects, } from '../../kibana_services'; import { getAppTitle } from '../../../common/i18n_getters'; import { MapSavedObjectAttributes } from '../../../common/map_saved_object_type'; -export const EMPTY_FILTER = ''; +interface MapItem { + id: string; + title: string; + description?: string; + references?: SavedObjectReference[]; +} + +const savedObjectsTagging = getSavedObjectsTagging(); +const searchFilters = savedObjectsTagging + ? [savedObjectsTagging.ui.getSearchBarFilter({ useName: true })] + : []; + +const tableColumns: Array> = [ + { + field: 'title', + name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', { + defaultMessage: 'Title', + }), + sortable: true, + render: (field: string, record: MapItem) => ( + { + e.preventDefault(); + goToSpecifiedPath(`/${MAP_PATH}/${record.id}`); + }} + data-test-subj={`mapListingTitleLink-${record.title.split(' ').join('-')}`} + > + {field} + + ), + }, + { + field: 'description', + name: i18n.translate('xpack.maps.mapListing.descriptionFieldTitle', { + defaultMessage: 'Description', + }), + dataType: 'string', + sortable: true, + }, +]; +if (savedObjectsTagging) { + tableColumns.push(savedObjectsTagging.ui.getTableColumnDefinition()); +} function navigateToNewMap() { const navigateToApp = getNavigateToApp(); @@ -51,447 +74,76 @@ function navigateToNewMap() { path: MAP_PATH, }); } -interface State { - sortField?: string | number | symbol; - sortDirection?: Direction; - hasInitialFetchReturned: boolean; - isFetchingItems: boolean; - showDeleteModal: boolean; - showLimitError: boolean; - filter: string; - items: TableRow[]; - selectedIds: string[]; - page: number; - perPage: number; - readOnly: boolean; - listingLimit: number; - totalItems?: number; -} - -interface TableRow { - id: string; - title: string; - description?: string; -} - -export class MapsListView extends React.Component { - _isMounted: boolean = false; - state: State = { - hasInitialFetchReturned: false, - isFetchingItems: false, - showDeleteModal: false, - showLimitError: false, - filter: EMPTY_FILTER, - items: [], - selectedIds: [], - page: 0, - perPage: 20, - readOnly: !getMapsCapabilities().save, - listingLimit: getUiSettings().get('savedObjects:listingLimit'), - }; - componentWillUnmount() { - this._isMounted = false; - this.debouncedFetch.cancel(); - } +async function findMaps(searchQuery: string) { + let searchTerm = searchQuery; + let tagReferences; - componentDidMount() { - this._isMounted = true; - this.initMapList(); - } - - async initMapList() { - this.fetchItems(); - getCoreChrome().docTitle.change(getAppTitle()); - getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]); - } - - debouncedFetch = _.debounce(async (filter) => { - const response = await getSavedObjectsClient().find({ - type: MAP_SAVED_OBJECT_TYPE, - search: filter ? `${filter}*` : undefined, - perPage: this.state.listingLimit, - page: 1, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - fields: ['description', 'title'], - }); - - if (!this._isMounted) { - return; - } - - // We need this check to handle the case where search results come back in a different - // order than they were sent out. Only load results for the most recent search. - if (filter === this.state.filter) { - this.setState({ - hasInitialFetchReturned: true, - isFetchingItems: false, - items: response.savedObjects.map((savedObject) => { - return { - id: savedObject.id, - title: savedObject.attributes.title, - description: savedObject.attributes.description, - }; - }), - totalItems: response.total, - showLimitError: response.total > this.state.listingLimit, - }); - } - }, 300); - - fetchItems = () => { - this.setState( - { - isFetchingItems: true, - }, - this.debouncedFetch.bind(null, this.state.filter) - ); - }; - - deleteSelectedItems = async () => { - try { - const deletions = this.state.selectedIds.map((id) => { - return getSavedObjectsClient().delete(MAP_SAVED_OBJECT_TYPE, id); - }); - await Promise.all(deletions); - } catch (error) { - getToasts().addDanger({ - title: i18n.translate('xpack.maps.mapListing.unableToDeleteToastTitle', { - defaultMessage: `Unable to delete map(s)`, - }), - text: `${error}`, - }); - } - this.fetchItems(); - this.setState({ - selectedIds: [], + if (savedObjectsTagging) { + const parsed = savedObjectsTagging.ui.parseSearchQuery(searchQuery, { + useName: true, }); - this.closeDeleteModal(); - }; - - closeDeleteModal = () => { - this.setState({ showDeleteModal: false }); - }; - - openDeleteModal = () => { - this.setState({ showDeleteModal: true }); - }; - - onTableChange = ({ page, sort }: CriteriaWithPagination) => { - const { index: pageIndex, size: pageSize } = page; - - let { field: sortField, direction: sortDirection } = sort || {}; - - // 3rd sorting state that is not captured by sort - native order (no sort) - // when switching from desc to asc for the same field - use native order - if ( - this.state.sortField === sortField && - this.state.sortDirection === 'desc' && - sortDirection === 'asc' - ) { - sortField = undefined; - sortDirection = undefined; - } - - this.setState({ - page: pageIndex, - perPage: pageSize, - sortField, - sortDirection, - }); - }; - - getPageOfItems = () => { - // do not sort original list to preserve elasticsearch ranking order - const itemsCopy = this.state.items.slice(); - - if (this.state.sortField) { - itemsCopy.sort((a, b) => { - const fieldA = _.get(a, this.state.sortField!, ''); - const fieldB = _.get(b, this.state.sortField!, ''); - let order = 1; - if (this.state.sortDirection === 'desc') { - order = -1; - } - return order * fieldA.toLowerCase().localeCompare(fieldB.toLowerCase()); - }); - } - - // If begin is greater than the length of the sequence, an empty array is returned. - const startIndex = this.state.page * this.state.perPage; - // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). - const lastIndex = startIndex + this.state.perPage; - return itemsCopy.slice(startIndex, lastIndex); - }; - - hasNoItems() { - if (!this.state.isFetchingItems && this.state.items.length === 0 && !this.state.filter) { - return true; - } - - return false; + searchTerm = parsed.searchTerm; + tagReferences = parsed.tagReferences; } - renderConfirmDeleteModal() { - return ( - - -

- -

-
-
- ); - } - - renderListingLimitWarning() { - if (this.state.showLimitError) { - return ( - - -

- - - - - . -

-
- -
- ); - } - } - - renderNoResultsMessage() { - if (this.state.isFetchingItems) { - return ''; - } - - if (this.hasNoItems()) { - return i18n.translate('xpack.maps.mapListing.noItemsDescription', { - defaultMessage: `Looks like you don't have any maps. Click the create button to create one.`, - }); - } - - return i18n.translate('xpack.maps.mapListing.noMatchDescription', { - defaultMessage: 'No items matched your search.', - }); - } - - renderSearchBar() { - let deleteBtn; - if (this.state.selectedIds.length > 0) { - deleteBtn = ( - - - - - - ); - } - - return ( - - {deleteBtn} - - { - this.setState( - { - filter: e.target.value, - }, - this.fetchItems - ); - }} - data-test-subj="searchFilter" - /> - - - ); - } - - renderTable() { - const tableColumns: Array> = [ - { - field: 'title', - name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', { - defaultMessage: 'Title', - }), - sortable: true, - render: (field, record) => ( - { - e.preventDefault(); - goToSpecifiedPath(`/${MAP_PATH}/${record.id}`); - }} - data-test-subj={`mapListingTitleLink-${record.title.split(' ').join('-')}`} - > - {field} - - ), - }, - { - field: 'description', - name: i18n.translate('xpack.maps.mapListing.descriptionFieldTitle', { - defaultMessage: 'Description', - }), - dataType: 'string', - sortable: true, - }, - ]; - const pagination = { - pageIndex: this.state.page, - pageSize: this.state.perPage, - totalItemCount: this.state.items.length, - pageSizeOptions: [10, 20, 50], - }; - - let selection; - if (!this.state.readOnly) { - selection = { - onSelectionChange: (s: TableRow[]) => { - this.setState({ - selectedIds: s.map((item) => { - return item.id; - }), - }); - }, - }; - } + const resp = await getSavedObjectsClient().find({ + type: MAP_SAVED_OBJECT_TYPE, + search: searchTerm ? `${searchTerm}*` : undefined, + perPage: getSavedObjects().settings.getListingLimit(), + page: 1, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + fields: ['description', 'title'], + hasReference: tagReferences, + }); - const sorting: EuiTableSortingType = {}; - if (this.state.sortField) { - sorting.sort = { - field: this.state.sortField, - direction: this.state.sortDirection!, + return { + total: resp.total, + hits: resp.savedObjects.map((savedObject) => { + return { + id: savedObject.id, + title: savedObject.attributes.title, + description: savedObject.attributes.description, + references: savedObject.references, }; - } - const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); - - return ( - - ); - } - - renderListing() { - let createButton; - if (!this.state.readOnly) { - createButton = ( - - - - ); - } - return ( - - {this.state.showDeleteModal && this.renderConfirmDeleteModal()} - - - - -

- -

-
-
- - {createButton} -
- - - - {this.renderListingLimitWarning()} - - {this.renderSearchBar()} - - - - {this.renderTable()} -
- ); - } - - renderPageContent() { - if (!this.state.hasInitialFetchReturned) { - return; - } + }), + }; +} - return {this.renderListing()}; - } +async function deleteMaps(items: object[]) { + const deletions = items.map((item) => { + return getSavedObjectsClient().delete(MAP_SAVED_OBJECT_TYPE, (item as MapItem).id); + }); + await Promise.all(deletions); +} - render() { - return ( - - {this.renderPageContent()} - - ); - } +export function MapsListView() { + const isReadOnly = !getMapsCapabilities().save; + + getCoreChrome().docTitle.change(getAppTitle()); + getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]); + + return ( + + ); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index 036f8cf11d374..98f428f9a2999 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -33,7 +33,12 @@ import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_sele import { getMapAttributeService } from '../../../map_attribute_service'; import { OnSaveProps } from '../../../../../../../src/plugins/saved_objects/public'; import { MapByReferenceInput, MapEmbeddableInput } from '../../../embeddable/types'; -import { getCoreChrome, getToasts, getIsAllowByValueEmbeddables } from '../../../kibana_services'; +import { + getCoreChrome, + getToasts, + getIsAllowByValueEmbeddables, + getSavedObjectsTagging, +} from '../../../kibana_services'; import { goToSpecifiedPath } from '../../../render_app'; import { LayerDescriptor } from '../../../../common/descriptor_types'; import { getInitialLayers } from './get_initial_layers'; @@ -51,6 +56,7 @@ export class SavedMap { private _originatingApp?: string; private readonly _stateTransfer?: EmbeddableStateTransfer; private readonly _store: MapStore; + private _tags: string[] = []; constructor({ defaultLayers = [], @@ -87,7 +93,14 @@ export class SavedMap { description: '', }; } else { - this._attributes = await getMapAttributeService().unwrapAttributes(this._mapEmbeddableInput); + const doc = await getMapAttributeService().unwrapAttributes(this._mapEmbeddableInput); + const references = doc.references; + delete doc.references; + this._attributes = doc; + const savedObjectsTagging = getSavedObjectsTagging(); + if (savedObjectsTagging && references && references.length) { + this._tags = savedObjectsTagging.ui.getTagIdsFromReferences(references); + } } if (this._attributes?.mapStateJSON) { @@ -216,6 +229,10 @@ export class SavedMap { return this._getStateTransfer().getAppNameFromId(appId); }; + public getTags(): string[] { + return this._tags; + } + public hasSaveAndReturnConfig() { const hasOriginatingApp = !!this._originatingApp; const isNewMap = !this.getSavedObjectId(); @@ -247,9 +264,11 @@ export class SavedMap { newTitle, newCopyOnSave, returnToOrigin, + newTags, saveByReference, }: OnSaveProps & { returnToOrigin: boolean; + newTags?: string[]; saveByReference: boolean; }) { if (!this._attributes) { @@ -264,8 +283,17 @@ export class SavedMap { let updatedMapEmbeddableInput: MapEmbeddableInput; try { + const savedObjectsTagging = getSavedObjectsTagging(); + // Attribute service deviates from Saved Object client by including references as a child to attributes in stead of a sibling + const attributes = + savedObjectsTagging && newTags + ? { + ...this._attributes, + references: savedObjectsTagging.ui.updateTagsReferences([], newTags), + } + : this._attributes; updatedMapEmbeddableInput = (await getMapAttributeService().wrapAttributes( - this._attributes, + attributes, saveByReference, newCopyOnSave ? undefined : this._mapEmbeddableInput )) as MapEmbeddableInput; diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 2d0a7d967a6cf..43a74a9c73012 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -14,6 +14,7 @@ import { getCoreI18n, getSavedObjectsClient, getCoreOverlays, + getSavedObjectsTagging, } from '../../kibana_services'; import { checkForDuplicateTitle, @@ -125,6 +126,19 @@ export function getTopNavConfig({ } }, run: () => { + let selectedTags = savedMap.getTags(); + function onTagsSelected(newTags: string[]) { + selectedTags = newTags; + } + + const savedObjectsTagging = getSavedObjectsTagging(); + const tagSelector = savedObjectsTagging ? ( + + ) : undefined; + const saveModal = ( ); showSaveModal(saveModal, getCoreI18n().Context); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 65d79272494f0..a79e5353048c8 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -185,7 +185,7 @@ export class MapsPlugin implements Plugin { catalogue: [APP_ID], savedObject: { all: [MAP_SAVED_OBJECT_TYPE, 'query'], - read: ['index-pattern'], + read: ['index-pattern', 'tag'], }, ui: ['save', 'show', 'saveQuery'], }, @@ -194,7 +194,7 @@ export class MapsPlugin implements Plugin { catalogue: [APP_ID], savedObject: { all: [], - read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query'], + read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query', 'tag'], }, ui: ['show'], }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ba6ac32f2e3d0..13159c4e824ac 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11284,24 +11284,9 @@ "xpack.maps.layerWizardSelect.solutionsCategoryLabel": "ソリューション", "xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "マップを読み込めません", "xpack.maps.map.initializeErrorTitle": "マップを初期化できません", - "xpack.maps.mapListing.advancedSettingsLinkText": "高度な設定", - "xpack.maps.mapListing.cancelTitle": "キャンセル", - "xpack.maps.mapListing.createMapButtonLabel": "マップを作成", - "xpack.maps.mapListing.deleteSelectedButtonLabel": "選択項目を削除", - "xpack.maps.mapListing.deleteSelectedItemsTitle": "選択項目を削除しますか?", - "xpack.maps.mapListing.deleteTitle": "削除", - "xpack.maps.mapListing.deleteWarning": "削除されたアイテムは復元できません。", "xpack.maps.mapListing.descriptionFieldTitle": "説明", "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "マップを読み込めません", - "xpack.maps.mapListing.limitExceededTitle": "リスティング制限超過", - "xpack.maps.mapListing.limitHelpDescription": "{totalItems} 個のアイテムがありますが、listingLimit の設定により {listingLimit} 個までしか下の表に表示できません。この設定は次の場所で変更できます ", - "xpack.maps.mapListing.listingTableTitle": "マップ", - "xpack.maps.mapListing.noItemsDescription": "マップがないようです。作成ボタンをクリックして作成してください。", - "xpack.maps.mapListing.noMatchDescription": "検索に一致するアイテムがありません。", - "xpack.maps.mapListing.searchAriaLabel": "フィルターアイテム", - "xpack.maps.mapListing.searchPlaceholder": "検索…", "xpack.maps.mapListing.titleFieldTitle": "タイトル", - "xpack.maps.mapListing.unableToDeleteToastTitle": "マップを削除できません", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "インデックスパターンを選択", "xpack.maps.mapSavedObjectLabel": "マップ", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "自動的にマップをデータ境界に合わせる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 822ccf5cc8409..2e9971f00dcc2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11297,24 +11297,9 @@ "xpack.maps.layerWizardSelect.solutionsCategoryLabel": "解决方案", "xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "无法加载地图", "xpack.maps.map.initializeErrorTitle": "无法初始化地图", - "xpack.maps.mapListing.advancedSettingsLinkText": "高级设置", - "xpack.maps.mapListing.cancelTitle": "取消", - "xpack.maps.mapListing.createMapButtonLabel": "创建地图", - "xpack.maps.mapListing.deleteSelectedButtonLabel": "删除选定", - "xpack.maps.mapListing.deleteSelectedItemsTitle": "删除选定项?", - "xpack.maps.mapListing.deleteTitle": "删除", - "xpack.maps.mapListing.deleteWarning": "您无法恢复已删除项。", "xpack.maps.mapListing.descriptionFieldTitle": "描述", "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "无法加载地图", - "xpack.maps.mapListing.limitExceededTitle": "已超过列表限制", - "xpack.maps.mapListing.limitHelpDescription": "您有 {totalItems} 项,但您的 listingLimit 设置阻止下表显示 {listingLimit} 项以上。此设置可在以下选项下更改: ", - "xpack.maps.mapListing.listingTableTitle": "Maps", - "xpack.maps.mapListing.noItemsDescription": "似乎您没有任何地图。单击创建按钮来创建。", - "xpack.maps.mapListing.noMatchDescription": "没有任何项匹配您的搜索。", - "xpack.maps.mapListing.searchAriaLabel": "筛选项", - "xpack.maps.mapListing.searchPlaceholder": "搜索......", "xpack.maps.mapListing.titleFieldTitle": "标题", - "xpack.maps.mapListing.unableToDeleteToastTitle": "无法删除地图", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "选择索引模式", "xpack.maps.mapSavedObjectLabel": "地图", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "使地图自适应数据边界", diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js index b5a7935a81eb5..6d86b93c3ec44 100644 --- a/x-pack/test/functional/apps/maps/mvt_super_fine.js +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }) { ['global_maps_all', 'test_logstash_reader', 'geoshape_data_reader'], false ); - await PageObjects.maps.loadSavedMap('geo grid vector grid example (SUPER_FINE resolution)'); + await PageObjects.maps.loadSavedMap('geo grid vector grid example SUPER_FINE resolution'); }); after(async () => { diff --git a/x-pack/test/functional/apps/maps/saved_object_management.js b/x-pack/test/functional/apps/maps/saved_object_management.js index 277a8a5651453..8c62136472921 100644 --- a/x-pack/test/functional/apps/maps/saved_object_management.js +++ b/x-pack/test/functional/apps/maps/saved_object_management.js @@ -139,8 +139,8 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.openNewMap(); await PageObjects.maps.saveMap(MAP1_NAME); - const count = await PageObjects.maps.getMapCountWithName(MAP1_NAME); - expect(count).to.equal(1); + + await PageObjects.maps.searchAndExpectItemsCount(MAP1_NAME, 1); }); it('should allow saving map that crosses dateline', async () => { @@ -148,8 +148,8 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.setView('64', '179', '5'); await PageObjects.maps.saveMap(MAP2_NAME); - const count = await PageObjects.maps.getMapCountWithName(MAP2_NAME); - expect(count).to.equal(1); + + await PageObjects.maps.searchAndExpectItemsCount(MAP2_NAME, 1); }); }); @@ -157,11 +157,9 @@ export default function ({ getPageObjects, getService }) { it('should delete selected saved objects', async () => { await PageObjects.maps.deleteSavedMaps(MAP_NAME_PREFIX); - const map1Count = await PageObjects.maps.getMapCountWithName(MAP1_NAME); - expect(map1Count).to.equal(0); + await PageObjects.maps.searchAndExpectItemsCount(MAP1_NAME, 0); - const map2Count = await PageObjects.maps.getMapCountWithName(MAP2_NAME); - expect(map2Count).to.equal(0); + await PageObjects.maps.searchAndExpectItemsCount(MAP2_NAME, 0); }); }); }); diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index e3a8743e60897..79e8c14cc3982 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -638,7 +638,7 @@ "description": "", "layerListJSON": "[{\"id\":\"g1xkv\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"resolution\": \"SUPER_FINE\",\"type\":\"ES_GEO_GRID\",\"id\":\"9305f6ea-4518-4c06-95b9-33321aa38d6a\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"metrics\":[{\"type\":\"count\"},{\"type\":\"max\",\"field\":\"bytes\"}]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max of bytes\",\"name\":\"max_of_bytes\",\"origin\":\"source\"},\"color\":\"Blues\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#cccccc\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"TILED_VECTOR\"}]", "mapStateJSON": "{\"zoom\":3.59,\"center\":{\"lon\":-98.05765,\"lat\":38.32288},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", - "title": "geo grid vector grid example (SUPER_FINE resolution)", + "title": "geo grid vector grid example SUPER_FINE resolution", "uiStateJSON": "{\"isDarkMode\":false}" }, "type": "map" diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index c4f1bd7dc2a6b..7e22acf785d36 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -21,6 +21,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const renderable = getService('renderable'); const browser = getService('browser'); const MenuToggle = getService('MenuToggle'); + const listingTable = getService('listingTable'); const setViewPopoverToggle = new MenuToggle({ name: 'SetView Popover', @@ -120,13 +121,10 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await retry.try(async () => { await this.searchForMapWithName(name); - await this.selectMap(name); + await listingTable.clickItemLink('map', name); await PageObjects.header.waitUntilLoadingHasFinished(); - - const onMapListingPage = await this.onMapListingPage(); - if (onMapListingPage) { - throw new Error(`Failed to open map ${name}`); - } + // check Map landing page is not present + await testSubjects.missingOrFail('mapLandingPage', { timeout: 10000 }); }); await this.waitForLayersToLoad(); @@ -134,8 +132,8 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte async deleteSavedMaps(search: string) { await this.searchForMapWithName(search); - await testSubjects.click('checkboxSelectAll'); - await testSubjects.click('deleteSelectedItems'); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); await PageObjects.common.clickConfirmOnModal(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -150,7 +148,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await renderable.waitForRender(); } - async saveMap(name: string, uncheckReturnToOriginModeSwitch = false) { + async saveMap(name: string, uncheckReturnToOriginModeSwitch = false, tags?: string[]) { await testSubjects.click('mapSaveButton'); await testSubjects.setValue('savedObjectTitle', name); if (uncheckReturnToOriginModeSwitch) { @@ -162,6 +160,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } await testSubjects.setEuiSwitch('returnToOriginModeSwitch', 'uncheck'); } + if (tags) { + await testSubjects.click('savedObjectTagSelector'); + for (const tagName of tags) { + await testSubjects.click(`tagSelectorOption-${tagName.replace(' ', '_')}`); + } + await testSubjects.click('savedObjectTitle'); + } await testSubjects.clickWhenNotDisabled('confirmSaveSavedObjectButton'); } @@ -174,7 +179,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } async expectMissingCreateNewButton() { - await testSubjects.missingOrFail('newMapLink'); + await testSubjects.missingOrFail('newItemButton'); } async expectMissingAddLayerButton() { @@ -187,8 +192,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte async onMapListingPage() { log.debug(`onMapListingPage`); - const exists = await testSubjects.exists('mapsListingPage', { timeout: 3500 }); - return exists; + return await listingTable.onListingPage('map'); } async searchForMapWithName(name: string) { @@ -196,21 +200,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await this.gotoMapListingPage(); - await retry.try(async () => { - const searchFilter = await testSubjects.find('searchFilter'); - await searchFilter.clearValue(); - await searchFilter.click(); - await searchFilter.type(name); - await PageObjects.common.pressEnterKey(); - }); + await listingTable.searchForItemWithName(name); await PageObjects.header.waitUntilLoadingHasFinished(); } - async selectMap(name: string) { - await testSubjects.click(`mapListingTitleLink-${name.split(' ').join('-')}`); - } - async getHits() { await inspector.open(); await inspector.openInspectorRequestsView(); @@ -232,13 +226,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } } - async getMapCountWithName(name: string) { + async searchAndExpectItemsCount(name: string, count: number) { await this.gotoMapListingPage(); - log.debug(`getMapCountWithName: ${name}`); - await this.searchForMapWithName(name); - const buttons = await find.allByButtonText(name); - return buttons.length; + log.debug(`searchAndExpectItemsCount: ${name}`); + await listingTable.searchAndExpectItemsCount('map', name, count); } async setView(lat: number, lon: number, zoom: number) { diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts index 4f08134365e95..8734b7cf5bb68 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts index 70884ba6c968b..8ca92ac472c6e 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts index 64f120fd75629..a2e3630622d67 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts @@ -53,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts index 1a354bbbcb660..9cde766b4f514 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts @@ -57,6 +57,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts index 61b859cf81992..677bdee56ed8b 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts @@ -63,6 +63,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts index 77bf9d7ca3287..3347eca9920d6 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json new file mode 100644 index 0000000000000..cdaf4fe171ec0 --- /dev/null +++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json @@ -0,0 +1,210 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "_reserved": true, + "description": "This is the default space", + "name": "Default Space" + }, + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-1", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-1", + "description": "My first tag!", + "color": "#FF00FF" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-2", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-2", + "description": "Another awesome tag", + "color": "#11FF22" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-3", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-3", + "description": "Last but not least", + "color": "#AA0077" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:6.3.0", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-04-11T20:43:55.434Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:63af0ed0-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map": { + "title" : "map 3 (tag-1 and tag-3)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"d897b506-e719-42b8-9927-351eedd7d357\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-1", + "name" : "tag-ref-tag-1" + }, + { + "type" : "tag", + "id" : "tag-3", + "name" : "tag-ref-tag-3" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:32:16.189Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:4afc6d10-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 1 (tag-2)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"6a91fb66-465c-4193-8c59-9b3f5f262756\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":16.22097},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-2", + "name" : "tag-ref-tag-2" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:31:34.753Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:562cce50-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 2 (tag-3)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"285d5190-aaf1-4dfc-912b-9c7d9e0104a8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-3", + "name" : "tag-ref-tag-3" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:31:53.525Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:6f021340-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 4 (tag-1)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"3deeb666-33cf-4e9a-ab78-e453ed9d721d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-1", + "name" : "tag-ref-tag-1" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:32:35.188Z" + } + } +} diff --git a/x-pack/test/saved_object_tagging/common/lib/authentication.ts b/x-pack/test/saved_object_tagging/common/lib/authentication.ts index c318755bedcdd..8917057ad685e 100644 --- a/x-pack/test/saved_object_tagging/common/lib/authentication.ts +++ b/x-pack/test/saved_object_tagging/common/lib/authentication.ts @@ -118,6 +118,19 @@ export const ROLES = { ], }, }, + KIBANA_RBAC_DEFAULT_SPACE_MAPS_READ_USER: { + name: 'kibana_rbac_default_space_maps_read_user', + privileges: { + kibana: [ + { + feature: { + maps: ['read'], + }, + spaces: ['default'], + }, + ], + }, + }, }; export const USERS = { @@ -185,4 +198,9 @@ export const USERS = { password: 'password', roles: [ROLES.KIBANA_RBAC_DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER.name], }, + DEFAULT_SPACE_MAPS_READ_USER: { + username: 'a_kibana_rbac_default_space_maps_read_user', + password: 'password', + roles: [ROLES.KIBANA_RBAC_DEFAULT_SPACE_MAPS_READ_USER.name], + }, }; diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index 43673487ba74f..0ddfa64d682a8 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./visualize_integration')); loadTestFile(require.resolve('./dashboard_integration')); loadTestFile(require.resolve('./feature_control')); + loadTestFile(require.resolve('./maps_integration')); }); } diff --git a/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts new file mode 100644 index 0000000000000..4e44659b4fc67 --- /dev/null +++ b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const PageObjects = getPageObjects(['maps', 'tagManagement', 'common']); + + /** + * Select tags in the searchbar's tag filter. + */ + const selectFilterTags = async (...tagNames: string[]) => { + // open the filter dropdown + const filterButton = await find.byCssSelector('.euiFilterGroup .euiFilterButton'); + await filterButton.click(); + // select the tags + for (const tagName of tagNames) { + await testSubjects.click( + `tag-searchbar-option-${PageObjects.tagManagement.testSubjFriendly(tagName)}` + ); + } + // click elsewhere to close the filter dropdown + const searchFilter = await find.byCssSelector('main .euiFieldSearch'); + await searchFilter.click(); + }; + + describe('maps integration', () => { + before(async () => { + await esArchiver.load('maps'); + }); + after(async () => { + await esArchiver.unload('maps'); + }); + + describe('listing', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory('maps', '/'); + await PageObjects.maps.gotoMapListingPage(); + }); + + it('allows to manually type tag filter query', async () => { + await listingTable.searchForItemWithName('tag:(tag-1)', { escape: false }); + + await listingTable.expectItemsCount('map', 2); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 4 (tag-1)']); + }); + + it('allows to filter by selecting a tag in the filter menu', async () => { + await selectFilterTags('tag-3'); + + await listingTable.expectItemsCount('map', 2); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 2 (tag-3)']); + }); + + it('allows to filter by multiple tags', async () => { + await selectFilterTags('tag-2', 'tag-3'); + + await listingTable.expectItemsCount('map', 3); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 1 (tag-2)', 'map 2 (tag-3)']); + }); + }); + + describe('creating', () => { + beforeEach(async () => { + await PageObjects.maps.openNewMap(); + }); + + it('allows to select tags for a new map', async () => { + await PageObjects.maps.saveMap('my-new-map', false, ['tag-1', 'tag-3']); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('tag-1'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('my-new-map'); + }); + + it('allows to create a tag from the tag selector', async () => { + const { tagModal } = PageObjects.tagManagement; + + await testSubjects.click('mapSaveButton'); + await testSubjects.setValue('savedObjectTitle', 'map-with-new-tag'); + + await testSubjects.click('savedObjectTagSelector'); + await testSubjects.click(`tagSelectorOption-action__create`); + + expect(await tagModal.isOpened()).to.be(true); + + await tagModal.fillForm( + { + name: 'my-new-tag', + color: '#FFCC33', + description: '', + }, + { + submit: true, + } + ); + + expect(await tagModal.isOpened()).to.be(false); + + await testSubjects.click('confirmSaveSavedObjectButton'); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('my-new-tag'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('map-with-new-tag'); + }); + }); + + describe('editing', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory('maps', '/'); + }); + + it('allows to select tags for an existing map', async () => { + await listingTable.clickItemLink('map', 'map 4 (tag-1)'); + + await PageObjects.maps.saveMap('map 4 (tag-1)', false, ['tag-3']); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('tag-3'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('map 4 (tag-1)'); + }); + }); + }); +} From 04d79f889161cd555dc4c7fc36e9950110f2bc93 Mon Sep 17 00:00:00 2001 From: Daniil Date: Tue, 17 Nov 2020 11:11:55 +0300 Subject: [PATCH 19/99] Fix styles loading order (#83299) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/timelion/public/index.scss | 2 +- .../vis_type_timelion/public/components/index.scss | 2 -- .../components/{_timelion_vis.scss => timelion_vis.scss} | 8 -------- .../public/components/timelion_vis_component.tsx | 2 +- ...melion_expression_input.scss => timelion_options.scss} | 8 ++++++++ src/plugins/vis_type_timelion/public/timelion_options.tsx | 2 ++ 6 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 src/plugins/vis_type_timelion/public/components/index.scss rename src/plugins/vis_type_timelion/public/components/{_timelion_vis.scss => timelion_vis.scss} (88%) rename src/plugins/vis_type_timelion/public/{components/_timelion_expression_input.scss => timelion_options.scss} (77%) diff --git a/src/plugins/timelion/public/index.scss b/src/plugins/timelion/public/index.scss index f39e0c18a2870..b93e99bf9bcc4 100644 --- a/src/plugins/timelion/public/index.scss +++ b/src/plugins/timelion/public/index.scss @@ -15,4 +15,4 @@ // styles for timelion visualization are lazy loaded only while a vis is opened // this will duplicate styles only if both Timelion app and timelion visualization are loaded // could be left here as it is since the Timelion app is deprecated -@import '../../vis_type_timelion/public/components/index.scss'; +@import '../../vis_type_timelion/public/components/timelion_vis.scss'; diff --git a/src/plugins/vis_type_timelion/public/components/index.scss b/src/plugins/vis_type_timelion/public/components/index.scss deleted file mode 100644 index a541c66e6e913..0000000000000 --- a/src/plugins/vis_type_timelion/public/components/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'timelion_vis'; -@import 'timelion_expression_input'; diff --git a/src/plugins/vis_type_timelion/public/components/_timelion_vis.scss b/src/plugins/vis_type_timelion/public/components/timelion_vis.scss similarity index 88% rename from src/plugins/vis_type_timelion/public/components/_timelion_vis.scss rename to src/plugins/vis_type_timelion/public/components/timelion_vis.scss index 6729d400523cd..c4d591bc82cad 100644 --- a/src/plugins/vis_type_timelion/public/components/_timelion_vis.scss +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis.scss @@ -58,11 +58,3 @@ white-space: nowrap; font-weight: $euiFontWeightBold; } - -.visEditor--timelion { - .visEditorSidebar__timelionOptions { - flex: 1 1 auto; - display: flex; - flex-direction: column; - } -} diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx index 953ec5e819f44..a448b58afe8a4 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx @@ -40,7 +40,7 @@ import { tickFormatters } from '../helpers/tick_formatters'; import { generateTicksProvider } from '../helpers/tick_generator'; import { TimelionVisDependencies } from '../plugin'; -import './index.scss'; +import './timelion_vis.scss'; interface CrosshairPlot extends jquery.flot.plot { setCrosshair: (pos: Position) => void; diff --git a/src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss b/src/plugins/vis_type_timelion/public/timelion_options.scss similarity index 77% rename from src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss rename to src/plugins/vis_type_timelion/public/timelion_options.scss index 3f274520cce63..7cd0c855653c8 100644 --- a/src/plugins/vis_type_timelion/public/components/_timelion_expression_input.scss +++ b/src/plugins/vis_type_timelion/public/timelion_options.scss @@ -26,3 +26,11 @@ max-height: $euiSize * 15; } } + +.visEditor--timelion { + .visEditorSidebar__timelionOptions { + flex: 1 1 auto; + display: flex; + flex-direction: column; + } +} diff --git a/src/plugins/vis_type_timelion/public/timelion_options.tsx b/src/plugins/vis_type_timelion/public/timelion_options.tsx index 1ef8088c7a714..90a39c2c90e26 100644 --- a/src/plugins/vis_type_timelion/public/timelion_options.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_options.tsx @@ -27,6 +27,8 @@ import { TimelionVisParams } from './timelion_vis_fn'; import { TimelionInterval, TimelionExpressionInput } from './components'; import { TimelionVisDependencies } from './plugin'; +import './timelion_options.scss'; + export type TimelionOptionsProps = VisOptionsProps; function TimelionOptions({ From 80f63f61599e9ef5f099d1dafbdc95ff329320fd Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 17 Nov 2020 09:15:23 +0100 Subject: [PATCH 20/99] Clean up UI Actions Enhanced server plugin (#83442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 remove unused code * refactor: 💡 don't pass plugin definition to a function * perf: ⚡️ fetch factory from registry only once --- .../server/dynamic_action_enhancement.ts | 22 ++++---- .../ui_actions_enhanced/server/plugin.ts | 54 ++----------------- 2 files changed, 14 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index ade78c31211ab..4cea7ddf4854a 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -6,23 +6,19 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; -import { DynamicActionsState, SerializedEvent } from './types'; -import { AdvancedUiActionsServerPlugin } from './plugin'; +import { ActionFactory, DynamicActionsState, SerializedEvent } from './types'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; export const dynamicActionEnhancement = ( - uiActionsEnhanced: AdvancedUiActionsServerPlugin + getActionFactory: (id: string) => undefined | ActionFactory ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', telemetry: (state: SerializableState, telemetry: Record) => { let telemetryData = telemetry; (state as DynamicActionsState).events.forEach((event: SerializedEvent) => { - if (uiActionsEnhanced.getActionFactory(event.action.factoryId)) { - telemetryData = uiActionsEnhanced - .getActionFactory(event.action.factoryId)! - .telemetry(event, telemetryData); - } + const factory = getActionFactory(event.action.factoryId); + if (factory) telemetryData = factory.telemetry(event, telemetryData); }); return telemetryData; }, @@ -30,8 +26,9 @@ export const dynamicActionEnhancement = ( const references: SavedObjectReference[] = []; const newState: DynamicActionsState = { events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { - const result = uiActionsEnhanced.getActionFactory(event.action.factoryId) - ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.extract(event) + const factory = getActionFactory(event.action.factoryId); + const result = factory + ? factory.extract(event) : { state: event, references: [], @@ -45,9 +42,8 @@ export const dynamicActionEnhancement = ( inject: (state: SerializableState, references: SavedObjectReference[]) => { return { events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { - return uiActionsEnhanced.getActionFactory(event.action.factoryId) - ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.inject(event, references) - : event; + const factory = getActionFactory(event.action.factoryId); + return factory ? factory.inject(event, references) : event; }), } as DynamicActionsState; }, diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts index 718304018730d..e6362418efc66 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -5,15 +5,10 @@ */ import { identity } from 'lodash'; -import { CoreSetup, Plugin, SavedObjectReference } from '../../../../src/core/server'; +import { CoreSetup, Plugin } from '../../../../src/core/server'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server'; import { dynamicActionEnhancement } from './dynamic_action_enhancement'; -import { - ActionFactoryRegistry, - SerializedEvent, - ActionFactoryDefinition, - DynamicActionsState, -} from './types'; +import { ActionFactoryRegistry, SerializedEvent, ActionFactoryDefinition } from './types'; export interface SetupContract { registerActionFactory: (definition: ActionFactoryDefinition) => void; @@ -32,7 +27,9 @@ export class AdvancedUiActionsServerPlugin constructor() {} public setup(core: CoreSetup, { embeddable }: SetupDependencies) { - embeddable.registerEnhancement(dynamicActionEnhancement(this)); + const getActionFactory = (actionFactoryId: string) => this.actionFactories.get(actionFactoryId); + + embeddable.registerEnhancement(dynamicActionEnhancement(getActionFactory)); return { registerActionFactory: this.registerActionFactory, @@ -64,45 +61,4 @@ export class AdvancedUiActionsServerPlugin migrations: definition.migrations || {}, }); }; - - public readonly getActionFactory = (actionFactoryId: string) => { - const actionFactory = this.actionFactories.get(actionFactoryId); - return actionFactory; - }; - - public readonly telemetry = (state: DynamicActionsState, telemetry: Record = {}) => { - state.events.forEach((event: SerializedEvent) => { - if (this.actionFactories.has(event.action.factoryId)) { - this.actionFactories.get(event.action.factoryId)!.telemetry(event, telemetry); - } - }); - return telemetry; - }; - - public readonly extract = (state: DynamicActionsState) => { - const references: SavedObjectReference[] = []; - const newState = { - events: state.events.map((event: SerializedEvent) => { - const result = this.actionFactories.has(event.action.factoryId) - ? this.actionFactories.get(event.action.factoryId)!.extract(event) - : { - state: event, - references: [], - }; - result.references.forEach((r) => references.push(r)); - return result.state; - }), - }; - return { state: newState, references }; - }; - - public readonly inject = (state: DynamicActionsState, references: SavedObjectReference[]) => { - return { - events: state.events.map((event: SerializedEvent) => { - return this.actionFactories.has(event.action.factoryId) - ? this.actionFactories.get(event.action.factoryId)!.inject(event, references) - : event; - }), - }; - }; } From c043eafcdaa0995f2b0b3076b16d0c7d2704ee70 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 17 Nov 2020 09:27:19 +0100 Subject: [PATCH 21/99] =?UTF-8?q?Upgrade=20`nodemailer`=20dependency=20(`4?= =?UTF-8?q?.7.0`=20=E2=86=92=20`6.4.16`).=20(#83445)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 2a0c292435403..0f65524d7161b 100644 --- a/package.json +++ b/package.json @@ -247,7 +247,7 @@ "nock": "12.0.3", "node-fetch": "^2.6.1", "node-forge": "^0.10.0", - "nodemailer": "^4.7.0", + "nodemailer": "^6.4.16", "normalize-path": "^3.0.0", "object-hash": "^1.3.1", "object-path-immutable": "^3.1.1", @@ -500,7 +500,7 @@ "@types/node": "12.19.4", "@types/node-fetch": "^2.5.7", "@types/node-forge": "^0.9.5", - "@types/nodemailer": "^6.2.1", + "@types/nodemailer": "^6.4.0", "@types/normalize-path": "^3.0.0", "@types/object-hash": "^1.3.0", "@types/opn": "^5.1.0", diff --git a/yarn.lock b/yarn.lock index 3497fdf83d7dd..91ae4b236adf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5277,10 +5277,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46" integrity sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w== -"@types/nodemailer@^6.2.1": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.2.1.tgz#8f089bf0ef826f04b9d8dd8750233b04978cb675" - integrity sha512-6f46rxxaFwyOW39psPoQiM7jHjL7apDRNT5WPHIuv+TZFv+7sBGSI9J7blIC3/NWff4O9/VSzgoQtO6aPLUdvQ== +"@types/nodemailer@^6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.0.tgz#d8c039be3ed685c4719a026455555be82c124b74" + integrity sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA== dependencies: "@types/node" "*" @@ -20791,10 +20791,10 @@ node-status-codes@^1.0.0: resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8= -nodemailer@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.7.0.tgz#4420e06abfffd77d0618f184ea49047db84f4ad8" - integrity sha512-IludxDypFpYw4xpzKdMAozBSkzKHmNBvGanUREjJItgJ2NYcK/s8+PggVhj7c2yGFQykKsnnmv1+Aqo0ZfjHmw== +nodemailer@^6.4.16: + version "6.4.16" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.16.tgz#5cb6391b1d79ab7eff32d6f9f48366b5a7117293" + integrity sha512-68K0LgZ6hmZ7PVmwL78gzNdjpj5viqBdFqKrTtr9bZbJYj6BRj5W6WGkxXrEnUl3Co3CBXi3CZBUlpV/foGnOQ== nodemon@^2.0.4: version "2.0.6" From 9caaa0c7e00de383f3bbacb442f7f8bbf5b8b156 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 17 Nov 2020 09:41:35 +0100 Subject: [PATCH 22/99] Bump Node.js from 12.19.0 to 12.19.1 (#83452) --- .ci/Dockerfile | 2 +- .node-version | 2 +- .nvmrc | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 9ac6c32772e4b..b2254c8fb1e05 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=12.19.0 +ARG NODE_VERSION=12.19.1 FROM node:${NODE_VERSION} AS base diff --git a/.node-version b/.node-version index 260a0e20f68fe..e9f788b12771f 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -12.19.0 +12.19.1 diff --git a/.nvmrc b/.nvmrc index 260a0e20f68fe..e9f788b12771f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.19.0 +12.19.1 diff --git a/package.json b/package.json index 0f65524d7161b..b45789172cee9 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "**/typescript": "4.0.2" }, "engines": { - "node": "12.19.0", + "node": "12.19.1", "yarn": "^1.21.1" }, "dependencies": { From 846b94d862ae46e16333035778db27bdf12820ad Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 17 Nov 2020 10:11:44 +0100 Subject: [PATCH 23/99] create metric before bucket agg (#83432) --- .../indexpattern_suggestions.test.tsx | 80 ++++++++++--------- .../indexpattern_suggestions.ts | 8 +- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index c88af50e525d4..9fbad553d441a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -188,7 +188,7 @@ describe('IndexPattern Data Source suggestions', () => { }; } - it('should apply a bucketed aggregation for a string field', () => { + it('should apply a bucketed aggregation for a string field, using metric for sorting', () => { const suggestions = getDatasourceSuggestionsForField(stateWithoutLayer(), '1', { name: 'source', displayName: 'source', @@ -202,14 +202,17 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { id1: expect.objectContaining({ - columnOrder: ['id2', 'id3'], + columnOrder: ['id3', 'id2'], columns: { - id2: expect.objectContaining({ + id3: expect.objectContaining({ operationType: 'terms', sourceField: 'source', - params: expect.objectContaining({ size: 5 }), + params: expect.objectContaining({ + size: 5, + orderBy: { columnId: 'id2', type: 'column' }, + }), }), - id3: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'count', }), }, @@ -222,10 +225,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id2', + columnId: 'id3', }), expect.objectContaining({ - columnId: 'id3', + columnId: 'id2', }), ], layerId: 'id1', @@ -248,13 +251,13 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { id1: expect.objectContaining({ - columnOrder: ['id2', 'id3'], + columnOrder: ['id3', 'id2'], columns: { - id2: expect.objectContaining({ + id3: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), - id3: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'count', }), }, @@ -267,10 +270,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id2', + columnId: 'id3', }), expect.objectContaining({ - columnId: 'id3', + columnId: 'id2', }), ], layerId: 'id1', @@ -408,7 +411,7 @@ describe('IndexPattern Data Source suggestions', () => { }; } - it('should apply a bucketed aggregation for a string field', () => { + it('should apply a bucketed aggregation for a string field, using metric for sorting', () => { const suggestions = getDatasourceSuggestionsForField(stateWithEmptyLayer(), '1', { name: 'source', displayName: 'source', @@ -422,14 +425,17 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { previousLayer: expect.objectContaining({ - columnOrder: ['id1', 'id2'], + columnOrder: ['id2', 'id1'], columns: { - id1: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'terms', sourceField: 'source', - params: expect.objectContaining({ size: 5 }), + params: expect.objectContaining({ + size: 5, + orderBy: { columnId: 'id1', type: 'column' }, + }), }), - id2: expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'count', }), }, @@ -442,10 +448,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id1', + columnId: 'id2', }), expect.objectContaining({ - columnId: 'id2', + columnId: 'id1', }), ], layerId: 'previousLayer', @@ -468,13 +474,13 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { previousLayer: expect.objectContaining({ - columnOrder: ['id1', 'id2'], + columnOrder: ['id2', 'id1'], columns: { - id1: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), - id2: expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'count', }), }, @@ -487,10 +493,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id1', + columnId: 'id2', }), expect.objectContaining({ - columnId: 'id2', + columnId: 'id1', }), ], layerId: 'previousLayer', @@ -1050,13 +1056,13 @@ describe('IndexPattern Data Source suggestions', () => { layers: { previousLayer: initialState.layers.previousLayer, currentLayer: expect.objectContaining({ - columnOrder: ['id1', 'id2'], + columnOrder: ['id2', 'id1'], columns: { - id1: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), - id2: expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'count', }), }, @@ -1069,10 +1075,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id1', + columnId: 'id2', }), expect.objectContaining({ - columnId: 'id2', + columnId: 'id1', }), ], layerId: 'currentLayer', @@ -1097,13 +1103,13 @@ describe('IndexPattern Data Source suggestions', () => { layers: { currentLayer: initialState.layers.currentLayer, previousLayer: expect.objectContaining({ - columnOrder: ['id1', 'id2'], + columnOrder: ['id2', 'id1'], columns: { - id1: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'date_histogram', sourceField: 'timestamp', }), - id2: expect.objectContaining({ + id1: expect.objectContaining({ operationType: 'count', }), }, @@ -1146,14 +1152,14 @@ describe('IndexPattern Data Source suggestions', () => { state: expect.objectContaining({ layers: { id1: expect.objectContaining({ - columnOrder: ['id2', 'id3'], + columnOrder: ['id3', 'id2'], columns: { - id2: expect.objectContaining({ + id3: expect.objectContaining({ operationType: 'terms', sourceField: 'source', params: expect.objectContaining({ size: 5 }), }), - id3: expect.objectContaining({ + id2: expect.objectContaining({ operationType: 'count', }), }, @@ -1166,10 +1172,10 @@ describe('IndexPattern Data Source suggestions', () => { isMultiRow: true, columns: [ expect.objectContaining({ - columnId: 'id2', + columnId: 'id3', }), expect.objectContaining({ - columnId: 'id3', + columnId: 'id2', }), ], layerId: 'id1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index b74d75207e112..ccdefee62ad5c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -289,16 +289,16 @@ function createNewLayerWithBucketAggregation( operation: OperationType ): IndexPatternLayer { return insertNewColumn({ - op: 'count', + op: operation, layer: insertNewColumn({ - op: operation, + op: 'count', layer: { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] }, columnId: generateId(), - field, + field: documentField, indexPattern, }), columnId: generateId(), - field: documentField, + field, indexPattern, }); } From 95e44f25a69772cfe9554ae1debd6bd6c69fba90 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 17 Nov 2020 10:19:13 +0100 Subject: [PATCH 24/99] [Lens] Color in dimension trigger (#76871) --- .../charts/public/services/palettes/mock.ts | 25 ++- .../visualization.test.tsx | 2 +- .../datatable_visualization/visualization.tsx | 12 +- .../lens/public/drag_drop/drag_drop.scss | 2 +- .../config_panel/color_indicator.tsx | 71 ++++++++ .../config_panel/layer_panel.scss | 46 ++++-- .../config_panel/layer_panel.test.tsx | 18 +- .../editor_frame/config_panel/layer_panel.tsx | 63 ++++--- .../config_panel/palette_indicator.tsx | 27 +++ .../dimension_panel/dimension_panel.tsx | 28 ++-- .../metric_visualization/visualization.tsx | 2 +- .../pie_visualization/visualization.tsx | 19 ++- x-pack/plugins/lens/public/types.ts | 10 +- .../xy_visualization/color_assignment.test.ts | 50 ++++++ .../xy_visualization/color_assignment.ts | 64 +++++--- .../xy_visualization/expression.test.tsx | 4 +- .../lens/public/xy_visualization/index.ts | 6 +- .../public/xy_visualization/state_helpers.ts | 14 +- .../xy_visualization/to_expression.test.ts | 2 + .../public/xy_visualization/to_expression.ts | 13 +- .../xy_visualization/visualization.test.ts | 155 +++++++++++++++++- .../public/xy_visualization/visualization.tsx | 97 ++++++++++- .../xy_visualization/xy_suggestions.test.ts | 2 + 23 files changed, 604 insertions(+), 128 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx diff --git a/src/plugins/charts/public/services/palettes/mock.ts b/src/plugins/charts/public/services/palettes/mock.ts index a7ec3cc16ce6f..2e45d93999b8e 100644 --- a/src/plugins/charts/public/services/palettes/mock.ts +++ b/src/plugins/charts/public/services/palettes/mock.ts @@ -22,7 +22,7 @@ import { PaletteService } from './service'; import { PaletteDefinition, SeriesLayer } from './types'; export const getPaletteRegistry = () => { - const mockPalette: jest.Mocked = { + const mockPalette1: jest.Mocked = { id: 'default', title: 'My Palette', getColor: jest.fn((_: SeriesLayer[]) => 'black'), @@ -41,9 +41,28 @@ export const getPaletteRegistry = () => { })), }; + const mockPalette2: jest.Mocked = { + id: 'mocked', + title: 'Mocked Palette', + getColor: jest.fn((_: SeriesLayer[]) => 'blue'), + getColors: jest.fn((num: number) => ['blue', 'yellow']), + toExpression: jest.fn(() => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'system_palette', + arguments: { + name: ['mocked'], + }, + }, + ], + })), + }; + return { - get: (_: string) => mockPalette, - getAll: () => [mockPalette], + get: (name: string) => (name !== 'default' ? mockPalette2 : mockPalette1), + getAll: () => [mockPalette1, mockPalette2], }; }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index d82c7b092c38a..0af8e01d7290d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -284,7 +284,7 @@ describe('Datatable Visualization', () => { state: { layers: [layer] }, frame, }).groups[1].accessors - ).toEqual(['c', 'b']); + ).toEqual([{ columnId: 'c' }, { columnId: 'b' }]); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index e0f6ae31719ca..8b5d2d7d73348 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -149,9 +149,9 @@ export const datatableVisualization: Visualization defaultMessage: 'Break down by', }), layerId: state.layers[0].layerId, - accessors: sortedColumns.filter( - (c) => datasource!.getOperationForColumnId(c)?.isBucketed - ), + accessors: sortedColumns + .filter((c) => datasource!.getOperationForColumnId(c)?.isBucketed) + .map((accessor) => ({ columnId: accessor })), supportsMoreColumns: true, filterOperations: (op) => op.isBucketed, dataTestSubj: 'lnsDatatable_column', @@ -162,9 +162,9 @@ export const datatableVisualization: Visualization defaultMessage: 'Metrics', }), layerId: state.layers[0].layerId, - accessors: sortedColumns.filter( - (c) => !datasource!.getOperationForColumnId(c)?.isBucketed - ), + accessors: sortedColumns + .filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed) + .map((accessor) => ({ columnId: accessor })), supportsMoreColumns: true, filterOperations: (op) => !op.isBucketed, required: true, diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index 8766c9f0acabf..ded0b4552a4e5 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -47,7 +47,7 @@ // Drop area will be replacing existing content .lnsDragDrop-isReplacing { &, - .lnsLayerPanel__triggerLink { + .lnsLayerPanel__triggerText { text-decoration: line-through; } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx new file mode 100644 index 0000000000000..5ee1139ff09a2 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AccessorConfig } from '../../../types'; + +export function ColorIndicator({ + accessorConfig, + children, +}: { + accessorConfig: AccessorConfig; + children: React.ReactChild; +}) { + let indicatorIcon = null; + if (accessorConfig.triggerIcon && accessorConfig.triggerIcon !== 'none') { + const baseIconProps = { + size: 's', + className: 'lnsLayerPanel__colorIndicator', + } as const; + + indicatorIcon = ( + + {accessorConfig.triggerIcon === 'color' && accessorConfig.color && ( + + )} + {accessorConfig.triggerIcon === 'disabled' && ( + + )} + {accessorConfig.triggerIcon === 'colorBy' && ( + + )} + + ); + } + + return ( + + {indicatorIcon} + {children} + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index b98d5b748edb8..a1a072be77f81 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -52,6 +52,7 @@ align-items: center; overflow: hidden; min-height: $euiSizeXXL; + position: relative; // NativeRenderer is messing this up > div { @@ -80,28 +81,18 @@ margin-right: $euiSizeS; } -.lnsLayerPanel__triggerLink { +.lnsLayerPanel__triggerText { width: 100%; padding: $euiSizeS; min-height: $euiSizeXXL - 2; word-break: break-word; - - &:focus { - background-color: transparent !important; // sass-lint:disable-line no-important - outline: none !important; // sass-lint:disable-line no-important - } - - &:focus .lnsLayerPanel__triggerLinkLabel, - &:focus-within .lnsLayerPanel__triggerLinkLabel { - background-color: transparentize($euiColorVis1, .9); - } } -.lnsLayerPanel__triggerLinkLabel { +.lnsLayerPanel__triggerTextLabel { transition: background-color $euiAnimSpeedFast ease-in-out; } -.lnsLayerPanel__triggerLinkContent { +.lnsLayerPanel__triggerTextContent { // Make EUI button content not centered justify-content: flex-start; padding: 0 !important; // sass-lint:disable-line no-important @@ -111,3 +102,32 @@ .lnsLayerPanel__styleEditor { padding: 0 $euiSizeS $euiSizeS; } + +.lnsLayerPanel__colorIndicator { + margin-left: $euiSizeS; +} + +.lnsLayerPanel__paletteContainer { + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +.lnsLayerPanel__paletteColor { + height: $euiSizeXS; +} + +.lnsLayerPanel__dimensionLink { + width: 100%; + + &:focus { + background-color: transparent !important; // sass-lint:disable-line no-important + outline: none !important; // sass-lint:disable-line no-important + } + + &:focus .lnsLayerPanel__triggerTextLabel, + &:focus-within .lnsLayerPanel__triggerTextLabel { + background-color: transparentize($euiColorVis1, .9); + } +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index f440042801ca6..37dc039df498b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -137,7 +137,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['x'], + accessors: [{ columnId: 'x' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroup', @@ -177,7 +177,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['x'], + accessors: [{ columnId: 'x' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroup', @@ -209,7 +209,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['newid'], + accessors: [{ columnId: 'newid' }], filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroup', @@ -257,7 +257,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['newid'], + accessors: [{ columnId: 'newid' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroup', @@ -302,7 +302,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['newid'], + accessors: [{ columnId: 'newid' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroup', @@ -377,7 +377,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['a'], + accessors: [{ columnId: 'a' }], filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroup', @@ -416,7 +416,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['a'], + accessors: [{ columnId: 'a' }], filterOperations: () => true, supportsMoreColumns: false, dataTestSubj: 'lnsGroupA', @@ -424,7 +424,7 @@ describe('LayerPanel', () => { { groupLabel: 'B', groupId: 'b', - accessors: ['b'], + accessors: [{ columnId: 'b' }], filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroupB', @@ -480,7 +480,7 @@ describe('LayerPanel', () => { { groupLabel: 'A', groupId: 'a', - accessors: ['a', 'b'], + accessors: [{ columnId: 'a' }, { columnId: 'b' }], filterOperations: () => true, supportsMoreColumns: true, dataTestSubj: 'lnsGroup', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index f780f9c3f22d7..f5b31fb881167 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiButtonEmpty, EuiFormRow, + EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -25,6 +26,8 @@ import { trackUiEvent } from '../../../lens_ui_telemetry'; import { generateId } from '../../../id_generator'; import { ConfigPanelWrapperProps, ActiveDimensionState } from './types'; import { DimensionContainer } from './dimension_container'; +import { ColorIndicator } from './color_indicator'; +import { PaletteIndicator } from './palette_indicator'; const initialActiveDimensionState = { isNew: false, @@ -181,6 +184,10 @@ export function LayerPanel( const newId = generateId(); const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + const triggerLinkA11yText = i18n.translate('xpack.lens.configure.editConfig', { + defaultMessage: 'Click to edit configuration or drag to move', + }); + return ( <> - {group.accessors.map((accessor) => { + {group.accessors.map((accessorConfig) => { + const accessor = accessorConfig.columnId; const { dragging } = dragDropContext; const dragType = isDraggedOperation(dragging) && accessor === dragging.columnId @@ -253,7 +261,9 @@ export function LayerPanel( dragType={dragType} dropType={dropType} data-test-subj={group.dataTestSubj} - itemsInGroup={group.accessors} + itemsInGroup={group.accessors.map((a) => + typeof a === 'string' ? a : a.columnId + )} className={'lnsLayerPanel__dimensionContainer'} value={{ columnId: accessor, @@ -304,25 +314,33 @@ export function LayerPanel( }} >
- { - if (activeId) { - setActiveDimension(initialActiveDimensionState); - } else { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: accessor, - }); - } - }, + { + if (activeId) { + setActiveDimension(initialActiveDimensionState); + } else { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: accessor, + }); + } }} - /> + aria-label={triggerLinkA11yText} + title={triggerLinkA11yText} + > + + + + +
); @@ -409,12 +428,12 @@ export function LayerPanel( >
{ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx new file mode 100644 index 0000000000000..7e65fe7025932 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/palette_indicator.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AccessorConfig } from '../../../types'; + +export function PaletteIndicator({ accessorConfig }: { accessorConfig: AccessorConfig }) { + if (accessorConfig.triggerIcon !== 'colorBy' || !accessorConfig.palette) return null; + return ( + + {accessorConfig.palette.map((color) => ( + + ))} + + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index baf4f6bb9a6a3..94018bd84b517 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -6,7 +6,7 @@ import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiLink, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiText, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types'; @@ -66,10 +66,6 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens } const formattedLabel = wrapOnDot(uniqueLabel); - const triggerLinkA11yText = i18n.translate('xpack.lens.configure.editConfig', { - defaultMessage: 'Click to edit configuration or drag to move', - }); - if (currentFieldIsInvalid) { return ( - @@ -101,26 +95,24 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens {selectedColumn.label} - + ); } return ( - - {formattedLabel} + {formattedLabel} - + ); }; diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index b75ac89d7e4d8..d8c475734e67e 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -96,7 +96,7 @@ export const metricVisualization: Visualization = { groupId: 'metric', groupLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }), layerId: props.state.layerId, - accessors: props.state.accessor ? [props.state.accessor] : [], + accessors: props.state.accessor ? [{ columnId: props.state.accessor }] : [], supportsMoreColumns: !props.state.accessor, filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number', }, diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 62e99396edbc7..91f0ddb54ad41 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -9,7 +9,7 @@ import { render } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { PaletteRegistry } from 'src/plugins/charts/public'; -import { Visualization, OperationMetadata } from '../types'; +import { Visualization, OperationMetadata, AccessorConfig } from '../types'; import { toExpression, toPreviewExpression } from './to_expression'; import { LayerState, PieVisualizationState } from './types'; import { suggestions } from './suggestions'; @@ -113,7 +113,18 @@ export const getPieVisualization = ({ .map(({ columnId }) => columnId) .filter((columnId) => columnId !== layer.metric); // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.groups))); + const sortedColumns: AccessorConfig[] = Array.from( + new Set(originalOrder.concat(layer.groups)) + ).map((accessor) => ({ columnId: accessor })); + if (sortedColumns.length > 0) { + sortedColumns[0] = { + columnId: sortedColumns[0].columnId, + triggerIcon: 'colorBy', + palette: paletteService + .get(state.palette?.name || 'default') + .getColors(10, state.palette?.params), + }; + } if (state.shape === 'treemap') { return { @@ -137,7 +148,7 @@ export const getPieVisualization = ({ defaultMessage: 'Size by', }), layerId, - accessors: layer.metric ? [layer.metric] : [], + accessors: layer.metric ? [{ columnId: layer.metric }] : [], supportsMoreColumns: !layer.metric, filterOperations: numberMetricOperations, required: true, @@ -168,7 +179,7 @@ export const getPieVisualization = ({ defaultMessage: 'Size by', }), layerId, - accessors: layer.metric ? [layer.metric] : [], + accessors: layer.metric ? [{ columnId: layer.metric }] : [], supportsMoreColumns: !layer.metric, filterOperations: numberMetricOperations, required: true, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index b8bceb5454bc8..225fedb987c76 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -242,7 +242,6 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & { dragDropContext: DragContextState; - onClick: () => void; }; export interface DatasourceLayerPanelProps { @@ -341,12 +340,19 @@ export type VisualizationDimensionEditorProps = VisualizationConfig setState: (newState: T) => void; }; +export interface AccessorConfig { + columnId: string; + triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none'; + color?: string; + palette?: string[]; +} + export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; /** ID is passed back to visualization. For example, `x` */ groupId: string; - accessors: string[]; + accessors: AccessorConfig[]; supportsMoreColumns: boolean; /** If required, a warning will appear if accessors are empty */ required?: boolean; diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts index b59e09e8c1976..666b0d5098218 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.test.ts @@ -128,6 +128,31 @@ describe('color_assignment', () => { expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3); expect(formatMock).toHaveBeenCalledWith(complexObject); }); + + it('should handle missing tables', () => { + const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory); + // if there is no data, just assume a single split + expect(assignments.palette1.totalSeriesCount).toEqual(2); + }); + + it('should handle missing columns', () => { + const assignments = getColorAssignments( + layers, + { + ...data, + tables: { + ...data.tables, + '1': { + ...data.tables['1'], + columns: [], + }, + }, + }, + formatFactory + ); + // if the split column is missing, just assume a single split + expect(assignments.palette1.totalSeriesCount).toEqual(2); + }); }); describe('getRank', () => { @@ -178,5 +203,30 @@ describe('color_assignment', () => { // 3 series in front of (complex object)/y1 - abc/y1, abc/y2 expect(assignments.palette1.getRank(layers[0], 'formatted', 'y1')).toEqual(2); }); + + it('should handle missing tables', () => { + const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory); + // if there is no data, assume it is the first splitted series. One series in front - 0/y1 + expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); + }); + + it('should handle missing columns', () => { + const assignments = getColorAssignments( + layers, + { + ...data, + tables: { + ...data.tables, + '1': { + ...data.tables['1'], + columns: [], + }, + }, + }, + formatFactory + ); + // if the split column is missing, assume it is the first splitted series. One series in front - 0/y1 + expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1); + }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index 5f72dd1b0453b..68c47e11acfc0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -5,20 +5,36 @@ */ import { uniq, mapValues } from 'lodash'; -import { FormatFactory, LensMultiTable } from '../types'; -import { LayerArgs, LayerConfig } from './types'; +import { PaletteOutput } from 'src/plugins/charts/public'; +import { Datatable } from 'src/plugins/expressions'; +import { FormatFactory } from '../types'; const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object'; +interface LayerColorConfig { + palette?: PaletteOutput; + splitAccessor?: string; + accessors: string[]; + layerId: string; +} + +export type ColorAssignments = Record< + string, + { + totalSeriesCount: number; + getRank(layer: LayerColorConfig, seriesKey: string, yAccessor: string): number; + } +>; + export function getColorAssignments( - layers: LayerArgs[], - data: LensMultiTable, + layers: LayerColorConfig[], + data: { tables: Record }, formatFactory: FormatFactory -) { - const layersPerPalette: Record = {}; +): ColorAssignments { + const layersPerPalette: Record = {}; layers.forEach((layer) => { - const palette = layer.palette?.name || 'palette'; + const palette = layer.palette?.name || 'default'; if (!layersPerPalette[palette]) { layersPerPalette[palette] = []; } @@ -31,18 +47,21 @@ export function getColorAssignments( return { numberOfSeries: layer.accessors.length, splits: [] }; } const splitAccessor = layer.splitAccessor; - const column = data.tables[layer.layerId].columns.find(({ id }) => id === splitAccessor)!; - const splits = uniq( - data.tables[layer.layerId].rows.map((row) => { - let value = row[splitAccessor]; - if (value && !isPrimitive(value)) { - value = formatFactory(column.meta.params).convert(value); - } else { - value = String(value); - } - return value; - }) - ); + const column = data.tables[layer.layerId]?.columns.find(({ id }) => id === splitAccessor); + const splits = + !column || !data.tables[layer.layerId] + ? [] + : uniq( + data.tables[layer.layerId].rows.map((row) => { + let value = row[splitAccessor]; + if (value && !isPrimitive(value)) { + value = formatFactory(column.meta.params).convert(value); + } else { + value = String(value); + } + return value; + }) + ); return { numberOfSeries: (splits.length || 1) * layer.accessors.length, splits }; }); const totalSeriesCount = seriesPerLayer.reduce( @@ -51,18 +70,17 @@ export function getColorAssignments( ); return { totalSeriesCount, - getRank(layer: LayerArgs, seriesKey: string, yAccessor: string) { + getRank(layer: LayerColorConfig, seriesKey: string, yAccessor: string) { const layerIndex = paletteLayers.indexOf(layer); const currentSeriesPerLayer = seriesPerLayer[layerIndex]; + const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey); return ( (layerIndex === 0 ? 0 : seriesPerLayer .slice(0, layerIndex) .reduce((sum, perLayer) => sum + perLayer.numberOfSeries, 0)) + - (layer.splitAccessor - ? currentSeriesPerLayer.splits.indexOf(seriesKey) * layer.accessors.length - : 0) + + (layer.splitAccessor && splitRank !== -1 ? splitRank * layer.accessors.length : 0) + layer.accessors.indexOf(yAccessor) ); }, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 1bcae4d09e7e7..a4c1e1bd4ba16 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -1386,13 +1386,13 @@ describe('xy_expression', () => { yAccessor: 'a', seriesKeys: ['a'], }) - ).toEqual('black'); + ).toEqual('blue'); expect( (component.find(LineSeries).at(1).prop('color') as Function)!({ yAccessor: 'c', seriesKeys: ['c'], }) - ).toEqual('black'); + ).toEqual('blue'); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 4891a51b3124b..5e5eef2f01c17 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -10,6 +10,7 @@ import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public' import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; import { EditorFrameSetup, FormatFactory } from '../types'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import { LensPluginStartDependencies } from '../plugin'; export interface XyVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; @@ -31,7 +32,7 @@ export class XyVisualization { constructor() {} setup( - core: CoreSetup, + core: CoreSetup, { expressions, formatFactory, editorFrame, charts }: XyVisualizationPluginSetupPlugins ) { editorFrame.registerVisualization(async () => { @@ -46,6 +47,7 @@ export class XyVisualization { getXyChartRenderer, getXyVisualization, } = await import('../async_services'); + const [, { data }] = await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); expressions.registerFunction(() => legendConfig); expressions.registerFunction(() => yAxisConfig); @@ -64,7 +66,7 @@ export class XyVisualization { histogramBarTarget: core.uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), }) ); - return getXyVisualization({ paletteService: palettes }); + return getXyVisualization({ paletteService: palettes, data }); }); } } diff --git a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts index bf4ffaa36a870..bd479062e2a06 100644 --- a/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts +++ b/x-pack/plugins/lens/public/xy_visualization/state_helpers.ts @@ -5,7 +5,7 @@ */ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { FramePublicAPI } from '../types'; +import { FramePublicAPI, DatasourcePublicAPI } from '../types'; import { SeriesType, visualizationTypes, LayerConfig, YConfig, ValidLayer } from './types'; export function isHorizontalSeries(seriesType: SeriesType) { @@ -39,6 +39,18 @@ export const getSeriesColor = (layer: LayerConfig, accessor: string) => { ); }; +export const getColumnToLabelMap = (layer: LayerConfig, datasource: DatasourcePublicAPI) => { + const columnToLabel: Record = {}; + + layer.accessors.concat(layer.splitAccessor ? [layer.splitAccessor] : []).forEach((accessor) => { + const operation = datasource.getOperationForColumnId(accessor); + if (operation?.label) { + columnToLabel[accessor] = operation.label; + } + }); + return columnToLabel; +}; + export function hasHistogramSeries( layers: ValidLayer[] = [], datasourceLayers?: FramePublicAPI['datasourceLayers'] diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 05a4b7f460adb..a715e4359da47 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -10,10 +10,12 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks' import { getXyVisualization } from './xy_visualization'; import { Operation } from '../types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), + data: dataPluginMock.createStartContract(), }); let mockDatasource: ReturnType; let frame: ReturnType; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index df773146cde4d..fda7c93af03a5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -9,6 +9,7 @@ import { ScaleType } from '@elastic/charts'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { State, ValidLayer, LayerConfig } from './types'; import { OperationMetadata, DatasourcePublicAPI } from '../types'; +import { getColumnToLabelMap } from './state_helpers'; export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: LayerConfig) => { const originalOrder = datasource @@ -196,17 +197,7 @@ export const buildExpression = ( ], valueLabels: [state?.valueLabels || 'hide'], layers: validLayers.map((layer) => { - const columnToLabel: Record = {}; - - const datasource = datasourceLayers[layer.layerId]; - layer.accessors - .concat(layer.splitAccessor ? [layer.splitAccessor] : []) - .forEach((accessor) => { - const operation = datasource.getOperationForColumnId(accessor); - if (operation?.label) { - columnToLabel[accessor] = operation.label; - } - }); + const columnToLabel = getColumnToLabelMap(layer, datasourceLayers[layer.layerId]); const xAxisOperation = datasourceLayers && diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 5127e5c2c2597..546cf06d4014e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -7,10 +7,11 @@ import { getXyVisualization } from './visualization'; import { Position } from '@elastic/charts'; import { Operation } from '../types'; -import { State, SeriesType } from './types'; +import { State, SeriesType, LayerConfig } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { LensIconChartBar } from '../assets/chart_bar'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; function exampleState(): State { return { @@ -28,9 +29,12 @@ function exampleState(): State { ], }; } +const paletteServiceMock = chartPluginMock.createPaletteRegistry(); +const dataMock = dataPluginMock.createStartContract(); const xyVisualization = getXyVisualization({ - paletteService: chartPluginMock.createPaletteRegistry(), + paletteService: paletteServiceMock, + data: dataMock, }); describe('xy_visualization', () => { @@ -307,6 +311,14 @@ describe('xy_visualization', () => { frame.datasourceLayers = { first: mockDatasource.publicAPIMock, }; + + frame.activeData = { + first: { + type: 'datatable', + rows: [], + columns: [], + }, + }; }); it('should return options for 3 dimensions', () => { @@ -408,6 +420,145 @@ describe('xy_visualization', () => { ]; expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']); }); + + describe('color assignment', () => { + function callConfig(layerConfigOverride: Partial) { + const baseState = exampleState(); + const options = xyVisualization.getConfiguration({ + state: { + ...baseState, + layers: [ + { + ...baseState.layers[0], + splitAccessor: undefined, + ...layerConfigOverride, + }, + ], + }, + frame, + layerId: 'first', + }).groups; + return options; + } + + function callConfigForYConfigs(layerConfigOverride: Partial) { + return callConfig(layerConfigOverride).find(({ groupId }) => groupId === 'y'); + } + + function callConfigForBreakdownConfigs(layerConfigOverride: Partial) { + return callConfig(layerConfigOverride).find(({ groupId }) => groupId === 'breakdown'); + } + + function callConfigAndFindYConfig( + layerConfigOverride: Partial, + assertionAccessor: string + ) { + const accessorConfig = callConfigForYConfigs(layerConfigOverride)?.accessors.find( + (accessor) => typeof accessor !== 'string' && accessor.columnId === assertionAccessor + ); + if (!accessorConfig || typeof accessorConfig === 'string') { + throw new Error('could not find accessor'); + } + return accessorConfig; + } + + it('should pass custom y color in accessor config', () => { + const accessorConfig = callConfigAndFindYConfig( + { + yConfig: [ + { + forAccessor: 'b', + color: 'red', + }, + ], + }, + 'b' + ); + expect(accessorConfig.triggerIcon).toEqual('color'); + expect(accessorConfig.color).toEqual('red'); + }); + + it('should query palette to fill in colors for other dimensions', () => { + const palette = paletteServiceMock.get('default'); + (palette.getColor as jest.Mock).mockClear(); + const accessorConfig = callConfigAndFindYConfig({}, 'c'); + expect(accessorConfig.triggerIcon).toEqual('color'); + // black is the color returned from the palette mock + expect(accessorConfig.color).toEqual('black'); + expect(palette.getColor).toHaveBeenCalledWith( + [ + { + name: 'c', + // rank 1 because it's the second y metric + rankAtDepth: 1, + totalSeriesAtDepth: 2, + }, + ], + { maxDepth: 1, totalSeries: 2 }, + undefined + ); + }); + + it('should pass name of current series along', () => { + (frame.datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({ + label: 'Overwritten label', + }); + const palette = paletteServiceMock.get('default'); + (palette.getColor as jest.Mock).mockClear(); + callConfigAndFindYConfig({}, 'c'); + expect(palette.getColor).toHaveBeenCalledWith( + [ + expect.objectContaining({ + name: 'Overwritten label', + }), + ], + expect.anything(), + undefined + ); + }); + + it('should use custom palette if layer contains palette', () => { + const palette = paletteServiceMock.get('mock'); + callConfigAndFindYConfig( + { + palette: { type: 'palette', name: 'mock', params: {} }, + }, + 'c' + ); + expect(palette.getColor).toHaveBeenCalled(); + }); + + it('should not show any indicator as long as there is no data', () => { + frame.activeData = undefined; + const yConfigs = callConfigForYConfigs({}); + expect(yConfigs!.accessors.length).toEqual(2); + yConfigs!.accessors.forEach((accessor) => { + expect(accessor.triggerIcon).toBeUndefined(); + }); + }); + + it('should show disable icon for splitted series', () => { + const accessorConfig = callConfigAndFindYConfig( + { + splitAccessor: 'd', + }, + 'b' + ); + expect(accessorConfig.triggerIcon).toEqual('disabled'); + }); + + it('should show current palette for break down by dimension', () => { + const palette = paletteServiceMock.get('mock'); + const customColors = ['yellow', 'green']; + (palette.getColors as jest.Mock).mockReturnValue(customColors); + const breakdownConfig = callConfigForBreakdownConfigs({ + palette: { type: 'palette', name: 'mock', params: {} }, + splitAccessor: 'd', + }); + const accessorConfig = breakdownConfig!.accessors[0]; + expect(typeof accessorConfig !== 'string' && accessorConfig.palette).toEqual(customColors); + }); + }); }); describe('#getErrorMessages', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 1f135929dac21..5748e649c181e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -10,16 +10,24 @@ import { render } from 'react-dom'; import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { PaletteRegistry } from 'src/plugins/charts/public'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; -import { Visualization, OperationMetadata, VisualizationType } from '../types'; +import { + Visualization, + OperationMetadata, + VisualizationType, + AccessorConfig, + FramePublicAPI, +} from '../types'; import { State, SeriesType, visualizationTypes, LayerConfig } from './types'; -import { isHorizontalChart } from './state_helpers'; +import { getColumnToLabelMap, isHorizontalChart } from './state_helpers'; import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression'; import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; +import { ColorAssignments, getColorAssignments } from './color_assignment'; const defaultIcon = LensIconChartBarStacked; const defaultSeriesType = 'bar_stacked'; @@ -76,8 +84,10 @@ function getDescription(state?: State) { export const getXyVisualization = ({ paletteService, + data, }: { paletteService: PaletteRegistry; + data: DataPublicPluginStart; }): Visualization => ({ id: 'lnsXY', @@ -168,7 +178,25 @@ export const getXyVisualization = ({ const datasource = frame.datasourceLayers[layer.layerId]; - const sortedAccessors = getSortedAccessors(datasource, layer); + const sortedAccessors: string[] = getSortedAccessors(datasource, layer); + let mappedAccessors: AccessorConfig[] = sortedAccessors.map((accessor) => ({ + columnId: accessor, + })); + + if (frame.activeData) { + const colorAssignments = getColorAssignments( + state.layers, + { tables: frame.activeData }, + data.fieldFormats.deserialize + ); + mappedAccessors = getAccessorColorConfig( + colorAssignments, + frame, + layer, + sortedAccessors, + paletteService + ); + } const isHorizontal = isHorizontalChart(state.layers); return { @@ -176,7 +204,7 @@ export const getXyVisualization = ({ { groupId: 'x', groupLabel: getAxisName('x', { isHorizontal }), - accessors: layer.xAccessor ? [layer.xAccessor] : [], + accessors: layer.xAccessor ? [{ columnId: layer.xAccessor }] : [], filterOperations: isBucketed, supportsMoreColumns: !layer.xAccessor, dataTestSubj: 'lnsXY_xDimensionPanel', @@ -184,7 +212,7 @@ export const getXyVisualization = ({ { groupId: 'y', groupLabel: getAxisName('y', { isHorizontal }), - accessors: sortedAccessors, + accessors: mappedAccessors, filterOperations: isNumericMetric, supportsMoreColumns: true, required: true, @@ -196,7 +224,17 @@ export const getXyVisualization = ({ groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', { defaultMessage: 'Break down by', }), - accessors: layer.splitAccessor ? [layer.splitAccessor] : [], + accessors: layer.splitAccessor + ? [ + { + columnId: layer.splitAccessor, + triggerIcon: 'colorBy', + palette: paletteService + .get(layer.palette?.name || 'default') + .getColors(10, layer.palette?.params), + }, + ] + : [], filterOperations: isBucketed, supportsMoreColumns: !layer.splitAccessor, dataTestSubj: 'lnsXY_splitDimensionPanel', @@ -333,6 +371,51 @@ export const getXyVisualization = ({ }, }); +function getAccessorColorConfig( + colorAssignments: ColorAssignments, + frame: FramePublicAPI, + layer: LayerConfig, + sortedAccessors: string[], + paletteService: PaletteRegistry +): AccessorConfig[] { + const layerContainsSplits = Boolean(layer.splitAccessor); + const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; + const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount; + return sortedAccessors.map((accessor) => { + const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); + if (layerContainsSplits) { + return { + columnId: accessor as string, + triggerIcon: 'disabled', + }; + } + const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]); + const rank = colorAssignments[currentPalette.name].getRank( + layer, + columnToLabel[accessor] || accessor, + accessor + ); + const customColor = + currentYConfig?.color || + paletteService.get(currentPalette.name).getColor( + [ + { + name: columnToLabel[accessor] || accessor, + rankAtDepth: rank, + totalSeriesAtDepth: totalSeriesCount, + }, + ], + { maxDepth: 1, totalSeries: totalSeriesCount }, + currentPalette.params + ); + return { + columnId: accessor as string, + triggerIcon: customColor ? 'color' : 'disabled', + color: customColor ? customColor : undefined, + }; + }); +} + function validateLayersForDimension( dimension: string, layers: LayerConfig[], diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index e8c3282146097..d214554de340c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -15,12 +15,14 @@ import { State, XYState, visualizationTypes } from './types'; import { generateId } from '../id_generator'; import { getXyVisualization } from './xy_visualization'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { PaletteOutput } from 'src/plugins/charts/public'; jest.mock('../id_generator'); const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), + data: dataPluginMock.createStartContract(), }); describe('xy_suggestions', () => { From 95ff10b47ae6b7c47b65ce7e38a7769373c68f7a Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Tue, 17 Nov 2020 12:48:25 +0200 Subject: [PATCH 25/99] [ML] Update console autocomplete for ML data frame evaluate API (#83151) We have added evaluation for all types of data frame analysis since the last update. This commit updates autocomplete accordingly. --- .../overrides/ml.evaluate_data_frame.json | 86 ++++++++++++++++--- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.evaluate_data_frame.json b/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.evaluate_data_frame.json index bf78cf7f09aaf..f6cc8bf2693ce 100644 --- a/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.evaluate_data_frame.json +++ b/x-pack/plugins/console_extensions/server/lib/spec_definitions/json/overrides/ml.evaluate_data_frame.json @@ -1,23 +1,83 @@ { "ml.evaluate_data_frame": { "data_autocomplete_rules": { + "index": "", "evaluation": { - "actual_field": "FIELD_NAME", - "predicted_probability_field": "FIELD_NAME", - "metrics": { - "auc_roc": { - "include_curve": true + "__one_of": [ + { + "outlier_detection": { + "__template": { + "actual_field": "FIELD_NAME", + "predicted_probability_field": "FIELD_NAME" + }, + "actual_field": "FIELD_NAME", + "predicted_probability_field": "FIELD_NAME", + "metrics": { + "auc_roc": { + "include_curve": false + }, + "precision": { + "at": [] + }, + "recall": { + "at": [] + }, + "confusion_matrix": { + "at": [] + } + } + } }, - "precision": { - "at": [] + { + "regression": { + "__template": { + "actual_field": "FIELD_NAME", + "predicted_field": "FIELD_NAME" + }, + "actual_field": "FIELD_NAME", + "predicted_field": "FIELD_NAME", + "metrics": { + "mse": {}, + "msle": { + "offset": 1.0 + }, + "r_squared": {}, + "huber": { + "delta": 1.0 + } + } + } }, - "recall": { - "at": [] - }, - "confusion_matrix": { - "at": [] + { + "classification": { + "__template": { + "actual_field": "FIELD_NAME", + "predicted_field": "FIELD_NAME", + "top_classes_field": "FIELD_NAME" + }, + "actual_field": "FIELD_NAME", + "predicted_field": "FIELD_NAME", + "top_classes_field": "FIELD_NAME", + "metrics": { + "accuracy": {}, + "precision": {}, + "recall": {}, + "multiclass_confusion_matrix": { + "size": 10 + }, + "precision": {}, + "recall": {}, + "auc_roc": { + "__template": { + "class_name": "" + }, + "class_name": "", + "include_curve": false + } + } + } } - } + ] } } } From 48231c8400d81c8628313368e4bd90cf37864657 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 17 Nov 2020 14:28:41 +0300 Subject: [PATCH 26/99] remove headers timeout hack, rely on nodejs timeouts (#83419) --- src/core/server/http/http_tools.ts | 4 ---- test/server_integration/http/platform/headers.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 14adbced95c7a..1e69669e080ec 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -103,10 +103,6 @@ interface ListenerOptions { export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) { const server = new Server(serverOptions); - // remove fix + test as soon as update node.js to v12.19 https://github.com/elastic/kibana/pull/61587 - server.listener.headersTimeout = - listenerOptions.keepaliveTimeout + 2 * server.listener.headersTimeout; - server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout; server.listener.setTimeout(listenerOptions.socketTimeout); server.listener.on('timeout', (socket) => { diff --git a/test/server_integration/http/platform/headers.ts b/test/server_integration/http/platform/headers.ts index 260bc37bd1328..50cfa5c702231 100644 --- a/test/server_integration/http/platform/headers.ts +++ b/test/server_integration/http/platform/headers.ts @@ -31,7 +31,7 @@ export default function ({ getService }: FtrProviderContext) { const config = getService('config'); describe('headers timeout ', () => { - it('issue-73849', async () => { + it('handles correctly. See issue #73849', async () => { const agent = new Http.Agent({ keepAlive: true, }); @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { } await performRequest(); - const defaultHeadersTimeout = 40 * oneSec; + const defaultHeadersTimeout = 60 * oneSec; await delay(defaultHeadersTimeout + oneSec); await performRequest(); }); From de8931546db005294805637285225f4c97dabc22 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 17 Nov 2020 13:06:51 +0100 Subject: [PATCH 27/99] [Uptime] Fix monitor list down histogram (#83411) --- .../server/lib/requests/get_monitor_states.ts | 2 +- .../monitor_states_real_data.snap | 369 ++++++++++++++++++ .../api_integration/apis/uptime/rest/index.ts | 3 + .../uptime/rest/monitor_states_real_data.ts | 11 + 4 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 020fcf5331188..2ff1043d79e84 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -151,7 +151,7 @@ export const getHistogramForMonitors = async ( }, }, }; - const result = await queryContext.search(params); + const { body: result } = await queryContext.search(params); const histoBuckets: any[] = result.aggregations?.histogram.buckets ?? []; const simplified = histoBuckets.map((histoBucket: any): { timestamp: number; byId: any } => { diff --git a/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap new file mode 100644 index 0000000000000..50625683b605d --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/__snapshots__/monitor_states_real_data.snap @@ -0,0 +1,369 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`monitor states endpoint will fetch monitor state data for the given down filters 1`] = ` +Object { + "nextPagePagination": "{\\"cursorDirection\\":\\"AFTER\\",\\"sortOrder\\":\\"ASC\\",\\"cursorKey\\":{\\"monitor_id\\":\\"0020-down\\"}}", + "prevPagePagination": null, + "summaries": Array [ + Object { + "histogram": Object { + "points": Array [ + Object { + "down": 1, + "timestamp": 1568172624744, + }, + Object { + "down": 2, + "timestamp": 1568172677247, + }, + Object { + "down": 1, + "timestamp": 1568172729750, + }, + Object { + "down": 2, + "timestamp": 1568172782253, + }, + Object { + "down": 2, + "timestamp": 1568172834756, + }, + Object { + "down": 2, + "timestamp": 1568172887259, + }, + Object { + "down": 1, + "timestamp": 1568172939762, + }, + Object { + "down": 2, + "timestamp": 1568172992265, + }, + Object { + "down": 2, + "timestamp": 1568173044768, + }, + Object { + "down": 2, + "timestamp": 1568173097271, + }, + Object { + "down": 1, + "timestamp": 1568173149774, + }, + Object { + "down": 2, + "timestamp": 1568173202277, + }, + ], + }, + "minInterval": 52503, + "monitor_id": "0010-down", + "state": Object { + "monitor": Object { + "name": "", + }, + "observer": Object { + "geo": Object { + "name": Array [ + "mpls", + ], + }, + }, + "summary": Object { + "down": 1, + "status": "down", + "up": 0, + }, + "summaryPings": Array [ + Object { + "@timestamp": "2019-09-11T03:40:34.371Z", + "agent": Object { + "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", + "hostname": "avc-x1x", + "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", + "type": "heartbeat", + "version": "8.0.0", + }, + "docId": "rZtoHm0B0I9WX_CznN_V", + "ecs": Object { + "version": "1.1.0", + }, + "error": Object { + "message": "400 Bad Request", + "type": "validate", + }, + "event": Object { + "dataset": "uptime", + }, + "host": Object { + "name": "avc-x1x", + }, + "http": Object { + "response": Object { + "body": Object { + "bytes": 3, + "content": "400", + "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", + }, + "status_code": 400, + }, + "rtt": Object { + "content": Object { + "us": 41, + }, + "response_header": Object { + "us": 36777, + }, + "total": Object { + "us": 37821, + }, + "validate": Object { + "us": 36818, + }, + "write_request": Object { + "us": 53, + }, + }, + }, + "monitor": Object { + "check_group": "d76f07d1-d445-11e9-88e3-3e80641b9c71", + "duration": Object { + "us": 37926, + }, + "id": "0010-down", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "http", + }, + "observer": Object { + "geo": Object { + "location": "37.926868, -78.024902", + "name": "mpls", + }, + "hostname": "avc-x1x", + }, + "resolve": Object { + "ip": "127.0.0.1", + "rtt": Object { + "us": 56, + }, + }, + "summary": Object { + "down": 1, + "up": 0, + }, + "tcp": Object { + "rtt": Object { + "connect": Object { + "us": 890, + }, + }, + }, + "timestamp": "2019-09-11T03:40:34.371Z", + "url": Object { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=400x1", + "path": "/pattern", + "port": 5678, + "query": "r=400x1", + "scheme": "http", + }, + }, + ], + "timestamp": "2019-09-11T03:40:34.371Z", + "url": Object { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=400x1", + "path": "/pattern", + "port": 5678, + "query": "r=400x1", + "scheme": "http", + }, + }, + }, + Object { + "histogram": Object { + "points": Array [ + Object { + "down": 1, + "timestamp": 1568172624744, + }, + Object { + "down": 2, + "timestamp": 1568172677247, + }, + Object { + "down": 1, + "timestamp": 1568172729750, + }, + Object { + "down": 2, + "timestamp": 1568172782253, + }, + Object { + "down": 2, + "timestamp": 1568172834756, + }, + Object { + "down": 2, + "timestamp": 1568172887259, + }, + Object { + "down": 1, + "timestamp": 1568172939762, + }, + Object { + "down": 2, + "timestamp": 1568172992265, + }, + Object { + "down": 2, + "timestamp": 1568173044768, + }, + Object { + "down": 2, + "timestamp": 1568173097271, + }, + Object { + "down": 1, + "timestamp": 1568173149774, + }, + Object { + "down": 2, + "timestamp": 1568173202277, + }, + ], + }, + "minInterval": 52503, + "monitor_id": "0020-down", + "state": Object { + "monitor": Object { + "name": "", + }, + "observer": Object { + "geo": Object { + "name": Array [ + "mpls", + ], + }, + }, + "summary": Object { + "down": 1, + "status": "down", + "up": 0, + }, + "summaryPings": Array [ + Object { + "@timestamp": "2019-09-11T03:40:34.372Z", + "agent": Object { + "ephemeral_id": "412a92a8-2142-4b1a-a7a2-1afd32e12f85", + "hostname": "avc-x1x", + "id": "04e1d082-65bc-4929-8d65-d0768a2621c4", + "type": "heartbeat", + "version": "8.0.0", + }, + "docId": "X5toHm0B0I9WX_CznN-6", + "ecs": Object { + "version": "1.1.0", + }, + "error": Object { + "message": "400 Bad Request", + "type": "validate", + }, + "event": Object { + "dataset": "uptime", + }, + "host": Object { + "name": "avc-x1x", + }, + "http": Object { + "response": Object { + "body": Object { + "bytes": 3, + "content": "400", + "hash": "26d228663f13a88592a12d16cf9587caab0388b262d6d9f126ed62f9333aca94", + }, + "status_code": 400, + }, + "rtt": Object { + "content": Object { + "us": 54, + }, + "response_header": Object { + "us": 180, + }, + "total": Object { + "us": 555, + }, + "validate": Object { + "us": 234, + }, + "write_request": Object { + "us": 63, + }, + }, + }, + "monitor": Object { + "check_group": "d7712ecb-d445-11e9-88e3-3e80641b9c71", + "duration": Object { + "us": 14900, + }, + "id": "0020-down", + "ip": "127.0.0.1", + "name": "", + "status": "down", + "type": "http", + }, + "observer": Object { + "geo": Object { + "location": "37.926868, -78.024902", + "name": "mpls", + }, + "hostname": "avc-x1x", + }, + "resolve": Object { + "ip": "127.0.0.1", + "rtt": Object { + "us": 14294, + }, + }, + "summary": Object { + "down": 1, + "up": 0, + }, + "tcp": Object { + "rtt": Object { + "connect": Object { + "us": 105, + }, + }, + }, + "timestamp": "2019-09-11T03:40:34.372Z", + "url": Object { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=400x1", + "path": "/pattern", + "port": 5678, + "query": "r=400x1", + "scheme": "http", + }, + }, + ], + "timestamp": "2019-09-11T03:40:34.372Z", + "url": Object { + "domain": "localhost", + "full": "http://localhost:5678/pattern?r=400x1", + "path": "/pattern", + "port": 5678, + "query": "r=400x1", + "scheme": "http", + }, + }, + }, + ], + "totalSummaryCount": 2000, +} +`; diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index f59b79a6b7bfc..6f410add0fa4d 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -9,12 +9,15 @@ import { settingsObjectId, settingsObjectType, } from '../../../../../plugins/uptime/server/lib/saved_objects'; +import { registerMochaHooksForSnapshots } from '../../../../apm_api_integration/common/match_snapshot'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const server = getService('kibanaServer'); describe('uptime REST endpoints', () => { + registerMochaHooksForSnapshots(); + beforeEach('clear settings', async () => { try { await server.savedObjects.delete({ diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts index d3c49bb49ff52..08a339ed59326 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -9,6 +9,7 @@ import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { MonitorSummariesResultType } from '../../../../../plugins/uptime/common/runtime_types'; import { API_URLS } from '../../../../../plugins/uptime/common/constants'; +import { expectSnapshot } from '../../../../apm_api_integration/common/match_snapshot'; interface ExpectedMonitorStatesPage { response: any; @@ -90,6 +91,16 @@ export default function ({ getService }: FtrProviderContext) { }); }); + it('will fetch monitor state data for the given down filters', async () => { + const statusFilter = 'down'; + const size = 2; + const { body } = await supertest.get( + `${API_URLS.MONITOR_LIST}?dateRangeStart=${from}&dateRangeEnd=${to}&statusFilter=${statusFilter}&pageSize=${size}` + ); + + expectSnapshot(body).toMatch(); + }); + it('can navigate forward and backward using pagination', async () => { const expectedResultsCount = 100; const size = 10; From 0a7f4629398048cbdd9a8995628203d5cc557637 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 17 Nov 2020 13:18:07 +0100 Subject: [PATCH 28/99] [Discover] Allow custom name for fields via index pattern field management (#70039) Co-authored-by: Matt Kime --- ...ugins-data-public.ifieldtype.customname.md | 11 +++ ...a-plugin-plugins-data-public.ifieldtype.md | 1 + ...ins-data-public.indexpattern.fieldattrs.md | 11 +++ ...ublic.indexpattern.getassavedobjectbody.md | 2 + ...-data-public.indexpattern.getfieldattrs.md | 15 +++ ...indexpattern.getoriginalsavedobjectbody.md | 1 + ...plugin-plugins-data-public.indexpattern.md | 4 +- ...ublic.indexpatternattributes.fieldattrs.md | 11 +++ ...gins-data-public.indexpatternattributes.md | 1 + ...-public.indexpatternfield._constructor_.md | 3 +- ...ata-public.indexpatternfield.customname.md | 13 +++ ...ta-public.indexpatternfield.displayname.md | 2 +- ...n-plugins-data-public.indexpatternfield.md | 3 +- ...ns-data-public.indexpatternfield.tojson.md | 2 + ...data-public.indexpatternspec.fieldattrs.md | 11 +++ ...in-plugins-data-public.indexpatternspec.md | 1 + ...ic.indexpatternsservice.fieldarraytomap.md | 2 +- ...lugins-data-public.indexpatternsservice.md | 2 +- ...ugins-data-server.ifieldtype.customname.md | 11 +++ ...a-plugin-plugins-data-server.ifieldtype.md | 1 + ...ins-data-server.indexpattern.fieldattrs.md | 11 +++ ...erver.indexpattern.getassavedobjectbody.md | 2 + ...-data-server.indexpattern.getfieldattrs.md | 15 +++ ...indexpattern.getoriginalsavedobjectbody.md | 1 + ...plugin-plugins-data-server.indexpattern.md | 4 +- ...erver.indexpatternattributes.fieldattrs.md | 11 +++ ...gins-data-server.indexpatternattributes.md | 1 + .../index_pattern_field.test.ts.snap | 2 + .../index_patterns/fields/field_list.ts | 8 +- .../fields/index_pattern_field.test.ts | 6 +- .../fields/index_pattern_field.ts | 25 ++++- .../common/index_patterns/fields/types.ts | 1 + .../__snapshots__/index_pattern.test.ts.snap | 57 +++++++++++ .../__snapshots__/index_patterns.test.ts.snap | 1 + .../index_patterns/index_pattern.ts | 23 ++++- .../index_patterns/index_patterns.ts | 44 ++++++--- .../data/common/index_patterns/types.ts | 10 +- src/plugins/data/public/public.api.md | 30 +++++- src/plugins/data/server/server.api.md | 20 +++- .../components/table_header/helpers.tsx | 3 +- .../table_header/table_header.test.tsx | 2 + .../table_header/table_header_column.tsx | 2 +- .../__snapshots__/field_name.test.tsx.snap | 4 +- .../components/field_name/field_name.test.tsx | 6 +- .../components/field_name/field_name.tsx | 13 +-- .../sidebar/discover_field.test.tsx | 46 ++++----- .../components/sidebar/discover_field.tsx | 17 +--- .../sidebar/discover_field_details.test.tsx | 69 ++++++-------- .../sidebar/discover_sidebar.test.tsx | 2 - .../components/sidebar/discover_sidebar.tsx | 6 +- .../sidebar/lib/field_filter.test.ts | 4 + .../components/sidebar/lib/field_filter.ts | 4 +- .../application/components/table/table.tsx | 8 +- .../components/table/table_row.tsx | 1 + .../application/doc_views/doc_views_types.ts | 1 + .../public/application/helpers/index.ts | 1 - .../helpers/shorten_dotted_string.ts | 26 ----- .../components/table/table.tsx | 25 ++++- .../indexed_fields_table.test.tsx | 5 +- .../indexed_fields_table.tsx | 6 +- .../__snapshots__/field_editor.test.tsx.snap | 94 ++++++++++++++++++- .../field_editor/field_editor.test.tsx | 56 +++++++++++ .../components/field_editor/field_editor.tsx | 38 +++++++- test/functional/apps/discover/_discover.js | 10 ++ test/functional/apps/visualize/_data_table.js | 23 +++++ .../fixtures/es_archiver/discover/data.json | 3 +- .../es_archiver/discover/mappings.json | 3 + .../fixtures/es_archiver/visualize/data.json | 3 +- .../es_archiver/visualize/mappings.json | 3 + test/functional/page_objects/discover_page.ts | 4 +- .../page_objects/visualize_chart_page.ts | 7 ++ .../discover/default/mappings.json | 3 + 72 files changed, 690 insertions(+), 188 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md delete mode 100644 src/plugins/discover/public/application/helpers/shorten_dotted_string.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md new file mode 100644 index 0000000000000..b30201f9e3991 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.customname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) > [customName](./kibana-plugin-plugins-data-public.ifieldtype.customname.md) + +## IFieldType.customName property + +Signature: + +```typescript +customName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 3ff2afafcc514..6f3876ff82f04 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -16,6 +16,7 @@ export interface IFieldType | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-public.ifieldtype.aggregatable.md) | boolean | | | [count](./kibana-plugin-plugins-data-public.ifieldtype.count.md) | number | | +| [customName](./kibana-plugin-plugins-data-public.ifieldtype.customname.md) | string | | | [displayName](./kibana-plugin-plugins-data-public.ifieldtype.displayname.md) | string | | | [esTypes](./kibana-plugin-plugins-data-public.ifieldtype.estypes.md) | string[] | | | [filterable](./kibana-plugin-plugins-data-public.ifieldtype.filterable.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md new file mode 100644 index 0000000000000..c2e0b9bb855f4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [fieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md) + +## IndexPattern.fieldAttrs property + +Signature: + +```typescript +fieldAttrs: FieldAttrs; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md index 2c5f30e4889ea..a370341000960 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md @@ -10,6 +10,7 @@ Returns index pattern as saved object body for saving ```typescript getAsSavedObjectBody(): { + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; @@ -23,6 +24,7 @@ getAsSavedObjectBody(): { Returns: `{ + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md new file mode 100644 index 0000000000000..f81edf4b94b42 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) + +## IndexPattern.getFieldAttrs property + +Signature: + +```typescript +getFieldAttrs: () => { + [x: string]: { + customName: string; + }; + }; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md index 349da63c13ca7..0c89a6a3d20ba 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md @@ -10,6 +10,7 @@ Get last saved saved object fields ```typescript getOriginalSavedObjectBody: () => { + fieldAttrs?: string | undefined; title?: string | undefined; timeFieldName?: string | undefined; intervalName?: string | undefined; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 7e3192481dfff..1228bf7adc2ef 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -21,12 +21,14 @@ export declare class IndexPattern implements IIndexPattern | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [deleteFieldFormat](./kibana-plugin-plugins-data-public.indexpattern.deletefieldformat.md) | | (fieldName: string) => void | | +| [fieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.fieldattrs.md) | | FieldAttrs | | | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | Record<string, any> | | | [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | | [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | | [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | FormatFieldFn | | | [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | -| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) | | () => {
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | +| [getFieldAttrs](./kibana-plugin-plugins-data-public.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customName: string;
};
} | | +| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) | | () => {
fieldAttrs?: string | undefined;
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md new file mode 100644 index 0000000000000..6af981eb6996c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md) + +## IndexPatternAttributes.fieldAttrs property + +Signature: + +```typescript +fieldAttrs?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index 77a8ebb0b2d3f..c5ea38278e820 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -14,6 +14,7 @@ export interface IndexPatternAttributes | Property | Type | Description | | --- | --- | --- | +| [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldattrs.md) | string | | | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-public.indexpatternattributes.fields.md) | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md index 5d467a7a9cbce..e0abf8aeeaee6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPatternField` class Signature: ```typescript -constructor(spec: FieldSpec, displayName: string); +constructor(spec: FieldSpec); ``` ## Parameters @@ -17,5 +17,4 @@ constructor(spec: FieldSpec, displayName: string); | Parameter | Type | Description | | --- | --- | --- | | spec | FieldSpec | | -| displayName | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md new file mode 100644 index 0000000000000..ef8f9f1d31e4f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.customname.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [customName](./kibana-plugin-plugins-data-public.indexpatternfield.customname.md) + +## IndexPatternField.customName property + +Signature: + +```typescript +get customName(): string | undefined; + +set customName(label: string | undefined); +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md index c0ce2fff419bf..913d63c93e3c0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly displayName: string; +get displayName(): string; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 4f49a9a8fc3ab..ef99b4353a70b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -14,7 +14,7 @@ export declare class IndexPatternField implements IFieldType | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(spec, displayName)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the IndexPatternField class | +| [(constructor)(spec)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the IndexPatternField class | ## Properties @@ -23,6 +23,7 @@ export declare class IndexPatternField implements IFieldType | [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | | [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | undefined | Description of field type conflicts across different indices in the same index pattern | | [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | Count is used for field popularity | +| [customName](./kibana-plugin-plugins-data-public.indexpatternfield.customname.md) | | string | undefined | | | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md index a6a3a5a093c8e..c7237701ae49d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tojson.md @@ -20,6 +20,7 @@ toJSON(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; + customName: string | undefined; }; ``` Returns: @@ -37,5 +38,6 @@ toJSON(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; + customName: string | undefined; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md new file mode 100644 index 0000000000000..e558c3ab19189 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md) + +## IndexPatternSpec.fieldAttrs property + +Signature: + +```typescript +fieldAttrs?: FieldAttrs; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md index f3b692209ca67..06917fcac1b4d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md @@ -14,6 +14,7 @@ export interface IndexPatternSpec | Property | Type | Description | | --- | --- | --- | +| [fieldAttrs](./kibana-plugin-plugins-data-public.indexpatternspec.fieldattrs.md) | FieldAttrs | | | [fieldFormats](./kibana-plugin-plugins-data-public.indexpatternspec.fieldformats.md) | Record<string, SerializedFieldFormat> | | | [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap | | | [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md index ed365fe03f980..2a09d5b3adb1d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md @@ -9,5 +9,5 @@ Converts field array to map Signature: ```typescript -fieldArrayToMap: (fields: FieldSpec[]) => Record; +fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 57bb98de09ebd..48019fe410b97 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -22,7 +22,7 @@ export declare class IndexPatternsService | --- | --- | --- | --- | | [clearCache](./kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | | [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | -| [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[]) => Record<string, FieldSpec> | Converts field array to map | +| [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record<string, FieldSpec> | Converts field array to map | | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | | [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md new file mode 100644 index 0000000000000..f5fbc084237f2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.customname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) > [customName](./kibana-plugin-plugins-data-server.ifieldtype.customname.md) + +## IFieldType.customName property + +Signature: + +```typescript +customName?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index d106f3a35a91c..638700b1d24f8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -16,6 +16,7 @@ export interface IFieldType | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-server.ifieldtype.aggregatable.md) | boolean | | | [count](./kibana-plugin-plugins-data-server.ifieldtype.count.md) | number | | +| [customName](./kibana-plugin-plugins-data-server.ifieldtype.customname.md) | string | | | [displayName](./kibana-plugin-plugins-data-server.ifieldtype.displayname.md) | string | | | [esTypes](./kibana-plugin-plugins-data-server.ifieldtype.estypes.md) | string[] | | | [filterable](./kibana-plugin-plugins-data-server.ifieldtype.filterable.md) | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md new file mode 100644 index 0000000000000..c8bad55dee2e4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md) + +## IndexPattern.fieldAttrs property + +Signature: + +```typescript +fieldAttrs: FieldAttrs; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md index f1bdb2f729414..274a475872b0b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md @@ -10,6 +10,7 @@ Returns index pattern as saved object body for saving ```typescript getAsSavedObjectBody(): { + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; @@ -23,6 +24,7 @@ getAsSavedObjectBody(): { Returns: `{ + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md new file mode 100644 index 0000000000000..80dd329232ed8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) + +## IndexPattern.getFieldAttrs property + +Signature: + +```typescript +getFieldAttrs: () => { + [x: string]: { + customName: string; + }; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md index 324f9d0152ab5..9923c82f389ad 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md @@ -10,6 +10,7 @@ Get last saved saved object fields ```typescript getOriginalSavedObjectBody: () => { + fieldAttrs?: string | undefined; title?: string | undefined; timeFieldName?: string | undefined; intervalName?: string | undefined; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 2e15c8d3867ec..3d2b021b29515 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -21,12 +21,14 @@ export declare class IndexPattern implements IIndexPattern | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [deleteFieldFormat](./kibana-plugin-plugins-data-server.indexpattern.deletefieldformat.md) | | (fieldName: string) => void | | +| [fieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.fieldattrs.md) | | FieldAttrs | | | [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | Record<string, any> | | | [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | | [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | | [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) | | FormatFieldFn | | | [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | -| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) | | () => {
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | +| [getFieldAttrs](./kibana-plugin-plugins-data-server.indexpattern.getfieldattrs.md) | | () => {
[x: string]: {
customName: string;
};
} | | +| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) | | () => {
fieldAttrs?: string | undefined;
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) | | string[] | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md new file mode 100644 index 0000000000000..fded3ebac8b2c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [fieldAttrs](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md) + +## IndexPatternAttributes.fieldAttrs property + +Signature: + +```typescript +fieldAttrs?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index 40b029da00469..6559b4d7110be 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -14,6 +14,7 @@ export interface IndexPatternAttributes | Property | Type | Description | | --- | --- | --- | +| [fieldAttrs](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldattrs.md) | string | | | [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-server.indexpatternattributes.fields.md) | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md) | string | | diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index 4279dd320ad62..afaa2d00d8cfd 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -47,6 +47,7 @@ Object { ], }, "count": 1, + "customName": undefined, "esTypes": Array [ "text", ], @@ -62,6 +63,7 @@ Object { "script": "script", "scripted": true, "searchable": true, + "shortDotsEnable": undefined, "subType": Object { "multi": Object { "parent": "parent", diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index c0eb55a15fead..7fe2a17124b78 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -22,7 +22,6 @@ import { IFieldType } from './types'; import { IndexPatternField } from './index_pattern_field'; import { FieldSpec, IndexPatternFieldMap } from '../types'; import { IndexPattern } from '../index_patterns'; -import { shortenDottedString } from '../../utils'; type FieldMap = Map; @@ -58,8 +57,7 @@ export const fieldList = ( this.groups.get(field.type)!.set(field.name, field); }; private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name); - private calcDisplayName = (name: string) => - shortDotsEnable ? shortenDottedString(name) : name; + constructor() { super(); specs.map((field) => this.add(field)); @@ -71,7 +69,7 @@ export const fieldList = ( ...(this.groups.get(type) || new Map()).values(), ]; public readonly add = (field: FieldSpec) => { - const newField = new IndexPatternField(field, this.calcDisplayName(field.name)); + const newField = new IndexPatternField({ ...field, shortDotsEnable }); this.push(newField); this.setByName(newField); this.setByGroup(newField); @@ -86,7 +84,7 @@ export const fieldList = ( }; public readonly update = (field: FieldSpec) => { - const newField = new IndexPatternField(field, this.calcDisplayName(field.name)); + const newField = new IndexPatternField(field); const index = this.findIndex((f) => f.name === newField.name); this.splice(index, 1, newField); this.setByName(newField); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index be7836de31246..81c7d6b9b237b 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -28,7 +28,7 @@ describe('Field', function () { } function getField(values = {}) { - return new IndexPatternField({ ...fieldValues, ...values }, 'displayName'); + return new IndexPatternField({ ...fieldValues, ...values }); } const fieldValues = { @@ -150,12 +150,12 @@ describe('Field', function () { }); it('exports the property to JSON', () => { - const field = new IndexPatternField(fieldValues, 'displayName'); + const field = new IndexPatternField(fieldValues); expect(flatten(field)).toMatchSnapshot(); }); it('spec snapshot', () => { - const field = new IndexPatternField(fieldValues, 'displayName'); + const field = new IndexPatternField(fieldValues); const getFormatterForField = () => ({ toJSON: () => ({ diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 4a22508f7fef3..850c5a312fda1 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -21,16 +21,15 @@ import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; +import { shortenDottedString } from '../../utils'; export class IndexPatternField implements IFieldType { readonly spec: FieldSpec; // not writable or serialized - readonly displayName: string; private readonly kbnFieldType: KbnFieldType; - constructor(spec: FieldSpec, displayName: string) { + constructor(spec: FieldSpec) { this.spec = { ...spec, type: spec.name === '_source' ? '_source' : spec.type }; - this.displayName = displayName; this.kbnFieldType = getKbnFieldType(spec.type); } @@ -69,6 +68,14 @@ export class IndexPatternField implements IFieldType { this.spec.lang = lang; } + public get customName() { + return this.spec.customName; + } + + public set customName(label) { + this.spec.customName = label; + } + /** * Description of field type conflicts across different indices in the same index pattern */ @@ -85,6 +92,14 @@ export class IndexPatternField implements IFieldType { return this.spec.name; } + public get displayName(): string { + return this.spec.customName + ? this.spec.customName + : this.spec.shortDotsEnable + ? shortenDottedString(this.spec.name) + : this.spec.name; + } + public get type() { return this.spec.type; } @@ -140,7 +155,6 @@ export class IndexPatternField implements IFieldType { script: this.script, lang: this.lang, conflictDescriptions: this.conflictDescriptions, - name: this.name, type: this.type, esTypes: this.esTypes, @@ -149,6 +163,7 @@ export class IndexPatternField implements IFieldType { aggregatable: this.aggregatable, readFromDocValues: this.readFromDocValues, subType: this.subType, + customName: this.customName, }; } @@ -171,6 +186,8 @@ export class IndexPatternField implements IFieldType { readFromDocValues: this.readFromDocValues, subType: this.subType, format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined, + customName: this.customName, + shortDotsEnable: this.spec.shortDotsEnable, }; } } diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index 5814760601a67..86c22b0116ead 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -37,6 +37,7 @@ export interface IFieldType { scripted?: boolean; subType?: IFieldSubType; displayName?: string; + customName?: string; format?: any; toSpec?: (options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }) => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index dc4da2456b47b..2741322acec0f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -2,12 +2,14 @@ exports[`IndexPattern toSpec should match snapshot 1`] = ` Object { + "fieldAttrs": Object {}, "fieldFormats": Object {}, "fields": Object { "@tags": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "keyword", ], @@ -23,6 +25,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -30,6 +33,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 30, + "customName": undefined, "esTypes": Array [ "date", ], @@ -45,6 +49,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "date", }, @@ -52,6 +57,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "_id", ], @@ -67,6 +73,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -74,6 +81,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "_source", ], @@ -89,6 +97,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "_source", }, @@ -96,6 +105,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "_type", ], @@ -111,6 +121,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -118,6 +129,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "geo_shape", ], @@ -133,6 +145,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "geo_shape", }, @@ -140,6 +153,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 10, + "customName": undefined, "esTypes": Array [ "long", ], @@ -155,6 +169,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "number", }, @@ -162,6 +177,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "conflict", ], @@ -177,6 +193,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "conflict", }, @@ -184,6 +201,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -199,6 +217,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -206,6 +225,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "keyword", ], @@ -221,6 +241,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": Object { "multi": Object { "parent": "extension", @@ -232,6 +253,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "geo_point", ], @@ -247,6 +269,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "geo_point", }, @@ -254,6 +277,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "keyword", ], @@ -269,6 +293,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -276,6 +301,7 @@ Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "murmur3", ], @@ -291,6 +317,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "murmur3", }, @@ -298,6 +325,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "ip", ], @@ -313,6 +341,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "ip", }, @@ -320,6 +349,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -335,6 +365,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -342,6 +373,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "keyword", ], @@ -357,6 +389,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": Object { "multi": Object { "parent": "machine.os", @@ -368,6 +401,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -383,6 +417,7 @@ Object { "script": undefined, "scripted": false, "searchable": false, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -390,6 +425,7 @@ Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -405,6 +441,7 @@ Object { "script": undefined, "scripted": false, "searchable": false, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -412,6 +449,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "integer", ], @@ -427,6 +465,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "number", }, @@ -434,6 +473,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "geo_point", ], @@ -449,6 +489,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "geo_point", }, @@ -456,6 +497,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "attachment", ], @@ -471,6 +513,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "attachment", }, @@ -478,6 +521,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "date", ], @@ -493,6 +537,7 @@ Object { "script": "1234", "scripted": true, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "date", }, @@ -500,6 +545,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "murmur3", ], @@ -515,6 +561,7 @@ Object { "script": "1234", "scripted": true, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "murmur3", }, @@ -522,6 +569,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "long", ], @@ -537,6 +585,7 @@ Object { "script": "1234", "scripted": true, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "number", }, @@ -544,6 +593,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "text", ], @@ -559,6 +609,7 @@ Object { "script": "'i am a string'", "scripted": true, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "string", }, @@ -566,6 +617,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 20, + "customName": undefined, "esTypes": Array [ "boolean", ], @@ -581,6 +633,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "boolean", }, @@ -588,6 +641,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 30, + "customName": undefined, "esTypes": Array [ "date", ], @@ -603,6 +657,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "date", }, @@ -610,6 +665,7 @@ Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, + "customName": undefined, "esTypes": Array [ "date", ], @@ -625,6 +681,7 @@ Object { "script": undefined, "scripted": false, "searchable": true, + "shortDotsEnable": false, "subType": undefined, "type": "date", }, diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap index a3d19f311b765..c020e7595c565 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap @@ -2,6 +2,7 @@ exports[`IndexPatterns savedObjectToSpec 1`] = ` Object { + "fieldAttrs": Object {}, "fieldFormats": Object { "field": Object {}, }, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 4508d7b1d9082..c3a0c98745e21 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,6 +18,7 @@ */ import _, { each, reject } from 'lodash'; +import { FieldAttrs } from '../..'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; @@ -36,6 +37,7 @@ interface IndexPatternDeps { } interface SavedObjectBody { + fieldAttrs?: string; title?: string; timeFieldName?: string; intervalName?: string; @@ -70,6 +72,8 @@ export class IndexPattern implements IIndexPattern { private originalSavedObjectBody: SavedObjectBody = {}; private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; + // make private once manual field refresh is removed + public fieldAttrs: FieldAttrs; constructor({ spec = {}, @@ -101,10 +105,10 @@ export class IndexPattern implements IIndexPattern { this.title = spec.title || ''; this.timeFieldName = spec.timeFieldName; this.sourceFilters = spec.sourceFilters; - this.fields.replaceAll(Object.values(spec.fields || {})); this.type = spec.type; this.typeMeta = spec.typeMeta; + this.fieldAttrs = spec.fieldAttrs || {}; } setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => { @@ -127,6 +131,20 @@ export class IndexPattern implements IIndexPattern { this.originalSavedObjectBody = this.getAsSavedObjectBody(); }; + getFieldAttrs = () => { + const newFieldAttrs = { ...this.fieldAttrs }; + + this.fields.forEach((field) => { + if (field.customName) { + newFieldAttrs[field.name] = { customName: field.customName }; + } else { + delete newFieldAttrs[field.name]; + } + }); + + return newFieldAttrs; + }; + getComputedFields() { const scriptFields: any = {}; if (!this.fields) { @@ -180,6 +198,7 @@ export class IndexPattern implements IIndexPattern { typeMeta: this.typeMeta, type: this.type, fieldFormats: this.fieldFormatMap, + fieldAttrs: this.fieldAttrs, }; } @@ -271,8 +290,10 @@ export class IndexPattern implements IIndexPattern { const fieldFormatMap = _.isEmpty(this.fieldFormatMap) ? undefined : JSON.stringify(this.fieldFormatMap); + const fieldAttrs = this.getFieldAttrs(); return { + fieldAttrs: fieldAttrs ? JSON.stringify(fieldAttrs) : undefined, title: this.title, timeFieldName: this.timeFieldName, intervalName: this.intervalName, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 19c6e9c7b8a7a..4f91079c1e139 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -35,6 +35,7 @@ import { GetFieldsOptions, IndexPatternSpec, IndexPatternAttributes, + FieldAttrs, FieldSpec, IndexPatternFieldMap, } from '../types'; @@ -249,7 +250,11 @@ export class IndexPatternsService { try { const fields = await this.getFieldsForIndexPattern(indexPattern); const scripted = indexPattern.getScriptedFields().map((field) => field.spec); - indexPattern.fields.replaceAll([...fields, ...scripted]); + const fieldAttrs = indexPattern.getFieldAttrs(); + const fieldsWithSavedAttrs = Object.values( + this.fieldArrayToMap([...fields, ...scripted], fieldAttrs) + ); + indexPattern.fields.replaceAll(fieldsWithSavedAttrs); } catch (err) { if (err instanceof IndexPatternMissingIndices) { this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); @@ -275,12 +280,13 @@ export class IndexPatternsService { fields: IndexPatternFieldMap, id: string, title: string, - options: GetFieldsOptions + options: GetFieldsOptions, + fieldAttrs: FieldAttrs = {} ) => { const scriptdFields = Object.values(fields).filter((field) => field.scripted); try { - const newFields = await this.getFieldsForWildcard(options); - return this.fieldArrayToMap([...newFields, ...scriptdFields]); + const newFields = (await this.getFieldsForWildcard(options)) as FieldSpec[]; + return this.fieldArrayToMap([...newFields, ...scriptdFields], fieldAttrs); } catch (err) { if (err instanceof IndexPatternMissingIndices) { this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); @@ -301,9 +307,9 @@ export class IndexPatternsService { * Converts field array to map * @param fields */ - fieldArrayToMap = (fields: FieldSpec[]) => + fieldArrayToMap = (fields: FieldSpec[], fieldAttrs?: FieldAttrs) => fields.reduce((collector, field) => { - collector[field.name] = field; + collector[field.name] = { ...field, customName: fieldAttrs?.[field.name]?.customName }; return collector; }, {}); @@ -325,6 +331,7 @@ export class IndexPatternsService { fieldFormatMap, typeMeta, type, + fieldAttrs, }, } = savedObject; @@ -332,6 +339,7 @@ export class IndexPatternsService { const parsedTypeMeta = typeMeta ? JSON.parse(typeMeta) : undefined; const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {}; const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : []; + const parsedFieldAttrs: FieldAttrs = fieldAttrs ? JSON.parse(fieldAttrs) : {}; return { id, @@ -340,10 +348,11 @@ export class IndexPatternsService { intervalName, timeFieldName, sourceFilters: parsedSourceFilters, - fields: this.fieldArrayToMap(parsedFields), + fields: this.fieldArrayToMap(parsedFields, parsedFieldAttrs), typeMeta: parsedTypeMeta, type, fieldFormats: parsedFieldFormatMap, + fieldAttrs: parsedFieldAttrs, }; }; @@ -369,17 +378,26 @@ export class IndexPatternsService { const spec = this.savedObjectToSpec(savedObject); const { title, type, typeMeta } = spec; + spec.fieldAttrs = savedObject.attributes.fieldAttrs + ? JSON.parse(savedObject.attributes.fieldAttrs) + : {}; const isFieldRefreshRequired = this.isFieldRefreshRequired(spec.fields); let isSaveRequired = isFieldRefreshRequired; try { spec.fields = isFieldRefreshRequired - ? await this.refreshFieldSpecMap(spec.fields || {}, id, spec.title as string, { - pattern: title as string, - metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), - type, - rollupIndex: typeMeta?.params?.rollupIndex, - }) + ? await this.refreshFieldSpecMap( + spec.fields || {}, + id, + spec.title as string, + { + pattern: title as string, + metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), + type, + rollupIndex: typeMeta?.params?.rollupIndex, + }, + spec.fieldAttrs + ) : spec.fields; } catch (err) { isSaveRequired = false; diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index b381cc0963333..22c400562f6d4 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -48,6 +48,11 @@ export interface IndexPatternAttributes { intervalName?: string; sourceFilters?: string; fieldFormatMap?: string; + fieldAttrs?: string; +} + +export interface FieldAttrs { + [key: string]: { customName: string }; } export type OnNotification = (toastInputFields: ToastInputFields) => void; @@ -155,7 +160,6 @@ export interface FieldSpec { lang?: string; conflictDescriptions?: Record; format?: SerializedFieldFormat; - name: string; type: string; esTypes?: string[]; @@ -165,6 +169,9 @@ export interface FieldSpec { readFromDocValues?: boolean; subType?: IFieldSubType; indexed?: boolean; + customName?: string; + // not persisted + shortDotsEnable?: boolean; } export type IndexPatternFieldMap = Record; @@ -180,6 +187,7 @@ export interface IndexPatternSpec { typeMeta?: TypeMeta; type?: string; fieldFormats?: Record; + fieldAttrs?: FieldAttrs; } export interface SourceFilter { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 78b974758f8c0..0768658e40299 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -975,6 +975,8 @@ export interface IFieldType { // (undocumented) count?: number; // (undocumented) + customName?: string; + // (undocumented) displayName?: string; // (undocumented) esTypes?: string[]; @@ -1096,6 +1098,10 @@ export class IndexPattern implements IIndexPattern { addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) deleteFieldFormat: (fieldName: string) => void; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fieldAttrs: FieldAttrs; // (undocumented) fieldFormatMap: Record; // (undocumented) @@ -1121,6 +1127,7 @@ export class IndexPattern implements IIndexPattern { time_zone?: string | undefined; }>> | undefined; getAsSavedObjectBody(): { + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; @@ -1140,12 +1147,19 @@ export class IndexPattern implements IIndexPattern { }[]; }; // (undocumented) + getFieldAttrs: () => { + [x: string]: { + customName: string; + }; + }; + // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; // (undocumented) getNonScriptedFields(): IndexPatternField[]; getOriginalSavedObjectBody: () => { + fieldAttrs?: string | undefined; title?: string | undefined; timeFieldName?: string | undefined; intervalName?: string | undefined; @@ -1210,6 +1224,8 @@ export type IndexPatternAggRestrictions = Record | undefined; @@ -1240,7 +1256,10 @@ export class IndexPatternField implements IFieldType { get count(): number; set count(count: number); // (undocumented) - readonly displayName: string; + get customName(): string | undefined; + set customName(label: string | undefined); + // (undocumented) + get displayName(): string; // (undocumented) get esTypes(): string[] | undefined; // (undocumented) @@ -1277,6 +1296,7 @@ export class IndexPatternField implements IFieldType { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; + customName: string | undefined; }; // (undocumented) toSpec({ getFormatterForField, }?: { @@ -1324,6 +1344,8 @@ export type IndexPatternSelectProps = Required, 'isLo // // @public (undocumented) export interface IndexPatternSpec { + // (undocumented) + fieldAttrs?: FieldAttrs; // (undocumented) fieldFormats?: Record; // (undocumented) @@ -1361,7 +1383,7 @@ export class IndexPatternsService { // // (undocumented) ensureDefaultIndexPattern: EnsureDefaultIndexPattern; - fieldArrayToMap: (fields: FieldSpec[]) => Record; + fieldArrayToMap: (fields: FieldSpec[], fieldAttrs?: FieldAttrs | undefined) => Record; get: (id: string) => Promise; // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts // @@ -2325,7 +2347,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:113:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index bb7a8f58c926c..b2db4f5c74729 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -507,6 +507,8 @@ export interface IFieldType { // (undocumented) count?: number; // (undocumented) + customName?: string; + // (undocumented) displayName?: string; // (undocumented) esTypes?: string[]; @@ -557,6 +559,10 @@ export class IndexPattern implements IIndexPattern { addScriptedField(name: string, script: string, fieldType?: string): Promise; // (undocumented) deleteFieldFormat: (fieldName: string) => void; + // Warning: (ae-forgotten-export) The symbol "FieldAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fieldAttrs: FieldAttrs; // (undocumented) fieldFormatMap: Record; // Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts @@ -584,6 +590,7 @@ export class IndexPattern implements IIndexPattern { time_zone?: string | undefined; }>> | undefined; getAsSavedObjectBody(): { + fieldAttrs: string | undefined; title: string; timeFieldName: string | undefined; intervalName: string | undefined; @@ -603,6 +610,12 @@ export class IndexPattern implements IIndexPattern { }[]; }; // (undocumented) + getFieldAttrs: () => { + [x: string]: { + customName: string; + }; + }; + // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; @@ -611,6 +624,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getNonScriptedFields(): IndexPatternField[]; getOriginalSavedObjectBody: () => { + fieldAttrs?: string | undefined; title?: string | undefined; timeFieldName?: string | undefined; intervalName?: string | undefined; @@ -669,6 +683,8 @@ export class IndexPattern implements IIndexPattern { // // @public (undocumented) export interface IndexPatternAttributes { + // (undocumented) + fieldAttrs?: string; // (undocumented) fieldFormatMap?: string; // (undocumented) @@ -1195,8 +1211,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:56:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:62:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:58:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx index bd48b1e083871..b456fa0773b85 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx @@ -17,7 +17,6 @@ * under the License. */ import { IndexPattern } from '../../../../../kibana_services'; -import { shortenDottedString } from '../../../../helpers'; export type SortOrder = [string, string]; export interface ColumnProps { @@ -67,7 +66,7 @@ export function getDisplayedColumns( const field = indexPattern.getFieldByName(column); return { name: column, - displayName: isShortDots ? shortenDottedString(column) : column, + displayName: field ? field.displayName : column, isSortable: field && field.sortable ? true : false, isRemoveable: column !== '_source' || columns.length > 1, colLeftIdx: idx - 1 < 0 ? -1 : idx - 1, diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx index 3d5698e2e0d96..7636939194ce1 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx @@ -35,6 +35,7 @@ function getMockIndexPattern() { if (name === 'test1') { return { name, + displayName: name, type: 'string', aggregatable: false, searchable: true, @@ -43,6 +44,7 @@ function getMockIndexPattern() { } else { return { name, + displayName: name, type: 'string', aggregatable: false, searchable: true, diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx index ac986fcaf0cbc..08a2d07d0b8e0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx @@ -24,7 +24,7 @@ import { SortOrder } from './helpers'; interface Props { colLeftIdx: number; // idx of the column to the left, -1 if moving is not possible colRightIdx: number; // idx of the column to the right, -1 if moving is not possible - displayName: string; + displayName?: string; isRemoveable: boolean; isSortable: boolean; name: string; diff --git a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap index d00a956b7c73d..2fa96f9372380 100644 --- a/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/plugins/discover/public/application/components/field_name/__snapshots__/field_name.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` +exports[`FieldName renders a geo field 1`] = `
@@ -24,7 +24,7 @@ exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - t.t.test + test.test.test
diff --git a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx index e6cf8a57686f1..0deddce1c40a8 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx @@ -32,9 +32,7 @@ test('FieldName renders a number field by providing a field record, useShortDots expect(component).toMatchSnapshot(); }); -test('FieldName renders a geo field, useShortDots is set to true', () => { - const component = render( - - ); +test('FieldName renders a geo field', () => { + const component = render(); expect(component).toMatchSnapshot(); }); diff --git a/src/plugins/discover/public/application/components/field_name/field_name.tsx b/src/plugins/discover/public/application/components/field_name/field_name.tsx index cf11f971ef76c..b8f664d6cf38a 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.tsx @@ -18,30 +18,31 @@ */ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; - import { FieldIcon, FieldIconProps } from '../../../../../kibana_react/public'; -import { shortenDottedString } from '../../helpers'; import { getFieldTypeName } from './field_type_name'; +import { FieldMapping } from '../../doc_views/doc_views_types'; // properties fieldType and fieldName are provided in kbn_doc_view // this should be changed when both components are deangularized interface Props { fieldName: string; fieldType: string; - useShortDots?: boolean; + fieldMapping?: FieldMapping; fieldIconProps?: Omit; scripted?: boolean; } export function FieldName({ fieldName, + fieldMapping, fieldType, - useShortDots, fieldIconProps, scripted = false, }: Props) { const typeName = getFieldTypeName(fieldType); - const displayName = useShortDots ? shortenDottedString(fieldName) : fieldName; + const displayName = + fieldMapping && fieldMapping.displayName ? fieldMapping.displayName : fieldName; + const tooltip = displayName !== fieldName ? `${fieldName} (${displayName})` : fieldName; return ( @@ -51,7 +52,7 @@ export function FieldName({ diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 02ed17cd01f07..391e15485f074 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -43,8 +43,6 @@ jest.mock('../../../kibana_services', () => ({ get: (key: string) => { if (key === 'fields:popularLimit') { return 5; - } else if (key === 'shortDots:enable') { - return false; } }, }, @@ -54,7 +52,6 @@ jest.mock('../../../kibana_services', () => ({ function getComponent({ selected = false, showDetails = false, - useShortDots = false, field, }: { selected?: boolean; @@ -72,19 +69,16 @@ function getComponent({ const finalField = field ?? - new IndexPatternField( - { - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'bytes' - ); + new IndexPatternField({ + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const props = { indexPattern, @@ -95,7 +89,6 @@ function getComponent({ onRemoveField: jest.fn(), showDetails, selected, - useShortDots, }; const comp = mountWithIntl(); return { comp, props }; @@ -118,17 +111,14 @@ describe('discover sidebar field', function () { expect(props.getDetails).toHaveBeenCalledWith(props.field); }); it('should not allow clicking on _source', function () { - const field = new IndexPatternField( - { - name: '_source', - type: '_source', - esTypes: ['_source'], - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - '_source' - ); + const field = new IndexPatternField({ + name: '_source', + type: '_source', + esTypes: ['_source'], + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const { comp, props } = getComponent({ selected: true, field, diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 0329b3a34580c..35515a6a0e7a5 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -24,7 +24,6 @@ import { DiscoverFieldDetails } from './discover_field_details'; import { FieldIcon, FieldButton } from '../../../../../kibana_react/public'; import { FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; -import { shortenDottedString } from '../../helpers'; import { getFieldTypeName } from './lib/get_field_type_name'; import './discover_field.scss'; @@ -58,10 +57,6 @@ export interface DiscoverFieldProps { * Determines whether the field is selected */ selected?: boolean; - /** - * Determines whether the field name is shortened test.sub1.sub2 = t.s.sub2 - */ - useShortDots?: boolean; /** * Metric tracking function * @param metricType @@ -78,7 +73,6 @@ export function DiscoverField({ onAddFilter, getDetails, selected, - useShortDots, trackUiMetric, }: DiscoverFieldProps) { const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { @@ -118,13 +112,12 @@ export function DiscoverField({ ); + const title = + field.displayName !== field.name ? `${field.name} (${field.displayName} )` : field.displayName; + const fieldName = ( - - {useShortDots ? wrapOnDot(shortenDottedString(field.name)) : wrapOnDot(field.displayName)} + + {wrapOnDot(field.displayName)} ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx index 8607873b98d3d..0618e53d15dbb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx @@ -48,55 +48,46 @@ describe('discover sidebar field details', function () { } it('should enable the visualize link for a number field', function () { - const visualizableField = new IndexPatternField( - { - name: 'bytes', - type: 'number', - esTypes: ['long'], - count: 10, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'bytes' - ); + const visualizableField = new IndexPatternField({ + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const comp = mountComponent(visualizableField); expect(findTestSubject(comp, 'fieldVisualize-bytes')).toBeTruthy(); }); it('should disable the visualize link for an _id field', function () { - const conflictField = new IndexPatternField( - { - name: '_id', - type: 'string', - esTypes: ['_id'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'test' - ); + const conflictField = new IndexPatternField({ + name: '_id', + type: 'string', + esTypes: ['_id'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const comp = mountComponent(conflictField); expect(findTestSubject(comp, 'fieldVisualize-_id')).toEqual({}); }); it('should disable the visualize link for an unknown field', function () { - const unknownField = new IndexPatternField( - { - name: 'test', - type: 'unknown', - esTypes: ['double'], - count: 0, - scripted: false, - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, - 'test' - ); + const unknownField = new IndexPatternField({ + name: 'test', + type: 'unknown', + esTypes: ['double'], + count: 0, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); const comp = mountComponent(unknownField); expect(findTestSubject(comp, 'fieldVisualize-test')).toEqual({}); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 7504d181d82b2..23d2fa0a39f34 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -51,8 +51,6 @@ jest.mock('../../../kibana_services', () => ({ get: (key: string) => { if (key === 'fields:popularLimit') { return 5; - } else if (key === 'shortDots:enable') { - return false; } }, }, diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index dfd09ccee9337..b8e09ce4d17e8 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -30,7 +30,7 @@ import { IndexPatternAttributes } from '../../../../../data/common'; import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; -import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public'; +import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getDetails } from './lib/get_details'; import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; @@ -117,7 +117,6 @@ export function DiscoverSidebar({ ); const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); - const useShortDots = services.uiSettings.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const { selected: selectedFields, @@ -201,7 +200,6 @@ export function DiscoverSidebar({ onAddFilter={onAddFilter} getDetails={getDetailsByField} selected={true} - useShortDots={useShortDots} trackUiMetric={trackUiMetric} /> @@ -276,7 +274,6 @@ export function DiscoverSidebar({ onRemoveField={onRemoveField} onAddFilter={onAddFilter} getDetails={getDetailsByField} - useShortDots={useShortDots} trackUiMetric={trackUiMetric} /> @@ -307,7 +304,6 @@ export function DiscoverSidebar({ onRemoveField={onRemoveField} onAddFilter={onAddFilter} getDetails={getDetailsByField} - useShortDots={useShortDots} trackUiMetric={trackUiMetric} /> diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts index eb139f97c7b00..ebbffae83125c 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.test.ts @@ -59,6 +59,7 @@ describe('field_filter', function () { const fieldList = [ { name: 'bytes', + displayName: 'Bye,bye,Bytes', type: 'number', esTypes: ['long'], count: 10, @@ -68,6 +69,7 @@ describe('field_filter', function () { }, { name: 'extension', + displayName: 'Extension', type: 'string', esTypes: ['text'], count: 10, @@ -80,6 +82,8 @@ describe('field_filter', function () { [ { filter: {}, result: ['bytes', 'extension'] }, { filter: { name: 'by' }, result: ['bytes'] }, + { filter: { name: 'Ext' }, result: ['extension'] }, + { filter: { name: 'Bytes' }, result: ['bytes'] }, { filter: { aggregatable: true }, result: ['extension'] }, { filter: { aggregatable: true, searchable: false }, result: [] }, { filter: { type: 'string' }, result: ['extension'] }, diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts index f0d9a2d8af20f..2e1d9b76606df 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_filter.ts @@ -72,7 +72,9 @@ export function isFieldFiltered( field.type === '_source' || field.scripted || fieldCounts[field.name] > 0; - const matchName = !filterState.name || field.name.indexOf(filterState.name) !== -1; + const needle = filterState.name ? filterState.name.toLowerCase() : ''; + const haystack = `${field.name}${field.displayName || ''}`.toLowerCase(); + const matchName = !filterState.name || haystack.indexOf(needle) !== -1; return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName; } diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 628045bd32f61..5d37f598b38f6 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -46,7 +46,13 @@ export function DocViewTable({ {Object.keys(flattened) - .sort() + .sort((fieldA, fieldB) => { + const mappingA = mapping(fieldA); + const mappingB = mapping(fieldB); + const nameA = !mappingA || !mappingA.displayName ? fieldA : mappingA.displayName; + const nameB = !mappingB || !mappingB.displayName ? fieldB : mappingB.displayName; + return nameA.localeCompare(nameB); + }) .map((field) => { const valueRaw = flattened[field]; const value = trimAngularSpan(String(formatted[field])); diff --git a/src/plugins/discover/public/application/components/table/table_row.tsx b/src/plugins/discover/public/application/components/table/table_row.tsx index 5f7dd9f37dcd3..3d75e175951d5 100644 --- a/src/plugins/discover/public/application/components/table/table_row.tsx +++ b/src/plugins/discover/public/application/components/table/table_row.tsx @@ -91,6 +91,7 @@ export function DocViewTableRow({ diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 6c90861e26727..01145402e0f29 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -36,6 +36,7 @@ export interface FieldMapping { rowCount?: number; type: string; name: string; + displayName?: string; } export type DocViewFilterFn = ( diff --git a/src/plugins/discover/public/application/helpers/index.ts b/src/plugins/discover/public/application/helpers/index.ts index 3555d24924e80..f7497c29a2bda 100644 --- a/src/plugins/discover/public/application/helpers/index.ts +++ b/src/plugins/discover/public/application/helpers/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { shortenDottedString } from './shorten_dotted_string'; export { formatNumWithCommas } from './format_number_with_commas'; diff --git a/src/plugins/discover/public/application/helpers/shorten_dotted_string.ts b/src/plugins/discover/public/application/helpers/shorten_dotted_string.ts deleted file mode 100644 index 9d78a96784339..0000000000000 --- a/src/plugins/discover/public/application/helpers/shorten_dotted_string.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const DOT_PREFIX_RE = /(.).+?\./g; - -/** - * Convert a dot.notated.string into a short - * version (d.n.string) - */ -export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.'); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index e1359eafe1c67..4b63eb5c56fd1 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -19,7 +19,14 @@ import React, { PureComponent } from 'react'; -import { EuiIcon, EuiInMemoryTable, EuiIconTip, EuiBasicTableColumn } from '@elastic/eui'; +import { + EuiIcon, + EuiInMemoryTable, + EuiIconTip, + EuiBasicTableColumn, + EuiBadge, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -144,6 +151,11 @@ const editDescription = i18n.translate( { defaultMessage: 'Edit' } ); +const customNameDescription = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.customNameTooltip', + { defaultMessage: 'A custom name for the field.' } +); + interface IndexedFieldProps { indexPattern: IIndexPattern; items: IndexedFieldItem[]; @@ -160,7 +172,7 @@ export class Table extends PureComponent { return ( - {name} + {field.name} {field.info && field.info.length ? (   @@ -185,6 +197,15 @@ export class Table extends PureComponent { /> ) : null} + {field.customName && field.customName !== field.name ? ( +
+ + + {field.customName} + + +
+ ) : null}
); } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 23f0a83c591de..1a04aaf784839 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -47,10 +47,7 @@ const indexPattern = ({ } as unknown) as IndexPattern; const mockFieldToIndexPatternField = (spec: Record) => { - return new IndexPatternField( - (spec as unknown) as IndexPatternField['spec'], - spec.displayName as string - ); + return new IndexPatternField((spec as unknown) as IndexPatternField['spec']); }; const fields = [ diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 92f0c4576e931..e097271248bbd 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -89,7 +89,11 @@ export class IndexedFieldsTable extends Component< (fields, fieldFilter, indexedFieldTypeFilter) => { if (fieldFilter) { const normalizedFieldFilter = fieldFilter.toLowerCase(); - fields = fields.filter((field) => field.name.toLowerCase().includes(normalizedFieldFilter)); + fields = fields.filter( + (field) => + field.name.toLowerCase().includes(normalizedFieldFilter) || + (field.displayName && field.displayName.toLowerCase().includes(normalizedFieldFilter)) + ); } if (indexedFieldTypeFilter) { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index 1e8fb6f9492fe..babfbbfc2a763 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -51,6 +51,22 @@ exports[`FieldEditor should render create new scripted field correctly 1`] = ` value="" /> + + } + label="Custom name" + > + + - + + } + label="Custom name" + > + + - + + } + label="Custom name" + > + + - + + } + label="Custom name" + > + + @@ -954,7 +1021,7 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` isInvalid={false} label="Script" > - + + } + label="Custom name" + > + + - ({})); @@ -37,6 +38,7 @@ jest.mock('@elastic/eui', () => ({ EuiButtonEmpty: 'eui-button-empty', EuiCallOut: 'eui-call-out', EuiCode: 'eui-code', + EuiCodeEditor: 'eui-code-editor', EuiConfirmModal: 'eui-confirm-modal', EuiFieldNumber: 'eui-field-number', EuiFieldText: 'eui-field-text', @@ -173,6 +175,60 @@ describe('FieldEditor', () => { expect(component).toMatchSnapshot(); }); + it('should display and update a customName correctly', async () => { + let testField = ({ + name: 'test', + format: new Format(), + lang: undefined, + type: 'string', + customName: 'Test', + } as unknown) as IndexPatternField; + fieldList.push(testField); + indexPattern.fields.getByName = (name) => { + const flds = { + [testField.name]: testField, + }; + return flds[name]; + }; + indexPattern.fields = { + ...indexPattern.fields, + ...{ + update: (fld) => { + testField = (fld as unknown) as IndexPatternField; + }, + add: jest.fn(), + }, + }; + indexPattern.fieldFormatMap = { test: field }; + indexPattern.deleteFieldFormat = jest.fn(); + + const component = createComponentWithContext( + FieldEditor, + { + indexPattern, + spec: (testField as unknown) as IndexPatternField, + services: { + redirectAway: () => {}, + indexPatternService: ({ + updateSavedObject: jest.fn(() => Promise.resolve()), + } as unknown) as IndexPatternsService, + }, + }, + mockContext + ); + + await new Promise((resolve) => process.nextTick(resolve)); + component.update(); + const input = findTestSubject(component, 'editorFieldCustomName'); + expect(input.props().value).toBe('Test'); + input.simulate('change', { target: { value: 'new Test' } }); + const saveBtn = findTestSubject(component, 'fieldSaveButton'); + + await saveBtn.simulate('click'); + await new Promise((resolve) => process.nextTick(resolve)); + expect(testField.customName).toEqual('new Test'); + }); + it('should show deprecated lang warning', async () => { const testField = { ...field, diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index d02338a6aee24..97d30d88e018c 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -126,6 +126,7 @@ export interface FieldEditorState { errors?: string[]; format: any; spec: IndexPatternField['spec']; + customName: string; } export interface FieldEdiorProps { @@ -166,6 +167,7 @@ export class FieldEditor extends PureComponent + } + > + { + this.setState({ customName: e.target.value }); + }} + /> + + ); + } + /** * renders a warning and a table of conflicting indices * in case there are indices with different types @@ -772,7 +802,7 @@ export class FieldEditor extends PureComponent { const field = this.state.spec; const { indexPattern } = this.props; - const { fieldFormatId, fieldFormatParams } = this.state; + const { fieldFormatId, fieldFormatParams, customName } = this.state; if (field.scripted) { this.setState({ @@ -813,6 +843,11 @@ export class FieldEditor extends PureComponent { @@ -873,6 +908,7 @@ export class FieldEditor extends PureComponent {this.renderScriptingPanels()} {this.renderName()} + {this.renderCustomName()} {this.renderLanguage()} {this.renderType()} {this.renderTypeConflict()} diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 3c9996ca44ff8..fe5c04c001731 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -293,6 +293,16 @@ export default function ({ getService, getPageObjects }) { const currentUrlWithoutScore = await browser.getCurrentUrl(); expect(currentUrlWithoutScore).not.to.contain('_score'); }); + it('should add a field with customLabel, sort by it, display it correctly', async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.clickFieldListItemAdd('referer'); + await PageObjects.discover.clickFieldSort('referer'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('Referer custom'); + expect(await PageObjects.discover.getAllFieldNames()).to.contain('Referer custom'); + const url = await browser.getCurrentUrl(); + expect(url).to.contain('referer'); + }); }); describe('refresh interval', function () { diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js index bd7511d373b90..5b0b7af56b332 100644 --- a/test/functional/apps/visualize/_data_table.js +++ b/test/functional/apps/visualize/_data_table.js @@ -209,6 +209,29 @@ export default function ({ getService, getPageObjects }) { ]); }); + it('should show correct data when selecting a field by its custom name', async () => { + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickDataTable(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('UTC time'); + await PageObjects.visEditor.setInterval('Day'); + await PageObjects.visEditor.clickGo(); + const data = await PageObjects.visChart.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql([ + '2015-09-20', + '4,757', + '2015-09-21', + '4,614', + '2015-09-22', + '4,633', + ]); + const header = await PageObjects.visChart.getTableVisHeader(); + expect(header).to.contain('UTC time'); + }); + it('should correctly filter for applied time filter on the main timefield', async () => { await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); diff --git a/test/functional/fixtures/es_archiver/discover/data.json b/test/functional/fixtures/es_archiver/discover/data.json index 9158a3023fc5e..0f9820a6c2f6e 100644 --- a/test/functional/fixtures/es_archiver/discover/data.json +++ b/test/functional/fixtures/es_archiver/discover/data.json @@ -7,7 +7,8 @@ "index-pattern": { "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@message\"}}},{\"name\":\"@tags\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"@tags\"}}},{\"name\":\"@timestamp\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"agent\"}}},{\"name\":\"bytes\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"extension\"}}},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"headings\"}}},{\"name\":\"host\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"host\"}}},{\"name\":\"id\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"index\"}}},{\"name\":\"ip\",\"type\":\"ip\",\"esTypes\":[\"ip\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"links\"}}},{\"name\":\"machine.os\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"machine.os\"}}},{\"name\":\"machine.ram\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"esTypes\":[\"double\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"nestedField.child\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}},{\"name\":\"phpmemory\",\"type\":\"number\",\"esTypes\":[\"long\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:section\"}}},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.article:tag\"}}},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:description\"}}},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image\"}}},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:height\"}}},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:image:width\"}}},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:site_name\"}}},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:title\"}}},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:type\"}}},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.og:url\"}}},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:card\"}}},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:description\"}}},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:image\"}}},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:site\"}}},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.twitter:title\"}}},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"relatedContent.url\"}}},{\"name\":\"request\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"request\"}}},{\"name\":\"response\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"response\"}}},{\"name\":\"spaces\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"spaces\"}}},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"url\"}}},{\"name\":\"utc_time\",\"type\":\"date\",\"esTypes\":[\"date\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"esTypes\":[\"text\"],\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"xss\"}}}]", "timeFieldName": "@timestamp", - "title": "logstash-*" + "title": "logstash-*", + "fieldAttrs": "{\"referer\":{\"customName\":\"Referer custom\"}}" }, "type": "index-pattern" } diff --git a/test/functional/fixtures/es_archiver/discover/mappings.json b/test/functional/fixtures/es_archiver/discover/mappings.json index 82002c095bcc5..53bbe8a5baa5b 100644 --- a/test/functional/fixtures/es_archiver/discover/mappings.json +++ b/test/functional/fixtures/es_archiver/discover/mappings.json @@ -93,6 +93,9 @@ }, "title": { "type": "text" + }, + "fieldAttrs": { + "type": "text" } } }, diff --git a/test/functional/fixtures/es_archiver/visualize/data.json b/test/functional/fixtures/es_archiver/visualize/data.json index abca5a98bf7fd..c57cdb40ae952 100644 --- a/test/functional/fixtures/es_archiver/visualize/data.json +++ b/test/functional/fixtures/es_archiver/visualize/data.json @@ -8,7 +8,8 @@ "fieldFormatMap": "{\"bytes\":{\"id\":\"bytes\"}}", "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", "timeFieldName": "@timestamp", - "title": "logstash-*" + "title": "logstash-*", + "fieldAttrs": "{\"utc_time\":{\"customName\":\"UTC time\"}}" }, "type": "index-pattern" } diff --git a/test/functional/fixtures/es_archiver/visualize/mappings.json b/test/functional/fixtures/es_archiver/visualize/mappings.json index a50aed233eea6..464f6751eac5c 100644 --- a/test/functional/fixtures/es_archiver/visualize/mappings.json +++ b/test/functional/fixtures/es_archiver/visualize/mappings.json @@ -93,6 +93,9 @@ }, "title": { "type": "text" + }, + "fieldAttrs": { + "type": "text" } } }, diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 2423f66a4b34e..9c5bedf7c242d 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -246,9 +246,9 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider public async getAllFieldNames() { const sidebar = await testSubjects.find('discover-sidebar'); const $ = await sidebar.parseDomContent(); - return $('.dscSidebar__item[attr-field]') + return $('.dscSidebarField__name') .toArray() - .map((field) => $(field).find('span.eui-textTruncate').text()); + .map((field) => $(field).text()); } public async getSidebarWidth() { diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 1acea624ad4cd..3e3f60ca17131 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -328,6 +328,13 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr return await testSubjects.getVisibleText('paginated-table-body'); } + /** + * This function returns the text displayed in the Table Vis header + */ + public async getTableVisHeader() { + return await testSubjects.getVisibleText('paginated-table-header'); + } + /** * This function is the newer function to retrieve data from within a table visualization. * It uses a better return format, than the old getTableVisData, by properly splitting diff --git a/x-pack/test/functional/es_archives/discover/default/mappings.json b/x-pack/test/functional/es_archives/discover/default/mappings.json index 82002c095bcc5..53bbe8a5baa5b 100644 --- a/x-pack/test/functional/es_archives/discover/default/mappings.json +++ b/x-pack/test/functional/es_archives/discover/default/mappings.json @@ -93,6 +93,9 @@ }, "title": { "type": "text" + }, + "fieldAttrs": { + "type": "text" } } }, From ee81b5fc04c6337c94d74684e7ed8620915f0644 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Tue, 17 Nov 2020 07:29:10 -0500 Subject: [PATCH 29/99] [Alerting UI] Fix console error when setting connector params (#83333) * Fixing console errors * Setting defaults for undefined inputs in text area/field with message variables * Cleanup * Cleanup * Fixing pagerduty timestamp validation * Fixing test * Pagerduty params * Reverting unnecessary changes --- .../components/text_area_with_message_variables.tsx | 2 +- .../components/text_field_with_message_variables.tsx | 2 +- .../sections/action_connector_form/connector_add_flyout.tsx | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx index e60785f70bffe..f5095101d96b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_area_with_message_variables.tsx @@ -63,7 +63,7 @@ export const TextAreaWithMessageVariables: React.FunctionComponent = ({ fullWidth isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined} name={paramsProperty} - value={inputTargetValue} + value={inputTargetValue || ''} data-test-subj={`${paramsProperty}TextArea`} onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} onFocus={(e: React.FocusEvent) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx index fc05b237ccf5e..946bf064eb9ce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/text_field_with_message_variables.tsx @@ -50,7 +50,7 @@ export const TextFieldWithMessageVariables: React.FunctionComponent = ({ id={`${paramsProperty}Id`} isInvalid={errors && errors.length > 0 && inputTargetValue !== undefined} data-test-subj={`${paramsProperty}Input`} - value={inputTargetValue} + value={inputTargetValue || ''} onChange={(e: React.ChangeEvent) => onChangeWithMessageVariable(e)} onFocus={(e: React.FocusEvent) => { setCurrentTextElement(e.target); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 00ff6fc132cdc..b53d0816ea068 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -72,10 +72,8 @@ export const ConnectorAddFlyout = ({ const [isSaving, setIsSaving] = useState(false); const closeFlyout = useCallback(() => { - setActionType(undefined); - setConnector(initialConnector); onClose(); - }, [onClose, initialConnector]); + }, [onClose]); const canSave = hasSaveActionsCapability(capabilities); From 9b5605f4c412c25d9831ba35ea756780ddebdbf7 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Tue, 17 Nov 2020 08:09:21 -0500 Subject: [PATCH 30/99] [App Search] Added all Document related routes and logic (#83324) --- .../documents/document_detail_logic.test.ts | 156 ++++++++++++++++++ .../documents/document_detail_logic.ts | 92 +++++++++++ .../documents/documents_logic.test.ts | 66 ++++++++ .../components/documents/documents_logic.ts | 35 ++++ .../app_search/components/documents/index.ts | 8 + .../app_search/components/documents/types.ts | 11 ++ .../routes/app_search/documents.test.ts | 61 +++++++ .../server/routes/app_search/documents.ts | 47 ++++++ .../server/routes/app_search/index.ts | 2 + 9 files changed, 478 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts new file mode 100644 index 0000000000000..782b8159c94a1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { mockHttpValues } from '../../../__mocks__'; +jest.mock('../../../shared/http', () => ({ + HttpLogic: { values: mockHttpValues }, +})); +const { http } = mockHttpValues; + +jest.mock('../engine', () => ({ + EngineLogic: { values: { engineName: 'engine1' } }, +})); + +jest.mock('../../../shared/flash_messages', () => ({ + setQueuedSuccessMessage: jest.fn(), + flashAPIErrors: jest.fn(), +})); +import { setQueuedSuccessMessage, flashAPIErrors } from '../../../shared/flash_messages'; + +import { DocumentDetailLogic } from './document_detail_logic'; + +describe('DocumentDetailLogic', () => { + const DEFAULT_VALUES = { + dataLoading: true, + fields: [], + }; + + const mount = (defaults?: object) => { + if (!defaults) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + document_detail_logic: { + ...defaults, + }, + }, + }, + }, + }); + } + DocumentDetailLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('actions', () => { + describe('setFields', () => { + it('should set fields to the provided value and dataLoading to false', () => { + const fields = [{ name: 'foo', value: ['foo'], type: 'string' }]; + + mount({ + dataLoading: true, + fields: [], + }); + + DocumentDetailLogic.actions.setFields(fields); + + expect(DocumentDetailLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + fields, + }); + }); + }); + + describe('getDocumentDetails', () => { + it('will call an API endpoint and then store the result', async () => { + const fields = [{ name: 'name', value: 'python', type: 'string' }]; + jest.spyOn(DocumentDetailLogic.actions, 'setFields'); + const promise = Promise.resolve({ fields }); + http.get.mockReturnValue(promise); + + DocumentDetailLogic.actions.getDocumentDetails('1'); + + expect(http.get).toHaveBeenCalledWith(`/api/app_search/engines/engine1/documents/1`); + await promise; + expect(DocumentDetailLogic.actions.setFields).toHaveBeenCalledWith(fields); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occurred'); + http.get.mockReturnValue(promise); + + try { + DocumentDetailLogic.actions.getDocumentDetails('1'); + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); + }); + }); + + describe('deleteDocument', () => { + let confirmSpy: any; + let promise: Promise; + + beforeEach(() => { + confirmSpy = jest.spyOn(window, 'confirm'); + confirmSpy.mockImplementation(jest.fn(() => true)); + promise = Promise.resolve({}); + http.delete.mockReturnValue(promise); + }); + + afterEach(() => { + confirmSpy.mockRestore(); + }); + + it('will call an API endpoint and show a success message', async () => { + mount(); + DocumentDetailLogic.actions.deleteDocument('1'); + + expect(http.delete).toHaveBeenCalledWith(`/api/app_search/engines/engine1/documents/1`); + await promise; + expect(setQueuedSuccessMessage).toHaveBeenCalledWith( + 'Successfully marked document for deletion. It will be deleted momentarily.' + ); + }); + + it('will do nothing if not confirmed', async () => { + mount(); + window.confirm = () => false; + + DocumentDetailLogic.actions.deleteDocument('1'); + + expect(http.delete).not.toHaveBeenCalled(); + await promise; + }); + + it('handles errors', async () => { + mount(); + promise = Promise.reject('An error occured'); + http.delete.mockReturnValue(promise); + + try { + DocumentDetailLogic.actions.deleteDocument('1'); + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts new file mode 100644 index 0000000000000..87bf149fb1680 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; +import { i18n } from '@kbn/i18n'; + +import { HttpLogic } from '../../../shared/http'; +import { EngineLogic } from '../engine'; +import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; +import { FieldDetails } from './types'; + +interface DocumentDetailLogicValues { + dataLoading: boolean; + fields: FieldDetails[]; +} + +interface DocumentDetailLogicActions { + setFields(fields: FieldDetails[]): { fields: FieldDetails[] }; + deleteDocument(documentId: string): { documentId: string }; + getDocumentDetails(documentId: string): { documentId: string }; +} + +type DocumentDetailLogicType = MakeLogicType; + +const CONFIRM_DELETE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentDetail.confirmDelete', + { + defaultMessage: 'Are you sure you want to delete this document?', + } +); +const DELETE_SUCCESS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentDetail.deleteSuccess', + { + defaultMessage: 'Successfully marked document for deletion. It will be deleted momentarily.', + } +); + +export const DocumentDetailLogic = kea({ + path: ['enterprise_search', 'app_search', 'document_detail_logic'], + actions: () => ({ + setFields: (fields) => ({ fields }), + getDocumentDetails: (documentId) => ({ documentId }), + deleteDocument: (documentId) => ({ documentId }), + }), + reducers: () => ({ + dataLoading: [ + true, + { + setFields: () => false, + }, + ], + fields: [ + [], + { + setFields: (_, { fields }) => fields, + }, + ], + }), + listeners: ({ actions }) => ({ + getDocumentDetails: async ({ documentId }) => { + const { engineName } = EngineLogic.values; + + try { + const { http } = HttpLogic.values; + // TODO: Handle 404s + const response = await http.get( + `/api/app_search/engines/${engineName}/documents/${documentId}` + ); + actions.setFields(response.fields); + } catch (e) { + flashAPIErrors(e); + } + }, + deleteDocument: async ({ documentId }) => { + const { engineName } = EngineLogic.values; + + if (window.confirm(CONFIRM_DELETE)) { + try { + const { http } = HttpLogic.values; + await http.delete(`/api/app_search/engines/${engineName}/documents/${documentId}`); + setQueuedSuccessMessage(DELETE_SUCCESS); + // TODO Handle routing after success + } catch (e) { + flashAPIErrors(e); + } + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts new file mode 100644 index 0000000000000..236172f0f7bdf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { DocumentsLogic } from './documents_logic'; + +describe('DocumentsLogic', () => { + const DEFAULT_VALUES = { + isDocumentCreationOpen: false, + }; + + const mount = (defaults?: object) => { + if (!defaults) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + documents_logic: { + ...defaults, + }, + }, + }, + }, + }); + } + DocumentsLogic.mount(); + }; + + describe('actions', () => { + describe('openDocumentCreation', () => { + it('should toggle isDocumentCreationOpen to true', () => { + mount({ + isDocumentCreationOpen: false, + }); + + DocumentsLogic.actions.openDocumentCreation(); + + expect(DocumentsLogic.values).toEqual({ + ...DEFAULT_VALUES, + isDocumentCreationOpen: true, + }); + }); + }); + + describe('closeDocumentCreation', () => { + it('should toggle isDocumentCreationOpen to false', () => { + mount({ + isDocumentCreationOpen: true, + }); + + DocumentsLogic.actions.closeDocumentCreation(); + + expect(DocumentsLogic.values).toEqual({ + ...DEFAULT_VALUES, + isDocumentCreationOpen: false, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.ts new file mode 100644 index 0000000000000..dcf1a883bd3b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents_logic.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +interface DocumentsLogicValues { + isDocumentCreationOpen: boolean; +} + +interface DocumentsLogicActions { + closeDocumentCreation(): void; + openDocumentCreation(): void; +} + +type DocumentsLogicType = MakeLogicType; + +export const DocumentsLogic = kea({ + path: ['enterprise_search', 'app_search', 'documents_logic'], + actions: () => ({ + openDocumentCreation: true, + closeDocumentCreation: true, + }), + reducers: () => ({ + isDocumentCreationOpen: [ + false, + { + openDocumentCreation: () => true, + closeDocumentCreation: () => false, + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts new file mode 100644 index 0000000000000..d374098d70788 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DocumentDetailLogic } from './document_detail_logic'; +export { DocumentsLogic } from './documents_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts new file mode 100644 index 0000000000000..6a7c1cd1d5d2f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface FieldDetails { + name: string; + value: string | string[]; + type: string; +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts new file mode 100644 index 0000000000000..d5fed4c6f97cb --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerDocumentRoutes } from './documents'; + +describe('document routes', () => { + describe('GET /api/app_search/engines/{engineName}/documents/{documentId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{engineName}/documents/{documentId}', + }); + + registerDocumentRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/some-engine/documents/1', + }); + }); + }); + + describe('DELETE /api/app_search/engines/{engineName}/documents/{documentId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/engines/{engineName}/documents/{documentId}', + }); + + registerDocumentRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/some-engine/documents/1', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts new file mode 100644 index 0000000000000..a2f4b323a91aa --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerDocumentRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{engineName}/documents/{documentId}', + validate: { + params: schema.object({ + engineName: schema.string(), + documentId: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`, + })(context, request, response); + } + ); + router.delete( + { + path: '/api/app_search/engines/{engineName}/documents/{documentId}', + validate: { + params: schema.object({ + engineName: schema.string(), + documentId: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`, + })(context, request, response); + } + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index faf74203cf17d..f64e45c656fa1 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -9,9 +9,11 @@ import { RouteDependencies } from '../../plugin'; import { registerEnginesRoutes } from './engines'; import { registerCredentialsRoutes } from './credentials'; import { registerSettingsRoutes } from './settings'; +import { registerDocumentRoutes } from './documents'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerEnginesRoutes(dependencies); registerCredentialsRoutes(dependencies); registerSettingsRoutes(dependencies); + registerDocumentRoutes(dependencies); }; From e4516ee0e9eb2a0b36219c10ce7634444039ebd6 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 17 Nov 2020 07:37:46 -0600 Subject: [PATCH 31/99] [Workplace Search] Enable check for org context based on URL (#83487) * Add regex check to determine whether url is org As a part of the Kibana migration, we are switching the URL structure to put the prefix on the personal dashboard. In ent-search, org routes were prefixed with `/org`. In Kibana the prefix switches to non-org routes and they will be prefixed with`/p` * Add isOrganization boolean to logic --- .../workplace_search/app_logic.test.ts | 10 ++++++++++ .../workplace_search/app_logic.ts | 9 +++++++++ .../workplace_search/index.test.tsx | 10 ++++------ .../applications/workplace_search/index.tsx | 19 +++++++++++++++++-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index 974e07069ddba..d77faf471facc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -19,6 +19,7 @@ describe('AppLogic', () => { account: {}, hasInitialized: false, isFederatedAuth: true, + isOrganization: false, organization: {}, }; @@ -34,6 +35,7 @@ describe('AppLogic', () => { }, hasInitialized: true, isFederatedAuth: false, + isOrganization: false, organization: { defaultOrgName: 'My Organization', name: 'ACME Donuts', @@ -61,4 +63,12 @@ describe('AppLogic', () => { }); }); }); + + describe('setContext()', () => { + it('sets context', () => { + AppLogic.actions.setContext(true); + + expect(AppLogic.values.isOrganization).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index b7476a5187749..f5f534807fabf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -16,9 +16,11 @@ import { interface AppValues extends WorkplaceSearchInitialData { hasInitialized: boolean; isFederatedAuth: boolean; + isOrganization: boolean; } interface AppActions { initializeAppData(props: InitialAppData): InitialAppData; + setContext(isOrganization: boolean): boolean; } const emptyOrg = {} as Organization; @@ -31,6 +33,7 @@ export const AppLogic = kea>({ workplaceSearch, isFederatedAuth, }), + setContext: (isOrganization) => isOrganization, }, reducers: { hasInitialized: [ @@ -45,6 +48,12 @@ export const AppLogic = kea>({ initializeAppData: (_, { isFederatedAuth }) => !!isFederatedAuth, }, ], + isOrganization: [ + false, + { + setContext: (_, isOrganization) => isOrganization, + }, + ], organization: [ emptyOrg, { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 25544b4a9bb68..5f1e2dd18d3b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -46,9 +46,12 @@ describe('WorkplaceSearchUnconfigured', () => { }); describe('WorkplaceSearchConfigured', () => { + const initializeAppData = jest.fn(); + const setContext = jest.fn(); + beforeEach(() => { jest.clearAllMocks(); - setMockActions({ initializeAppData: () => {} }); + setMockActions({ initializeAppData, setContext }); }); it('renders layout and header actions', () => { @@ -60,17 +63,12 @@ describe('WorkplaceSearchConfigured', () => { }); it('initializes app data with passed props', () => { - const initializeAppData = jest.fn(); - setMockActions({ initializeAppData }); - shallow(); expect(initializeAppData).toHaveBeenCalledWith({ isFederatedAuth: true }); }); it('does not re-initialize app data or re-render header actions', () => { - const initializeAppData = jest.fn(); - setMockActions({ initializeAppData }); setMockValues({ hasInitialized: true }); shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 311f30a891eb9..776cae24dfdfb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect } from 'react'; -import { Route, Redirect, Switch } from 'react-router-dom'; +import { Route, Redirect, Switch, useLocation } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; @@ -31,10 +31,21 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); - const { initializeAppData } = useActions(AppLogic); + const { initializeAppData, setContext } = useActions(AppLogic); const { renderHeaderActions } = useValues(KibanaLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); + const { pathname } = useLocation(); + + /** + * Personal dashboard urls begin with /p/ + * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources + */ + const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' + + // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. + const isOrganization = !pathname.match(personalSourceUrlRegex); + useEffect(() => { if (!hasInitialized) { initializeAppData(props); @@ -42,6 +53,10 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { } }, [hasInitialized]); + useEffect(() => { + setContext(isOrganization); + }, [isOrganization]); + return ( From 2fb04a6d41c7db1ddc74e5fe4ea7045eec0f7aa2 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 17 Nov 2020 14:44:06 +0100 Subject: [PATCH 32/99] [Uptime] Mock implementation to account for math flakiness test (#83535) --- .../lib/requests/__tests__/get_ping_histogram.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index 86e5f2876ca28..427061b6c16d4 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -7,8 +7,13 @@ import { getPingHistogram } from '../get_ping_histogram'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; +import * as intervalHelper from '../../helper/get_histogram_interval'; describe('getPingHistogram', () => { + beforeEach(() => { + jest.spyOn(intervalHelper, 'getHistogramInterval').mockReturnValue(36000); + }); + const standardMockResponse: any = { aggregations: { timeseries: { @@ -36,7 +41,7 @@ describe('getPingHistogram', () => { }, }; - it.skip('returns a single bucket if array has 1', async () => { + it('returns a single bucket if array has 1', async () => { expect.assertions(2); const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); From 8b658fbcd2e97546b59a156df57b02d866882710 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 17 Nov 2020 06:44:54 -0800 Subject: [PATCH 33/99] Used SO for saving the API key IDs that should be deleted (#82211) * Used SO for saving the API key IDs that should be deleted and create a configuration option where can set an execution interval for a TM task which will get the data from this SO and remove marked for delete keys. * removed invalidateApiKey from AlertsClient * Fixed type checks * Fixed jest tests * Removed test code * Changed SO name * fixed type cheks * Moved invalidate logic out of alerts client * fixed type check * Added functional tests * Fixed due to comments * added configurable delay for invalidation task * added interval to the task response * Fixed jest tests * Fixed due to comments * Fixed task * fixed paging * Fixed date filter * Fixed jest tests * fixed due to comments * fixed due to comments * Fixed e2e test * Fixed e2e test * Fixed due to comments. Changed api key invalidation task to use SavedObjectClient * Use encryptedSavedObjectClient * set back flaky test comment --- .../server/alerts_client/alerts_client.ts | 82 ++++--- .../alerts_client/tests/aggregate.test.ts | 1 - .../server/alerts_client/tests/create.test.ts | 19 +- .../server/alerts_client/tests/delete.test.ts | 56 ++++- .../alerts_client/tests/disable.test.ts | 65 ++++- .../server/alerts_client/tests/enable.test.ts | 42 +++- .../server/alerts_client/tests/find.test.ts | 1 - .../server/alerts_client/tests/get.test.ts | 1 - .../tests/get_alert_instance_summary.test.ts | 1 - .../tests/get_alert_state.test.ts | 1 - .../alerts/server/alerts_client/tests/lib.ts | 8 - .../tests/list_alert_types.test.ts | 1 - .../alerts_client/tests/mute_all.test.ts | 1 - .../alerts_client/tests/mute_instance.test.ts | 1 - .../alerts_client/tests/unmute_all.test.ts | 1 - .../tests/unmute_instance.test.ts | 1 - .../server/alerts_client/tests/update.test.ts | 54 ++++- .../tests/update_api_key.test.ts | 59 ++++- .../alerts_client_conflict_retries.test.ts | 11 +- .../server/alerts_client_factory.test.ts | 6 +- .../alerts/server/alerts_client_factory.ts | 20 +- x-pack/plugins/alerts/server/config.test.ts | 4 + x-pack/plugins/alerts/server/config.ts | 4 + .../mark_api_key_for_invalidation.test.ts | 47 ++++ .../mark_api_key_for_invalidation.ts | 25 ++ .../invalidate_pending_api_keys/task.ts | 226 ++++++++++++++++++ .../plugins/alerts/server/lib/get_cadence.ts | 53 ++++ x-pack/plugins/alerts/server/plugin.test.ts | 12 + x-pack/plugins/alerts/server/plugin.ts | 14 ++ .../alerts/server/saved_objects/index.ts | 22 ++ x-pack/plugins/alerts/server/types.ts | 12 + .../alerting_api_integration/common/config.ts | 1 + .../plugins/alerts/server/alert_types.ts | 16 ++ .../fixtures/plugins/alerts/server/plugin.ts | 3 + .../fixtures/plugins/alerts/server/routes.ts | 28 +++ .../tests/alerting/update.ts | 74 ++++++ 36 files changed, 847 insertions(+), 126 deletions(-) create mode 100644 x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts create mode 100644 x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts create mode 100644 x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts create mode 100644 x-pack/plugins/alerts/server/lib/get_cadence.ts diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index 14bddceb1c03d..e97b37f16faf0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -31,7 +31,6 @@ import { } from '../types'; import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib'; import { - InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../security/server'; @@ -48,6 +47,7 @@ import { IEvent } from '../../../event_log/server'; import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; import { partiallyUpdateAlert } from '../saved_objects'; +import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -72,7 +72,6 @@ export interface ConstructorOptions { namespace?: string; getUserName: () => Promise; createAPIKey: (name: string) => Promise; - invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; getActionsClient: () => Promise; getEventLogClient: () => Promise; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; @@ -172,9 +171,6 @@ export class AlertsClient { private readonly authorization: AlertsAuthorization; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: (name: string) => Promise; - private readonly invalidateAPIKey: ( - params: InvalidateAPIKeyParams - ) => Promise; private readonly getActionsClient: () => Promise; private readonly actionsAuthorization: ActionsAuthorization; private readonly getEventLogClient: () => Promise; @@ -191,7 +187,6 @@ export class AlertsClient { namespace, getUserName, createAPIKey, - invalidateAPIKey, encryptedSavedObjectsClient, getActionsClient, actionsAuthorization, @@ -207,7 +202,6 @@ export class AlertsClient { this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; this.authorization = authorization; this.createAPIKey = createAPIKey; - this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; this.getActionsClient = getActionsClient; this.actionsAuthorization = actionsAuthorization; @@ -263,7 +257,11 @@ export class AlertsClient { ); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: rawAlert.apiKey }); + markApiKeyForInvalidation( + { apiKey: rawAlert.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } if (data.enabled) { @@ -487,7 +485,13 @@ export class AlertsClient { await Promise.all([ taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null, - apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, + apiKeyToInvalidate + ? markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, ]); return removeResult; @@ -526,7 +530,11 @@ export class AlertsClient { await Promise.all([ alertSavedObject.attributes.apiKey - ? this.invalidateApiKey({ apiKey: alertSavedObject.attributes.apiKey }) + ? markApiKeyForInvalidation( + { apiKey: alertSavedObject.attributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ) : null, (async () => { if ( @@ -591,7 +599,11 @@ export class AlertsClient { ); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: createAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: createAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } @@ -671,28 +683,20 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: updateAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: updateAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } if (apiKeyToInvalidate) { - await this.invalidateApiKey({ apiKey: apiKeyToInvalidate }); - } - } - - private async invalidateApiKey({ apiKey }: { apiKey: string | null }): Promise { - if (!apiKey) { - return; - } - - try { - const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0]; - const response = await this.invalidateAPIKey({ id: apiKeyId }); - if (response.apiKeysEnabled === true && response.result.error_count > 0) { - this.logger.error(`Failed to invalidate API Key [id="${apiKeyId}"]`); - } - } catch (e) { - this.logger.error(`Failed to invalidate API Key: ${e.message}`); + await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ); } } @@ -752,7 +756,11 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: updateAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: updateAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } const scheduledTask = await this.scheduleAlert( @@ -764,7 +772,11 @@ export class AlertsClient { scheduledTaskId: scheduledTask.id, }); if (apiKeyToInvalidate) { - await this.invalidateApiKey({ apiKey: apiKeyToInvalidate }); + await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ); } } } @@ -825,7 +837,13 @@ export class AlertsClient { attributes.scheduledTaskId ? deleteTaskIfItExists(this.taskManager, attributes.scheduledTaskId) : null, - apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, + apiKeyToInvalidate + ? await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, ]); } } diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts index 0f89fc6c9c25c..cc5d10c3346e8 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index 965ea1949bf3a..ee407b1a6d50c 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -711,7 +710,7 @@ describe('create()', () => { expect(taskManager.schedule).not.toHaveBeenCalled(); }); - test('throws error and invalidates API key when create saved object fails', async () => { + test('throws error and add API key to invalidatePendingApiKey SO when create saved object fails', async () => { const data = getMockData(); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -731,11 +730,25 @@ describe('create()', () => { ], }); unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); + const createdAt = new Date().toISOString(); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test failure"` ); expect(taskManager.schedule).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); + expect(unsecuredSavedObjectsClient.create.mock.calls[1][1]).toStrictEqual({ + apiKeyId: '123', + createdAt, + }); }); test('attempts to remove saved object if scheduling failed', async () => { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts index d9b253c3a56e8..e7b975aec8eb0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts @@ -32,7 +32,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -94,11 +93,22 @@ describe('delete()', () => { }); test('successfully removes an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); @@ -107,12 +117,21 @@ describe('delete()', () => { test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'delete(): Failed to load API key to invalidate on alert 1: Fail' @@ -133,6 +152,15 @@ describe('delete()', () => { }); test(`doesn't invalidate API key when apiKey is null`, async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ ...existingAlert, attributes: { @@ -142,24 +170,34 @@ describe('delete()', () => { }); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); test('swallows error when getDecryptedAsInternalUser throws an error', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'delete(): Failed to load API key to invalidate on alert 1: Fail' ); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts index d0557df622028..11ce0027f82d8 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts @@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -33,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -108,6 +108,15 @@ describe('disable()', () => { }); test('disables an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -145,11 +154,22 @@ describe('disable()', () => { } ); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); }); test('falls back when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); @@ -188,7 +208,7 @@ describe('disable()', () => { } ); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test(`doesn't disable already disabled alerts`, async () => { @@ -201,26 +221,54 @@ describe('disable()', () => { }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.remove).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test(`doesn't invalidate when no API key is used`, async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert); await alertsClient.disable({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when failing to load decrypted saved object', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(taskManager.remove).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'disable(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -235,11 +283,10 @@ describe('disable()', () => { }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts index 215493c71aec7..16e83c42d8930 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts @@ -14,6 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -34,7 +35,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -147,6 +147,7 @@ describe('enable()', () => { }); test('enables an alert', async () => { + const createdAt = new Date().toISOString(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ ...existingAlert, attributes: { @@ -157,13 +158,22 @@ describe('enable()', () => { updatedBy: 'elastic', }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation'); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', @@ -217,6 +227,7 @@ describe('enable()', () => { }); test('invalidates API key if ever one existed prior to updating', async () => { + const createdAt = new Date().toISOString(); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ ...existingAlert, attributes: { @@ -224,13 +235,24 @@ describe('enable()', () => { apiKey: Buffer.from('123:abc').toString('base64'), }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); }); test(`doesn't enable already enabled alerts`, async () => { @@ -312,19 +334,31 @@ describe('enable()', () => { }); test('throws error when failing to update the first time', async () => { + const createdAt = new Date().toISOString(); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to update"` ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); expect(taskManager.schedule).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts index c1adaddc80d9e..1b3a776bd23e0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts @@ -35,7 +35,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts index 004230403de2e..5c0d80f159b31 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 9cb2a33222d23..269b2eb2ab7a7 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -39,7 +39,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts index 8b32f05f6d5a1..79a064beba166 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts index 5ebb4e90d4b50..028a7c6737474 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts @@ -46,14 +46,6 @@ export function getBeforeSetup( ) { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); - alertsClientParams.invalidateAPIKey.mockResolvedValue({ - apiKeysEnabled: true, - result: { - invalidated_api_keys: [], - previously_invalidated_api_keys: [], - error_count: 0, - }, - }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); const actionsClient = actionsClientMock.create(); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts index b2f5c5498f848..8cbe47655ef68 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts index 88199dfd1f7b9..868fa3d8c6aa2 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts @@ -32,7 +32,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts index cd7112b3551b3..05ca741f480ca 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts index 07666c1cc6261..5ef1af9b6f0ee 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts index 97711b8c14579..88692239ac2fe 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index 1dcde6addb9bf..ad58e36ade722 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -10,7 +10,7 @@ import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; -import { IntervalSchedule } from '../../types'; +import { IntervalSchedule, InvalidatePendingApiKey } from '../../types'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; @@ -38,7 +38,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -161,6 +160,15 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -241,7 +249,7 @@ describe('update()', () => { namespace: 'default', }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` @@ -376,6 +384,24 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -423,7 +449,7 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` @@ -530,6 +556,15 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -578,7 +613,7 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` @@ -732,7 +767,6 @@ describe('update()', () => { }); it('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { @@ -775,6 +809,7 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); // add ApiKey to invalidate await alertsClient.update({ id: '1', data: { @@ -797,7 +832,7 @@ describe('update()', () => { }, }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); @@ -965,8 +1000,9 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[1][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('234'); }); describe('updating an alert schedule', () => { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts index 1f3b567b2c031..af178a1fac5f5 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts @@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -32,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -80,6 +80,15 @@ describe('updateApiKey()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -121,11 +130,22 @@ describe('updateApiKey()', () => { }, { version: '123' } ); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); }); test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.updateApiKey({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); @@ -160,28 +180,37 @@ describe('updateApiKey()', () => { }, { version: '123' } ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.updateApiKey({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); }); test('swallows error when getting decrypted object throws', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.updateApiKey({ id: '1' }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' ); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { @@ -190,12 +219,22 @@ describe('updateApiKey()', () => { result: { id: '234', name: '234', api_key: 'abc' }, }); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail"` ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('234'); }); describe('authorization', () => { diff --git a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts index b1ac5ac4c6783..ca9389ece310c 100644 --- a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts @@ -45,7 +45,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger, encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -115,7 +114,7 @@ async function update(success: boolean) { ); return expectConflict(success, err, 'create'); } - expectSuccess(success, 2, 'create'); + expectSuccess(success, 3, 'create'); // only checking the debug messages in this test expect(logger.debug).nthCalledWith(1, `alertsClient.update('alert-id') conflict, retrying ...`); @@ -306,14 +305,6 @@ beforeEach(() => { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); - alertsClientParams.invalidateAPIKey.mockResolvedValue({ - apiKeysEnabled: true, - result: { - invalidated_api_keys: [], - previously_invalidated_api_keys: [], - error_count: 0, - }, - }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 3cf6666e90eb0..bdbfc726dab8f 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -92,7 +92,7 @@ test('creates an alerts client with proper constructor arguments when security i expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); @@ -125,7 +125,6 @@ test('creates an alerts client with proper constructor arguments when security i getActionsClient: expect.any(Function), getEventLogClient: expect.any(Function), createAPIKey: expect.any(Function), - invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, kibanaVersion: '7.10.0', }); @@ -142,7 +141,7 @@ test('creates an alerts client with proper constructor arguments', async () => { expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); @@ -167,7 +166,6 @@ test('creates an alerts client with proper constructor arguments', async () => { namespace: 'default', getUserName: expect.any(Function), createAPIKey: expect.any(Function), - invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, getActionsClient: expect.any(Function), getEventLogClient: expect.any(Function), diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index eccd810391307..069703be72f8a 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -14,7 +14,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions import { AlertsClient } from './alerts_client'; import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; @@ -94,7 +94,7 @@ export class AlertsClientFactory { alertTypeRegistry: this.alertTypeRegistry, unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), authorization, actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), @@ -129,22 +129,6 @@ export class AlertsClientFactory { result: createAPIKeyResult, }; }, - async invalidateAPIKey(params: InvalidateAPIKeyParams) { - if (!securityPluginSetup) { - return { apiKeysEnabled: false }; - } - const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser( - params - ); - // Null when Elasticsearch security is disabled - if (!invalidateAPIKeyResult) { - return { apiKeysEnabled: false }; - } - return { - apiKeysEnabled: true, - result: invalidateAPIKeyResult, - }; - }, async getActionsClient() { return actions.getActionsClientWithRequest(request); }, diff --git a/x-pack/plugins/alerts/server/config.test.ts b/x-pack/plugins/alerts/server/config.test.ts index 93aa3c38a0460..bf3b30b5d2378 100644 --- a/x-pack/plugins/alerts/server/config.test.ts +++ b/x-pack/plugins/alerts/server/config.test.ts @@ -13,6 +13,10 @@ describe('config validation', () => { "healthCheck": Object { "interval": "60m", }, + "invalidateApiKeysTask": Object { + "interval": "5m", + "removalDelay": "5m", + }, } `); }); diff --git a/x-pack/plugins/alerts/server/config.ts b/x-pack/plugins/alerts/server/config.ts index a6d2196a407b5..41340c7dfe5fc 100644 --- a/x-pack/plugins/alerts/server/config.ts +++ b/x-pack/plugins/alerts/server/config.ts @@ -11,6 +11,10 @@ export const configSchema = schema.object({ healthCheck: schema.object({ interval: schema.string({ validate: validateDurationSchema, defaultValue: '60m' }), }), + invalidateApiKeysTask: schema.object({ + interval: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), + removalDelay: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), + }), }); export type AlertsConfig = TypeOf; diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts new file mode 100644 index 0000000000000..7b30c22c47f8a --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loggingSystemMock, savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { markApiKeyForInvalidation } from './mark_api_key_for_invalidation'; + +describe('markApiKeyForInvalidation', () => { + test('should call savedObjectsClient create with the proper params', async () => { + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + await markApiKeyForInvalidation( + { apiKey: Buffer.from('123:abc').toString('base64') }, + loggingSystemMock.create().get(), + unsecuredSavedObjectsClient + ); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(2); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual( + 'api_key_pending_invalidation' + ); + }); + + test('should log the proper error when savedObjectsClient create failed', async () => { + const logger = loggingSystemMock.create().get(); + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); + await markApiKeyForInvalidation( + { apiKey: Buffer.from('123').toString('base64') }, + logger, + unsecuredSavedObjectsClient + ); + expect(logger.error).toHaveBeenCalledWith( + 'Failed to mark for API key [id="MTIz"] for invalidation: Fail' + ); + }); +}); diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts new file mode 100644 index 0000000000000..db25f5b3e19eb --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger, SavedObjectsClientContract } from 'src/core/server'; + +export const markApiKeyForInvalidation = async ( + { apiKey }: { apiKey: string | null }, + logger: Logger, + savedObjectsClient: SavedObjectsClientContract +): Promise => { + if (!apiKey) { + return; + } + try { + const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0]; + await savedObjectsClient.create('api_key_pending_invalidation', { + apiKeyId, + createdAt: new Date().toISOString(), + }); + } catch (e) { + logger.error(`Failed to mark for API key [id="${apiKey}"] for invalidation: ${e.message}`); + } +}; diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts new file mode 100644 index 0000000000000..77cbb9f4a4a85 --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Logger, + CoreStart, + SavedObjectsFindResponse, + KibanaRequest, + SavedObjectsClientContract, +} from 'kibana/server'; +import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; +import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../security/server'; +import { + RunContext, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../task_manager/server'; +import { InvalidateAPIKeyResult } from '../alerts_client'; +import { AlertsConfig } from '../config'; +import { timePeriodBeforeDate } from '../lib/get_cadence'; +import { AlertingPluginsStart } from '../plugin'; +import { InvalidatePendingApiKey } from '../types'; + +const TASK_TYPE = 'alerts_invalidate_api_keys'; +export const TASK_ID = `Alerts-${TASK_TYPE}`; + +const invalidateAPIKey = async ( + params: InvalidateAPIKeyParams, + securityPluginSetup?: SecurityPluginSetup +): Promise => { + if (!securityPluginSetup) { + return { apiKeysEnabled: false }; + } + const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser( + params + ); + // Null when Elasticsearch security is disabled + if (!invalidateAPIKeyResult) { + return { apiKeysEnabled: false }; + } + return { + apiKeysEnabled: true, + result: invalidateAPIKeyResult, + }; +}; + +export function initializeApiKeyInvalidator( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + taskManager: TaskManagerSetupContract, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + registerApiKeyInvalitorTaskDefinition( + logger, + coreStartServices, + taskManager, + config, + securityPluginSetup + ); +} + +export async function scheduleApiKeyInvalidatorTask( + logger: Logger, + config: Promise, + taskManager: TaskManagerStartContract +) { + const interval = (await config).invalidateApiKeysTask.interval; + try { + await taskManager.ensureScheduled({ + id: TASK_ID, + taskType: TASK_TYPE, + schedule: { + interval, + }, + state: {}, + params: {}, + }); + } catch (e) { + logger.debug(`Error scheduling task, received ${e.message}`); + } +} + +function registerApiKeyInvalitorTaskDefinition( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + taskManager: TaskManagerSetupContract, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + taskManager.registerTaskDefinitions({ + [TASK_TYPE]: { + title: 'Invalidate alert API Keys', + createTaskRunner: taskRunner(logger, coreStartServices, config, securityPluginSetup), + }, + }); +} + +function getFakeKibanaRequest(basePath: string) { + const requestHeaders: Record = {}; + return ({ + headers: requestHeaders, + getBasePath: () => basePath, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as unknown) as KibanaRequest; +} + +function taskRunner( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + return ({ taskInstance }: RunContext) => { + const { state } = taskInstance; + return { + async run() { + let totalInvalidated = 0; + const configResult = await config; + try { + const [{ savedObjects, http }, { encryptedSavedObjects }] = await coreStartServices; + const savedObjectsClient = savedObjects.getScopedClient( + getFakeKibanaRequest(http.basePath.serverBasePath), + { + includedHiddenTypes: ['api_key_pending_invalidation'], + excludedWrappers: ['security'], + } + ); + const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({ + includedHiddenTypes: ['api_key_pending_invalidation'], + }); + const configuredDelay = configResult.invalidateApiKeysTask.removalDelay; + const delay = timePeriodBeforeDate(new Date(), configuredDelay).toISOString(); + + let hasApiKeysPendingInvalidation = true; + const PAGE_SIZE = 100; + do { + const apiKeysToInvalidate = await savedObjectsClient.find({ + type: 'api_key_pending_invalidation', + filter: `api_key_pending_invalidation.attributes.createdAt <= "${delay}"`, + page: 1, + sortField: 'createdAt', + sortOrder: 'asc', + perPage: PAGE_SIZE, + }); + totalInvalidated += await invalidateApiKeys( + logger, + savedObjectsClient, + apiKeysToInvalidate, + encryptedSavedObjectsClient, + securityPluginSetup + ); + + hasApiKeysPendingInvalidation = apiKeysToInvalidate.total > PAGE_SIZE; + } while (hasApiKeysPendingInvalidation); + + return { + state: { + runs: (state.runs || 0) + 1, + total_invalidated: totalInvalidated, + }, + schedule: { + interval: configResult.invalidateApiKeysTask.interval, + }, + }; + } catch (e) { + logger.warn(`Error executing alerting apiKey invalidation task: ${e.message}`); + return { + state: { + runs: (state.runs || 0) + 1, + total_invalidated: totalInvalidated, + }, + schedule: { + interval: configResult.invalidateApiKeysTask.interval, + }, + }; + } + }, + }; + }; +} + +async function invalidateApiKeys( + logger: Logger, + savedObjectsClient: SavedObjectsClientContract, + apiKeysToInvalidate: SavedObjectsFindResponse, + encryptedSavedObjectsClient: EncryptedSavedObjectsClient, + securityPluginSetup?: SecurityPluginSetup +) { + let totalInvalidated = 0; + await Promise.all( + apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => { + const decryptedApiKey = await encryptedSavedObjectsClient.getDecryptedAsInternalUser< + InvalidatePendingApiKey + >('api_key_pending_invalidation', apiKeyObj.id); + const apiKeyId = decryptedApiKey.attributes.apiKeyId; + const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginSetup); + if (response.apiKeysEnabled === true && response.result.error_count > 0) { + logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`); + } else { + try { + await savedObjectsClient.delete('api_key_pending_invalidation', apiKeyObj.id); + totalInvalidated++; + } catch (err) { + logger.error( + `Failed to cleanup api key "${apiKeyObj.attributes.apiKeyId}". Error: ${err.message}` + ); + } + } + }) + ); + logger.debug(`Total invalidated api keys "${totalInvalidated}"`); + return totalInvalidated; +} diff --git a/x-pack/plugins/alerts/server/lib/get_cadence.ts b/x-pack/plugins/alerts/server/lib/get_cadence.ts new file mode 100644 index 0000000000000..d09ed0c2122cd --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/get_cadence.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { memoize } from 'lodash'; + +export enum TimeUnit { + Minute = 'm', + Second = 's', + Hour = 'h', + Day = 'd', +} +const VALID_CADENCE = new Set(Object.values(TimeUnit)); +const CADENCE_IN_MS: Record = { + [TimeUnit.Second]: 1000, + [TimeUnit.Minute]: 60 * 1000, + [TimeUnit.Hour]: 60 * 60 * 1000, + [TimeUnit.Day]: 24 * 60 * 60 * 1000, +}; + +const isNumeric = (numAsStr: string) => /^\d+$/.test(numAsStr); + +export const parseIntervalAsMillisecond = memoize((value: string): number => { + const numericAsStr: string = value.slice(0, -1); + const numeric: number = parseInt(numericAsStr, 10); + const cadence: TimeUnit | string = value.slice(-1); + if ( + !VALID_CADENCE.has(cadence as TimeUnit) || + isNaN(numeric) || + numeric <= 0 || + !isNumeric(numericAsStr) + ) { + throw new Error( + `Invalid time value "${value}". Time must be of the form {number}m. Example: 5m.` + ); + } + return numeric * CADENCE_IN_MS[cadence as TimeUnit]; +}); + +/** + * Returns a date that is the specified interval from given date. + * + * @param {Date} date - The date to add interval to + * @param {string} interval - THe time of the form `Nm` such as `5m` + */ +export function timePeriodBeforeDate(date: Date, timePeriod: string): Date { + const result = new Date(date.valueOf()); + const milisecFromTime = parseIntervalAsMillisecond(timePeriod); + result.setMilliseconds(result.getMilliseconds() - milisecFromTime); + return result; +} diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 715fbc6aeed45..62f4b7d5a3fc4 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -22,6 +22,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); @@ -67,6 +71,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); @@ -114,6 +122,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 1fa89606a76fc..0c91e93938346 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -65,6 +65,10 @@ import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/ import { IEventLogger, IEventLogService, IEventLogClientService } from '../../event_log/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; import { setupSavedObjects } from './saved_objects'; +import { + initializeApiKeyInvalidator, + scheduleApiKeyInvalidatorTask, +} from './invalidate_pending_api_keys/task'; import { getHealthStatusStream, scheduleAlertingHealthCheck, @@ -200,6 +204,14 @@ export class AlertingPlugin { }); } + initializeApiKeyInvalidator( + this.logger, + core.getStartServices(), + plugins.taskManager, + this.config, + this.security + ); + core.getStartServices().then(async ([, startPlugins]) => { core.status.set( combineLatest([ @@ -308,7 +320,9 @@ export class AlertingPlugin { }); scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager); + scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager); + scheduleApiKeyInvalidatorTask(this.telemetryLogger, this.config, plugins.taskManager); return { listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!), diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index 9aa1f86676eaa..da30273e93c6b 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -42,10 +42,32 @@ export function setupSavedObjects( mappings: mappings.alert, }); + savedObjects.registerType({ + name: 'api_key_pending_invalidation', + hidden: true, + namespaceType: 'agnostic', + mappings: { + properties: { + apiKeyId: { + type: 'keyword', + }, + createdAt: { + type: 'date', + }, + }, + }, + }); + // Encrypted attributes encryptedSavedObjects.registerType({ type: 'alert', attributesToEncrypt: new Set(['apiKey']), attributesToExcludeFromAAD: new Set(AlertAttributesExcludedFromAAD), }); + + // Encrypted attributes + encryptedSavedObjects.registerType({ + type: 'api_key_pending_invalidation', + attributesToEncrypt: new Set(['apiKeyId']), + }); } diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index 9226461f6e30a..dde1628156658 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -180,4 +180,16 @@ export interface AlertsConfigType { }; } +export interface AlertsConfigType { + invalidateApiKeysTask: { + interval: string; + removalDelay: string; + }; +} + +export interface InvalidatePendingApiKey { + apiKeyId: string; + createdAt: string; +} + export type AlertTypeRegistry = PublicMethodsOf; diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index f9fdfaed1c79b..cb78e76bdd697 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -92,6 +92,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + '--xpack.alerts.invalidateApiKeysTask.interval="15s"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, ...actionsProxyUrl, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 7ed864afac4cc..998ec6ab2ed0e 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -437,6 +437,21 @@ export function defineAlertTypes( throw new Error('this alert is intended to fail'); }, }; + const longRunningAlertType: AlertType = { + id: 'test.longRunning', + name: 'Test: Long Running', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + producer: 'alertsFixture', + defaultActionGroupId: 'default', + async executor() { + await new Promise((resolve) => setTimeout(resolve, 5000)); + }, + }; alerts.registerType(getAlwaysFiringAlertType()); alerts.registerType(getCumulativeFiringAlertType()); @@ -449,4 +464,5 @@ export function defineAlertTypes( alerts.registerType(onlyStateVariablesAlertType); alerts.registerType(getPatternFiringAlertType()); alerts.registerType(throwAlertType); + alerts.registerType(longRunningAlertType); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index fbf3b798500d3..d832902fe066d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -50,6 +50,7 @@ export class FixturePlugin implements Plugin, + res: KibanaResponseFactory + ): Promise> => { + try { + const [{ savedObjects }] = await core.getStartServices(); + const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, { + includedHiddenTypes: ['api_key_pending_invalidation'], + }); + const findResult = await savedObjectsWithTasksAndAlerts.find({ + type: 'api_key_pending_invalidation', + }); + return res.ok({ + body: { apiKeysToInvalidate: findResult.saved_objects }, + }); + } catch (err) { + return res.badRequest({ body: err }); + } + } + ); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 8836bc2e4db2f..9c3d2801c0886 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -836,6 +836,80 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); + it('should handle updates for a long running alert type without failing the underlying tasks due to invalidated ApiKey', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send({ + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.longRunning', + consumer: 'alertsFixture', + schedule: { interval: '1s' }, + throttle: '1m', + actions: [], + params: {}, + }) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + const updatedData = { + name: 'bcd', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '1m' }, + actions: [], + throttle: '1m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + const statusUpdates: string[] = []; + await retry.try(async () => { + const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; + statusUpdates.push(alertTask.status); + expect(alertTask.status).to.eql('idle'); + }); + + expect(statusUpdates.find((status) => status === 'failed')).to.be(undefined); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.longRunning', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + await retry.try(async () => { + const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; + expect(alertTask.status).to.eql('idle'); + // ensure the alert is rescheduled to a minute from now + ensureDatetimeIsWithinRange(Date.parse(alertTask.runAt), 60 * 1000); + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should handle updates to an alert schedule by setting the new schedule for the underlying task', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) From d0441e1fee646f8dbb1fcddb07105023402a36dd Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 17 Nov 2020 09:19:46 -0600 Subject: [PATCH 34/99] Move src/legacy/server/keystore to src/cli (#83483) Currently keystore parsing is in the legacy http server folder. Keystore references end in src/cli so instead of migrating to core/server the I opted for the closest fit in the CLI folder. Closes #52107 --- .github/CODEOWNERS | 2 +- src/{legacy/server => cli}/keystore/errors.js | 0 src/{legacy/server => cli}/keystore/index.js | 0 src/{legacy/server => cli}/keystore/keystore.js | 0 src/{legacy/server => cli}/keystore/keystore.test.js | 0 src/cli/{serve => keystore}/read_keystore.js | 2 +- src/cli/{serve => keystore}/read_keystore.test.js | 4 ++-- src/cli/serve/serve.js | 2 +- src/cli_keystore/add.test.js | 2 +- src/cli_keystore/cli_keystore.js | 2 +- src/cli_keystore/create.test.js | 2 +- src/cli_keystore/list.test.js | 2 +- src/cli_keystore/remove.test.js | 2 +- 13 files changed, 10 insertions(+), 10 deletions(-) rename src/{legacy/server => cli}/keystore/errors.js (100%) rename src/{legacy/server => cli}/keystore/index.js (100%) rename src/{legacy/server => cli}/keystore/keystore.js (100%) rename src/{legacy/server => cli}/keystore/keystore.test.js (100%) rename src/cli/{serve => keystore}/read_keystore.js (95%) rename src/cli/{serve => keystore}/read_keystore.test.js (94%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b7fb3ff04db71..af010089e4892 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -158,7 +158,7 @@ /packages/kbn-ui-shared-deps/ @elastic/kibana-operations /packages/kbn-es-archiver/ @elastic/kibana-operations /packages/kbn-utils/ @elastic/kibana-operations -/src/legacy/server/keystore/ @elastic/kibana-operations +/src/cli/keystore/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations /vars/ @elastic/kibana-operations diff --git a/src/legacy/server/keystore/errors.js b/src/cli/keystore/errors.js similarity index 100% rename from src/legacy/server/keystore/errors.js rename to src/cli/keystore/errors.js diff --git a/src/legacy/server/keystore/index.js b/src/cli/keystore/index.js similarity index 100% rename from src/legacy/server/keystore/index.js rename to src/cli/keystore/index.js diff --git a/src/legacy/server/keystore/keystore.js b/src/cli/keystore/keystore.js similarity index 100% rename from src/legacy/server/keystore/keystore.js rename to src/cli/keystore/keystore.js diff --git a/src/legacy/server/keystore/keystore.test.js b/src/cli/keystore/keystore.test.js similarity index 100% rename from src/legacy/server/keystore/keystore.test.js rename to src/cli/keystore/keystore.test.js diff --git a/src/cli/serve/read_keystore.js b/src/cli/keystore/read_keystore.js similarity index 95% rename from src/cli/serve/read_keystore.js rename to src/cli/keystore/read_keystore.js index 38d0e68bd5c4e..b3bca4cf11c39 100644 --- a/src/cli/serve/read_keystore.js +++ b/src/cli/keystore/read_keystore.js @@ -19,7 +19,7 @@ import { set } from '@elastic/safer-lodash-set'; -import { Keystore } from '../../legacy/server/keystore'; +import { Keystore } from '../keystore'; import { getKeystore } from '../../cli_keystore/get_keystore'; export function readKeystore(keystorePath = getKeystore()) { diff --git a/src/cli/serve/read_keystore.test.js b/src/cli/keystore/read_keystore.test.js similarity index 94% rename from src/cli/serve/read_keystore.test.js rename to src/cli/keystore/read_keystore.test.js index e5407b257a909..a35258febfb8e 100644 --- a/src/cli/serve/read_keystore.test.js +++ b/src/cli/keystore/read_keystore.test.js @@ -20,8 +20,8 @@ import path from 'path'; import { readKeystore } from './read_keystore'; -jest.mock('../../legacy/server/keystore'); -import { Keystore } from '../../legacy/server/keystore'; +jest.mock('../keystore'); +import { Keystore } from '../keystore'; describe('cli/serve/read_keystore', () => { beforeEach(() => { diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index a1715cf3dba2c..f344d3b70ed9d 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -27,7 +27,7 @@ import { getConfigPath } from '@kbn/utils'; import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; import { fromRoot } from '../../core/server/utils'; import { bootstrap } from '../../core/server'; -import { readKeystore } from './read_keystore'; +import { readKeystore } from '../keystore/read_keystore'; function canRequire(path) { try { diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js index ba381ca2f3e14..74a72fe44d398 100644 --- a/src/cli_keystore/add.test.js +++ b/src/cli_keystore/add.test.js @@ -39,7 +39,7 @@ jest.mock('fs', () => ({ import sinon from 'sinon'; import { PassThrough } from 'stream'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli/keystore'; import { add } from './add'; import { Logger } from '../cli_plugin/lib/logger'; import * as prompt from './utils/prompt'; diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js index d12c80b361c92..9fbea8f195122 100644 --- a/src/cli_keystore/cli_keystore.js +++ b/src/cli_keystore/cli_keystore.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import { pkg } from '../core/server/utils'; import Command from '../cli/command'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli/keystore'; import { createCli } from './create'; import { listCli } from './list'; diff --git a/src/cli_keystore/create.test.js b/src/cli_keystore/create.test.js index cb85475eab1cb..346fa9e055129 100644 --- a/src/cli_keystore/create.test.js +++ b/src/cli_keystore/create.test.js @@ -38,7 +38,7 @@ jest.mock('fs', () => ({ import sinon from 'sinon'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli/keystore'; import { create } from './create'; import { Logger } from '../cli_plugin/lib/logger'; import * as prompt from './utils/prompt'; diff --git a/src/cli_keystore/list.test.js b/src/cli_keystore/list.test.js index 11c474f908216..8da235a1932e6 100644 --- a/src/cli_keystore/list.test.js +++ b/src/cli_keystore/list.test.js @@ -36,7 +36,7 @@ jest.mock('fs', () => ({ })); import sinon from 'sinon'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli//keystore'; import { list } from './list'; import { Logger } from '../cli_plugin/lib/logger'; diff --git a/src/cli_keystore/remove.test.js b/src/cli_keystore/remove.test.js index fae8924c67287..fb700e6a8b9e2 100644 --- a/src/cli_keystore/remove.test.js +++ b/src/cli_keystore/remove.test.js @@ -30,7 +30,7 @@ jest.mock('fs', () => ({ import sinon from 'sinon'; -import { Keystore } from '../legacy/server/keystore'; +import { Keystore } from '../cli/keystore'; import { remove } from './remove'; describe('Kibana keystore', () => { From cf7aacc1c02fbdde93d5a40c2765dd9b5eb55311 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 17 Nov 2020 10:42:00 -0500 Subject: [PATCH 35/99] [Fleet][EPM] Unified install and archive (#83384) ## Summary * Further reduce differences between installing uploaded vs registry package * Improve cache/store names, TS types, etc. Including key by name + version + source * Add a cache/store for PackageInfo (e.g. results metadata from registry's /package/version/ response) * Remove ensureCachedArchiveInfo --- .../server/services/epm/archive/cache.ts | 64 +++++++++++++---- .../server/services/epm/archive/index.ts | 69 ++++++++----------- .../server/services/epm/archive/validation.ts | 14 ++-- .../epm/kibana/index_pattern/install.ts | 8 --- .../server/services/epm/packages/assets.ts | 10 ++- .../fleet/server/services/epm/packages/get.ts | 6 +- .../server/services/epm/packages/install.ts | 42 +++++++---- .../server/services/epm/packages/remove.ts | 6 +- .../server/services/epm/registry/index.ts | 56 ++++++++++----- x-pack/plugins/fleet/server/types/index.tsx | 1 + 10 files changed, 165 insertions(+), 111 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts index 102324c18bd43..280c34744289e 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/cache.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/cache.ts @@ -3,21 +3,57 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { pkgToPkgKey } from '../registry/index'; +import { ArchiveEntry } from './index'; +import { InstallSource, ArchivePackage, RegistryPackage } from '../../../../common'; -const cache: Map = new Map(); -export const cacheGet = (key: string) => cache.get(key); -export const cacheSet = (key: string, value: Buffer) => cache.set(key, value); -export const cacheHas = (key: string) => cache.has(key); -export const cacheClear = () => cache.clear(); -export const cacheDelete = (key: string) => cache.delete(key); +const archiveEntryCache: Map = new Map(); +export const getArchiveEntry = (key: string) => archiveEntryCache.get(key); +export const setArchiveEntry = (key: string, value: Buffer) => archiveEntryCache.set(key, value); +export const hasArchiveEntry = (key: string) => archiveEntryCache.has(key); +export const clearArchiveEntries = () => archiveEntryCache.clear(); +export const deleteArchiveEntry = (key: string) => archiveEntryCache.delete(key); -const archiveFilelistCache: Map = new Map(); -export const getArchiveFilelist = (name: string, version: string) => - archiveFilelistCache.get(pkgToPkgKey({ name, version })); +export interface SharedKey { + name: string; + version: string; + installSource: InstallSource; +} +type SharedKeyString = string; -export const setArchiveFilelist = (name: string, version: string, paths: string[]) => - archiveFilelistCache.set(pkgToPkgKey({ name, version }), paths); +type ArchiveFilelist = string[]; +const archiveFilelistCache: Map = new Map(); +export const getArchiveFilelist = (keyArgs: SharedKey) => + archiveFilelistCache.get(sharedKey(keyArgs)); -export const deleteArchiveFilelist = (name: string, version: string) => - archiveFilelistCache.delete(pkgToPkgKey({ name, version })); +export const setArchiveFilelist = (keyArgs: SharedKey, paths: string[]) => + archiveFilelistCache.set(sharedKey(keyArgs), paths); + +export const deleteArchiveFilelist = (keyArgs: SharedKey) => + archiveFilelistCache.delete(sharedKey(keyArgs)); + +const packageInfoCache: Map = new Map(); +const sharedKey = ({ name, version, installSource }: SharedKey) => + `${name}-${version}-${installSource}`; + +export const getPackageInfo = (args: SharedKey) => { + const packageInfo = packageInfoCache.get(sharedKey(args)); + if (args.installSource === 'registry') { + return packageInfo as RegistryPackage; + } else if (args.installSource === 'upload') { + return packageInfo as ArchivePackage; + } else { + throw new Error(`Unknown installSource: ${args.installSource}`); + } +}; + +export const setPackageInfo = ({ + name, + version, + installSource, + packageInfo, +}: SharedKey & { packageInfo: ArchivePackage | RegistryPackage }) => { + const key = sharedKey({ name, version, installSource }); + return packageInfoCache.set(key, packageInfo); +}; + +export const deletePackageInfo = (args: SharedKey) => packageInfoCache.delete(sharedKey(args)); diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts index 6d1150b3ac8bd..ddaf9b640c86a 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts @@ -4,68 +4,57 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ArchivePackage, AssetParts } from '../../../../common/types'; +import { AssetParts, InstallSource } from '../../../../common/types'; import { PackageInvalidArchiveError, PackageUnsupportedMediaTypeError } from '../../../errors'; import { - cacheGet, - cacheSet, - cacheDelete, + SharedKey, + getArchiveEntry, + setArchiveEntry, + deleteArchiveEntry, getArchiveFilelist, setArchiveFilelist, deleteArchiveFilelist, + deletePackageInfo, } from './cache'; import { getBufferExtractor } from './extract'; -import { parseAndVerifyArchiveEntries } from './validation'; export * from './cache'; -export { untarBuffer, unzipBuffer, getBufferExtractor } from './extract'; +export { getBufferExtractor, untarBuffer, unzipBuffer } from './extract'; +export { parseAndVerifyArchiveBuffer as parseAndVerifyArchiveEntries } from './validation'; export interface ArchiveEntry { path: string; buffer?: Buffer; } -export async function getArchivePackage({ - archiveBuffer, +export async function unpackBufferToCache({ + name, + version, contentType, + archiveBuffer, + installSource, }: { - archiveBuffer: Buffer; + name: string; + version: string; contentType: string; -}): Promise<{ paths: string[]; archivePackageInfo: ArchivePackage }> { - const entries = await unpackArchiveEntries(archiveBuffer, contentType); - const { archivePackageInfo } = await parseAndVerifyArchiveEntries(entries); - const paths = addEntriesToMemoryStore(entries); - - setArchiveFilelist(archivePackageInfo.name, archivePackageInfo.version, paths); - - return { - paths, - archivePackageInfo, - }; -} - -export async function unpackArchiveToCache( - archiveBuffer: Buffer, - contentType: string -): Promise { - const entries = await unpackArchiveEntries(archiveBuffer, contentType); - return addEntriesToMemoryStore(entries); -} - -function addEntriesToMemoryStore(entries: ArchiveEntry[]) { + archiveBuffer: Buffer; + installSource: InstallSource; +}): Promise { + const entries = await unpackBufferEntries(archiveBuffer, contentType); const paths: string[] = []; entries.forEach((entry) => { const { path, buffer } = entry; if (buffer) { - cacheSet(path, buffer); + setArchiveEntry(path, buffer); paths.push(path); } }); + setArchiveFilelist({ name, version, installSource }, paths); return paths; } -export async function unpackArchiveEntries( +export async function unpackBufferEntries( archiveBuffer: Buffer, contentType: string ): Promise { @@ -96,16 +85,18 @@ export async function unpackArchiveEntries( return entries; } -export const deletePackageCache = (name: string, version: string) => { +export const deletePackageCache = ({ name, version, installSource }: SharedKey) => { // get cached archive filelist - const paths = getArchiveFilelist(name, version); + const paths = getArchiveFilelist({ name, version, installSource }); // delete cached archive filelist - deleteArchiveFilelist(name, version); + deleteArchiveFilelist({ name, version, installSource }); // delete cached archive files - // this has been populated in unpackArchiveToCache() - paths?.forEach((path) => cacheDelete(path)); + // this has been populated in unpackBufferToCache() + paths?.forEach(deleteArchiveEntry); + + deletePackageInfo({ name, version, installSource }); }; export function getPathParts(path: string): AssetParts { @@ -139,7 +130,7 @@ export function getPathParts(path: string): AssetParts { } export function getAsset(key: string) { - const buffer = cacheGet(key); + const buffer = getArchiveEntry(key); if (buffer === undefined) throw new Error(`Cannot find asset ${key}`); return buffer; diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index 992020cb073ad..dc7a91e08799c 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -15,7 +15,7 @@ import { RegistryVarsEntry, } from '../../../../common/types'; import { PackageInvalidArchiveError } from '../../../errors'; -import { ArchiveEntry } from './index'; +import { unpackBufferEntries } from './index'; import { pkgToPkgKey } from '../registry'; const MANIFESTS: Record = {}; @@ -24,9 +24,11 @@ const MANIFEST_NAME = 'manifest.yml'; // TODO: everything below performs verification of manifest.yml files, and hence duplicates functionality already implemented in the // package registry. At some point this should probably be replaced (or enhanced) with verification based on // https://github.com/elastic/package-spec/ -export async function parseAndVerifyArchiveEntries( - entries: ArchiveEntry[] -): Promise<{ paths: string[]; archivePackageInfo: ArchivePackage }> { +export async function parseAndVerifyArchiveBuffer( + archiveBuffer: Buffer, + contentType: string +): Promise<{ paths: string[]; packageInfo: ArchivePackage }> { + const entries = await unpackBufferEntries(archiveBuffer, contentType); const paths: string[] = []; entries.forEach(({ path, buffer }) => { paths.push(path); @@ -34,12 +36,12 @@ export async function parseAndVerifyArchiveEntries( }); return { - archivePackageInfo: parseAndVerifyArchive(paths), + packageInfo: parseAndVerifyArchive(paths), paths, }; } -export function parseAndVerifyArchive(paths: string[]): ArchivePackage { +function parseAndVerifyArchive(paths: string[]): ArchivePackage { // The top-level directory must match pkgName-pkgVersion, and no other top-level files or directories may be present const toplevelDir = paths[0].split('/')[0]; paths.forEach((path) => { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts index d18f43d62436a..ee5257b8a3ef6 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts @@ -85,14 +85,6 @@ export async function installIndexPatterns( savedObjectsClient, installationStatuses.Installed ); - // TODO: move to install package - // cache all installed packages if they don't exist - const packagePromises = installedPackages.map((pkg) => - // TODO: this hard-codes 'registry' as installSource, so uploaded packages are ignored - // and their fields will be removed from the generated index patterns after this runs. - Registry.ensureCachedArchiveInfo(pkg.pkgName, pkg.pkgVersion, 'registry') - ); - await Promise.all(packagePromises); const packageVersionsToFetch = [...installedPackages]; if (pkgName && pkgVersion) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 80e1cbba6484b..770f342c0a6e7 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -5,7 +5,6 @@ */ import { InstallablePackage } from '../../../types'; -import * as Registry from '../registry'; import { ArchiveEntry, getArchiveFilelist, getAsset } from '../archive'; // paths from RegistryPackage are routes to the assets on EPR @@ -21,7 +20,8 @@ export function getAssets( datasetName?: string ): string[] { const assets: string[] = []; - const paths = getArchiveFilelist(packageInfo.name, packageInfo.version); + const { name, version } = packageInfo; + const paths = getArchiveFilelist({ name, version, installSource: 'registry' }); // TODO: might be better to throw a PackageCacheError here if (!paths || paths.length === 0) return assets; @@ -47,15 +47,13 @@ export function getAssets( return assets; } +// ASK: Does getAssetsData need an installSource now? +// if so, should it be an Installation vs InstallablePackage or add another argument? export async function getAssetsData( packageInfo: InstallablePackage, filter = (path: string): boolean => true, datasetName?: string ): Promise { - // TODO: Needs to be called to fill the cache but should not be required - - await Registry.ensureCachedArchiveInfo(packageInfo.name, packageInfo.version, 'registry'); - // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); const entries: ArchiveEntry[] = assets.map((path) => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 893df1733c58b..2d4a94a2332d6 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -109,11 +109,7 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise { const { savedObjectsClient, pkgName, pkgVersion } = options; - const [ - savedObject, - latestPackage, - { paths: assets, registryPackageInfo: item }, - ] = await Promise.all([ + const [savedObject, latestPackage, { paths: assets, packageInfo: item }] = await Promise.all([ getInstallationObject({ savedObjectsClient, pkgName }), Registry.fetchFindLatestPackage(pkgName), Registry.getRegistryPackage(pkgName, pkgVersion), diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index c471ea732b9dc..e73a5d3533828 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -28,6 +28,7 @@ import { KibanaAssetType, } from '../../../types'; import * as Registry from '../registry'; +import { setPackageInfo, parseAndVerifyArchiveEntries, unpackBufferToCache } from '../archive'; import { getInstallation, getInstallationObject, @@ -43,7 +44,6 @@ import { } from '../../../errors'; import { getPackageSavedObjects } from './get'; import { appContextService } from '../../app_context'; -import { getArchivePackage } from '../archive'; import { _installPackage } from './_install_package'; export async function installLatestPackage(options: { @@ -245,29 +245,26 @@ async function installPackageFromRegistry({ }: InstallRegistryPackageParams): Promise { // TODO: change epm API to /packageName/version so we don't need to do this const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); - // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge - // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets - const latestPackage = await Registry.fetchFindLatestPackage(pkgName); // get the currently installed package const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installType = getInstallType({ pkgVersion, installedPkg }); - // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update const installOutOfDateVersionOk = installType === 'reinstall' || installType === 'reupdate' || installType === 'rollback'; + + const latestPackage = await Registry.fetchFindLatestPackage(pkgName); if (semverLt(pkgVersion, latestPackage.version) && !force && !installOutOfDateVersionOk) { throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); } - const { paths, registryPackageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); + const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); return _installPackage({ savedObjectsClient, callCluster, installedPkg, paths, - packageInfo: registryPackageInfo, + packageInfo, installType, installSource: 'registry', }); @@ -290,27 +287,44 @@ async function installPackageByUpload({ archiveBuffer, contentType, }: InstallUploadedArchiveParams): Promise { - const { paths, archivePackageInfo } = await getArchivePackage({ archiveBuffer, contentType }); + const { packageInfo } = await parseAndVerifyArchiveEntries(archiveBuffer, contentType); const installedPkg = await getInstallationObject({ savedObjectsClient, - pkgName: archivePackageInfo.name, + pkgName: packageInfo.name, }); - const installType = getInstallType({ pkgVersion: archivePackageInfo.version, installedPkg }); + + const installType = getInstallType({ pkgVersion: packageInfo.version, installedPkg }); if (installType !== 'install') { throw new PackageOperationNotSupportedError( - `Package upload only supports fresh installations. Package ${archivePackageInfo.name} is already installed, please uninstall first.` + `Package upload only supports fresh installations. Package ${packageInfo.name} is already installed, please uninstall first.` ); } + const installSource = 'upload'; + const paths = await unpackBufferToCache({ + name: packageInfo.name, + version: packageInfo.version, + installSource, + archiveBuffer, + contentType, + }); + + setPackageInfo({ + name: packageInfo.name, + version: packageInfo.version, + installSource, + packageInfo, + }); + return _installPackage({ savedObjectsClient, callCluster, installedPkg, paths, - packageInfo: archivePackageInfo, + packageInfo, installType, - installSource: 'upload', + installSource, }); } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 9fabbaf72474e..ca84980107fe3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -63,7 +63,11 @@ export async function removeInstallation(options: { // remove the package archive and its contents from the cache so that a reinstall fetches // a fresh copy from the registry - deletePackageCache(pkgName, pkgVersion); + deletePackageCache({ + name: pkgName, + version: pkgVersion, + installSource: installation.install_source, + }); // successful delete's in SO client return {}. return something more useful return installedAssets; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index a6f42ebb96752..2d496055df78a 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -20,8 +20,9 @@ import { import { getArchiveFilelist, getPathParts, - setArchiveFilelist, - unpackArchiveToCache, + unpackBufferToCache, + getPackageInfo, + setPackageInfo, } from '../archive'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from '../streams'; @@ -126,25 +127,44 @@ export async function fetchCategories(params?: CategoriesParams): Promise { - let paths = getArchiveFilelist(pkgName, pkgVersion); + name: string, + version: string +): Promise<{ paths: string[]; packageInfo: RegistryPackage }> { + const installSource = 'registry'; + let paths = getArchiveFilelist({ name, version, installSource }); if (!paths || paths.length === 0) { - const { archiveBuffer, archivePath } = await fetchArchiveBuffer(pkgName, pkgVersion); - const contentType = mime.lookup(archivePath); - if (!contentType) { - throw new Error(`Unknown compression format for '${archivePath}'. Please use .zip or .gz`); - } - paths = await unpackArchiveToCache(archiveBuffer, contentType); - setArchiveFilelist(pkgName, pkgVersion, paths); + const { archiveBuffer, archivePath } = await fetchArchiveBuffer(name, version); + paths = await unpackBufferToCache({ + name, + version, + installSource, + archiveBuffer, + contentType: ensureContentType(archivePath), + }); } - // TODO: cache this as well? - const registryPackageInfo = await fetchInfo(pkgName, pkgVersion); + const packageInfo = await getInfo(name, version); - return { paths, registryPackageInfo }; + return { paths, packageInfo }; +} + +function ensureContentType(archivePath: string) { + const contentType = mime.lookup(archivePath); + if (!contentType) { + throw new Error(`Unknown compression format for '${archivePath}'. Please use .zip or .gz`); + } + return contentType; } export async function ensureCachedArchiveInfo( @@ -152,7 +172,7 @@ export async function ensureCachedArchiveInfo( version: string, installSource: InstallSource = 'registry' ) { - const paths = getArchiveFilelist(name, version); + const paths = getArchiveFilelist({ name, version, installSource }); if (!paths || paths.length === 0) { if (installSource === 'registry') { await getRegistryPackage(name, version); @@ -168,7 +188,7 @@ async function fetchArchiveBuffer( pkgName: string, pkgVersion: string ): Promise<{ archiveBuffer: Buffer; archivePath: string }> { - const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); + const { download: archivePath } = await getInfo(pkgName, pkgVersion); const archiveUrl = `${getRegistryUrl()}${archivePath}`; const archiveBuffer = await getResponseStream(archiveUrl).then(streamToBuffer); diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 5cf43d2830489..7e6e6d5e408b4 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -44,6 +44,7 @@ export { EpmPackageInstallStatus, InstallationStatus, PackageInfo, + ArchivePackage, RegistryVarsEntry, RegistryDataStream, RegistryElasticsearch, From 389e7a1d6a46254d898457e835601717c9dd6858 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 17 Nov 2020 10:57:52 -0500 Subject: [PATCH 36/99] Replace experimental badge with Beta (#83468) --- .../pages/analytics_management/page.tsx | 13 +++++-------- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index 44085384f7536..c31a3b08aa756 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -59,16 +59,13 @@ export const Page: FC = () => { />   diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 13159c4e824ac..1d4cf7435aeeb 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -12176,8 +12176,6 @@ "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state": "ステータス", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.stats": "統計", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel": "ジョブの詳細", - "xpack.ml.dataframe.analyticsList.experimentalBadgeLabel": "実験的", - "xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analyticsList.fetchSourceIndexPatternForCloneErrorMessage": "インデックスパターン{indexPattern}が存在するかどうかを確認するときにエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.forceStopModalBody": "{analyticsId}は失敗状態です。ジョブを停止して、エラーを修正する必要があります。", "xpack.ml.dataframe.analyticsList.forceStopModalCancelButton": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2e9971f00dcc2..fb2c5341ac952 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12190,8 +12190,6 @@ "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state": "状态", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.stats": "统计", "xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel": "作业详情", - "xpack.ml.dataframe.analyticsList.experimentalBadgeLabel": "实验性", - "xpack.ml.dataframe.analyticsList.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analyticsList.fetchSourceIndexPatternForCloneErrorMessage": "检查索引模式 {indexPattern} 是否存在时发生错误:{error}", "xpack.ml.dataframe.analyticsList.forceStopModalBody": "{analyticsId} 处于失败状态。您必须停止该作业并修复失败问题。", "xpack.ml.dataframe.analyticsList.forceStopModalCancelButton": "取消", From 292dbcc7391e5239a4077cb64d5e319e028d79d4 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Tue, 17 Nov 2020 16:03:39 +0000 Subject: [PATCH 37/99] [Maps] Improve icons for all layer types (#83503) * Improve icons for all layer types * EMS boundaries icon * Adding tracks layer icon --- .../maps/public/classes/layers/_layers.scss | 6 ++- .../choropleth_layer_wizard.tsx | 2 +- .../cloropleth_layer_icon.tsx | 0 .../icons}/clusters_layer_icon.tsx | 0 .../icons/documents_layer_icon.tsx} | 2 +- .../icons/ems_boundaries_layer_icon.tsx | 33 +++++++++++++ .../icons}/heatmap_layer_icon.tsx | 0 .../icons}/point_2_point_layer_icon.tsx | 0 .../layers/icons/tracks_layer_icon.tsx | 21 ++++++++ .../layers/icons/vector_tile_layer_icon.tsx | 48 +++++++++++++++++++ .../icons/web_map_service_layer_icon.tsx | 27 +++++++++++ .../layers/icons/world_map_layer_icon.tsx | 29 +++++++++++ .../ems_boundaries_layer_wizard.tsx | 3 +- .../ems_base_map_layer_wizard.tsx | 3 +- .../clusters_layer_wizard.tsx | 2 +- .../heatmap_layer_wizard.tsx | 2 +- .../point_2_point_layer_wizard.tsx | 2 +- .../es_documents_layer_wizard.tsx | 4 +- .../layer_wizard.tsx | 3 +- .../sources/wms_source/wms_layer_wizard.tsx | 3 +- .../sources/xyz_tms_source/layer_wizard.tsx | 3 +- 21 files changed, 180 insertions(+), 13 deletions(-) rename x-pack/plugins/maps/public/classes/layers/{choropleth_layer_wizard => icons}/cloropleth_layer_icon.tsx (100%) rename x-pack/plugins/maps/public/classes/{sources/es_geo_grid_source => layers/icons}/clusters_layer_icon.tsx (100%) rename x-pack/plugins/maps/public/classes/{sources/es_search_source/es_documents_layer_icon.tsx => layers/icons/documents_layer_icon.tsx} (98%) create mode 100644 x-pack/plugins/maps/public/classes/layers/icons/ems_boundaries_layer_icon.tsx rename x-pack/plugins/maps/public/classes/{sources/es_geo_grid_source => layers/icons}/heatmap_layer_icon.tsx (100%) rename x-pack/plugins/maps/public/classes/{sources/es_pew_pew_source => layers/icons}/point_2_point_layer_icon.tsx (100%) create mode 100644 x-pack/plugins/maps/public/classes/layers/icons/tracks_layer_icon.tsx create mode 100644 x-pack/plugins/maps/public/classes/layers/icons/vector_tile_layer_icon.tsx create mode 100644 x-pack/plugins/maps/public/classes/layers/icons/web_map_service_layer_icon.tsx create mode 100644 x-pack/plugins/maps/public/classes/layers/icons/world_map_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/layers/_layers.scss b/x-pack/plugins/maps/public/classes/layers/_layers.scss index 54ab7d85ef170..f3685b163e397 100644 --- a/x-pack/plugins/maps/public/classes/layers/_layers.scss +++ b/x-pack/plugins/maps/public/classes/layers/_layers.scss @@ -6,6 +6,10 @@ } &__background { - fill: $euiColorLightShade; + fill: lightOrDarkTheme($euiColorLightShade, $euiColorMediumShade); + } + + &__backgroundDarker { + fill: lightOrDarkTheme($euiColorMediumShade, $euiColorDarkShade); } } diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx index d87302a6a9f2e..670c775c3cc92 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/choropleth_layer_wizard.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { LayerWizard, RenderWizardArguments } from '../layer_wizard_registry'; import { LayerTemplate } from './layer_template'; -import { ChoroplethLayerIcon } from './cloropleth_layer_icon'; +import { ChoroplethLayerIcon } from '../icons/cloropleth_layer_icon'; export const choroplethLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/cloropleth_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/cloropleth_layer_icon.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/cloropleth_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/cloropleth_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/clusters_layer_icon.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/clusters_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/documents_layer_icon.tsx similarity index 98% rename from x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/documents_layer_icon.tsx index dcd4985f44280..168c8f8072b32 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_icon.tsx +++ b/x-pack/plugins/maps/public/classes/layers/icons/documents_layer_icon.tsx @@ -6,7 +6,7 @@ import React, { FunctionComponent } from 'react'; -export const EsDocumentsLayerIcon: FunctionComponent = () => ( +export const DocumentsLayerIcon: FunctionComponent = () => ( ( + + + + +); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/heatmap_layer_icon.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/heatmap_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/point_2_point_layer_icon.tsx similarity index 100% rename from x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_icon.tsx rename to x-pack/plugins/maps/public/classes/layers/icons/point_2_point_layer_icon.tsx diff --git a/x-pack/plugins/maps/public/classes/layers/icons/tracks_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/tracks_layer_icon.tsx new file mode 100644 index 0000000000000..5070e49ddc2b6 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/icons/tracks_layer_icon.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const TracksLayerIcon: FunctionComponent = () => ( + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/layers/icons/vector_tile_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/vector_tile_layer_icon.tsx new file mode 100644 index 0000000000000..8c4cd11221622 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/icons/vector_tile_layer_icon.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const VectorTileLayerIcon: FunctionComponent = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/layers/icons/web_map_service_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/web_map_service_layer_icon.tsx new file mode 100644 index 0000000000000..d845a66b93748 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/icons/web_map_service_layer_icon.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const WebMapServiceLayerIcon: FunctionComponent = () => ( + + + + +); diff --git a/x-pack/plugins/maps/public/classes/layers/icons/world_map_layer_icon.tsx b/x-pack/plugins/maps/public/classes/layers/icons/world_map_layer_icon.tsx new file mode 100644 index 0000000000000..702371e52996f --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/icons/world_map_layer_icon.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const WorldMapLayerIcon: FunctionComponent = () => ( + + + +); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index 768bbd1d94700..78100fe501208 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -14,6 +14,7 @@ import { EMSFileSource, sourceTitle } from './ems_file_source'; import { getEMSSettings } from '../../../kibana_services'; import { EMSFileSourceDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { EMSBoundariesLayerIcon } from '../../layers/icons/ems_boundaries_layer_icon'; export const emsBoundariesLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], @@ -24,7 +25,7 @@ export const emsBoundariesLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.emsFileDescription', { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), - icon: 'emsApp', + icon: EMSBoundariesLayerIcon, renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index bfa46574f007a..ff65889e15d27 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -15,6 +15,7 @@ import { VectorTileLayer } from '../../layers/vector_tile_layer/vector_tile_laye import { TileServiceSelect } from './tile_service_select'; import { getEMSSettings } from '../../../kibana_services'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { WorldMapLayerIcon } from '../../layers/icons/world_map_layer_icon'; export const emsBaseMapLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], @@ -25,7 +26,7 @@ export const emsBaseMapLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.emsTileDescription', { defaultMessage: 'Tile map service from Elastic Maps Service', }), - icon: 'emsApp', + icon: WorldMapLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { const layerDescriptor = VectorTileLayer.createDescriptor({ diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index 5d0a414cd0d18..0f596c47fc9b6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -29,7 +29,7 @@ import { STYLE_TYPE, } from '../../../../common/constants'; import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; -import { ClustersLayerIcon } from './clusters_layer_icon'; +import { ClustersLayerIcon } from '../../layers/icons/clusters_layer_icon'; export const clustersLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index 652514a3b9d34..dcad1e1e0b4b9 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -15,7 +15,7 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re import { HeatmapLayer } from '../../layers/heatmap_layer/heatmap_layer'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; import { LAYER_WIZARD_CATEGORY, RENDER_AS } from '../../../../common/constants'; -import { HeatmapLayerIcon } from './heatmap_layer_icon'; +import { HeatmapLayerIcon } from '../../layers/icons/heatmap_layer_icon'; export const heatmapLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index 74e690d4d3204..9d3ccf128fe05 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -23,7 +23,7 @@ import { NUMERICAL_COLOR_PALETTES } from '../../styles/color_palettes'; import { CreateSourceEditor } from './create_source_editor'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types'; -import { Point2PointLayerIcon } from './point_2_point_layer_icon'; +import { Point2PointLayerIcon } from '../../layers/icons/point_2_point_layer_icon'; export const point2PointLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 80cc88f432cad..04671b931d56b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -15,7 +15,7 @@ import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_ve import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { LAYER_WIZARD_CATEGORY, SCALING_TYPES } from '../../../../common/constants'; import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; -import { EsDocumentsLayerIcon } from './es_documents_layer_icon'; +import { DocumentsLayerIcon } from '../../layers/icons/documents_layer_icon'; import { ESSearchSourceDescriptor, VectorLayerDescriptor, @@ -41,7 +41,7 @@ export const esDocumentsLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.esSearchDescription', { defaultMessage: 'Points, lines, and polygons from Elasticsearch', }), - icon: EsDocumentsLayerIcon, + icon: DocumentsLayerIcon, renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: Partial) => { if (!sourceConfig) { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx index 32fa329be85df..db05570af05d5 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -12,13 +12,14 @@ import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_re import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types'; +import { VectorTileLayerIcon } from '../../layers/icons/vector_tile_layer_icon'; export const mvtVectorSourceWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { defaultMessage: 'Data service implementing the Mapbox vector tile specification', }), - icon: 'grid', + icon: VectorTileLayerIcon, renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => { const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); diff --git a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx index b3950baf8dbeb..47a426e34a420 100644 --- a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx @@ -13,13 +13,14 @@ import { sourceTitle, WMSSource } from './wms_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { TileLayer } from '../../layers/tile_layer/tile_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { WebMapServiceLayerIcon } from '../../layers/icons/web_map_service_layer_icon'; export const wmsLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.wmsDescription', { defaultMessage: 'Maps from OGC Standard WMS', }), - icon: 'grid', + icon: WebMapServiceLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: unknown) => { if (!sourceConfig) { diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx index b0344a3e0e318..e442907d172e9 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -11,13 +11,14 @@ import { XYZTMSSource, sourceTitle } from './xyz_tms_source'; import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { TileLayer } from '../../layers/tile_layer/tile_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; +import { WorldMapLayerIcon } from '../../layers/icons/world_map_layer_icon'; export const tmsLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], description: i18n.translate('xpack.maps.source.ems_xyzDescription', { defaultMessage: 'Tile map service configured in interface', }), - icon: 'grid', + icon: WorldMapLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig | null) => { if (!sourceConfig) { From 9c0164a2d8cc80234e1e199d52d81196c852a0da Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 17 Nov 2020 10:07:46 -0600 Subject: [PATCH 38/99] [ML] Add UI test for feature importance features (#82677) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../decision_path_chart.tsx | 102 ++++----- .../decision_path_classification.tsx | 1 + .../decision_path_popover.tsx | 5 +- .../feature_importance_summary.tsx | 58 ++--- .../timeseries_chart/timeseries_chart.js | 12 +- .../timeseriesexplorer/timeseriesexplorer.js | 1 + .../feature_importance.ts | 211 ++++++++++++++++++ .../apps/ml/data_frame_analytics/index.ts | 1 + .../ml/data_frame_analytics_results.ts | 56 +++++ 9 files changed, 367 insertions(+), 80 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx index 908755b197fd7..6bfd7a66331df 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx @@ -116,57 +116,59 @@ export const DecisionPathChart = ({ const tickFormatter = useCallback((d) => formatSingleValue(d, '').toString(), []); return ( - - - {baselineData && ( - - )} - - + + + {baselineData && ( + )} - showGridLines={false} - position={Position.Top} - showOverlappingTicks - domain={ - minDomain && maxDomain - ? { - min: minDomain, - max: maxDomain, - } - : undefined - } - /> - - - + /> + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx index 496bc37f571ce..7b091a06d1064 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx @@ -98,6 +98,7 @@ export const ClassificationDecisionPath: FC = ( {options !== undefined && ( = ({ ]; return ( - <> +
{tabs.map((tab) => ( setSelectedTabId(tab.id)} key={tab.id} @@ -146,6 +147,6 @@ export const DecisionPathPopover: FC = ({ {selectedTabId === DECISION_PATH_TABS.JSON && ( )} - +
); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx index 64835e7ca4c6d..0fab1cf75259e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx @@ -210,6 +210,7 @@ export const FeatureImportanceSummaryPanel: FC - +
+ + - - - - + + + + +
) } /> diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 8df186c5c3c6e..b2d054becbb1a 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -51,6 +51,7 @@ import { unhighlightFocusChartAnnotation, ANNOTATION_MIN_WIDTH, } from './timeseries_chart_annotations'; +import { distinctUntilChanged } from 'rxjs/operators'; const focusZoomPanelHeight = 25; const focusChartHeight = 310; @@ -570,6 +571,7 @@ class TimeseriesChartIntl extends Component { } renderFocusChart() { + console.log('renderFocusChart'); const { focusAggregationInterval, focusAnnotationData: focusAnnotationDataOriginalPropValue, @@ -1798,7 +1800,15 @@ class TimeseriesChartIntl extends Component { } export const TimeseriesChart = (props) => { - const annotationProp = useObservable(annotation$); + const annotationProp = useObservable( + annotation$.pipe( + distinctUntilChanged((prev, curr) => { + // prevent re-rendering + return prev !== null && curr !== null; + }) + ) + ); + if (annotationProp === undefined) { return null; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index e3b6e38f47bab..f14f11e5d6149 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1014,6 +1014,7 @@ export class TimeSeriesExplorer extends React.Component { this.previousShowForecast = showForecast; this.previousShowModelBounds = showModelBounds; + console.log('Timeseriesexplorer rerendered'); return ( {fieldNamesWithEmptyValues.length > 0 && ( diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts new file mode 100644 index 0000000000000..ff2bbd077fa8f --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DeepPartial } from '../../../../../plugins/ml/common/types/common'; +import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('total feature importance panel and decision path popover', function () { + const testDataList: Array<{ + suiteTitle: string; + archive: string; + indexPattern: { name: string; timeField: string }; + job: DeepPartial; + }> = (() => { + const timestamp = Date.now(); + + return [ + { + suiteTitle: 'binary classification job', + archive: 'ml/ihp_outlier', + indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' }, + job: { + id: `ihp_fi_binary_${timestamp}`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'", + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-ihp_fi_binary_${timestamp}`; + }, + results_field: 'ml_central_air', + }, + analyzed_fields: { + includes: [ + 'CentralAir', + 'GarageArea', + 'GarageCars', + 'YearBuilt', + 'Electrical', + 'Neighborhood', + 'Heating', + '1stFlrSF', + ], + }, + analysis: { + classification: { + dependent_variable: 'CentralAir', + num_top_feature_importance_values: 5, + training_percent: 35, + prediction_field_name: 'CentralAir_prediction', + num_top_classes: -1, + }, + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + }, + { + suiteTitle: 'multi class classification job', + archive: 'ml/ihp_outlier', + indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' }, + job: { + id: `ihp_fi_multi_${timestamp}`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'", + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-ihp_fi_multi_${timestamp}`; + }, + results_field: 'ml_heating_qc', + }, + analyzed_fields: { + includes: [ + 'CentralAir', + 'GarageArea', + 'GarageCars', + 'Electrical', + 'Neighborhood', + 'Heating', + '1stFlrSF', + 'HeatingQC', + ], + }, + analysis: { + classification: { + dependent_variable: 'HeatingQC', + num_top_feature_importance_values: 5, + training_percent: 35, + prediction_field_name: 'heatingqc', + num_top_classes: -1, + }, + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + }, + { + suiteTitle: 'regression job', + archive: 'ml/egs_regression', + indexPattern: { name: 'ft_egs_regression', timeField: '@timestamp' }, + job: { + id: `egs_fi_reg_${timestamp}`, + description: 'This is the job description', + source: { + index: ['ft_egs_regression'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-egs_fi_reg_${timestamp}`; + }, + results_field: 'ml', + }, + analysis: { + regression: { + prediction_field_name: 'test', + dependent_variable: 'stab', + num_top_feature_importance_values: 5, + training_percent: 35, + }, + }, + analyzed_fields: { + includes: [ + 'g1', + 'g2', + 'g3', + 'g4', + 'p1', + 'p2', + 'p3', + 'p4', + 'stab', + 'tau1', + 'tau2', + 'tau3', + 'tau4', + ], + excludes: [], + }, + model_memory_limit: '20mb', + }, + }, + ]; + })(); + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + for (const testData of testDataList) { + await esArchiver.loadIfNeeded(testData.archive); + await ml.testResources.createIndexPatternIfNeeded( + testData.indexPattern.name, + testData.indexPattern.timeField + ); + await ml.api.createAndRunDFAJob(testData.job as DataFrameAnalyticsConfig); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function () { + before(async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataFrameAnalytics(); + await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.job.id as string); + }); + + after(async () => { + await ml.api.deleteIndices(testData.job.dest!.index as string); + await ml.testResources.deleteIndexPatternByTitle(testData.job.dest!.index as string); + }); + + it('should display the total feature importance in the results view', async () => { + await ml.dataFrameAnalyticsResults.assertTotalFeatureImportanceEvaluatePanelExists(); + }); + + it('should display the feature importance decision path in the data grid', async () => { + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + await ml.dataFrameAnalyticsResults.openFeatureImportanceDecisionPathPopover(); + await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathElementsExists(); + await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathChartElementsExists(); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts index 0202c8431ce34..a57d26b536b4f 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./regression_creation')); loadTestFile(require.resolve('./classification_creation')); loadTestFile(require.resolve('./cloning')); + loadTestFile(require.resolve('./feature_importance')); }); } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts index 8781a2cd216f2..1ac11a0149897 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -5,12 +5,14 @@ */ import expect from '@kbn/expect'; +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataFrameAnalyticsResultsProvider({ getService, }: FtrProviderContext) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return { @@ -60,5 +62,59 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({ `DFA results table should have at least one row (got '${resultTableRows.length}')` ); }, + + async assertTotalFeatureImportanceEvaluatePanelExists() { + await testSubjects.existOrFail('mlDFExpandableSection-FeatureImportanceSummary'); + await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 5000 }); + }, + + async assertFeatureImportanceDecisionPathElementsExists() { + await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_chart', { + timeout: 5000, + }); + await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_json', { + timeout: 5000, + }); + }, + + async assertFeatureImportanceDecisionPathChartElementsExists() { + await testSubjects.existOrFail('mlDFADecisionPathChart', { + timeout: 5000, + }); + }, + + async openFeatureImportanceDecisionPathPopover() { + this.assertResultsTableNotEmpty(); + + const featureImportanceCell = await this.getFirstFeatureImportanceCell(); + const interactionButton = await featureImportanceCell.findByTagName('button'); + + // simulate hover and wait for button to appear + await featureImportanceCell.moveMouseTo(); + await this.waitForInteractionButtonToDisplay(interactionButton); + + // open popover + await interactionButton.click(); + await testSubjects.existOrFail('mlDFADecisionPathPopover'); + }, + + async getFirstFeatureImportanceCell(): Promise { + // get first row of the data grid + const firstDataGridRow = await testSubjects.find( + 'mlExplorationDataGrid loaded > dataGridRow' + ); + // find the feature importance cell in that row + const featureImportanceCell = await firstDataGridRow.findByCssSelector( + '[data-test-subj="dataGridRowCell"][class*="featureImportance"]' + ); + return featureImportanceCell; + }, + + async waitForInteractionButtonToDisplay(interactionButton: WebElementWrapper) { + await retry.tryForTime(5000, async () => { + const buttonVisible = await interactionButton.isDisplayed(); + expect(buttonVisible).to.equal(true, 'Expected data grid cell button to be visible'); + }); + }, }; } From 46d587a19f678e2f3ef1cb3ba35817d843d095e9 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 17 Nov 2020 10:28:44 -0600 Subject: [PATCH 39/99] [Workplace Search] Migrate SourcesLogic from ent-search (#83544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Copy and paste sources logic This is simply a copy & paste of the sources_logic file from ent-search. The only changes were adding the comment at the top and changing how lodash imports, per linting requirements * Add types The “I” prefix has been removed, per agreed-upon standard * Add type declaration to staticSourceData Yay TypeScript :roll_eyes: * Update route path For all other routes, we use the account/org syntax. For this one, I missed it and forgot to add ‘account’ for the route path. This fixes it * Update SourcesLogic to work with Kibana - Remove routes/http in favor of HttpLogic - Remove local flash messages in favor of global messages - Update paths to imports - Remove "I"s from interface names - Varions type fixes --- .../applications/workplace_search/types.ts | 54 ++++ .../views/content_sources/source_data.tsx | 4 +- .../views/content_sources/sources_logic.ts | 284 ++++++++++++++++++ .../routes/workplace_search/sources.test.ts | 4 +- .../server/routes/workplace_search/sources.ts | 2 +- 5 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index f09160d513344..801bcda2a319a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -36,6 +36,41 @@ export interface User { groupIds: string[]; } +export interface Features { + basicOrgContext?: FeatureIds[]; + basicOrgContextExcludedFeatures?: FeatureIds[]; + platinumOrgContext?: FeatureIds[]; + platinumPrivateContext: FeatureIds[]; +} + +export interface Configuration { + isPublicKey: boolean; + needsBaseUrl: boolean; + needsSubdomain?: boolean; + needsConfiguration?: boolean; + hasOauthRedirect: boolean; + baseUrlTitle?: string; + helpText: string; + documentationUrl: string; + applicationPortalUrl?: string; + applicationLinkTitle?: string; +} + +export interface SourceDataItem { + name: string; + serviceType: string; + configuration: Configuration; + configured?: boolean; + connected?: boolean; + features?: Features; + objTypes?: string[]; + sourceDescription: string; + connectStepDescription: string; + addPath: string; + editPath: string; + accountContextOnly: boolean; +} + export interface ContentSource { id: string; serviceType: string; @@ -54,6 +89,25 @@ export interface ContentSourceDetails extends ContentSource { boost: number; } +export interface ContentSourceStatus { + id: string; + name: string; + service_type: string; + status: { + status: string; + synced_at: string; + error_reason: number; + }; +} + +export interface Connector { + serviceType: string; + name: string; + configured: boolean; + supportedByLicense: boolean; + accountContextOnly: boolean; +} + export interface SourcePriority { [id: string]: number; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index d04b2cb16d308..dff9895dd84f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -59,7 +59,7 @@ import { CUSTOM_SOURCE_DOCS_URL, } from '../../routes'; -import { FeatureIds } from '../../types'; +import { FeatureIds, SourceDataItem } from '../../types'; import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; @@ -740,4 +740,4 @@ export const staticSourceData = [ connectStepDescription: connectStepDescription.empty, accountContextOnly: false, }, -]; +] as SourceDataItem[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts new file mode 100644 index 0000000000000..eacba312d5da6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -0,0 +1,284 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cloneDeep, findIndex } from 'lodash'; + +import { kea, MakeLogicType } from 'kea'; + +import { HttpLogic } from '../../../shared/http'; + +import { + flashAPIErrors, + setSuccessMessage, + FlashMessagesLogic, +} from '../../../shared/flash_messages'; + +import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types'; + +import { staticSourceData } from './source_data'; + +import { AppLogic } from '../../app_logic'; + +const ORG_SOURCES_PATH = '/api/workplace_search/org/sources'; +const ACCOUNT_SOURCES_PATH = '/api/workplace_search/account/sources'; + +interface ServerStatuses { + [key: string]: string; +} + +export interface ISourcesActions { + setServerSourceStatuses(statuses: ContentSourceStatus[]): ContentSourceStatus[]; + onInitializeSources(serverResponse: ISourcesServerResponse): ISourcesServerResponse; + onSetSearchability( + sourceId: string, + searchable: boolean + ): { sourceId: string; searchable: boolean }; + setAddedSource( + addedSourceName: string, + additionalConfiguration: boolean, + serviceType: string + ): { addedSourceName: string; additionalConfiguration: boolean; serviceType: string }; + resetFlashMessages(): void; + resetPermissionsModal(): void; + resetSourcesState(): void; + initializeSources(): void; + pollForSourceStatusChanges(): void; + setSourceSearchability( + sourceId: string, + searchable: boolean + ): { sourceId: string; searchable: boolean }; +} + +export interface IPermissionsModalProps { + addedSourceName: string; + serviceType: string; + additionalConfiguration: boolean; +} + +type CombinedDataItem = SourceDataItem & ContentSourceDetails; + +export interface ISourcesValues { + contentSources: ContentSourceDetails[]; + privateContentSources: ContentSourceDetails[]; + sourceData: CombinedDataItem[]; + availableSources: SourceDataItem[]; + configuredSources: SourceDataItem[]; + serviceTypes: Connector[]; + permissionsModal: IPermissionsModalProps | null; + dataLoading: boolean; + serverStatuses: ServerStatuses | null; +} + +interface ISourcesServerResponse { + contentSources: ContentSourceDetails[]; + privateContentSources?: ContentSourceDetails[]; + serviceTypes: Connector[]; +} + +export const SourcesLogic = kea>({ + actions: { + setServerSourceStatuses: (statuses: ContentSourceStatus[]) => statuses, + onInitializeSources: (serverResponse: ISourcesServerResponse) => serverResponse, + onSetSearchability: (sourceId: string, searchable: boolean) => ({ sourceId, searchable }), + setAddedSource: ( + addedSourceName: string, + additionalConfiguration: boolean, + serviceType: string + ) => ({ addedSourceName, additionalConfiguration, serviceType }), + resetFlashMessages: () => true, + resetPermissionsModal: () => true, + resetSourcesState: () => true, + initializeSources: () => true, + pollForSourceStatusChanges: () => true, + setSourceSearchability: (sourceId: string, searchable: boolean) => ({ sourceId, searchable }), + }, + reducers: { + contentSources: [ + [], + { + onInitializeSources: (_, { contentSources }) => contentSources, + onSetSearchability: (contentSources, { sourceId, searchable }) => + updateSourcesOnToggle(contentSources, sourceId, searchable), + }, + ], + privateContentSources: [ + [], + { + onInitializeSources: (_, { privateContentSources }) => privateContentSources || [], + onSetSearchability: (privateContentSources, { sourceId, searchable }) => + updateSourcesOnToggle(privateContentSources, sourceId, searchable), + }, + ], + serviceTypes: [ + [], + { + onInitializeSources: (_, { serviceTypes }) => serviceTypes || [], + }, + ], + permissionsModal: [ + null, + { + setAddedSource: (_, data) => data, + resetPermissionsModal: () => null, + }, + ], + dataLoading: [ + true, + { + onInitializeSources: () => false, + resetSourcesState: () => true, + }, + ], + serverStatuses: [ + null, + { + setServerSourceStatuses: (_, sources) => { + const serverStatuses = {} as ServerStatuses; + sources.forEach((source) => { + serverStatuses[source.id as string] = source.status.status; + }); + return serverStatuses; + }, + }, + ], + }, + selectors: ({ selectors }) => ({ + availableSources: [ + () => [selectors.sourceData], + (sourceData: SourceDataItem[]) => sourceData.filter(({ configured }) => !configured), + ], + configuredSources: [ + () => [selectors.sourceData], + (sourceData: SourceDataItem[]) => sourceData.filter(({ configured }) => configured), + ], + sourceData: [ + () => [selectors.serviceTypes, selectors.contentSources], + (serviceTypes, contentSources) => + mergeServerAndStaticData(serviceTypes, staticSourceData, contentSources), + ], + }), + listeners: ({ actions, values }) => ({ + initializeSources: async () => { + const { isOrganization } = AppLogic.values; + const route = isOrganization ? ORG_SOURCES_PATH : ACCOUNT_SOURCES_PATH; + + try { + const response = await HttpLogic.values.http.get(route); + actions.onInitializeSources(response); + } catch (e) { + flashAPIErrors(e); + } + + if (isOrganization && !values.serverStatuses) { + // We want to get the initial statuses from the server to compare our polling results to. + const sourceStatuses = await fetchSourceStatuses(isOrganization); + actions.setServerSourceStatuses(sourceStatuses); + } + }, + // We poll the server and if the status update, we trigger a new fetch of the sources. + pollForSourceStatusChanges: async () => { + const { isOrganization } = AppLogic.values; + if (!isOrganization) return; + const serverStatuses = values.serverStatuses; + + const sourceStatuses = await fetchSourceStatuses(isOrganization); + + sourceStatuses.some((source: ContentSourceStatus) => { + if (serverStatuses && serverStatuses[source.id] !== source.status.status) { + return actions.initializeSources(); + } + }); + }, + setSourceSearchability: async ({ sourceId, searchable }) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/searchable` + : `/api/workplace_search/account/sources/${sourceId}/searchable`; + + try { + await HttpLogic.values.http.put(route, { + body: JSON.stringify({ searchable }), + }); + actions.onSetSearchability(sourceId, searchable); + } catch (e) { + flashAPIErrors(e); + } + }, + setAddedSource: ({ addedSourceName, additionalConfiguration }) => { + setSuccessMessage( + [ + `Successfully connected ${addedSourceName}.`, + additionalConfiguration ? 'This source requires additional configuration.' : '', + ].join(' ') + ); + }, + resetFlashMessages: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, + }), +}); + +const fetchSourceStatuses = async (isOrganization: boolean) => { + const route = isOrganization ? ORG_SOURCES_PATH : ACCOUNT_SOURCES_PATH; + let response; + + try { + response = await HttpLogic.values.http.get(route); + SourcesLogic.actions.setServerSourceStatuses(response); + } catch (e) { + flashAPIErrors(e); + } + + return response; +}; + +const updateSourcesOnToggle = ( + contentSources: ContentSourceDetails[], + sourceId: string, + searchable: boolean +): ContentSourceDetails[] => { + if (!contentSources) return []; + const sources = cloneDeep(contentSources) as ContentSourceDetails[]; + const index = findIndex(sources, ({ id }) => id === sourceId); + const updatedSource = sources[index]; + sources[index] = { + ...updatedSource, + searchable, + }; + return sources; +}; + +/** + * We have 3 different data sets we have to combine in the UI. The first is the static (`staticSourceData`) + * data that contains the UI componets, such as the Path for React Router and the copy and images. + * + * The second is the base list of available sources that the server sends back in the collection, + * `availableTypes` that is the source of truth for the name and whether the source has been configured. + * + * Fnally, also in the collection response is the current set of connected sources. We check for the + * existence of a `connectedSource` of the type in the loop and set `connected` to true so that the UI + * can diplay "Add New" instead of "Connect", the latter of which is displated only when a connector + * has been configured but there are no connected sources yet. + */ +const mergeServerAndStaticData = ( + serverData: ContentSourceDetails[], + staticData: SourceDataItem[], + contentSources: ContentSourceDetails[] +) => { + const combined = [] as CombinedDataItem[]; + serverData.forEach((serverItem) => { + const type = serverItem.serviceType; + const staticItem = staticData.find(({ serviceType }) => serviceType === type); + const connectedSource = contentSources.find(({ serviceType }) => serviceType === type); + combined.push({ + ...serverItem, + ...staticItem, + connected: !!connectedSource, + } as CombinedDataItem); + }); + + return combined; +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 6d22002222a66..9cf491b79fd24 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -411,7 +411,7 @@ describe('sources routes', () => { }); }); - describe('PUT /api/workplace_search/sources/{id}/searchable', () => { + describe('PUT /api/workplace_search/account/sources/{id}/searchable', () => { let mockRouter: MockRouter; beforeEach(() => { @@ -421,7 +421,7 @@ describe('sources routes', () => { it('creates a request handler', () => { mockRouter = new MockRouter({ method: 'put', - path: '/api/workplace_search/sources/{id}/searchable', + path: '/api/workplace_search/account/sources/{id}/searchable', payload: 'body', }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index efef53440117e..bdd048438dae5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -268,7 +268,7 @@ export function registerAccountSourceSearchableRoute({ }: RouteDependencies) { router.put( { - path: '/api/workplace_search/sources/{id}/searchable', + path: '/api/workplace_search/account/sources/{id}/searchable', validate: { body: schema.object({ searchable: schema.boolean(), From 55119c2152f4f81e3a27b57d9b8200fffcfc4838 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 17 Nov 2020 11:34:15 -0600 Subject: [PATCH 40/99] [ML] Improve support for script and aggregation fields in anomaly detection jobs (#81923) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/ml/common/constants/messages.ts | 10 ++++ .../types/anomaly_detection_jobs/datafeed.ts | 11 ++-- x-pack/plugins/ml/common/types/fields.ts | 13 +++++ .../plugins/ml/common/util/datafeed_utils.ts | 22 ++++++++ .../plugins/ml/common/util/job_utils.test.ts | 6 +- x-pack/plugins/ml/common/util/job_utils.ts | 23 +++++++- .../ml/common/util/validation_utils.ts | 19 +++++++ .../explorer_charts_container_service.js | 3 +- .../util/model_memory_estimator.ts | 1 + .../common/job_validator/job_validator.ts | 15 +++++ .../jobs/new_job/common/job_validator/util.ts | 9 +++ .../datafeed_preview.tsx | 9 ++- .../summary_count_field/description.tsx | 29 ++++++++-- .../summary_count_field.tsx | 14 ++++- .../services/ml_api_service/index.ts | 3 + .../results_service/result_service_rx.ts | 38 +++++++++++-- .../results_service/results_service.js | 4 +- .../timeseries_search_service.ts | 3 +- .../calculate_model_memory_limit.ts | 14 +++-- .../models/data_visualizer/data_visualizer.ts | 56 ++++++++++++++----- .../models/fields_service/fields_service.ts | 47 +++++++++++++--- .../models/job_validation/job_validation.ts | 7 +++ .../job_validation/validate_cardinality.ts | 23 +++++++- .../validate_model_memory_limit.ts | 3 +- .../ml/server/routes/job_validation.ts | 16 +++++- .../routes/schemas/job_validation_schema.ts | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apis/ml/job_validation/validate.ts | 6 ++ 29 files changed, 345 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/ml/common/util/datafeed_utils.ts diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index a9e4cdc4a0434..1027ee5bf9a89 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -442,6 +442,16 @@ export const getMessages = once(() => { url: 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', }, + missing_summary_count_field_name: { + status: VALIDATION_STATUS.ERROR, + text: i18n.translate( + 'xpack.ml.models.jobValidation.messages.missingSummaryCountFieldNameMessage', + { + defaultMessage: + 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', + } + ), + }, skipped_extended_tests: { status: VALIDATION_STATUS.WARNING, text: i18n.translate('xpack.ml.models.jobValidation.messages.skippedExtendedTestsMessage', { diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts index 47ff618ffa77f..e5294112dc095 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts @@ -19,7 +19,7 @@ export interface Datafeed { job_id: JobId; query: object; query_delay?: string; - script_fields?: object; + script_fields?: Record; scroll_size?: number; delayed_data_check_config?: object; indices_options?: IndicesOptions; @@ -30,16 +30,17 @@ export interface ChunkingConfig { time_span?: string; } -interface Aggregation { - buckets: { +export type Aggregation = Record< + string, + { date_histogram: { field: string; fixed_interval: string; }; aggregations?: { [key: string]: any }; aggs?: { [key: string]: any }; - }; -} + } +>; interface IndicesOptions { expand_wildcards?: 'all' | 'open' | 'closed' | 'hidden' | 'none'; diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index 58eddba83db9d..512d12ca53253 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -89,3 +89,16 @@ export const mlCategory: Field = { type: ES_FIELD_TYPES.KEYWORD, aggregatable: false, }; + +export interface FieldAggCardinality { + field: string; + percent?: any; +} + +export interface ScriptAggCardinality { + script: any; +} + +export interface AggCardinality { + cardinality: FieldAggCardinality | ScriptAggCardinality; +} diff --git a/x-pack/plugins/ml/common/util/datafeed_utils.ts b/x-pack/plugins/ml/common/util/datafeed_utils.ts new file mode 100644 index 0000000000000..d86ee50baca19 --- /dev/null +++ b/x-pack/plugins/ml/common/util/datafeed_utils.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Aggregation, Datafeed } from '../types/anomaly_detection_jobs'; + +export const getDatafeedAggregations = ( + datafeedConfig: Partial | undefined +): Aggregation | undefined => { + if (datafeedConfig?.aggregations !== undefined) return datafeedConfig.aggregations; + if (datafeedConfig?.aggs !== undefined) return datafeedConfig.aggs; + return undefined; +}; + +export const getAggregationBucketsName = (aggregations: any): string | undefined => { + if (typeof aggregations === 'object') { + const keys = Object.keys(aggregations); + return keys.length > 0 ? keys[0] : undefined; + } +}; diff --git a/x-pack/plugins/ml/common/util/job_utils.test.ts b/x-pack/plugins/ml/common/util/job_utils.test.ts index a56ccd5208bab..1ea70c0c19b4e 100644 --- a/x-pack/plugins/ml/common/util/job_utils.test.ts +++ b/x-pack/plugins/ml/common/util/job_utils.test.ts @@ -188,8 +188,8 @@ describe('ML - job utils', () => { expect(isTimeSeriesViewDetector(job, 3)).toBe(false); }); - test('returns false for a detector using a script field as a metric field_name', () => { - expect(isTimeSeriesViewDetector(job, 4)).toBe(false); + test('returns true for a detector using a script field as a metric field_name', () => { + expect(isTimeSeriesViewDetector(job, 4)).toBe(true); }); }); @@ -281,6 +281,7 @@ describe('ML - job utils', () => { expect(isSourceDataChartableForDetector(job, 22)).toBe(true); expect(isSourceDataChartableForDetector(job, 23)).toBe(true); expect(isSourceDataChartableForDetector(job, 24)).toBe(true); + expect(isSourceDataChartableForDetector(job, 37)).toBe(true); }); test('returns false for expected detectors', () => { @@ -296,7 +297,6 @@ describe('ML - job utils', () => { expect(isSourceDataChartableForDetector(job, 34)).toBe(false); expect(isSourceDataChartableForDetector(job, 35)).toBe(false); expect(isSourceDataChartableForDetector(job, 36)).toBe(false); - expect(isSourceDataChartableForDetector(job, 37)).toBe(false); }); }); diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index a5b854a8d59a7..76990c61ff562 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -20,6 +20,7 @@ import { MlServerLimits } from '../types/ml_server_info'; import { JobValidationMessage, JobValidationMessageId } from '../constants/messages'; import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../constants/aggregation_types'; import { MLCATEGORY } from '../constants/field_types'; +import { getDatafeedAggregations } from './datafeed_utils'; export interface ValidationResults { valid: boolean; @@ -94,7 +95,6 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex // Perform extra check to see if the detector is using a scripted field. const scriptFields = Object.keys(job.datafeed_config.script_fields); isSourceDataChartable = - scriptFields.indexOf(dtr.field_name!) === -1 && scriptFields.indexOf(dtr.partition_field_name!) === -1 && scriptFields.indexOf(dtr.by_field_name!) === -1 && scriptFields.indexOf(dtr.over_field_name!) === -1; @@ -559,6 +559,27 @@ export function basicDatafeedValidation(datafeed: Datafeed): ValidationResults { }; } +export function basicJobAndDatafeedValidation(job: Job, datafeed: Datafeed): ValidationResults { + const messages: ValidationResults['messages'] = []; + let valid = true; + + if (datafeed && job) { + const datafeedAggregations = getDatafeedAggregations(datafeed); + + if (datafeedAggregations !== undefined && !job.analysis_config?.summary_count_field_name) { + valid = false; + messages.push({ id: 'missing_summary_count_field_name' }); + } + } + + return { + messages, + valid, + contains: (id) => messages.some((m) => id === m.id), + find: (id) => messages.find((m) => id === m.id), + }; +} + export function validateModelMemoryLimit(job: Job, limits: MlServerLimits): ValidationResults { const messages: ValidationResults['messages'] = []; let valid = true; diff --git a/x-pack/plugins/ml/common/util/validation_utils.ts b/x-pack/plugins/ml/common/util/validation_utils.ts index ee4be34c6f600..b4f424a053b56 100644 --- a/x-pack/plugins/ml/common/util/validation_utils.ts +++ b/x-pack/plugins/ml/common/util/validation_utils.ts @@ -31,3 +31,22 @@ export function isValidJson(json: string) { return false; } } + +export function findAggField(aggs: Record, fieldName: string): any { + let value; + Object.keys(aggs).some(function (k) { + if (k === fieldName) { + value = aggs[k]; + return true; + } + if (aggs.hasOwnProperty(k) && typeof aggs[k] === 'object') { + value = findAggField(aggs[k], fieldName); + return value !== undefined; + } + }); + return value; +} + +export function isValidAggregationField(aggs: Record, fieldName: string): boolean { + return findAggField(aggs, fieldName) !== undefined; +} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 39166841a4e1b..95c721a7043dc 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -123,7 +123,8 @@ export const anomalyDataChange = function ( config.timeField, range.min, range.max, - bucketSpanSeconds * 1000 + bucketSpanSeconds * 1000, + config.datafeedConfig ) .toPromise(); } else { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts index 6671aaa83abe0..f23807f156576 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts @@ -134,6 +134,7 @@ export const useModelMemoryEstimator = ( // Update model memory estimation payload on the job creator updates useEffect(() => { modelMemoryEstimator.update({ + datafeedConfig: jobCreator.datafeedConfig, analysisConfig: jobCreator.jobConfig.analysis_config, indexPattern: jobCreator.indexPatternTitle, query: jobCreator.datafeedConfig.query, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 635322a6c4469..1c012033e97c8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -10,6 +10,7 @@ import { map, startWith, tap } from 'rxjs/operators'; import { basicJobValidation, basicDatafeedValidation, + basicJobAndDatafeedValidation, } from '../../../../../../common/util/job_utils'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_creator'; @@ -53,6 +54,7 @@ export interface BasicValidations { scrollSize: Validation; categorizerMissingPerPartition: Validation; categorizerVaryingPerPartitionField: Validation; + summaryCountField: Validation; } export interface AdvancedValidations { @@ -80,6 +82,7 @@ export class JobValidator { scrollSize: { valid: true }, categorizerMissingPerPartition: { valid: true }, categorizerVaryingPerPartitionField: { valid: true }, + summaryCountField: { valid: true }, }; private _advancedValidations: AdvancedValidations = { categorizationFieldValid: { valid: true }, @@ -197,6 +200,14 @@ export class JobValidator { datafeedConfig ); + const basicJobAndDatafeedResults = basicJobAndDatafeedValidation(jobConfig, datafeedConfig); + populateValidationMessages( + basicJobAndDatafeedResults, + this._basicValidations, + jobConfig, + datafeedConfig + ); + // run addition job and group id validation const idResults = checkForExistingJobAndGroupIds( this._jobCreator.jobId, @@ -228,6 +239,9 @@ export class JobValidator { public get bucketSpan(): Validation { return this._basicValidations.bucketSpan; } + public get summaryCountField(): Validation { + return this._basicValidations.summaryCountField; + } public get duplicateDetectors(): Validation { return this._basicValidations.duplicateDetectors; @@ -297,6 +311,7 @@ export class JobValidator { this.duplicateDetectors.valid && this.categorizerMissingPerPartition.valid && this.categorizerVaryingPerPartitionField.valid && + this.summaryCountField.valid && !this.validating && (this._jobCreator.type !== JOB_TYPE.CATEGORIZATION || (this._jobCreator.type === JOB_TYPE.CATEGORIZATION && this.categorizationField)) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts index 1ce81bf0dcdf0..04be935ed4399 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts @@ -193,6 +193,15 @@ export function populateValidationMessages( basicValidations.frequency.valid = false; basicValidations.frequency.message = invalidTimeIntervalMessage(datafeedConfig.frequency); } + if (validationResults.contains('missing_summary_count_field_name')) { + basicValidations.summaryCountField.valid = false; + basicValidations.summaryCountField.message = i18n.translate( + 'xpack.ml.newJob.wizard.validateJob.summaryCountFieldMissing', + { + defaultMessage: 'Required field as the datafeed uses aggregations.', + } + ); + } } export function checkForExistingJobAndGroupIds( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx index 0dd802855ea67..cf98625672019 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -61,9 +61,12 @@ export const DatafeedPreview: FC<{ if (combinedJob.datafeed_config && combinedJob.datafeed_config.indices.length) { try { const resp = await mlJobService.searchPreview(combinedJob); - const data = resp.aggregations - ? resp.aggregations.buckets.buckets.slice(0, ML_DATA_PREVIEW_COUNT) - : resp.hits.hits; + let data = resp.hits.hits; + // the first item under aggregations can be any name + if (typeof resp.aggregations === 'object' && Object.keys(resp.aggregations).length > 0) { + const accessor = Object.keys(resp.aggregations)[0]; + data = resp.aggregations[accessor].buckets.slice(0, ML_DATA_PREVIEW_COUNT); + } setPreviewJsonString(JSON.stringify(data, null, 2)); } catch (error) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx index 5109718268ac3..a09b6540e101f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/description.tsx @@ -7,23 +7,44 @@ import React, { memo, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiFormRow, EuiLink } from '@elastic/eui'; +import { Validation } from '../../../../../common/job_validator'; +import { useMlKibana } from '../../../../../../../contexts/kibana'; -export const Description: FC = memo(({ children }) => { +interface Props { + validation: Validation; +} + +export const Description: FC = memo(({ children, validation }) => { const title = i18n.translate('xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title', { defaultMessage: 'Summary count field', }); + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + const docsUrl = `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-aggregation.html`; return ( {title}} description={ + + + ), + }} /> } > - + <>{children} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx index af759117b8501..70eaa39f71c69 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field.tsx @@ -17,13 +17,23 @@ import { import { Description } from './description'; export const SummaryCountField: FC = () => { - const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const { + jobCreator: jc, + jobCreatorUpdate, + jobCreatorUpdated, + jobValidator, + jobValidatorUpdated, + } = useContext(JobCreatorContext); const jobCreator = jc as MultiMetricJobCreator | PopulationJobCreator | AdvancedJobCreator; const { fields } = newJobCapsService; const [summaryCountFieldName, setSummaryCountFieldName] = useState( jobCreator.summaryCountFieldName ); + const [validation, setValidation] = useState(jobValidator.summaryCountField); + useEffect(() => { + setValidation(jobValidator.summaryCountField); + }, [jobValidatorUpdated]); useEffect(() => { jobCreator.summaryCountFieldName = summaryCountFieldName; @@ -35,7 +45,7 @@ export const SummaryCountField: FC = () => { }, [jobCreatorUpdated]); return ( - + { + const scriptFields = datafeedConfig?.script_fields; + const aggFields = getDatafeedAggregations(datafeedConfig); + // Build the criteria to use in the bool filter part of the request. // Add criteria for the time range, entity fields, // plus any additional supplied query. @@ -151,15 +157,35 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { body.aggs.byTime.aggs = {}; const metricAgg: any = { - [metricFunction]: { - field: metricFieldName, - }, + [metricFunction]: {}, }; + if (scriptFields !== undefined && scriptFields[metricFieldName] !== undefined) { + metricAgg[metricFunction].script = scriptFields[metricFieldName].script; + } else { + metricAgg[metricFunction].field = metricFieldName; + } if (metricFunction === 'percentiles') { metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS]; } - body.aggs.byTime.aggs.metric = metricAgg; + + // when the field is an aggregation field, because the field doesn't actually exist in the indices + // we need to pass all the sub aggs from the original datafeed config + // so that we can access the aggregated field + if (typeof aggFields === 'object' && Object.keys(aggFields).length > 0) { + // first item under aggregations can be any name, not necessarily 'buckets' + const accessor = Object.keys(aggFields)[0]; + const tempAggs = { ...(aggFields[accessor].aggs ?? aggFields[accessor].aggregations) }; + const foundValue = findAggField(tempAggs, metricFieldName); + + if (foundValue !== undefined) { + tempAggs.metric = foundValue; + delete tempAggs[metricFieldName]; + } + body.aggs.byTime.aggs = tempAggs; + } else { + body.aggs.byTime.aggs.metric = metricAgg; + } } return mlApiServices.esSearch$({ index, body }).pipe( diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index d053d69b4d1f2..8419660a52a9a 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -286,7 +286,7 @@ export function resultsServiceProvider(mlApiServices) { influencerFieldValues: { terms: { field: 'influencer_field_value', - size: maxFieldValues, + size: !!maxFieldValues ? maxFieldValues : ANOMALY_SWIM_LANE_HARD_LIMIT, order: { maxAnomalyScore: 'desc', }, @@ -416,7 +416,7 @@ export function resultsServiceProvider(mlApiServices) { influencerFieldValues: { terms: { field: 'influencer_field_value', - size: maxResults !== undefined ? maxResults : 2, + size: !!maxResults ? maxResults : 2, order: { maxAnomalyScore: 'desc', }, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 0d7abdab90be0..90c39497a9a18 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -94,7 +94,8 @@ function getMetricData( chartConfig.timeField, earliestMs, latestMs, - intervalMs + intervalMs, + chartConfig?.datafeedConfig ) .pipe( map((resp) => { diff --git a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts index 180b4e71dfa9c..865f305f2ff9f 100644 --- a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts @@ -7,7 +7,7 @@ import numeral from '@elastic/numeral'; import { IScopedClusterClient } from 'kibana/server'; import { MLCATEGORY } from '../../../common/constants/field_types'; -import { AnalysisConfig } from '../../../common/types/anomaly_detection_jobs'; +import { AnalysisConfig, Datafeed } from '../../../common/types/anomaly_detection_jobs'; import { fieldsServiceProvider } from '../fields_service'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import type { MlClient } from '../../lib/ml_client'; @@ -46,7 +46,8 @@ const cardinalityCheckProvider = (client: IScopedClusterClient) => { query: any, timeFieldName: string, earliestMs: number, - latestMs: number + latestMs: number, + datafeedConfig?: Datafeed ): Promise<{ overallCardinality: { [key: string]: number }; maxBucketCardinality: { [key: string]: number }; @@ -101,7 +102,8 @@ const cardinalityCheckProvider = (client: IScopedClusterClient) => { query, timeFieldName, earliestMs, - latestMs + latestMs, + datafeedConfig ); } @@ -142,7 +144,8 @@ export function calculateModelMemoryLimitProvider( timeFieldName: string, earliestMs: number, latestMs: number, - allowMMLGreaterThanMax = false + allowMMLGreaterThanMax = false, + datafeedConfig?: Datafeed ): Promise { const { body: info } = await mlClient.info(); const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase(); @@ -154,7 +157,8 @@ export function calculateModelMemoryLimitProvider( query, timeFieldName, earliestMs, - latestMs + latestMs, + datafeedConfig ); const { body } = await mlClient.estimateModelMemory({ diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 1f59e990096a4..0142e44276eee 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -15,6 +15,9 @@ import { buildSamplerAggregation, getSamplerAggregationsResponsePath, } from '../../lib/query_utils'; +import { AggCardinality } from '../../../common/types/fields'; +import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; +import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; const SAMPLER_TOP_TERMS_THRESHOLD = 100000; const SAMPLER_TOP_TERMS_SHARD_SIZE = 5000; @@ -121,12 +124,6 @@ interface AggHistogram { }; } -interface AggCardinality { - cardinality: { - field: string; - }; -} - interface AggTerms { terms: { field: string; @@ -597,23 +594,35 @@ export class DataVisualizer { samplerShardSize: number, timeFieldName: string, earliestMs?: number, - latestMs?: number + latestMs?: number, + datafeedConfig?: Datafeed ) { const index = indexPatternTitle; const size = 0; const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); + const datafeedAggregations = getDatafeedAggregations(datafeedConfig); // Value count aggregation faster way of checking if field exists than using // filter aggregation with exists query. - const aggs: Aggs = {}; + const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {}; + aggregatableFields.forEach((field, i) => { const safeFieldName = getSafeAggregationName(field, i); aggs[`${safeFieldName}_count`] = { filter: { exists: { field } }, }; - aggs[`${safeFieldName}_cardinality`] = { - cardinality: { field }, - }; + + let cardinalityField: AggCardinality; + if (datafeedConfig?.script_fields?.hasOwnProperty(field)) { + cardinalityField = aggs[`${safeFieldName}_cardinality`] = { + cardinality: { script: datafeedConfig?.script_fields[field].script }, + }; + } else { + cardinalityField = { + cardinality: { field }, + }; + } + aggs[`${safeFieldName}_cardinality`] = cardinalityField; }); const searchBody = { @@ -661,10 +670,27 @@ export class DataVisualizer { }, }); } else { - stats.aggregatableNotExistsFields.push({ - fieldName: field, - existsInDocs: false, - }); + if (datafeedConfig?.script_fields?.hasOwnProperty(field)) { + const cardinality = get( + aggregations, + [...aggsPath, `${safeFieldName}_cardinality`, 'value'], + 0 + ); + stats.aggregatableExistsFields.push({ + fieldName: field, + existsInDocs: true, + stats: { + sampleCount, + count, + cardinality, + }, + }); + } else { + stats.aggregatableNotExistsFields.push({ + fieldName: field, + existsInDocs: false, + }); + } } }); diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index ed8d3f48e387c..17f35967a626d 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -9,6 +9,10 @@ import { IScopedClusterClient } from 'kibana/server'; import { duration } from 'moment'; import { parseInterval } from '../../../common/util/parse_interval'; import { initCardinalityFieldsCache } from './fields_aggs_cache'; +import { AggCardinality } from '../../../common/types/fields'; +import { isValidAggregationField } from '../../../common/util/validation_utils'; +import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; +import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; /** * Service for carrying out queries to obtain data @@ -35,14 +39,29 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { */ async function getAggregatableFields( index: string | string[], - fieldNames: string[] + fieldNames: string[], + datafeedConfig?: Datafeed ): Promise { const { body } = await asCurrentUser.fieldCaps({ index, fields: fieldNames, }); const aggregatableFields: string[] = []; + const datafeedAggregations = getDatafeedAggregations(datafeedConfig); + fieldNames.forEach((fieldName) => { + if ( + typeof datafeedConfig?.script_fields === 'object' && + datafeedConfig.script_fields.hasOwnProperty(fieldName) + ) { + aggregatableFields.push(fieldName); + } + if ( + datafeedAggregations !== undefined && + isValidAggregationField(datafeedAggregations, fieldName) + ) { + aggregatableFields.push(fieldName); + } const fieldInfo = body.fields[fieldName]; const typeKeys = fieldInfo !== undefined ? Object.keys(fieldInfo) : []; if (typeKeys.length > 0) { @@ -67,10 +86,12 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { query: any, timeFieldName: string, earliestMs: number, - latestMs: number + latestMs: number, + datafeedConfig?: Datafeed ): Promise<{ [key: string]: number }> { - const aggregatableFields = await getAggregatableFields(index, fieldNames); + const aggregatableFields = await getAggregatableFields(index, fieldNames, datafeedConfig); + // getAggregatableFields doesn't account for scripted or aggregated fields if (aggregatableFields.length === 0) { return {}; } @@ -112,10 +133,22 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { mustCriteria.push(query); } - const aggs = fieldsToAgg.reduce((obj, field) => { - obj[field] = { cardinality: { field } }; - return obj; - }, {} as { [field: string]: { cardinality: { field: string } } }); + const aggs = fieldsToAgg.reduce( + (obj, field) => { + if ( + typeof datafeedConfig?.script_fields === 'object' && + datafeedConfig.script_fields.hasOwnProperty(field) + ) { + obj[field] = { cardinality: { script: datafeedConfig.script_fields[field].script } }; + } else { + obj[field] = { cardinality: { field } }; + } + return obj; + }, + {} as { + [field: string]: AggCardinality; + } + ); const body = { query: { diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index e3fcc69596dc9..3526f9cebb89b 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -27,6 +27,7 @@ import { validateTimeRange, isValidTimeField } from './validate_time_range'; import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { MlClient } from '../../lib/ml_client'; +import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; export type ValidateJobPayload = TypeOf; @@ -100,6 +101,12 @@ export async function validateJob( ...(await validateModelMemoryLimit(client, mlClient, job, duration)) ); } + + // if datafeed has aggregation, require job config to include a valid summary_doc_field_name + const datafeedAggregations = getDatafeedAggregations(job.datafeed_config); + if (datafeedAggregations !== undefined && !job.analysis_config?.summary_count_field_name) { + validationMessages.push({ id: 'missing_summary_count_field_name' }); + } } else { validationMessages = basicValidation.messages; validationMessages.push({ id: 'skipped_extended_tests' }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts index c5822b863c83d..f2bcc6e50d86e 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts @@ -11,6 +11,8 @@ import { validateJobObject } from './validate_job_object'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { Detector } from '../../../common/types/anomaly_detection_jobs'; import { MessageId, JobValidationMessage } from '../../../common/constants/messages'; +import { isValidAggregationField } from '../../../common/util/validation_utils'; +import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; function isValidCategorizationConfig(job: CombinedJob, fieldName: string): boolean { return ( @@ -66,6 +68,7 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida const relevantDetectors = detectors.filter((detector) => { return typeof detector[fieldName] !== 'undefined'; }); + const datafeedConfig = job.datafeed_config; if (relevantDetectors.length > 0) { try { @@ -78,11 +81,26 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida index: job.datafeed_config.indices.join(','), fields: uniqueFieldNames, }); + const datafeedAggregations = getDatafeedAggregations(datafeedConfig); let aggregatableFieldNames: string[] = []; // parse fieldCaps to return an array of just the fields which are aggregatable if (typeof fieldCaps === 'object' && typeof fieldCaps.fields === 'object') { aggregatableFieldNames = uniqueFieldNames.filter((field) => { + if ( + typeof datafeedConfig?.script_fields === 'object' && + datafeedConfig?.script_fields.hasOwnProperty(field) + ) { + return true; + } + // if datafeed has aggregation fields, check recursively if field exist + if ( + datafeedAggregations !== undefined && + isValidAggregationField(datafeedAggregations, field) + ) { + return true; + } + if (typeof fieldCaps.fields[field] !== 'undefined') { const fieldType = Object.keys(fieldCaps.fields[field])[0]; return fieldCaps.fields[field][fieldType].aggregatable; @@ -96,7 +114,10 @@ const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Valida job.datafeed_config.query, aggregatableFieldNames, 0, - job.data_description.time_field + job.data_description.time_field, + undefined, + undefined, + datafeedConfig ); uniqueFieldNames.forEach((uniqueFieldName) => { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts index 6721605355d96..f72885cf223fd 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts @@ -65,7 +65,8 @@ export async function validateModelMemoryLimit( job.data_description.time_field, duration!.start as number, duration!.end as number, - true + true, + job.datafeed_config ); // @ts-expect-error const mmlEstimateBytes: number = numeral(modelMemoryLimit).value(); diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index c11569b8bc1f3..769405c6ef7c2 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; -import { AnalysisConfig } from '../../common/types/anomaly_detection_jobs'; +import { AnalysisConfig, Datafeed } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -35,7 +35,15 @@ export function jobValidationRoutes( mlClient: MlClient, payload: CalculateModelMemoryLimitPayload ) { - const { analysisConfig, indexPattern, query, timeFieldName, earliestMs, latestMs } = payload; + const { + datafeedConfig, + analysisConfig, + indexPattern, + query, + timeFieldName, + earliestMs, + latestMs, + } = payload; return calculateModelMemoryLimitProvider(client, mlClient)( analysisConfig as AnalysisConfig, @@ -43,7 +51,9 @@ export function jobValidationRoutes( query, timeFieldName, earliestMs, - latestMs + latestMs, + undefined, + datafeedConfig as Datafeed ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts index ddfb49ce42cb8..f786607e70641 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts @@ -20,6 +20,7 @@ export const estimateBucketSpanSchema = schema.object({ }); export const modelMemoryLimitSchema = schema.object({ + datafeedConfig: datafeedConfigSchema, analysisConfig: analysisConfigSchema, indexPattern: schema.string(), query: schema.any(), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1d4cf7435aeeb..cd45a4f01fc64 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13188,7 +13188,6 @@ "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsErrorCallout": "停止したパーティションのリストの取得中にエラーが発生しました。", "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsExistCallout": "パーティション単位の分類とstop_on_warn設定が有効です。ジョブ「{jobId}」の一部のパーティションは分類に適さず、さらなる分類または異常検知分析から除外されました。", "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName": "停止したパーティション名", - "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.description": "オプション。インプットデータが事前にまとめられている場合に使用、例: \\{docCountParam\\}。", "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title": "サマリーカウントフィールド", "xpack.ml.newJob.wizard.previewJsonButton": "JSON をプレビュー", "xpack.ml.newJob.wizard.previousStepButton": "前へ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fb2c5341ac952..97396b09ca6c6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13202,7 +13202,6 @@ "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsErrorCallout": "提取已停止分区的列表时发生错误。", "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsExistCallout": "启用按分区分类和 stop_on_warn 设置。作业“{jobId}”中的某些分区不适合进行分类,已从进一步分类或异常检测分析中排除。", "xpack.ml.newJob.wizard.pickFieldsStep.stoppedPartitionsPreviewColumnName": "已停止的分区名称", - "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.description": "可选,用于输入数据已预汇总时,例如 \\{docCountParam\\}。", "xpack.ml.newJob.wizard.pickFieldsStep.summaryCountField.title": "汇总计数字段", "xpack.ml.newJob.wizard.previewJsonButton": "预览 JSON", "xpack.ml.newJob.wizard.previousStepButton": "上一页", diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts index e4e6adca9640f..cb663115b958b 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -303,6 +303,12 @@ export default ({ getService }: FtrProviderContext) => { url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#model-memory-limits`, status: 'warning', }, + { + id: 'missing_summary_count_field_name', + status: 'error', + text: + 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', + }, ]; expect(body.length).to.eql( From 8d4642b53841a6a8341af4df25f9d28c4416ddde Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 17 Nov 2020 09:37:31 -0800 Subject: [PATCH 41/99] skip flaky suite (#77279) --- src/cli/serve/integration_tests/reload_logging_config.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts index 0a2c90460430f..02692fb914fda 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.ts +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -82,7 +82,8 @@ function createConfigManager(configPath: string) { }; } -describe('Server logging configuration', function () { +// Failing: See https://github.com/elastic/kibana/issues/77279 +describe.skip('Server logging configuration', function () { let child: undefined | Child.ChildProcess; beforeEach(() => { From 7c8b9a1f0059dfe7522c73933eb982b06a3fbe7d Mon Sep 17 00:00:00 2001 From: Julien Guay Date: Tue, 17 Nov 2020 17:59:57 +0000 Subject: [PATCH 42/99] adds xpack.security.authc.selector.enabled setting (#83551) setting xpack.security.authc.selector.enabled introduced in 7.7.0 by https://github.com/elastic/kibana/pull/53010 is currently missing for docker entrypoint script --- .../os_packages/docker_generator/resources/bin/kibana-docker | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 274d7a4e5a488..4c833f5be6c5b 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -237,6 +237,7 @@ kibana_vars=( xpack.security.authc.oidc.realm xpack.security.authc.saml.realm xpack.security.authc.saml.maxRedirectURLSize + xpack.security.authc.selector.enabled xpack.security.cookieName xpack.security.enabled xpack.security.encryptionKey From bc22b67ba78daa0e7fb9e12fc2d10a830ac25c71 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 17 Nov 2020 19:29:47 +0100 Subject: [PATCH 43/99] [ML] Persisted URL state for the Data frame analytics jobs and models pages (#83439) * [ML] support table settings from the URL state * [ML] fix management page * [ML] models page support * [ML] update URL generator * [ML] rename id column * [ML] update types and tests * [ML] fix id column name and field * [ML] remove legacy functions * [ML] set id key for the job query text * [ML] fix id column rendering * [ML] ad jobs with usePageUrlState * [ML] update unit tests for solutions --- .../MachineLearningLinks/MLLink.test.tsx | 2 +- .../common/constants/data_frame_analytics.ts | 1 + .../ml/common/constants/ml_url_generator.ts | 3 + x-pack/plugins/ml/common/types/common.ts | 14 +++ x-pack/plugins/ml/common/util/string_utils.ts | 2 +- .../analytics_list/analytics_list.tsx | 76 ++++++++-------- .../components/analytics_list/common.ts | 16 ++-- .../components/analytics_list/use_columns.tsx | 16 ++-- .../analytics_list/use_table_settings.ts | 86 ++++++++----------- .../analytics_navigation_bar.tsx | 9 +- .../components/models_management/index.ts | 10 +-- .../models_management/models_list.tsx | 28 +++++- .../pages/analytics_management/page.tsx | 22 ++++- .../job_filter_bar/job_filter_bar.tsx | 4 +- .../jobs/jobs_list/components/utils.d.ts | 8 -- .../jobs/jobs_list/components/utils.js | 29 ------- .../jobs/jobs_list/components/utils.test.ts | 35 -------- .../application/jobs/jobs_list/jobs.tsx | 42 ++------- .../jobs_list_page/jobs_list_page.tsx | 38 ++++---- .../ml/public/application/util/url_state.tsx | 33 +++++++ .../anomaly_detection_urls_generator.ts | 10 ++- .../data_frame_analytics_urls_generator.ts | 23 +++-- .../ml_url_generator/ml_url_generator.test.ts | 14 ++- .../ml_popover/jobs_table/jobs_table.test.tsx | 6 +- 24 files changed, 269 insertions(+), 258 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts delete mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index 30d4bb34ea345..c453de709a5d2 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -19,6 +19,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/jobs?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` + `"/app/ml/jobs?_a=(jobs:(queryText:'id:(something)%20groups:(apm)'))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 5c8000566bb38..958d5ae250185 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -9,6 +9,7 @@ export const ANALYSIS_CONFIG_TYPE = { REGRESSION: 'regression', CLASSIFICATION: 'classification', } as const; + export const DEFAULT_RESULTS_FIELD = 'ml'; export const JOB_MAP_NODE_TYPES = { diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts index a79e72a84c08e..0c931d281d2d5 100644 --- a/x-pack/plugins/ml/common/constants/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -11,6 +11,7 @@ export const ML_PAGES = { ANOMALY_EXPLORER: 'explorer', SINGLE_METRIC_VIEWER: 'timeseriesexplorer', DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics', + DATA_FRAME_ANALYTICS_MODELS_MANAGE: 'data_frame_analytics/models', DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration', DATA_FRAME_ANALYTICS_MAP: 'data_frame_analytics/map', /** @@ -45,3 +46,5 @@ export const ML_PAGES = { ACCESS_DENIED: 'access-denied', OVERVIEW: 'overview', } as const; + +export type MlPages = typeof ML_PAGES[keyof typeof ML_PAGES]; diff --git a/x-pack/plugins/ml/common/types/common.ts b/x-pack/plugins/ml/common/types/common.ts index f04ff2539e4e9..4ae542c510a26 100644 --- a/x-pack/plugins/ml/common/types/common.ts +++ b/x-pack/plugins/ml/common/types/common.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MlPages } from '../constants/ml_url_generator'; + export interface Dictionary { [id: string]: TValue; } @@ -31,3 +33,15 @@ export type DeepReadonly = T extends Array type DeepReadonlyObject = { readonly [P in keyof T]: DeepReadonly; }; + +export interface ListingPageUrlState { + pageSize: number; + pageIndex: number; + sortField: string; + sortDirection: string; + queryText?: string; +} + +export type AppPageState = { + [key in MlPages]?: Partial; +}; diff --git a/x-pack/plugins/ml/common/util/string_utils.ts b/x-pack/plugins/ml/common/util/string_utils.ts index 4691bac0a065a..ffb8b19dc9aa1 100644 --- a/x-pack/plugins/ml/common/util/string_utils.ts +++ b/x-pack/plugins/ml/common/util/string_utils.ts @@ -45,5 +45,5 @@ export function getGroupQueryText(groupIds: string[]): string { } export function getJobQueryText(jobIds: string | string[]): string { - return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; + return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : `id:${jobIds}`; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 17ef84179ce63..63b7074ec3aaa 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -7,10 +7,10 @@ import React, { FC, useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiInMemoryTable, EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, EuiSearchBar, EuiSearchBarProps, EuiSpacer, @@ -30,13 +30,12 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar'; import { CreateAnalyticsButton } from '../create_analytics_button'; -import { getSelectedIdFromUrl } from '../../../../../jobs/jobs_list/components/utils'; import { SourceSelection } from '../source_selection'; import { filterAnalytics } from '../../../../common/search_bar_filters'; import { AnalyticsEmptyPrompt } from './empty_prompt'; import { useTableSettings } from './use_table_settings'; import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; -import { getGroupQueryText } from '../../../../../../../common/util/string_utils'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; const filters: EuiSearchBarProps['filters'] = [ { @@ -84,17 +83,28 @@ interface Props { isManagementTable?: boolean; isMlEnabledInSpace?: boolean; blockRefresh?: boolean; + pageState: ListingPageUrlState; + updatePageState: (update: Partial) => void; } export const DataFrameAnalyticsList: FC = ({ isManagementTable = false, isMlEnabledInSpace = true, blockRefresh = false, + pageState, + updatePageState, }) => { + const searchQueryText = pageState.queryText ?? ''; + const setSearchQueryText = useCallback( + (value) => { + updatePageState({ queryText: value }); + }, + [updatePageState] + ); + const [isInitialized, setIsInitialized] = useState(false); const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); const [filteredAnalytics, setFilteredAnalytics] = useState([]); - const [searchQueryText, setSearchQueryText] = useState(''); const [searchError, setSearchError] = useState(); const [analytics, setAnalytics] = useState([]); const [analyticsStats, setAnalyticsStats] = useState( @@ -102,9 +112,6 @@ export const DataFrameAnalyticsList: FC = ({ ); const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [errorMessage, setErrorMessage] = useState(undefined); - // Query text/job_id based on url but only after getAnalytics is done first - // selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly - const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false); const disabled = !checkPermission('canCreateDataFrameAnalytics') || @@ -119,17 +126,20 @@ export const DataFrameAnalyticsList: FC = ({ isManagementTable ); - const updateFilteredItems = (queryClauses: any) => { - if (queryClauses.length) { - const filtered = filterAnalytics(analytics, queryClauses); - setFilteredAnalytics(filtered); - } else { - setFilteredAnalytics(analytics); - } - }; + const updateFilteredItems = useCallback( + (queryClauses: any[]) => { + if (queryClauses.length) { + const filtered = filterAnalytics(analytics, queryClauses); + setFilteredAnalytics(filtered); + } else { + setFilteredAnalytics(analytics); + } + }, + [analytics] + ); const filterList = () => { - if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) { + if (searchQueryText !== '') { // trigger table filtering with query for job id to trigger table filter const query = EuiSearchBar.Query.parse(searchQueryText); let clauses: any = []; @@ -142,27 +152,9 @@ export const DataFrameAnalyticsList: FC = ({ } }; - useEffect(() => { - if (selectedIdFromUrlInitialized === false && analytics.length > 0) { - const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); - let queryText = ''; - - if (groupIds !== undefined) { - queryText = getGroupQueryText(groupIds); - } else if (jobId !== undefined) { - queryText = jobId; - } - - setSelectedIdFromUrlInitialized(true); - setSearchQueryText(queryText); - } else { - filterList(); - } - }, [selectedIdFromUrlInitialized, analytics]); - useEffect(() => { filterList(); - }, [selectedIdFromUrlInitialized, searchQueryText]); + }, [searchQueryText]); const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); @@ -183,19 +175,19 @@ export const DataFrameAnalyticsList: FC = ({ ); const { onTableChange, pagination, sorting } = useTableSettings( - DataFrameAnalyticsListColumn.id, - filteredAnalytics + filteredAnalytics, + pageState, + updatePageState ); const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => { if (search.error !== null) { setSearchError(search.error.message); - return false; + return; } setSearchError(undefined); setSearchQueryText(search.queryText); - return true; }; // Before the analytics have been loaded for the first time, display the loading indicator only. @@ -251,6 +243,7 @@ export const DataFrameAnalyticsList: FC = ({ ); + const search: EuiSearchBarProps = { query: searchQueryText, onChange: handleSearchOnChange, @@ -284,15 +277,13 @@ export const DataFrameAnalyticsList: FC = ({
allowNeutralSort={false} - className="mlAnalyticsInMemoryTable" columns={columns} - error={searchError} hasActions={false} isExpandable={true} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} isSelectable={false} items={analytics} itemId={DataFrameAnalyticsListColumn.id} - itemIdToExpandedRowMap={itemIdToExpandedRowMap} loading={isLoading} onTableChange={onTableChange} pagination={pagination} @@ -302,6 +293,7 @@ export const DataFrameAnalyticsList: FC = ({ rowProps={(item) => ({ 'data-test-subj': `mlAnalyticsTableRow row-${item.id}`, })} + error={searchError} />
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 8c7c8b9db8b64..84c37ac8b816b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -116,14 +116,14 @@ export interface DataFrameAnalyticsListRow { } // Used to pass on attribute names to table columns -export enum DataFrameAnalyticsListColumn { - configDestIndex = 'config.dest.index', - configSourceIndex = 'config.source.index', - configCreateTime = 'config.create_time', - description = 'config.description', - id = 'id', - memoryStatus = 'stats.memory_usage.status', -} +export const DataFrameAnalyticsListColumn = { + configDestIndex: 'config.dest.index', + configSourceIndex: 'config.source.index', + configCreateTime: 'config.create_time', + description: 'config.description', + id: 'id', + memoryStatus: 'stats.memory_usage.status', +} as const; export type ItemIdToExpandedRowMap = Record; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 2b63b9e780819..93868ce0c17e6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -135,13 +135,13 @@ export const progressColumn = { 'data-test-subj': 'mlAnalyticsTableColumnProgress', }; -export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => { +export const DFAnalyticsJobIdLink = ({ jobId }: { jobId: string }) => { const href = useMlLink({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, - pageState: { jobId: item.id }, + pageState: { jobId }, }); - return {item.id}; + return {jobId}; }; export const useColumns = ( @@ -199,13 +199,17 @@ export const useColumns = ( 'data-test-subj': 'mlAnalyticsTableRowDetailsToggle', }, { - name: 'ID', + field: DataFrameAnalyticsListColumn.id, + name: i18n.translate('xpack.ml.dataframe.analyticsList.id', { + defaultMessage: 'ID', + }), sortable: (item: DataFrameAnalyticsListRow) => item.id, truncateText: true, 'data-test-subj': 'mlAnalyticsTableColumnId', scope: 'row', - render: (item: DataFrameAnalyticsListRow) => - isManagementTable ? : item.id, + render: (jobId: string) => { + return isManagementTable ? : jobId; + }, }, { field: DataFrameAnalyticsListColumn.description, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts index 5b7d71dacccf8..68774fb86fe96 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState } from 'react'; import { Direction, EuiBasicTableProps, Pagination, PropertySort } from '@elastic/eui'; +import { useCallback, useMemo } from 'react'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; -const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; // Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange @@ -29,15 +29,6 @@ export interface CriteriaWithPagination extends Criteria { }; } -interface AnalyticsBasicTableSettings { - pageIndex: number; - pageSize: number; - totalItemCount: number; - hidePerPageOptions: boolean; - sortField: keyof T; - sortDirection: Direction; -} - interface UseTableSettingsReturnValue { onTableChange: EuiBasicTableProps['onChange']; pagination: Pagination; @@ -45,49 +36,44 @@ interface UseTableSettingsReturnValue { } export function useTableSettings( - sortByField: keyof TypeOfItem, - items: TypeOfItem[] + items: TypeOfItem[], + pageState: ListingPageUrlState, + updatePageState: (update: Partial) => void ): UseTableSettingsReturnValue { - const [tableSettings, setTableSettings] = useState>({ - pageIndex: 0, - pageSize: PAGE_SIZE, - totalItemCount: 0, - hidePerPageOptions: false, - sortField: sortByField, - sortDirection: 'asc', - }); - - const onTableChange: EuiBasicTableProps['onChange'] = ({ - page = { index: 0, size: PAGE_SIZE }, - sort = { field: sortByField, direction: 'asc' }, - }: CriteriaWithPagination) => { - const { index, size } = page; - const { field, direction } = sort; - - setTableSettings({ - ...tableSettings, - pageIndex: index, - pageSize: size, - sortField: field, - sortDirection: direction, - }); - }; + const { pageIndex, pageSize, sortField, sortDirection } = pageState; - const { pageIndex, pageSize, sortField, sortDirection } = tableSettings; + const onTableChange: EuiBasicTableProps['onChange'] = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + const result = { + pageIndex: page?.index ?? pageState.pageIndex, + pageSize: page?.size ?? pageState.pageSize, + sortField: (sort?.field as string) ?? pageState.sortField, + sortDirection: sort?.direction ?? pageState.sortDirection, + }; + updatePageState(result); + }, + [pageState, updatePageState] + ); - const pagination = { - pageIndex, - pageSize, - totalItemCount: items.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - }; + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount: items.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }), + [items, pageIndex, pageSize] + ); - const sorting = { - sort: { - field: sortField as string, - direction: sortDirection, - }, - }; + const sorting = useMemo( + () => ({ + sort: { + field: sortField as string, + direction: sortDirection as Direction, + }, + }), + [sortField, sortDirection] + ); return { onTableChange, pagination, sorting }; } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx index eaeae6cc64520..a5d3555fcc278 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_navigation_bar/analytics_navigation_bar.tsx @@ -50,9 +50,12 @@ export const AnalyticsNavigationBar: FC<{ selectedTabId?: string; jobId?: string return navTabs; }, [jobId !== undefined]); - const onTabClick = useCallback(async (tab: Tab) => { - await navigateToPath(tab.path, true); - }, []); + const onTabClick = useCallback( + async (tab: Tab) => { + await navigateToPath(tab.path, true); + }, + [navigateToPath] + ); return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts index 7c70a25071640..77c794dce10ce 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/index.ts @@ -6,8 +6,8 @@ export * from './models_list'; -export enum ModelsTableToConfigMapping { - id = 'model_id', - createdAt = 'create_time', - type = 'type', -} +export const ModelsTableToConfigMapping = { + id: 'model_id', + createdAt: 'create_time', + type: 'type', +} as const; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index a87f11df937d3..2d74d08c4550c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -52,6 +52,8 @@ import { filterAnalyticsModels } from '../../../../common/search_bar_filters'; import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; import { timeFormatter } from '../../../../../../../common/util/date_utils'; +import { ListingPageUrlState } from '../../../../../../../common/types/common'; +import { usePageUrlState } from '../../../../../util/url_state'; type Stats = Omit; @@ -63,6 +65,13 @@ export type ModelItem = TrainedModelConfigResponse & { export type ModelItemFull = Required; +export const getDefaultModelsListState = (): ListingPageUrlState => ({ + pageIndex: 0, + pageSize: 10, + sortField: ModelsTableToConfigMapping.id, + sortDirection: 'asc', +}); + export const ModelsList: FC = () => { const { services: { @@ -71,12 +80,24 @@ export const ModelsList: FC = () => { } = useMlKibana(); const urlGenerator = useMlUrlGenerator(); + const [pageState, updatePageState] = usePageUrlState( + ML_PAGES.DATA_FRAME_ANALYTICS_MODELS_MANAGE, + getDefaultModelsListState() + ); + + const searchQueryText = pageState.queryText ?? ''; + const setSearchQueryText = useCallback( + (value) => { + updatePageState({ queryText: value }); + }, + [updatePageState] + ); + const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean; const trainedModelsApiService = useTrainedModelsApiService(); const { toasts } = useNotifications(); - const [searchQueryText, setSearchQueryText] = useState(''); const [filteredModels, setFilteredModels] = useState([]); const [isLoading, setIsLoading] = useState(false); const [items, setItems] = useState([]); @@ -432,8 +453,9 @@ export const ModelsList: FC = () => { : []; const { onTableChange, pagination, sorting } = useTableSettings( - ModelsTableToConfigMapping.id, - filteredModels + filteredModels, + pageState, + updatePageState ); const toolsLeft = ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index c31a3b08aa756..5a17b91818a1c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -33,11 +33,27 @@ import { UpgradeWarning } from '../../../components/upgrade'; import { AnalyticsNavigationBar } from './components/analytics_navigation_bar'; import { ModelsList } from './components/models_management'; import { JobMap } from '../job_map'; +import { usePageUrlState } from '../../../util/url_state'; +import { ListingPageUrlState } from '../../../../../common/types/common'; +import { DataFrameAnalyticsListColumn } from './components/analytics_list/common'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; + +export const getDefaultDFAListState = (): ListingPageUrlState => ({ + pageIndex: 0, + pageSize: 10, + sortField: DataFrameAnalyticsListColumn.id, + sortDirection: 'asc', +}); export const Page: FC = () => { const [blockRefresh, setBlockRefresh] = useState(false); const [globalState] = useUrlState('_g'); + const [dfaPageState, setDfaPageState] = usePageUrlState( + ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + getDefaultDFAListState() + ); + useRefreshInterval(setBlockRefresh); const location = useLocation(); @@ -93,7 +109,11 @@ export const Page: FC = () => { {selectedTabId === 'map' && mapJobId && } {selectedTabId === 'data_frame_analytics' && ( - + )} {selectedTabId === 'models' && } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx index f0fa62b7a3d8a..1b1bea889925f 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx @@ -22,8 +22,6 @@ import { JobGroup } from '../job_group'; import { useMlKibana } from '../../../../contexts/kibana'; interface JobFilterBarProps { - jobId: string; - groupIds: string[]; setFilters: (query: Query | null) => void; queryText?: string; } @@ -75,7 +73,7 @@ export const JobFilterBar: FC = ({ queryText, setFilters }) = useEffect(() => { setFilters(queryInstance); - }, []); + }, [queryText]); const filters: SearchFilterConfig[] = useMemo( () => [ diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts deleted file mode 100644 index b781199c85237..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ /dev/null @@ -1,8 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export function getSelectedIdFromUrl(str: string): { groupIds?: string[]; jobId?: string }; -export function clearSelectedJobIdFromUrl(str: string): void; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 397062248689d..338222e3ac4a2 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -6,7 +6,6 @@ import { each } from 'lodash'; import { i18n } from '@kbn/i18n'; -import rison from 'rison-node'; import { mlJobService } from '../../../services/job_service'; import { @@ -367,31 +366,3 @@ function jobProperty(job, prop) { }; return job[propMap[prop]]; } - -function getUrlVars(url) { - const vars = {}; - url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (_, key, value) { - vars[key] = value; - }); - return vars; -} - -export function getSelectedIdFromUrl(url) { - const result = {}; - if (typeof url === 'string') { - const isGroup = url.includes('groupIds'); - url = decodeURIComponent(url); - - if (url.includes('mlManagement')) { - const urlParams = getUrlVars(url); - const decodedJson = rison.decode(urlParams.mlManagement); - - if (isGroup) { - result.groupIds = decodedJson.groupIds; - } else { - result.jobId = decodedJson.jobId; - } - } - } - return result; -} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts deleted file mode 100644 index 4414be0b4fdcb..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts +++ /dev/null @@ -1,35 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getSelectedIdFromUrl } from './utils'; - -describe('ML - Jobs List utils', () => { - const jobId = 'test_job_id_1'; - const jobIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(jobId:${jobId})`; - const groupIdOne = 'test_group_id_1'; - const groupIdTwo = 'test_group_id_2'; - const groupIdsUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne},${groupIdTwo}))`; - const groupIdUrl = `http://localhost:5601/aql/app/ml#/jobs?mlManagement=(groupIds:!(${groupIdOne}))`; - - describe('getSelectedIdFromUrl', () => { - it('should get selected job id from the url', () => { - const actual = getSelectedIdFromUrl(jobIdUrl); - expect(actual).toStrictEqual({ jobId }); - }); - - it('should get selected group ids from the url', () => { - const expected = { groupIds: [groupIdOne, groupIdTwo] }; - const actual = getSelectedIdFromUrl(groupIdsUrl); - expect(actual).toStrictEqual(expected); - }); - - it('should get selected group id from the url', () => { - const expected = { groupIds: [groupIdOne] }; - const actual = getSelectedIdFromUrl(groupIdUrl); - expect(actual).toStrictEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index 4c6469f6800a7..df50f53b811fa 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useCallback, useMemo } from 'react'; +import React, { FC } from 'react'; import { NavigationMenu } from '../../components/navigation_menu'; // @ts-ignore import { JobsListView } from './components/jobs_list_view/index'; -import { useUrlState } from '../../util/url_state'; +import { usePageUrlState } from '../../util/url_state'; +import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; +import { ListingPageUrlState } from '../../../../common/types/common'; interface JobsPageProps { blockRefresh?: boolean; @@ -17,15 +19,7 @@ interface JobsPageProps { lastRefresh?: number; } -export interface AnomalyDetectionJobsListState { - pageSize: number; - pageIndex: number; - sortField: string; - sortDirection: string; - queryText?: string; -} - -export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsListState => ({ +export const getDefaultAnomalyDetectionJobsListState = (): ListingPageUrlState => ({ pageIndex: 0, pageSize: 10, sortField: 'id', @@ -33,33 +27,15 @@ export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsL }); export const JobsPage: FC = (props) => { - const [appState, setAppState] = useUrlState('_a'); - - const jobListState: AnomalyDetectionJobsListState = useMemo(() => { - return { - ...getDefaultAnomalyDetectionJobsListState(), - ...(appState ?? {}), - }; - }, [appState]); - - const onJobsViewStateUpdate = useCallback( - (update: Partial) => { - setAppState({ - ...jobListState, - ...update, - }); - }, - [appState, setAppState] + const [pageState, setPageState] = usePageUrlState( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + getDefaultAnomalyDetectionJobsListState() ); return (
- +
); }; diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index ad4b9ad78902b..1089484449bab 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -35,11 +35,10 @@ import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_vi import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; -import { - AnomalyDetectionJobsListState, - getDefaultAnomalyDetectionJobsListState, -} from '../../../../jobs/jobs_list/jobs'; +import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs'; import { getMlGlobalServices } from '../../../../app'; +import { ListingPageUrlState } from '../../../../../../common/types/common'; +import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page'; interface Tab { 'data-test-subj': string; @@ -48,21 +47,28 @@ interface Tab { content: any; } -function useTabs(isMlEnabledInSpace: boolean): Tab[] { - const [jobsViewState, setJobsViewState] = useState( - getDefaultAnomalyDetectionJobsListState() - ); +function usePageState( + defaultState: T +): [T, (update: Partial) => void] { + const [pageState, setPageState] = useState(defaultState); const updateState = useCallback( - (update: Partial) => { - setJobsViewState({ - ...jobsViewState, + (update: Partial) => { + setPageState({ + ...pageState, ...update, }); }, - [jobsViewState] + [pageState] ); + return [pageState, updateState]; +} + +function useTabs(isMlEnabledInSpace: boolean): Tab[] { + const [adPageState, updateAdPageState] = usePageState(getDefaultAnomalyDetectionJobsListState()); + const [dfaPageState, updateDfaPageState] = usePageState(getDefaultDFAListState()); + return useMemo( () => [ { @@ -75,8 +81,8 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { @@ -95,12 +101,14 @@ function useTabs(isMlEnabledInSpace: boolean): Tab[] { ), }, ], - [isMlEnabledInSpace, jobsViewState, updateState] + [isMlEnabledInSpace, adPageState, updateAdPageState, dfaPageState, updateDfaPageState] ); } diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index a3c70e1130904..448a888ab32c2 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -13,6 +13,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import { Dictionary } from '../../../common/types/common'; import { getNestedProperty } from './object_utils'; +import { MlPages } from '../../../common/constants/ml_url_generator'; type Accessor = '_a' | '_g'; export type SetUrlState = ( @@ -150,3 +151,35 @@ export const useUrlState = (accessor: Accessor) => { ); return [urlState, setUrlState]; }; + +/** + * Hook for managing the URL state of the page. + */ +export const usePageUrlState = ( + pageKey: MlPages, + defaultState: PageUrlState +): [PageUrlState, (update: Partial) => void] => { + const [appState, setAppState] = useUrlState('_a'); + const pageState = appState?.[pageKey]; + + const resultPageState: PageUrlState = useMemo(() => { + return { + ...defaultState, + ...(pageState ?? {}), + }; + }, [pageState]); + + const onStateUpdate = useCallback( + (update: Partial, replace?: boolean) => { + setAppState(pageKey, { + ...(replace ? {} : resultPageState), + ...update, + }); + }, + [pageKey, resultPageState, setAppState] + ); + + return useMemo(() => { + return [resultPageState, onStateUpdate]; + }, [resultPageState, onStateUpdate]); +}; diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index 717d293ccd7fa..6d7e286a29476 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -19,8 +19,8 @@ import type { import { ML_PAGES } from '../../common/constants/ml_url_generator'; import { createGenericMlUrl } from './common'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; -import type { AnomalyDetectionJobsListState } from '../application/jobs/jobs_list/jobs'; import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils'; +import { AppPageState, ListingPageUrlState } from '../../common/types/common'; /** * Creates URL to the Anomaly Detection Job management page */ @@ -41,11 +41,15 @@ export function createAnomalyDetectionJobManagementUrl( if (groupIds) { queryTextArr.push(getGroupQueryText(groupIds)); } - const queryState: Partial = { + const jobsListState: Partial = { ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), }; - url = setStateToKbnUrl>( + const queryState: AppPageState = { + [ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE]: jobsListState, + }; + + url = setStateToKbnUrl>( '_a', queryState, { useHash: false, storeInHashQuery: false }, diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 6c58a9d28bcc2..dc9c3bd86cc63 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -10,12 +10,13 @@ import { DataFrameAnalyticsExplorationQueryState, DataFrameAnalyticsExplorationUrlState, - DataFrameAnalyticsQueryState, DataFrameAnalyticsUrlState, MlCommonGlobalState, } from '../../common/types/ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; +import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils'; +import { AppPageState, ListingPageUrlState } from '../../common/types/common'; export function createDataFrameAnalyticsJobManagementUrl( appBasePath: string, @@ -26,13 +27,23 @@ export function createDataFrameAnalyticsJobManagementUrl( if (mlUrlGeneratorState) { const { jobId, groupIds, globalState } = mlUrlGeneratorState; if (jobId || groupIds) { - const queryState: Partial = { - jobId, - groupIds, + const queryTextArr = []; + if (jobId) { + queryTextArr.push(getJobQueryText(jobId)); + } + if (groupIds) { + queryTextArr.push(getGroupQueryText(groupIds)); + } + const jobsListState: Partial = { + ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), + }; + + const queryState: AppPageState = { + [ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE]: jobsListState, }; - url = setStateToKbnUrl>( - 'mlManagement', + url = setStateToKbnUrl>( + '_a', queryState, { useHash: false, storeInHashQuery: false }, url diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts index e7f12ead3ffe9..3f3d88f1a31d9 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -30,7 +30,7 @@ describe('MlUrlGenerator', () => { jobId: 'fq_single_1', }, }); - expect(url).toBe('/app/ml/jobs?_a=(queryText:fq_single_1)'); + expect(url).toBe("/app/ml/jobs?_a=(jobs:(queryText:'id:fq_single_1'))"); }); it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { @@ -40,7 +40,9 @@ describe('MlUrlGenerator', () => { groupIds: ['farequote', 'categorization'], }, }); - expect(url).toBe("/app/ml/jobs?_a=(queryText:'groups:(farequote%20or%20categorization)')"); + expect(url).toBe( + "/app/ml/jobs?_a=(jobs:(queryText:'groups:(farequote%20or%20categorization)'))" + ); }); it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { @@ -180,7 +182,9 @@ describe('MlUrlGenerator', () => { jobId: 'grid_regression_1', }, }); - expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(jobId:grid_regression_1)'); + expect(url).toBe( + "/app/ml/data_frame_analytics?_a=(data_frame_analytics:(queryText:'id:grid_regression_1'))" + ); }); it('should generate valid URL for the Data Frame Analytics job management page with groupIds', async () => { @@ -190,7 +194,9 @@ describe('MlUrlGenerator', () => { groupIds: ['group_1', 'group_2'], }, }); - expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(groupIds:!(group_1,group_2))'); + expect(url).toBe( + "/app/ml/data_frame_analytics?_a=(data_frame_analytics:(queryText:'groups:(group_1%20or%20group_2)'))" + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx index b0965f8708558..90ab5c2f888fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -55,7 +55,9 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual('/app/ml/jobs?_a=(queryText:linux_anomalous_network_activity_ecs)') + expect(href).toEqual( + "/app/ml/jobs?_a=(jobs:(queryText:'id:linux_anomalous_network_activity_ecs'))" + ) ); }); @@ -72,7 +74,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual("/app/ml/jobs?_a=(queryText:'job%20id%20with%20spaces')") + expect(href).toEqual("/app/ml/jobs?_a=(jobs:(queryText:'id:job%20id%20with%20spaces'))") ); }); From 31014bfc9aa7df1ad71803d2be6bf23232fcbb02 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 17 Nov 2020 19:46:45 +0000 Subject: [PATCH 44/99] [docker] removes workaround for missing crypto-policies-scripts subpackage (#83455) * Revert "[build] Use 8.2 tag of ubi-minimal (#82688)" This reverts commit a89176e265fbdeedf9d514636addec51baa51533. * chore(NA): remove workaround for missing crypto-policies-scripts subpackage Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/build/tasks/os_packages/docker_generator/run.ts | 2 +- .../os_packages/docker_generator/templates/Dockerfile | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 8679cce9b11fc..19487efe1366c 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -40,7 +40,7 @@ export async function runDockerGenerator( ubi: boolean = false ) { // UBI var config - const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:8.2' : 'centos:8'; + const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8'; const ubiVersionTag = 'ubi8'; const ubiImageFlavor = ubi ? `-${ubiVersionTag}` : ''; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index d17b597eb6648..9c78821f331e4 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -46,16 +46,9 @@ EXPOSE 5601 {{#ubi}} # https://github.com/rpm-software-management/microdnf/issues/50 RUN mkdir -p /run/user/$(id -u) - - # crypto-policies not currently compatible with libnss :sadpanda: - RUN printf "[main]\nexclude=crypto-policies" > /etc/dnf/dnf.conf {{/ubi}} RUN for iter in {1..10}; do \ - {{#ubi}} - # update microdnf to have exclusion feature for dnf configuration - {{packageManager}} update microdnf --setopt=tsflags=nodocs -y && \ - {{/ubi}} {{packageManager}} update --setopt=tsflags=nodocs -y && \ {{packageManager}} install --setopt=tsflags=nodocs -y \ fontconfig freetype shadow-utils libnss3.so {{#ubi}}findutils{{/ubi}} && \ From 24c8b82b2c8f164ade666aaeddb5f97d1efdb1d4 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 17 Nov 2020 14:09:09 -0600 Subject: [PATCH 45/99] [rpm] Create default environment file at "/etc/sysconfig/kibana" (#82144) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/build/tasks/os_packages/run_fpm.ts | 7 +++++++ .../service_templates/{systemd/etc/default => env}/kibana | 0 2 files changed, 7 insertions(+) rename src/dev/build/tasks/os_packages/service_templates/{systemd/etc/default => env}/kibana (100%) diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index 7dff592eb9b83..cd39f6c7c256e 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -45,6 +45,8 @@ export async function runFpm( } }; + const envFolder = type === 'rpm' ? 'sysconfig' : 'default'; + const args = [ // Force output even if it will overwrite an existing file '--force', @@ -140,6 +142,11 @@ export async function runFpm( // copy package configurations `${resolveWithTrailingSlash(__dirname, 'service_templates/systemd/')}=/`, + + `${resolveWithTrailingSlash( + __dirname, + 'service_templates/env/kibana' + )}=/etc/${envFolder}/kibana`, ]; log.debug('calling fpm with args:', args); diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana b/src/dev/build/tasks/os_packages/service_templates/env/kibana similarity index 100% rename from src/dev/build/tasks/os_packages/service_templates/systemd/etc/default/kibana rename to src/dev/build/tasks/os_packages/service_templates/env/kibana From b328492c0fda1dbcb7077ac9e6e49cdc983c1518 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 17 Nov 2020 14:14:54 -0600 Subject: [PATCH 46/99] [deb/rpm] Create keystore after installation (#76465) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/cli/dev.js | 1 + src/cli/dist.js | 1 + src/cli_keystore/dev.js | 2 +- .../os_packages/package_scripts/post_install.sh | 15 ++++++++++++++- .../os_packages/package_scripts/post_trans.sh | 8 ++++++++ src/dev/build/tasks/os_packages/run_fpm.ts | 2 ++ src/setup_node_env/no_transpilation.js | 1 - 7 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/dev/build/tasks/os_packages/package_scripts/post_trans.sh diff --git a/src/cli/dev.js b/src/cli/dev.js index a284c82dfeb6e..99b7c1a696e49 100644 --- a/src/cli/dev.js +++ b/src/cli/dev.js @@ -19,4 +19,5 @@ require('../apm')(process.env.ELASTIC_APM_SERVICE_NAME || 'kibana-proxy'); require('../setup_node_env'); +require('../setup_node_env/root'); require('./cli'); diff --git a/src/cli/dist.js b/src/cli/dist.js index 05f0a68aa495c..bc14a3b530356 100644 --- a/src/cli/dist.js +++ b/src/cli/dist.js @@ -19,4 +19,5 @@ require('../apm')(); require('../setup_node_env/dist'); +require('../setup_node_env/root'); require('./cli'); diff --git a/src/cli_keystore/dev.js b/src/cli_keystore/dev.js index 12dc51134aad7..c229d26439bb5 100644 --- a/src/cli_keystore/dev.js +++ b/src/cli_keystore/dev.js @@ -17,5 +17,5 @@ * under the License. */ -require('../setup_node_env'); +require('../setup_node_env/no_transpilation'); require('./cli_keystore'); diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh index 6eb111e066c83..83ed630058041 100644 --- a/src/dev/build/tasks/os_packages/package_scripts/post_install.sh +++ b/src/dev/build/tasks/os_packages/package_scripts/post_install.sh @@ -44,9 +44,11 @@ case $1 in IS_UPGRADE=true fi + PACKAGE=deb setup ;; abort-deconfigure|abort-upgrade|abort-remove) + PACKAGE=deb ;; # Red Hat @@ -63,7 +65,8 @@ case $1 in if [ "$1" = "2" ]; then IS_UPGRADE=true fi - + + PACKAGE=rpm setup ;; @@ -86,3 +89,13 @@ if [ "$IS_UPGRADE" = "true" ]; then echo " OK" fi fi + +# the equivalent code for rpm is in posttrans +if [ "$PACKAGE" = "deb" ]; then + if [ ! -f "${KBN_PATH_CONF}"/kibana.keystore ]; then + /usr/share/kibana/bin/kibana-keystore create + chown root:<%= group %> "${KBN_PATH_CONF}"/kibana.keystore + chmod 660 "${KBN_PATH_CONF}"/kibana.keystore + md5sum "${KBN_PATH_CONF}"/kibana.keystore > "${KBN_PATH_CONF}"/.kibana.keystore.initial_md5sum + fi +fi diff --git a/src/dev/build/tasks/os_packages/package_scripts/post_trans.sh b/src/dev/build/tasks/os_packages/package_scripts/post_trans.sh new file mode 100644 index 0000000000000..3c1bd3ccf88b4 --- /dev/null +++ b/src/dev/build/tasks/os_packages/package_scripts/post_trans.sh @@ -0,0 +1,8 @@ +export KBN_PATH_CONF=${KBN_PATH_CONF:-<%= configDir %>} + +if [ ! -f "${KBN_PATH_CONF}"/kibana.keystore ]; then + /usr/share/kibana/bin/kibana-keystore create + chown root:<%= group %> "${KBN_PATH_CONF}"/kibana.keystore + chmod 660 "${KBN_PATH_CONF}"/kibana.keystore + md5sum "${KBN_PATH_CONF}"/kibana.keystore > "${KBN_PATH_CONF}"/.kibana.keystore.initial_md5sum +fi diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index cd39f6c7c256e..def0289f53641 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -94,6 +94,8 @@ export async function runFpm( resolve(__dirname, 'package_scripts/pre_remove.sh'), '--after-remove', resolve(__dirname, 'package_scripts/post_remove.sh'), + '--rpm-posttrans', + resolve(__dirname, 'package_scripts/post_trans.sh'), // tell fpm about the config file so that it is called out in the package definition '--config-files', diff --git a/src/setup_node_env/no_transpilation.js b/src/setup_node_env/no_transpilation.js index 71fdfa5ad29ea..e989fedcec66f 100644 --- a/src/setup_node_env/no_transpilation.js +++ b/src/setup_node_env/no_transpilation.js @@ -24,5 +24,4 @@ require('./harden'); require('symbol-observable'); require('source-map-support/register'); -require('./root'); require('./node_version_validator'); From 62436e3f03ff4803fd6f0cac55b7cb8588f7ac18 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 17 Nov 2020 21:15:41 +0100 Subject: [PATCH 47/99] Functional tests - fix esArchive mappings with runtime fields (#83530) * Functional tests - fix esArchive mappings with runtime fields * Modify jenkinsfile to run with unverified ES snapshot * Adjust security index fields test for the new mapping structure Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- Jenkinsfile | 1 + .../apis/security/index_fields.ts | 28 +++++++-------- .../logstash_functional/mappings.json | 35 ++++++++++--------- .../security/flstest/data/mappings.json | 17 ++++----- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 3b68cde206573..e33bf67f28df5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,6 @@ #!/bin/groovy +env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = 'true' library 'kibana-pipeline-library' kibanaLibrary.load() diff --git a/x-pack/test/api_integration/apis/security/index_fields.ts b/x-pack/test/api_integration/apis/security/index_fields.ts index 193d0eea1590e..b0462143a8ad4 100644 --- a/x-pack/test/api_integration/apis/security/index_fields.ts +++ b/x-pack/test/api_integration/apis/security/index_fields.ts @@ -6,15 +6,17 @@ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; - -interface FLSFieldMappingResponse { +interface FLSMappingResponse { flstest: { mappings: { - [fieldName: string]: { - mapping: { - [fieldName: string]: { - type: string; - }; + runtime?: { + [fieldName: string]: { + type: string; + }; + }; + properties: { + [fieldName: string]: { + type: string; }; }; }; @@ -56,16 +58,10 @@ export default function ({ getService }: FtrProviderContext) { it('should not include runtime fields', async () => { // First, make sure the mapping actually includes a runtime field - const fieldMapping = (await es.indices.getFieldMapping({ - index: 'flstest', - fields: '*', - includeDefaults: true, - })) as FLSFieldMappingResponse; + const mapping = (await es.indices.getMapping({ index: 'flstest' })) as FLSMappingResponse; - expect(Object.keys(fieldMapping.flstest.mappings)).to.contain('runtime_customer_ssn'); - expect( - fieldMapping.flstest.mappings.runtime_customer_ssn.mapping.runtime_customer_ssn.type - ).to.eql('runtime'); + expect(Object.keys(mapping.flstest.mappings)).to.contain('runtime'); + expect(Object.keys(mapping.flstest.mappings.runtime!)).to.contain('runtime_customer_ssn'); // Now, make sure it's not returned here const { body: actualFields } = (await supertest diff --git a/x-pack/test/functional/es_archives/logstash_functional/mappings.json b/x-pack/test/functional/es_archives/logstash_functional/mappings.json index ee7feedd77530..12853523615bd 100644 --- a/x-pack/test/functional/es_archives/logstash_functional/mappings.json +++ b/x-pack/test/functional/es_archives/logstash_functional/mappings.json @@ -19,6 +19,12 @@ } } ], + "runtime": { + "runtime_number": { + "type": "long", + "script" : { "source" : "emit(doc['bytes'].value)" } + } + }, "properties": { "@message": { "fields": { @@ -342,11 +348,6 @@ } }, "type": "text" - }, - "runtime_number": { - "type": "runtime", - "runtime_type" : "long", - "script" : { "source" : "emit(doc['bytes'].value)" } } } }, @@ -389,6 +390,12 @@ } } ], + "runtime": { + "runtime_number": { + "type": "long", + "script" : { "source" : "emit(doc['bytes'].value)" } + } + }, "properties": { "@message": { "fields": { @@ -712,11 +719,6 @@ } }, "type": "text" - }, - "runtime_number": { - "type": "runtime", - "runtime_type" : "long", - "script" : { "source" : "emit(doc['bytes'].value)" } } } }, @@ -759,6 +761,12 @@ } } ], + "runtime": { + "runtime_number": { + "type": "long", + "script" : { "source" : "emit(doc['bytes'].value)" } + } + }, "properties": { "@message": { "fields": { @@ -1082,11 +1090,6 @@ } }, "type": "text" - }, - "runtime_number": { - "type": "runtime", - "runtime_type" : "long", - "script" : { "source" : "emit(doc['bytes'].value)" } } } }, @@ -1106,4 +1109,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/security/flstest/data/mappings.json b/x-pack/test/functional/es_archives/security/flstest/data/mappings.json index 3605533618a93..4f419e4b6ade4 100644 --- a/x-pack/test/functional/es_archives/security/flstest/data/mappings.json +++ b/x-pack/test/functional/es_archives/security/flstest/data/mappings.json @@ -3,6 +3,14 @@ "value": { "index": "flstest", "mappings": { + "runtime": { + "runtime_customer_ssn": { + "type": "keyword", + "script": { + "source": "emit(doc['customer_ssn'].value + ' calculated at runtime')" + } + } + }, "properties": { "customer_name": { "fields": { @@ -30,13 +38,6 @@ } }, "type": "text" - }, - "runtime_customer_ssn": { - "type": "runtime", - "runtime_type": "keyword", - "script": { - "source": "emit(doc['customer_ssn'].value + ' calculated at runtime')" - } } } }, @@ -47,4 +48,4 @@ } } } -} \ No newline at end of file +} From bcc2afa9e2a7db1165d0f33c5de1525a21d63946 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Tue, 17 Nov 2020 14:40:02 -0600 Subject: [PATCH 48/99] Don't show loading screen during auto-reload (#83376) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../metrics/inventory_view/components/layout.tsx | 14 +++++++++++++- .../inventory_view/components/nodes_overview.tsx | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index f6e9d45c4d225..76512b8a366c5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import useInterval from 'react-use/lib/useInterval'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; @@ -32,6 +32,7 @@ import { BottomDrawer } from './bottom_drawer'; import { Legend } from './waffle/legend'; export const Layout = () => { + const [showLoading, setShowLoading] = useState(true); const { sourceId, source } = useSourceContext(); const { currentView, shouldLoadDefault } = useSavedViewContext(); const { @@ -100,6 +101,16 @@ export const Layout = () => { } }, [reload, currentView, shouldLoadDefault]); + useEffect(() => { + setShowLoading(true); + }, [options.metric, nodeType]); + + useEffect(() => { + const hasNodes = nodes && nodes.length; + // Don't show loading screen when we're auto-reloading + setShowLoading(!hasNodes); + }, [nodes]); + return ( <> @@ -130,6 +141,7 @@ export const Layout = () => { options={options} nodeType={nodeType} loading={loading} + showLoading={showLoading} reload={reload} onDrilldown={applyFilterQuery} currentTime={currentTime} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index aa6157dc48d5c..9b6853dcdc751 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -37,6 +37,7 @@ interface Props { formatter: InfraFormatter; bottomMargin: number; topMargin: number; + showLoading: boolean; } export const NodesOverview = ({ @@ -53,6 +54,7 @@ export const NodesOverview = ({ onDrilldown, bottomMargin, topMargin, + showLoading, }: Props) => { const handleDrilldown = useCallback( (filter: string) => { @@ -66,7 +68,8 @@ export const NodesOverview = ({ ); const noData = !loading && nodes && nodes.length === 0; - if (loading) { + if (loading && showLoading) { + // Don't show loading screen when we're auto-reloading return ( Date: Tue, 17 Nov 2020 14:29:57 -0700 Subject: [PATCH 49/99] Upgrade EUI to v30.2.0 (#82730) * eui to v30.2.0 * src snapshot updates * x-pack euipanel snapshot updates * x-pack external link updates * security_solution_cypress external link text * clean up --- package.json | 2 +- .../dashboard_empty_screen.test.tsx.snap | 4 +- .../CustomLinkFlyout/link_preview.test.tsx | 15 ++++- .../__snapshots__/asset.stories.storyshot | 4 +- .../asset_manager.stories.storyshot | 6 +- .../custom_element_modal.stories.storyshot | 8 +-- .../element_card.stories.storyshot | 8 +-- .../element_grid.stories.storyshot | 6 +- .../saved_elements_modal.stories.storyshot | 8 +-- .../shape_picker_popover.stories.storyshot | 6 +- .../__snapshots__/settings.test.tsx.snap | 10 ++-- .../__snapshots__/policy_table.test.tsx.snap | 2 +- .../__snapshots__/add_license.test.js.snap | 4 +- .../request_trial_extension.test.js.snap | 8 +-- .../revert_to_basic.test.js.snap | 6 +- .../__snapshots__/start_trial.test.js.snap | 8 +-- .../upload_license.test.tsx.snap | 10 ++-- .../upgrade_failure.test.js.snap | 6 +- .../__snapshots__/no_data.test.js.snap | 4 +- .../__snapshots__/exporters.test.js.snap | 20 +++++++ .../__snapshots__/reason_found.test.js.snap | 20 +++++++ .../__snapshots__/page_loading.test.js.snap | 2 +- .../api_keys_grid_page.test.tsx.snap | 27 ++++++++- .../roles_grid_page.test.tsx.snap | 2 +- .../reset_session_page.test.tsx.snap | 2 +- .../common/shared_exports.ts | 2 +- .../security_solution/common/test_utils.ts | 6 ++ .../alerts_detection_rules_custom.spec.ts | 9 ++- .../alerts_detection_rules_eql.spec.ts | 9 ++- .../alerts_detection_rules_ml.spec.ts | 9 ++- .../alerts_detection_rules_override.spec.ts | 9 ++- .../alerts_detection_rules_threshold.spec.ts | 9 ++- .../cypress/screens/rule_details.ts | 3 + .../common/components/links/index.test.tsx | 11 ++-- .../markdown_editor/renderer.test.tsx | 7 ++- .../__snapshots__/index.test.tsx.snap | 10 ++++ .../__snapshots__/index.test.tsx.snap | 60 +++++++++---------- .../__snapshots__/index.test.tsx.snap | 2 +- .../network/components/port/index.test.tsx | 5 +- .../source_destination/index.test.tsx | 15 +++-- .../source_destination_ip.test.tsx | 25 +++++--- .../certificate_fingerprint/index.test.tsx | 9 ++- .../components/ja3_fingerprint/index.test.tsx | 7 ++- .../components/netflow/index.test.tsx | 45 ++++++++------ .../note_card_body.test.tsx.snap | 10 ++++ .../body/renderers/get_row_renderer.test.tsx | 7 ++- .../suricata/suricata_details.test.tsx | 3 +- .../suricata/suricata_row_renderer.test.tsx | 3 +- .../system/generic_row_renderer.test.tsx | 15 ++--- .../body/renderers/zeek/zeek_details.test.tsx | 11 ++-- .../renderers/zeek/zeek_row_renderer.test.tsx | 3 +- .../renderers/zeek/zeek_signature.test.tsx | 3 +- .../components/health_check.test.tsx | 14 +++-- .../__snapshots__/expanded_row.test.tsx.snap | 10 ++++ .../monitor_status.bar.test.tsx.snap | 10 ++++ .../__snapshots__/empty_state.test.tsx.snap | 8 +-- .../__snapshots__/monitor_list.test.tsx.snap | 2 +- yarn.lock | 8 +-- 58 files changed, 371 insertions(+), 186 deletions(-) diff --git a/package.json b/package.json index b45789172cee9..a2c085c0424b1 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "7.10.0-rc.1", "@elastic/ems-client": "7.10.0", - "@elastic/eui": "30.1.1", + "@elastic/eui": "30.2.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/node-crypto": "1.2.1", diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 31ef011835917..dac84c87faf97 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -297,7 +297,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` paddingSize="none" >
+ str.replace(/\(opens in a new tab or window\)/g, ''); + describe('LinkPreview', () => { let callApmApiSpy: jest.SpyInstance; beforeAll(() => { @@ -53,7 +56,9 @@ describe('LinkPreview', () => { ); expect(getElementValue(container, 'preview-label')).toEqual('foo'); expect( - (getByTestId(container, 'preview-link') as HTMLAnchorElement).text + removeExternalLinkText( + (getByTestId(container, 'preview-link') as HTMLAnchorElement).text + ) ).toEqual('https://baz.co'); }); }); @@ -69,7 +74,9 @@ describe('LinkPreview', () => { ); expect(getElementValue(container, 'preview-label')).toEqual('foo'); expect( - (getByTestId(container, 'preview-link') as HTMLAnchorElement).text + removeExternalLinkText( + (getByTestId(container, 'preview-link') as HTMLAnchorElement).text + ) ).toEqual('https://baz.co?service.name={{invalid}'); expect(getByTestId(container, 'preview-warning')).toBeInTheDocument(); }); @@ -85,7 +92,9 @@ describe('LinkPreview', () => { await waitFor(() => expect(callApmApiSpy).toHaveBeenCalled()); expect(getElementValue(container, 'preview-label')).toEqual('foo'); expect( - (getByTestId(container, 'preview-link') as HTMLAnchorElement).text + removeExternalLinkText( + (getByTestId(container, 'preview-link') as HTMLAnchorElement).text + ) ).toEqual('https://baz.co?transaction=foo'); }); }); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot index e62785272a5ec..05339ca558562 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset.stories.storyshot @@ -12,7 +12,7 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` className="euiFlexItem" >
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap index 06cd77558b443..ba2ec28bf6bc1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap @@ -50,7 +50,7 @@ exports[`policy table should show empty state when there are not any policies 1` class="euiPageBody" >
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is active should display correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; -exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; +exports[`AddLicense component when license is expired should display with correct verbiage 1`] = `"
Update your license

If you already have a new license, upload it now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index db37bc6cb98c4..719fce35a2a68 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features(opens in a new tab or window), request an extension now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index 24598635b28e3..a0f3948785a80 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features(opens in a new tab or window).

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 24872a888b090..52a2da596c10e 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features(opens in a new tab or window) have to offer.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 72c04992566bd..cc41a3a4b4e22 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -270,7 +270,7 @@ exports[`UploadLicense should display a modal when license requires acknowledgem paddingSize="l" >
cloud dashboard. + + + (opens in a new tab or window) + For more information on Monitoring in Elastic Cloud, please see the documentation. + + + (opens in a new tab or window) +

diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap index e7b88e23c5f68..9e3b7c0e25d5d 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap @@ -185,6 +185,16 @@ Array [ target="_blank" > cloud dashboard. + + + (opens in a new tab or window) + For more information on Monitoring in Elastic Cloud, please see the documentation. + + + (opens in a new tab or window) +

diff --git a/x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap index f4d8232b2e340..fa223a2fe57e1 100644 --- a/x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap @@ -9,7 +9,7 @@ exports[`PageLoading should show a simple page loading component 1`] = ` class="euiPageBody" >
docs + + + + + + + (opens in a new tab or window) + + + to enable API keys. @@ -119,7 +144,7 @@ exports[`APIKeysGridPage renders permission denied if user does not have require paddingSize="l" >
renders permission denied if required 1`] = ` paddingSize="l" >
MockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected 1`] = `"MockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; diff --git a/x-pack/plugins/security_solution/common/shared_exports.ts b/x-pack/plugins/security_solution/common/shared_exports.ts index bee2e54d0e3ea..fb457933f4b54 100644 --- a/x-pack/plugins/security_solution/common/shared_exports.ts +++ b/x-pack/plugins/security_solution/common/shared_exports.ts @@ -13,7 +13,7 @@ export { DefaultVersionNumberDecoded, } from './detection_engine/schemas/types/default_version_number'; export { exactCheck } from './exact_check'; -export { getPaths, foldLeftRight } from './test_utils'; +export { getPaths, foldLeftRight, removeExternalLinkText } from './test_utils'; export { validate, validateEither } from './validate'; export { formatErrors } from './format_errors'; export { migratePackagePolicyToV7110 } from './endpoint/policy/migrations/to_v7_11.0'; diff --git a/x-pack/plugins/security_solution/common/test_utils.ts b/x-pack/plugins/security_solution/common/test_utils.ts index b96639ad7b034..2cdce67f364dc 100644 --- a/x-pack/plugins/security_solution/common/test_utils.ts +++ b/x-pack/plugins/security_solution/common/test_utils.ts @@ -41,3 +41,9 @@ export const getPaths = (validation: t.Validation): string[] => { ) ); }; + +/** + * Convenience utility to remove text appended to links by EUI + */ +export const removeExternalLinkText = (str: string) => + str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 783f8be840b7f..fb1f2920aaceb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -49,6 +49,7 @@ import { DEFINITION_DETAILS, FALSE_POSITIVES_DETAILS, getDetails, + removeExternalLinkText, INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, @@ -174,9 +175,13 @@ describe('Custom detection rules creation', () => { cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newRule.severity); getDetails(RISK_SCORE_DETAILS).should('have.text', newRule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts index 3d4aaca8bb78f..22d2a144932bf 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -32,6 +32,7 @@ import { DEFINITION_DETAILS, FALSE_POSITIVES_DETAILS, getDetails, + removeExternalLinkText, INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, @@ -136,9 +137,13 @@ describe('Detection rules, EQL', () => { cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', eqlRule.severity); getDetails(RISK_SCORE_DETAILS).should('have.text', eqlRule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index 153c55fae59fe..061b66faca054 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -23,6 +23,7 @@ import { DEFINITION_DETAILS, FALSE_POSITIVES_DETAILS, getDetails, + removeExternalLinkText, MACHINE_LEARNING_JOB_ID, MACHINE_LEARNING_JOB_STATUS, MITRE_ATTACK_DETAILS, @@ -122,9 +123,13 @@ describe('Detection rules, machine learning', () => { cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity); getDetails(RISK_SCORE_DETAILS).should('have.text', machineLearningRule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); cy.get(DEFINITION_DETAILS).within(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index e905365d1bbb3..b1ccca5e4f13c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -34,6 +34,7 @@ import { DETAILS_TITLE, FALSE_POSITIVES_DETAILS, getDetails, + removeExternalLinkText, INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, @@ -141,9 +142,13 @@ describe('Detection rules, override', () => { `${newOverrideRule.riskOverride}signal.rule.risk_score` ); getDetails(RULE_NAME_OVERRIDE_DETAILS).should('have.text', newOverrideRule.nameOverride); - getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); getDetails(TAGS_DETAILS).should('have.text', expectedTags); getDetails(TIMESTAMP_OVERRIDE_DETAILS).should('have.text', newOverrideRule.timestampOverride); cy.contains(DETAILS_TITLE, 'Severity override') diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index a9b43d82bb7fd..c3e7892d63279 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -32,6 +32,7 @@ import { FALSE_POSITIVES_DETAILS, DEFINITION_DETAILS, getDetails, + removeExternalLinkText, INDEX_PATTERNS_DETAILS, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, @@ -135,9 +136,13 @@ describe('Detection rules, threshold', () => { cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newThresholdRule.severity); getDetails(RISK_SCORE_DETAILS).should('have.text', newThresholdRule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should('have.text', expectedUrls); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should('have.text', expectedMitre); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); getDetails(TAGS_DETAILS).should('have.text', expectedTags); }); cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index d72210dd3e083..8cf0dfb5f6661 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -75,3 +75,6 @@ export const TIMESTAMP_OVERRIDE_DETAILS = 'Timestamp override'; export const getDetails = (title: string) => cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); + +export const removeExternalLinkText = (str: string) => + str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index 2ae5b1d20c3c3..ed7ec77b4f39b 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -6,6 +6,7 @@ import { mount, shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; +import { removeExternalLinkText } from '../../../../common/test_utils'; import { mountWithIntl } from '@kbn/test/jest'; import { encodeIpv6 } from '../../lib/helpers'; @@ -92,7 +93,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(wrapper.text()).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders props passed in as link', () => { @@ -448,7 +449,7 @@ describe('Custom Links', () => { describe('WhoisLink', () => { test('it renders ip passed in as domain', () => { const wrapper = mountWithIntl({'Example Link'}); - expect(wrapper.text()).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders correct href', () => { @@ -473,7 +474,7 @@ describe('Custom Links', () => { {'Example Link'} ); - expect(wrapper.text()).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders correct href', () => { @@ -504,7 +505,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(wrapper.text()).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders correct href', () => { @@ -533,7 +534,7 @@ describe('Custom Links', () => { const wrapper = mountWithIntl( {'Example Link'} ); - expect(wrapper.text()).toEqual('Example Link'); + expect(removeExternalLinkText(wrapper.text())).toEqual('Example Link'); }); test('it renders correct href when port is a number', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx index e6a38863d7e5f..8bb652816a975 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/renderer.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; +import { removeExternalLinkText } from '../../../../common/test_utils'; import { MarkdownRenderer } from './renderer'; describe('Markdown', () => { @@ -16,9 +17,9 @@ describe('Markdown', () => { test('it renders the expected link text', () => { const wrapper = mount({markdownWithLink}); - expect(wrapper.find('[data-test-subj="markdown-link"]').first().text()).toEqual( - 'External Site' - ); + expect( + removeExternalLinkText(wrapper.find('[data-test-subj="markdown-link"]').first().text()) + ).toEqual('External Site'); }); test('it renders the expected href', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index 6331a2e02b219..6d114258224d1 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -39,6 +39,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta "euiBorderColor": "#343741", "euiBorderEditable": "2px dotted #343741", "euiBorderRadius": "4px", + "euiBorderRadiusSmall": "2px", "euiBorderThick": "2px solid #343741", "euiBorderThin": "1px solid #343741", "euiBorderWidthThick": "2px", @@ -375,6 +376,15 @@ exports[`Paginated Table Component rendering it renders the default load more ta }, }, "euiPaletteColorBlindKeys": "'euiColorVis0', 'euiColorVis1', 'euiColorVis2', 'euiColorVis3', 'euiColorVis4', 'euiColorVis5', 'euiColorVis6', 'euiColorVis7', 'euiColorVis8', 'euiColorVis9'", + "euiPanelBackgroundColorModifiers": Object { + "plain": "#1d1e24", + "subdued": "#25262e", + "transparent": "rgba(0, 0, 0, 0)", + }, + "euiPanelBorderRadiusModifiers": Object { + "borderRadiusMedium": "4px", + "borderRadiusNone": 0, + }, "euiPanelPaddingModifiers": Object { "paddingLarge": "24px", "paddingMedium": "16px", diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index 6d45059099f8d..71b103949a80a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -121,7 +121,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" >
{ ); - expect(wrapper.find('[data-test-subj="port"]').first().text()).toEqual('443'); + expect(removeExternalLinkText(wrapper.find('[data-test-subj="port"]').first().text())).toEqual( + '443' + ); }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx index 2ba5cd868c2b8..2b0231999e6ee 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx @@ -9,6 +9,7 @@ import { shallow } from 'enzyme'; import { get } from 'lodash/fp'; import React from 'react'; +import { removeExternalLinkText } from '../../../../common/test_utils'; import { asArrayIfExists } from '../../../common/lib/helpers'; import { getMockNetflowData } from '../../../common/mock'; import '../../../common/mock/match_media'; @@ -189,9 +190,11 @@ describe('SourceDestination', () => { test('it renders the destination ip and port, separated with a colon', () => { const wrapper = mount({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text()).toEqual( - '10.1.2.3:80' - ); + expect( + removeExternalLinkText( + wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() + ) + ).toEqual('10.1.2.3:80'); }); test('it renders destination.packets', () => { @@ -313,9 +316,9 @@ describe('SourceDestination', () => { test('it renders the source ip and port, separated with a colon', () => { const wrapper = mount({getSourceDestinationInstance()}); - expect(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()).toEqual( - '192.168.1.2:9987' - ); + expect( + removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) + ).toEqual('192.168.1.2:9987'); }); test('it renders source.packets', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index 890add4222503..78f71a84d0b19 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -7,6 +7,7 @@ import { get } from 'lodash/fp'; import React from 'react'; +import { removeExternalLinkText } from '../../../../common/test_utils'; import { asArrayIfExists } from '../../../common/lib/helpers'; import { getMockNetflowData } from '../../../common/mock'; import '../../../common/mock/match_media'; @@ -976,9 +977,11 @@ describe('SourceDestinationIp', () => { ); - expect(wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text()).toEqual( - '9987' - ); + expect( + removeExternalLinkText( + wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() + ) + ).toEqual('9987'); }); test('it renders the expected destination port when type is `destination`, and both destinationIp and destinationPort are populated', () => { @@ -1028,7 +1031,9 @@ describe('SourceDestinationIp', () => { ); expect( - wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() + removeExternalLinkText( + wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() + ) ).toEqual('80'); }); @@ -1078,9 +1083,11 @@ describe('SourceDestinationIp', () => { ); - expect(wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text()).toEqual( - '9987' - ); + expect( + removeExternalLinkText( + wrapper.find('[data-test-subj="draggable-content-source.port"]').first().text() + ) + ).toEqual('9987'); }); test('it renders the expected destination port when type is `destination`, and only destinationPort is populated', () => { @@ -1131,7 +1138,9 @@ describe('SourceDestinationIp', () => { ); expect( - wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() + removeExternalLinkText( + wrapper.find('[data-test-subj="draggable-content-destination.port"]').first().text() + ) ).toEqual('80'); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index b31094b07a829..ef515751b6345 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { removeExternalLinkText } from '../../../../common/test_utils'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; @@ -43,9 +44,11 @@ describe('CertificateFingerprint', () => { /> ); - expect(wrapper.find('[data-test-subj="certificate-fingerprint-link"]').first().text()).toEqual( - '3f4c57934e089f02ae7511200aee2d7e7aabd272' - ); + expect( + removeExternalLinkText( + wrapper.find('[data-test-subj="certificate-fingerprint-link"]').first().text() + ) + ).toEqual('3f4c57934e089f02ae7511200aee2d7e7aabd272'); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx index 899a6d7486f94..c57546d5cd9aa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { removeExternalLinkText } from '../../../../common/test_utils'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; @@ -42,9 +43,9 @@ describe('Ja3Fingerprint', () => { ); - expect(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()).toEqual( - 'fff799d91b7c01ae3fe6787cfc895552' - ); + expect( + removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) + ).toEqual('fff799d91b7c01ae3fe6787cfc895552'); }); test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index c2026a71ac6ff..9aa462ee23a8b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -8,6 +8,7 @@ import { get } from 'lodash/fp'; import React from 'react'; import { shallow } from 'enzyme'; +import { removeExternalLinkText } from '../../../../common/test_utils'; import { asArrayIfExists } from '../../../common/lib/helpers'; import { getMockNetflowData } from '../../../common/mock'; import '../../../common/mock/match_media'; @@ -188,9 +189,11 @@ describe('Netflow', () => { test('it renders the destination ip and port, separated with a colon', () => { const wrapper = mount({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text()).toEqual( - '10.1.2.3:80' - ); + expect( + removeExternalLinkText( + wrapper.find('[data-test-subj="destination-ip-and-port"]').first().text() + ) + ).toEqual('10.1.2.3:80'); }); test('it renders destination.packets', () => { @@ -324,9 +327,9 @@ describe('Netflow', () => { test('it renders the source ip and port, separated with a colon', () => { const wrapper = mount({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()).toEqual( - '192.168.1.2:9987' - ); + expect( + removeExternalLinkText(wrapper.find('[data-test-subj="source-ip-and-port"]').first().text()) + ).toEqual('192.168.1.2:9987'); }); test('it renders source.packets', () => { @@ -353,11 +356,13 @@ describe('Netflow', () => { const wrapper = mount({getNetflowInstance()}); expect( - wrapper - .find('[data-test-subj="client-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .text() + removeExternalLinkText( + wrapper + .find('[data-test-subj="client-certificate-fingerprint"]') + .find('[data-test-subj="certificate-fingerprint-link"]') + .first() + .text() + ) ).toEqual('tls.client_certificate.fingerprint.sha1-value'); }); @@ -372,9 +377,9 @@ describe('Netflow', () => { test('renders tls.fingerprints.ja3.hash text', () => { const wrapper = mount({getNetflowInstance()}); - expect(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()).toEqual( - 'tls.fingerprints.ja3.hash-value' - ); + expect( + removeExternalLinkText(wrapper.find('[data-test-subj="ja3-fingerprint-link"]').first().text()) + ).toEqual('tls.fingerprints.ja3.hash-value'); }); test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { @@ -395,11 +400,13 @@ describe('Netflow', () => { const wrapper = mount({getNetflowInstance()}); expect( - wrapper - .find('[data-test-subj="server-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .text() + removeExternalLinkText( + wrapper + .find('[data-test-subj="server-certificate-fingerprint"]') + .find('[data-test-subj="certificate-fingerprint-link"]') + .first() + .text() + ) ).toEqual('tls.server_certificate.fingerprint.sha1-value'); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap index 17c614bd2c83c..03dc2afc625cd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap @@ -39,6 +39,7 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "euiBorderColor": "#343741", "euiBorderEditable": "2px dotted #343741", "euiBorderRadius": "4px", + "euiBorderRadiusSmall": "2px", "euiBorderThick": "2px solid #343741", "euiBorderThin": "1px solid #343741", "euiBorderWidthThick": "2px", @@ -375,6 +376,15 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` }, }, "euiPaletteColorBlindKeys": "'euiColorVis0', 'euiColorVis1', 'euiColorVis2', 'euiColorVis3', 'euiColorVis4', 'euiColorVis5', 'euiColorVis6', 'euiColorVis7', 'euiColorVis8', 'euiColorVis9'", + "euiPanelBackgroundColorModifiers": Object { + "plain": "#1d1e24", + "subdued": "#25262e", + "transparent": "rgba(0, 0, 0, 0)", + }, + "euiPanelBorderRadiusModifiers": Object { + "borderRadiusMedium": "4px", + "borderRadiusNone": 0, + }, "euiPanelPaddingModifiers": Object { "paddingLarge": "24px", "paddingMedium": "16px", diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 52b9b9a31fc99..bb61821d31315 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; +import { removeExternalLinkText } from '../../../../../../common/test_utils'; import '../../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../common/ecs'; @@ -75,7 +76,7 @@ describe('get_column_renderer', () => { {row} ); - expect(wrapper.text()).toContain( + expect(removeExternalLinkText(wrapper.text())).toContain( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -93,7 +94,7 @@ describe('get_column_renderer', () => { {row} ); - expect(wrapper.text()).toContain( + expect(removeExternalLinkText(wrapper.text())).toContain( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); @@ -111,7 +112,7 @@ describe('get_column_renderer', () => { {row} ); - expect(wrapper.text()).toContain( + expect(removeExternalLinkText(wrapper.text())).toContain( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 3b9752224e2c1..7ef034994ce5d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { removeExternalLinkText } from '../../../../../../../common/test_utils'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData } from '../../../../../../common/mock'; import '../../../../../../common/mock/match_media'; @@ -41,7 +42,7 @@ describe('SuricataDetails', () => { /> ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index bed2171715380..674f922bf54e6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import { removeExternalLinkText } from '../../../../../../../common/test_utils'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../../common/ecs'; import { mockTimelineData } from '../../../../../../common/mock'; @@ -58,7 +59,7 @@ describe('suricata_row_renderer', () => { {children} ); - expect(wrapper.text()).toContain( + expect(removeExternalLinkText(wrapper.text())).toContain( '4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index 6c2e9ad7535a1..45381bbc99935 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import { removeExternalLinkText } from '../../../../../../../common/test_utils'; import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../../common/ecs'; @@ -541,7 +542,7 @@ describe('GenericRowRenderer', () => { ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' ); }); @@ -569,7 +570,7 @@ describe('GenericRowRenderer', () => { ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' ); }); @@ -597,7 +598,7 @@ describe('GenericRowRenderer', () => { ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' ); }); @@ -625,7 +626,7 @@ describe('GenericRowRenderer', () => { ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' ); }); @@ -653,7 +654,7 @@ describe('GenericRowRenderer', () => { ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' ); }); @@ -681,7 +682,7 @@ describe('GenericRowRenderer', () => { ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' ); }); @@ -875,7 +876,7 @@ describe('GenericRowRenderer', () => { ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 434be7b23aeee..b980f723b5c0c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import { removeExternalLinkText } from '../../../../../../../common/test_utils'; import '../../../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; @@ -41,7 +42,7 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); @@ -56,7 +57,7 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'CyIrMA1L1JtLqdIuoldnsudpSource206.189.35.240:57475Destination67.207.67.3:53' ); }); @@ -71,7 +72,7 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'CZLkpC22NquQJOpkwehttp302Source206.189.35.240:36220Destination192.241.164.26:80' ); }); @@ -86,7 +87,7 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'noticeDropped:falseScan::Port_Scan8.42.77.171 scanned at least 15 unique ports of host 207.154.238.205 in 0m0sSource8.42.77.171' ); }); @@ -101,7 +102,7 @@ describe('ZeekDetails', () => { /> ); - expect(wrapper.text()).toEqual( + expect(removeExternalLinkText(wrapper.text())).toEqual( 'CmTxzt2OVXZLkGDaResslTLSv12TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256Source188.166.66.184:34514Destination91.189.95.15:443' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index 9c08fbacafcdc..043bde1c10698 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import { removeExternalLinkText } from '../../../../../../../common/test_utils'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../../common/ecs'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; @@ -57,7 +58,7 @@ describe('zeek_row_renderer', () => { {children} ); - expect(wrapper.text()).toContain( + expect(removeExternalLinkText(wrapper.text())).toContain( 'C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index efa22cb2c5617..f148ac5420b06 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import { removeExternalLinkText } from '../../../../../../../common/test_utils'; import '../../../../../../common/mock/match_media'; import { Ecs } from '../../../../../../../common/ecs'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; @@ -89,7 +90,7 @@ describe('ZeekSignature', () => { test('should render value', () => { const wrapper = mount(); - expect(wrapper.text()).toEqual('abc'); + expect(removeExternalLinkText(wrapper.text())).toEqual('abc'); }); test('should render value and link', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index a7de73c9aab29..a51765fc3a720 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -89,10 +89,12 @@ describe('health check', () => { const [description, action] = queryAllByText(/TLS/i); expect(description.textContent).toMatchInlineSnapshot( - `"Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. Learn how to enable TLS."` + `"Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. Learn how to enable TLS.(opens in a new tab or window)"` ); - expect(action.textContent).toMatchInlineSnapshot(`"Learn how to enable TLS."`); + expect(action.textContent).toMatchInlineSnapshot( + `"Learn how to enable TLS.(opens in a new tab or window)"` + ); expect(action.getAttribute('href')).toMatchInlineSnapshot( `"elastic.co/guide/en/kibana/current/configuring-tls.html"` @@ -118,11 +120,11 @@ describe('health check', () => { const description = queryByRole(/banner/i); expect(description!.textContent).toMatchInlineSnapshot( - `"To create an alert, set a value for xpack.encryptedSavedObjects.encryptionKey in your kibana.yml file. Learn how."` + `"To create an alert, set a value for xpack.encryptedSavedObjects.encryptionKey in your kibana.yml file. Learn how.(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); - expect(action!.textContent).toMatchInlineSnapshot(`"Learn how."`); + expect(action!.textContent).toMatchInlineSnapshot(`"Learn how.(opens in a new tab or window)"`); expect(action!.getAttribute('href')).toMatchInlineSnapshot( `"elastic.co/guide/en/kibana/current/alert-action-settings-kb.html#general-alert-action-settings"` ); @@ -148,11 +150,11 @@ describe('health check', () => { const description = queryByText(/Transport Layer Security/i); expect(description!.textContent).toMatchInlineSnapshot( - `"You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. Learn how"` + `"You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. Learn how(opens in a new tab or window)"` ); const action = queryByText(/Learn/i); - expect(action!.textContent).toMatchInlineSnapshot(`"Learn how"`); + expect(action!.textContent).toMatchInlineSnapshot(`"Learn how(opens in a new tab or window)"`); expect(action!.getAttribute('href')).toMatchInlineSnapshot( `"elastic.co/guide/en/kibana/current/alerting-getting-started.html#alerting-setup-prerequisites"` ); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap index 137d11d3f3b09..8ace0445d0eb7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap @@ -178,6 +178,16 @@ exports[`PingListExpandedRow renders link to docs if body is not recorded but it + + + (opens in a new tab or window) + for more information on recording response bodies.
diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap index 69c6bf3fadb1d..7cc96a42411d2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap @@ -55,6 +55,16 @@ Array [ + + + (opens in a new tab or window) +
Date: Tue, 17 Nov 2020 15:52:39 -0600 Subject: [PATCH 50/99] [DOCS] Adds Elastic Contributor Program link (#83561) --- docs/developer/contributing/index.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/contributing/index.asciidoc b/docs/developer/contributing/index.asciidoc index ba4ab89d17c27..bbf2903491bf6 100644 --- a/docs/developer/contributing/index.asciidoc +++ b/docs/developer/contributing/index.asciidoc @@ -2,7 +2,7 @@ == Contributing Whether you want to fix a bug, implement a feature, or add some other improvements or apis, the following sections will -guide you on the process. +guide you on the process. After committing your code, check out the link:https://www.elastic.co/community/contributor[Elastic Contributor Program] where you can earn points and rewards for your contributions. Read <> to get your environment up and running, then read <>. From 085698ed62dea51faa72e4cb92b1aeea78cf2ec6 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 18 Nov 2020 00:18:31 +0000 Subject: [PATCH 51/99] chore(NA): remove usage of unverified es snapshots (#83589) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- Jenkinsfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index e33bf67f28df5..3b68cde206573 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,5 @@ #!/bin/groovy -env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = 'true' library 'kibana-pipeline-library' kibanaLibrary.load() From 982639fc2a7ea07ab41abaabb451208dd98c3165 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Tue, 17 Nov 2020 17:47:58 -0700 Subject: [PATCH 52/99] [Maps] Fix threshold alert issue resolving nested fields (#83577) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../geo_threshold/geo_threshold.ts | 4 +- .../tests/es_sample_response.json | 43 +---- .../es_sample_response_with_nesting.json | 170 ++++++++++++++++++ .../geo_threshold/tests/geo_threshold.test.ts | 41 +++++ 4 files changed, 214 insertions(+), 44 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response_with_nesting.json diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts index 394ee8d606abe..e223cdb7ea545 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts @@ -38,7 +38,7 @@ export function transformResults( return _.map(subBuckets, (subBucket) => { const locationFieldResult = _.get( subBucket, - `entityHits.hits.hits[0].fields.${geoField}[0]`, + `entityHits.hits.hits[0].fields["${geoField}"][0]`, '' ); const location = locationFieldResult @@ -50,7 +50,7 @@ export function transformResults( : null; const dateInShape = _.get( subBucket, - `entityHits.hits.hits[0].fields.${dateField}[0]`, + `entityHits.hits.hits[0].fields["${dateField}"][0]`, null ); const docId = _.get(subBucket, `entityHits.hits.hits[0]._id`); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response.json b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response.json index 1281777c03761..70edbd09aa5a1 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response.json +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response.json @@ -13,48 +13,7 @@ "relation" : "gte" }, "max_score" : 0.0, - "hits" : [ - { - "_index" : "flight_tracks", - "_id" : "XOng1XQB6yyY-xQxbwWM", - "_score" : 0.0, - "fields" : { - "@timestamp" : [ - "2020-09-28T18:01:29.580Z" - ] - } - }, - { - "_index" : "flight_tracks", - "_id" : "Xeng1XQB6yyY-xQxbwWM", - "_score" : 0.0, - "fields" : { - "@timestamp" : [ - "2020-09-28T18:01:29.580Z" - ] - } - }, - { - "_index" : "flight_tracks", - "_id" : "Xung1XQB6yyY-xQxbwWM", - "_score" : 0.0, - "fields" : { - "@timestamp" : [ - "2020-09-28T18:01:29.580Z" - ] - } - }, - { - "_index" : "flight_tracks", - "_id" : "UOjg1XQB6yyY-xQxZvMz", - "_score" : 0.0, - "fields" : { - "@timestamp" : [ - "2020-09-28T18:01:27.266Z" - ] - } - } - ] + "hits" : [] }, "aggregations" : { "shapes" : { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response_with_nesting.json b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response_with_nesting.json new file mode 100644 index 0000000000000..a4b7b6872b341 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_sample_response_with_nesting.json @@ -0,0 +1,170 @@ +{ + "took" : 2760, + "timed_out" : false, + "_shards" : { + "total" : 1, + "successful" : 1, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : { + "value" : 10000, + "relation" : "gte" + }, + "max_score" : 0.0, + "hits" : [] + }, + "aggregations" : { + "shapes" : { + "meta" : { }, + "buckets" : { + "0DrJu3QB6yyY-xQxv6Ip" : { + "doc_count" : 1047, + "entitySplit" : { + "doc_count_error_upper_bound" : 0, + "sum_other_doc_count" : 957, + "buckets" : [ + { + "key" : "936", + "doc_count" : 9, + "entityHits" : { + "hits" : { + "total" : { + "value" : 9, + "relation" : "eq" + }, + "max_score" : null, + "hits" : [ + { + "_index" : "flight_tracks", + "_id" : "N-ng1XQB6yyY-xQxnGSM", + "_score" : null, + "fields" : { + "time_data.@timestamp" : [ + "2020-09-28T18:01:41.190Z" + ], + "geo.coords.location" : [ + "40.62806099653244, -82.8814151789993" + ], + "entity_id" : [ + "936" + ] + }, + "sort" : [ + 1601316101190 + ] + } + ] + } + } + }, + { + "key" : "AAL2019", + "doc_count" : 9, + "entityHits" : { + "hits" : { + "total" : { + "value" : 9, + "relation" : "eq" + }, + "max_score" : null, + "hits" : [ + { + "_index" : "flight_tracks", + "_id" : "iOng1XQB6yyY-xQxnGSM", + "_score" : null, + "fields" : { + "time_data.@timestamp" : [ + "2020-09-28T18:01:41.191Z" + ], + "geo.coords.location" : [ + "39.006176185794175, -82.22068064846098" + ], + "entity_id" : [ + "AAL2019" + ] + }, + "sort" : [ + 1601316101191 + ] + } + ] + } + } + }, + { + "key" : "AAL2323", + "doc_count" : 9, + "entityHits" : { + "hits" : { + "total" : { + "value" : 9, + "relation" : "eq" + }, + "max_score" : null, + "hits" : [ + { + "_index" : "flight_tracks", + "_id" : "n-ng1XQB6yyY-xQxnGSM", + "_score" : null, + "fields" : { + "time_data.@timestamp" : [ + "2020-09-28T18:01:41.191Z" + ], + "geo.coords.location" : [ + "41.6677269525826, -84.71324851736426" + ], + "entity_id" : [ + "AAL2323" + ] + }, + "sort" : [ + 1601316101191 + ] + } + ] + } + } + }, + { + "key" : "ABD5250", + "doc_count" : 9, + "entityHits" : { + "hits" : { + "total" : { + "value" : 9, + "relation" : "eq" + }, + "max_score" : null, + "hits" : [ + { + "_index" : "flight_tracks", + "_id" : "GOng1XQB6yyY-xQxnGWM", + "_score" : null, + "fields" : { + "time_data.@timestamp" : [ + "2020-09-28T18:01:41.192Z" + ], + "geo.coords.location" : [ + "39.07997465226799, 6.073727197945118" + ], + "entity_id" : [ + "ABD5250" + ] + }, + "sort" : [ + 1601316101192 + ] + } + ] + } + } + } + ] + } + } + } + } + } +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts index 0aaf30ab2f3fb..e4cee9c677713 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts @@ -5,6 +5,7 @@ */ import sampleJsonResponse from './es_sample_response.json'; +import sampleJsonResponseWithNesting from './es_sample_response_with_nesting.json'; import { getMovedEntities, transformResults } from '../geo_threshold'; import { OTHER_CATEGORY } from '../es_query_builder'; import { SearchResponse } from 'elasticsearch'; @@ -51,6 +52,46 @@ describe('geo_threshold', () => { ]); }); + const nestedDateField = 'time_data.@timestamp'; + const nestedGeoField = 'geo.coords.location'; + it('should correctly transform expected results if fields are nested', async () => { + const transformedResults = transformResults( + (sampleJsonResponseWithNesting as unknown) as SearchResponse, + nestedDateField, + nestedGeoField + ); + expect(transformedResults).toEqual([ + { + dateInShape: '2020-09-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 40.62806099653244], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + { + dateInShape: '2020-09-28T18:01:41.191Z', + docId: 'iOng1XQB6yyY-xQxnGSM', + entityName: 'AAL2019', + location: [-82.22068064846098, 39.006176185794175], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + { + dateInShape: '2020-09-28T18:01:41.191Z', + docId: 'n-ng1XQB6yyY-xQxnGSM', + entityName: 'AAL2323', + location: [-84.71324851736426, 41.6677269525826], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + { + dateInShape: '2020-09-28T18:01:41.192Z', + docId: 'GOng1XQB6yyY-xQxnGWM', + entityName: 'ABD5250', + location: [6.073727197945118, 39.07997465226799], + shapeLocationId: '0DrJu3QB6yyY-xQxv6Ip', + }, + ]); + }); + it('should return an empty array if no results', async () => { const transformedResults = transformResults(undefined, dateField, geoField); expect(transformedResults).toEqual([]); From e3c2dccf005cc99f4fc6b8d86f7e3b24fbb76014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 18 Nov 2020 09:10:00 +0100 Subject: [PATCH 53/99] [Runtime fields] Editor phase 1 (#81472) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: spalger --- docs/developer/plugin-list.asciidoc | 4 + packages/kbn-optimizer/limits.yml | 1 + .../hook_form_lib/components/use_field.tsx | 2 +- x-pack/.i18nrc.json | 3 +- x-pack/plugins/index_management/kibana.json | 3 +- .../painless_script_parameter.tsx | 10 +- .../runtime_type_parameter.tsx | 19 +- .../constants/field_options.tsx | 29 --- .../constants/parameters_definition.tsx | 4 +- .../mappings_editor/shared_imports.ts | 2 + .../mappings_editor/types/document_fields.ts | 4 +- x-pack/plugins/runtime_fields/README.md | 197 ++++++++++++++++++ x-pack/plugins/runtime_fields/kibana.json | 15 ++ .../public/__jest__/setup_environment.tsx | 44 ++++ .../runtime_fields/public/components/index.ts | 11 + .../components/runtime_field_editor/index.ts | 7 + .../runtime_field_editor.test.tsx | 71 +++++++ .../runtime_field_editor.tsx | 24 +++ .../index.ts | 7 + ...ntime_field_editor_flyout_content.test.tsx | 146 +++++++++++++ .../runtime_field_editor_flyout_content.tsx | 146 +++++++++++++ .../components/runtime_field_form/index.ts | 7 + .../runtime_field_form.test.tsx | 91 ++++++++ .../runtime_field_form/runtime_field_form.tsx | 149 +++++++++++++ .../components/runtime_field_form/schema.ts | 58 ++++++ .../runtime_fields/public/constants.ts | 37 ++++ x-pack/plugins/runtime_fields/public/index.ts | 18 ++ .../public/lib/documentation.ts | 16 ++ .../runtime_fields/public/lib/index.ts | 7 + .../runtime_fields/public/load_editor.tsx | 57 +++++ .../runtime_fields/public/plugin.test.ts | 82 ++++++++ .../plugins/runtime_fields/public/plugin.ts | 26 +++ .../runtime_fields/public/shared_imports.ts | 23 ++ .../runtime_fields/public/test_utils.ts | 7 + x-pack/plugins/runtime_fields/public/types.ts | 40 ++++ 35 files changed, 1320 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/runtime_fields/README.md create mode 100644 x-pack/plugins/runtime_fields/kibana.json create mode 100644 x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx create mode 100644 x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts create mode 100644 x-pack/plugins/runtime_fields/public/constants.ts create mode 100644 x-pack/plugins/runtime_fields/public/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/lib/documentation.ts create mode 100644 x-pack/plugins/runtime_fields/public/lib/index.ts create mode 100644 x-pack/plugins/runtime_fields/public/load_editor.tsx create mode 100644 x-pack/plugins/runtime_fields/public/plugin.test.ts create mode 100644 x-pack/plugins/runtime_fields/public/plugin.ts create mode 100644 x-pack/plugins/runtime_fields/public/shared_imports.ts create mode 100644 x-pack/plugins/runtime_fields/public/test_utils.ts create mode 100644 x-pack/plugins/runtime_fields/public/types.ts diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e89b6d86361c7..1436399a03dbc 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -479,6 +479,10 @@ Elastic. |Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. +|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields/README.md[runtimeFields] +|Welcome to the home of the runtime field editor and everything related to runtime fields! + + |{kib-repo}blob/{branch}/x-pack/plugins/saved_objects_tagging/README.md[savedObjectsTagging] |Add tagging capability to saved objects diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index e326c8e2cac39..7cdbe844c2901 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -102,4 +102,5 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 + runtimeFields: 41752 stackAlerts: 29684 diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx index a3a0984d4a736..4024eea008588 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -31,7 +31,7 @@ export interface Props { componentProps?: Record; readDefaultValueOnForm?: boolean; onChange?: (value: I) => void; - children?: (field: FieldHook) => JSX.Element; + children?: (field: FieldHook) => JSX.Element | null; [key: string]: any; } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 2be68b797ba5f..6937862d20536 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -38,10 +38,11 @@ "xpack.maps": ["plugins/maps"], "xpack.ml": ["plugins/ml"], "xpack.monitoring": ["plugins/monitoring"], - "xpack.remoteClusters": "plugins/remote_clusters", "xpack.painlessLab": "plugins/painless_lab", + "xpack.remoteClusters": "plugins/remote_clusters", "xpack.reporting": ["plugins/reporting"], "xpack.rollupJobs": ["plugins/rollup"], + "xpack.runtimeFields": "plugins/runtime_fields", "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index ff6a71d53894a..4e4ad9b8e1d31 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -18,6 +18,7 @@ "configPath": ["xpack", "index_management"], "requiredBundles": [ "kibanaReact", - "esUiShared" + "esUiShared", + "runtimeFields" ] } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx index 19746034b530c..9042e7f6ee328 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/painless_script_parameter.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { PainlessLang } from '@kbn/monaco'; import { EuiFormRow, EuiDescribedFormGroup } from '@elastic/eui'; import { CodeEditor, UseField } from '../../../shared_imports'; @@ -18,7 +19,7 @@ interface Props { export const PainlessScriptParameter = ({ stack }: Props) => { return ( - + path="script.source" config={getFieldConfig('script')}> {(scriptField) => { const error = scriptField.getErrorsMessages(); const isInvalid = error ? Boolean(error.length) : false; @@ -26,11 +27,10 @@ export const PainlessScriptParameter = ({ stack }: Props) => { const field = ( { return ( - + + path="runtime_type" + config={getFieldConfig('runtime_type')} + > {(runtimeTypeField) => { const { label, value, setValue } = runtimeTypeField; const typeDefinition = @@ -44,8 +47,14 @@ export const RuntimeTypeParameter = ({ stack }: Props) => { )} singleSelection={{ asPlainText: true }} options={RUNTIME_FIELD_OPTIONS} - selectedOptions={value as EuiComboBoxOptionOption[]} - onChange={setValue} + selectedOptions={value} + onChange={(newValue) => { + if (newValue.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + setValue(newValue); + }} isClearable={false} fullWidth /> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx index 25fdac5089b86..46292b7b2d357 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/field_options.tsx @@ -28,35 +28,6 @@ export const FIELD_TYPES_OPTIONS = Object.entries(MAIN_DATA_TYPE_DEFINITION).map }) ) as ComboBoxOption[]; -export const RUNTIME_FIELD_OPTIONS = [ - { - label: 'Keyword', - value: 'keyword', - }, - { - label: 'Long', - value: 'long', - }, - { - label: 'Double', - value: 'double', - }, - { - label: 'Date', - value: 'date', - }, - { - label: 'IP', - value: 'ip', - }, - { - label: 'Boolean', - value: 'boolean', - }, -] as ComboBoxOption[]; - -export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; - interface SuperSelectOptionConfig { inputDisplay: string; dropdownDisplay: JSX.Element; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index 1434b7d4b4429..64f84ee2611a0 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -16,11 +16,12 @@ import { ValidationFuncArg, fieldFormatters, FieldConfig, + RUNTIME_FIELD_OPTIONS, + RuntimeType, } from '../shared_imports'; import { AliasOption, DataType, - RuntimeType, ComboBoxOption, ParameterName, ParameterDefinition, @@ -28,7 +29,6 @@ import { import { documentationService } from '../../../services/documentation'; import { INDEX_DEFAULT } from './default_values'; import { TYPE_DEFINITION } from './data_types_definition'; -import { RUNTIME_FIELD_OPTIONS } from './field_options'; const { toInt } = fieldFormatters; const { emptyField, containsCharsField, numberGreaterThanField, isJsonField } = fieldValidators; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index 54b2486108183..68b40e876f655 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -53,3 +53,5 @@ export { } from '../../../../../../../src/plugins/es_ui_shared/public'; export { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; + +export { RUNTIME_FIELD_OPTIONS, RuntimeType } from '../../../../../runtime_fields/public'; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index ee4dd55a5801f..b143eedd4f9d4 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -8,7 +8,7 @@ import { ReactNode } from 'react'; import { GenericObject } from './mappings_editor'; import { FieldConfig } from '../shared_imports'; -import { PARAMETERS_DEFINITION, RUNTIME_FIELD_TYPES } from '../constants'; +import { PARAMETERS_DEFINITION } from '../constants'; export interface DataTypeDefinition { label: string; @@ -76,8 +76,6 @@ export type SubType = NumericType | RangeType; export type DataType = MainType | SubType; -export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; - export type NumericType = | 'long' | 'integer' diff --git a/x-pack/plugins/runtime_fields/README.md b/x-pack/plugins/runtime_fields/README.md new file mode 100644 index 0000000000000..d4664a3a07c61 --- /dev/null +++ b/x-pack/plugins/runtime_fields/README.md @@ -0,0 +1,197 @@ +# Runtime fields + +Welcome to the home of the runtime field editor and everything related to runtime fields! + +## The runtime field editor + +### Integration + +The recommended way to integrate the runtime fields editor is by adding a plugin dependency to the `"runtimeFields"` x-pack plugin. This way you will be able to lazy load the editor when it is required and it will not increment the bundle size of your plugin. + +```js +// 1. Add the plugin as a dependency in your kibana.json +{ + ... + "requiredBundles": [ + "runtimeFields", + ... + ] +} + +// 2. Access it in your plugin setup() +export class MyPlugin { + setup(core, { runtimeFields }) { + // logic to provide it to your app, probably through context + } +} + +// 3. Load the editor and open it anywhere in your app +const MyComponent = () => { + // Access the plugin through context + const { runtimeFields } = useAppPlugins(); + + // Ref of the handler to close the editor + const closeRuntimeFieldEditor = useRef(() => {}); + + const saveRuntimeField = (field: RuntimeField) => { + // Do something with the field + console.log(field); // { name: 'myField', type: 'boolean', script: "return 'hello'" } + }; + + const openRuntimeFieldsEditor = async() => { + // Lazy load the editor + const { openEditor } = await runtimeFields.loadEditor(); + + closeRuntimeFieldEditor.current = openEditor({ + onSave: saveRuntimeField, + /* defaultValue: optional field to edit */ + }); + }; + + useEffect(() => { + return () => { + // Make sure to remove the editor when the component unmounts + closeRuntimeFieldEditor.current(); + }; + }, []); + + return ( + + ) +} +``` + +#### Alternative + +The runtime field editor is also exported as static React component that you can import into your components. The editor is exported in 2 flavours: + +* As the content of a `` (it contains a flyout header and footer) +* As a standalone component that you can inline anywhere + +**Note:** The runtime field editor uses the `` that has a dependency on the `Provider` from the `"kibana_react"` plugin. If your app is not already wrapped by this provider you will need to add it at least around the runtime field editor. You can see an example in the ["Using the core.overlays.openFlyout()"](#using-the-coreoverlaysopenflyout) example below. + +### Content of a `` + +```js +import React, { useState } from 'react'; +import { EuiFlyoutBody, EuiButton } from '@elastic/eui'; +import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields/public'; + +const MyComponent = () => { + const { docLinksStart } = useCoreContext(); // access the core start service + const [isFlyoutVisilbe, setIsFlyoutVisible] = useState(false); + + const saveRuntimeField = useCallback((field: RuntimeField) => { + // Do something with the field + }, []); + + return ( + <> + setIsFlyoutVisible(true)}>Create field + + {isFlyoutVisible && ( + setIsFlyoutVisible(false)}> + setIsFlyoutVisible(false)} + docLinks={docLinksStart} + defaultValue={/*optional runtime field to edit*/} + /> + + )} + + ) +} +``` + +#### Using the `core.overlays.openFlyout()` + +As an alternative you can open the flyout with the `openFlyout()` helper from core. + +```js +import React, { useRef } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { OverlayRef } from 'src/core/public'; + +import { createKibanaReactContext, toMountPoint } from '../../src/plugins/kibana_react/public'; +import { RuntimeFieldEditorFlyoutContent, RuntimeField } from '../runtime_fields/public'; + +const MyComponent = () => { + // Access the core start service + const { docLinksStart, overlays, uiSettings } = useCoreContext(); + const flyoutEditor = useRef(null); + + const { openFlyout } = overlays; + + const saveRuntimeField = useCallback((field: RuntimeField) => { + // Do something with the field + }, []); + + const openRuntimeFieldEditor = useCallback(() => { + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings }); + + flyoutEditor.current = openFlyout( + toMountPoint( + + flyoutEditor.current?.close()} + docLinks={docLinksStart} + defaultValue={defaultRuntimeField} + /> + + ) + ); + }, [openFlyout, saveRuntimeField, uiSettings]); + + return ( + <> + Create field + + ) +} +``` + +### Standalone component + +```js +import React, { useState } from 'react'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; +import { RuntimeFieldEditor, RuntimeField, RuntimeFieldFormState } from '../runtime_fields/public'; + +const MyComponent = () => { + const { docLinksStart } = useCoreContext(); // access the core start service + const [runtimeFieldFormState, setRuntimeFieldFormState] = useState({ + isSubmitted: false, + isValid: undefined, + submit: async() => Promise.resolve({ isValid: false, data: {} as RuntimeField }) + }); + + const { submit, isValid: isFormValid, isSubmitted } = runtimeFieldFormState; + + const saveRuntimeField = useCallback(async () => { + const { isValid, data } = await submit(); + if (isValid) { + // Do something with the field (data) + } + }, [submit]); + + return ( + <> + + + + + + Save field + + + ) +} +``` \ No newline at end of file diff --git a/x-pack/plugins/runtime_fields/kibana.json b/x-pack/plugins/runtime_fields/kibana.json new file mode 100644 index 0000000000000..65932c723c474 --- /dev/null +++ b/x-pack/plugins/runtime_fields/kibana.json @@ -0,0 +1,15 @@ +{ + "id": "runtimeFields", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": [ + ], + "optionalPlugins": [ + ], + "configPath": ["xpack", "runtime_fields"], + "requiredBundles": [ + "kibanaReact", + "esUiShared" + ] +} diff --git a/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx b/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx new file mode 100644 index 0000000000000..ccfe426cfdb09 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +jest.mock('../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../src/plugins/kibana_react/public'); + + const CodeEditorMock = (props: any) => ( + ) => { + props.onChange(e.target.value); + }} + /> + ); + + return { + ...original, + CodeEditor: CodeEditorMock, + }; +}); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + }; +}); diff --git a/x-pack/plugins/runtime_fields/public/components/index.ts b/x-pack/plugins/runtime_fields/public/components/index.ts new file mode 100644 index 0000000000000..86ac968d39f21 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuntimeFieldForm, FormState as RuntimeFieldFormState } from './runtime_field_form'; + +export { RuntimeFieldEditor } from './runtime_field_editor'; + +export { RuntimeFieldEditorFlyoutContent } from './runtime_field_editor_flyout_content'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts new file mode 100644 index 0000000000000..62fa0bf991542 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuntimeFieldEditor } from './runtime_field_editor'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx new file mode 100644 index 0000000000000..c56bc16c304ad --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; +import { DocLinksStart } from 'src/core/public'; + +import '../../__jest__/setup_environment'; +import { registerTestBed, TestBed } from '../../test_utils'; +import { RuntimeField } from '../../types'; +import { RuntimeFieldForm, FormState } from '../runtime_field_form/runtime_field_form'; +import { RuntimeFieldEditor, Props } from './runtime_field_editor'; + +const setup = (props?: Props) => + registerTestBed(RuntimeFieldEditor, { + memoryRouter: { + wrapComponent: false, + }, + })(props) as TestBed; + +const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'https://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', + links: {} as any, +}; + +describe('Runtime field editor', () => { + let testBed: TestBed; + let onChange: jest.Mock = jest.fn(); + + const lastOnChangeCall = (): FormState[] => onChange.mock.calls[onChange.mock.calls.length - 1]; + + beforeEach(() => { + onChange = jest.fn(); + }); + + test('should render the ', () => { + testBed = setup({ docLinks }); + const { component } = testBed; + + expect(component.find(RuntimeFieldForm).length).toBe(1); + }); + + test('should accept a defaultValue and onChange prop to forward the form state', async () => { + const defaultValue: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + testBed = setup({ onChange, defaultValue, docLinks }); + + expect(onChange).toHaveBeenCalled(); + + let lastState = lastOnChangeCall()[0]; + expect(lastState.isValid).toBe(undefined); + expect(lastState.isSubmitted).toBe(false); + expect(lastState.submit).toBeDefined(); + + let data; + await act(async () => { + ({ data } = await lastState.submit()); + }); + expect(data).toEqual(defaultValue); + + // Make sure that both isValid and isSubmitted state are now "true" + lastState = lastOnChangeCall()[0]; + expect(lastState.isValid).toBe(true); + expect(lastState.isSubmitted).toBe(true); + }); +}); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx new file mode 100644 index 0000000000000..07935be171fd2 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { DocLinksStart } from 'src/core/public'; + +import { RuntimeField } from '../../types'; +import { getLinks } from '../../lib'; +import { RuntimeFieldForm, Props as FormProps } from '../runtime_field_form/runtime_field_form'; + +export interface Props { + docLinks: DocLinksStart; + defaultValue?: RuntimeField; + onChange?: FormProps['onChange']; +} + +export const RuntimeFieldEditor = ({ defaultValue, onChange, docLinks }: Props) => { + const links = getLinks(docLinks); + + return ; +}; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts new file mode 100644 index 0000000000000..32234bfcc5600 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuntimeFieldEditorFlyoutContent } from './runtime_field_editor_flyout_content'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx new file mode 100644 index 0000000000000..8e47472295f45 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; +import { DocLinksStart } from 'src/core/public'; + +import '../../__jest__/setup_environment'; +import { registerTestBed, TestBed } from '../../test_utils'; +import { RuntimeField } from '../../types'; +import { RuntimeFieldEditorFlyoutContent, Props } from './runtime_field_editor_flyout_content'; + +const setup = (props?: Props) => + registerTestBed(RuntimeFieldEditorFlyoutContent, { + memoryRouter: { + wrapComponent: false, + }, + })(props) as TestBed; + +const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', + links: {} as any, +}; + +const noop = () => {}; +const defaultProps = { onSave: noop, onCancel: noop, docLinks }; + +describe('Runtime field editor flyout', () => { + test('should have a flyout title', () => { + const { exists, find } = setup(defaultProps); + + expect(exists('flyoutTitle')).toBe(true); + expect(find('flyoutTitle').text()).toBe('Create new field'); + }); + + test('should allow a runtime field to be provided', () => { + const field: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + + const { find } = setup({ ...defaultProps, defaultValue: field }); + + expect(find('flyoutTitle').text()).toBe(`Edit ${field.name} field`); + expect(find('nameField.input').props().value).toBe(field.name); + expect(find('typeField').props().value).toBe(field.type); + expect(find('scriptField').props().value).toBe(field.script); + }); + + test('should accept an onSave prop', async () => { + const field: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + const onSave: jest.Mock = jest.fn(); + + const { find } = setup({ ...defaultProps, onSave, defaultValue: field }); + + await act(async () => { + find('saveFieldButton').simulate('click'); + }); + + expect(onSave).toHaveBeenCalled(); + const fieldReturned: RuntimeField = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual(field); + }); + + test('should accept an onCancel prop', () => { + const onCancel = jest.fn(); + const { find } = setup({ ...defaultProps, onCancel }); + + find('closeFlyoutButton').simulate('click'); + + expect(onCancel).toHaveBeenCalled(); + }); + + describe('validation', () => { + test('should validate the fields and prevent saving invalid form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { find, exists, form, component } = setup({ ...defaultProps, onSave }); + + expect(find('saveFieldButton').props().disabled).toBe(false); + + await act(async () => { + find('saveFieldButton').simulate('click'); + }); + component.update(); + + expect(onSave).toHaveBeenCalledTimes(0); + expect(find('saveFieldButton').props().disabled).toBe(true); + expect(form.getErrorsMessages()).toEqual([ + 'Give a name to the field.', + 'Script must emit() a value.', + ]); + expect(exists('formError')).toBe(true); + expect(find('formError').text()).toBe('Fix errors in form before continuing.'); + }); + + test('should forward values from the form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { find, form } = setup({ ...defaultProps, onSave }); + + act(() => { + form.setInputValue('nameField.input', 'someName'); + form.setInputValue('scriptField', 'script=123'); + }); + + await act(async () => { + find('saveFieldButton').simulate('click'); + }); + + expect(onSave).toHaveBeenCalled(); + let fieldReturned: RuntimeField = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'keyword', // default to keyword + script: 'script=123', + }); + + // Change the type and make sure it is forwarded + act(() => { + find('typeField').simulate('change', [ + { + label: 'Other type', + value: 'other_type', + }, + ]); + }); + await act(async () => { + find('saveFieldButton').simulate('click'); + }); + fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'other_type', + script: 'script=123', + }); + }); + }); +}); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx new file mode 100644 index 0000000000000..c7454cff0eb15 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiSpacer, + EuiCallOut, +} from '@elastic/eui'; +import { DocLinksStart } from 'src/core/public'; + +import { RuntimeField } from '../../types'; +import { FormState } from '../runtime_field_form'; +import { RuntimeFieldEditor } from '../runtime_field_editor'; + +const geti18nTexts = (field?: RuntimeField) => { + return { + flyoutTitle: field + ? i18n.translate('xpack.runtimeFields.editor.flyoutEditFieldTitle', { + defaultMessage: 'Edit {fieldName} field', + values: { + fieldName: field.name, + }, + }) + : i18n.translate('xpack.runtimeFields.editor.flyoutDefaultTitle', { + defaultMessage: 'Create new field', + }), + closeButtonLabel: i18n.translate('xpack.runtimeFields.editor.flyoutCloseButtonLabel', { + defaultMessage: 'Close', + }), + saveButtonLabel: i18n.translate('xpack.runtimeFields.editor.flyoutSaveButtonLabel', { + defaultMessage: 'Save', + }), + formErrorsCalloutTitle: i18n.translate('xpack.runtimeFields.editor.validationErrorTitle', { + defaultMessage: 'Fix errors in form before continuing.', + }), + }; +}; + +export interface Props { + /** + * Handler for the "save" footer button + */ + onSave: (field: RuntimeField) => void; + /** + * Handler for the "cancel" footer button + */ + onCancel: () => void; + /** + * The docLinks start service from core + */ + docLinks: DocLinksStart; + /** + * An optional runtime field to edit + */ + defaultValue?: RuntimeField; +} + +export const RuntimeFieldEditorFlyoutContent = ({ + onSave, + onCancel, + docLinks, + defaultValue: field, +}: Props) => { + const i18nTexts = geti18nTexts(field); + + const [formState, setFormState] = useState({ + isSubmitted: false, + isValid: field ? true : undefined, + submit: field + ? async () => ({ isValid: true, data: field }) + : async () => ({ isValid: false, data: {} as RuntimeField }), + }); + const { submit, isValid: isFormValid, isSubmitted } = formState; + + const onSaveField = useCallback(async () => { + const { isValid, data } = await submit(); + + if (isValid) { + onSave(data); + } + }, [submit, onSave]); + + return ( + <> + + +

{i18nTexts.flyoutTitle}

+
+
+ + + + + + + {isSubmitted && !isFormValid && ( + <> + + + + )} + + + + onCancel()} + data-test-subj="closeFlyoutButton" + > + {i18nTexts.closeButtonLabel} + + + + + onSaveField()} + data-test-subj="saveFieldButton" + disabled={isSubmitted && !isFormValid} + fill + > + {i18nTexts.saveButtonLabel} + + + + + + ); +}; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts new file mode 100644 index 0000000000000..4041a04aec4d1 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RuntimeFieldForm, FormState } from './runtime_field_form'; diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx new file mode 100644 index 0000000000000..1829514856eed --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; + +import '../../__jest__/setup_environment'; +import { registerTestBed, TestBed } from '../../test_utils'; +import { RuntimeField } from '../../types'; +import { RuntimeFieldForm, Props, FormState } from './runtime_field_form'; + +const setup = (props?: Props) => + registerTestBed(RuntimeFieldForm, { + memoryRouter: { + wrapComponent: false, + }, + })(props) as TestBed; + +const links = { + painlessSyntax: 'https://jestTest.elastic.co/to-be-defined.html', +}; + +describe('Runtime field form', () => { + let testBed: TestBed; + let onChange: jest.Mock = jest.fn(); + + const lastOnChangeCall = (): FormState[] => onChange.mock.calls[onChange.mock.calls.length - 1]; + + beforeEach(() => { + onChange = jest.fn(); + }); + + test('should render expected 3 fields (name, returnType, script)', () => { + testBed = setup({ links }); + const { exists } = testBed; + + expect(exists('nameField')).toBe(true); + expect(exists('typeField')).toBe(true); + expect(exists('scriptField')).toBe(true); + }); + + test('should have a link to learn more about painless syntax', () => { + testBed = setup({ links }); + const { exists, find } = testBed; + + expect(exists('painlessSyntaxLearnMoreLink')).toBe(true); + expect(find('painlessSyntaxLearnMoreLink').props().href).toBe(links.painlessSyntax); + }); + + test('should accept a "defaultValue" prop', () => { + const defaultValue: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + testBed = setup({ defaultValue, links }); + const { find } = testBed; + + expect(find('nameField.input').props().value).toBe(defaultValue.name); + expect(find('typeField').props().value).toBe(defaultValue.type); + expect(find('scriptField').props().value).toBe(defaultValue.script); + }); + + test('should accept an "onChange" prop to forward the form state', async () => { + const defaultValue: RuntimeField = { + name: 'foo', + type: 'date', + script: 'test=123', + }; + testBed = setup({ onChange, defaultValue, links }); + + expect(onChange).toHaveBeenCalled(); + + let lastState = lastOnChangeCall()[0]; + expect(lastState.isValid).toBe(undefined); + expect(lastState.isSubmitted).toBe(false); + expect(lastState.submit).toBeDefined(); + + let data; + await act(async () => { + ({ data } = await lastState.submit()); + }); + expect(data).toEqual(defaultValue); + + // Make sure that both isValid and isSubmitted state are now "true" + lastState = lastOnChangeCall()[0]; + expect(lastState.isValid).toBe(true); + expect(lastState.isSubmitted).toBe(true); + }); +}); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx new file mode 100644 index 0000000000000..6068302f5b269 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { PainlessLang } from '@kbn/monaco'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiFormRow, + EuiComboBox, + EuiComboBoxOptionOption, + EuiLink, +} from '@elastic/eui'; + +import { useForm, Form, FormHook, UseField, TextField, CodeEditor } from '../../shared_imports'; +import { RuntimeField } from '../../types'; +import { RUNTIME_FIELD_OPTIONS } from '../../constants'; +import { schema } from './schema'; + +export interface FormState { + isValid: boolean | undefined; + isSubmitted: boolean; + submit: FormHook['submit']; +} + +export interface Props { + links: { + painlessSyntax: string; + }; + defaultValue?: RuntimeField; + onChange?: (state: FormState) => void; +} + +const RuntimeFieldFormComp = ({ defaultValue, onChange, links }: Props) => { + const { form } = useForm({ defaultValue, schema }); + const { submit, isValid: isFormValid, isSubmitted } = form; + + useEffect(() => { + if (onChange) { + onChange({ isValid: isFormValid, isSubmitted, submit }); + } + }, [onChange, isFormValid, isSubmitted, submit]); + + return ( +
+ + {/* Name */} + + + + + {/* Return type */} + + path="type"> + {({ label, value, setValue }) => { + if (value === undefined) { + return null; + } + return ( + <> + + { + if (newValue.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + setValue(newValue); + }} + isClearable={false} + data-test-subj="typeField" + fullWidth + /> + + + ); + }} +
+ + + + + + {/* Script */} + path="script"> + {({ value, setValue, label, isValid, getErrorsMessages }) => { + return ( + + + + {i18n.translate('xpack.runtimeFields.form.script.learnMoreLinkText', { + defaultMessage: 'Learn more about syntax.', + })} + + + + } + fullWidth + > + + + ); + }} +
+ + ); +}; + +export const RuntimeFieldForm = React.memo(RuntimeFieldFormComp); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts new file mode 100644 index 0000000000000..abb7cf812200f --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +import { FormSchema, fieldValidators } from '../../shared_imports'; +import { RUNTIME_FIELD_OPTIONS } from '../../constants'; +import { RuntimeField, RuntimeType, ComboBoxOption } from '../../types'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + name: { + label: i18n.translate('xpack.runtimeFields.form.nameLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.runtimeFields.form.validations.nameIsRequiredErrorMessage', { + defaultMessage: 'Give a name to the field.', + }) + ), + }, + ], + }, + type: { + label: i18n.translate('xpack.runtimeFields.form.runtimeTypeLabel', { + defaultMessage: 'Type', + }), + defaultValue: 'keyword', + deserializer: (fieldType?: RuntimeType) => { + if (!fieldType) { + return []; + } + + const label = RUNTIME_FIELD_OPTIONS.find(({ value }) => value === fieldType)?.label; + return [{ label: label ?? fieldType, value: fieldType }]; + }, + serializer: (value: Array>) => value[0].value!, + }, + script: { + label: i18n.translate('xpack.runtimeFields.form.defineFieldLabel', { + defaultMessage: 'Define field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.runtimeFields.form.validations.scriptIsRequiredErrorMessage', { + defaultMessage: 'Script must emit() a value.', + }) + ), + }, + ], + }, +}; diff --git a/x-pack/plugins/runtime_fields/public/constants.ts b/x-pack/plugins/runtime_fields/public/constants.ts new file mode 100644 index 0000000000000..017b58c246afe --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/constants.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ComboBoxOption } from './types'; + +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; + +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export const RUNTIME_FIELD_OPTIONS: Array> = [ + { + label: 'Keyword', + value: 'keyword', + }, + { + label: 'Long', + value: 'long', + }, + { + label: 'Double', + value: 'double', + }, + { + label: 'Date', + value: 'date', + }, + { + label: 'IP', + value: 'ip', + }, + { + label: 'Boolean', + value: 'boolean', + }, +]; diff --git a/x-pack/plugins/runtime_fields/public/index.ts b/x-pack/plugins/runtime_fields/public/index.ts new file mode 100644 index 0000000000000..0eab32c0b3d97 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { RuntimeFieldsPlugin } from './plugin'; + +export { + RuntimeFieldEditorFlyoutContent, + RuntimeFieldEditor, + RuntimeFieldFormState, +} from './components'; +export { RUNTIME_FIELD_OPTIONS } from './constants'; +export { RuntimeField, RuntimeType, PluginSetup as RuntimeFieldsSetup } from './types'; + +export function plugin() { + return new RuntimeFieldsPlugin(); +} diff --git a/x-pack/plugins/runtime_fields/public/lib/documentation.ts b/x-pack/plugins/runtime_fields/public/lib/documentation.ts new file mode 100644 index 0000000000000..87eab8b7ed997 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/lib/documentation.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DocLinksStart } from 'src/core/public'; + +export const getLinks = (docLinks: DocLinksStart) => { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; + + return { + painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, + }; +}; diff --git a/x-pack/plugins/runtime_fields/public/lib/index.ts b/x-pack/plugins/runtime_fields/public/lib/index.ts new file mode 100644 index 0000000000000..11c9914bf2e81 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getLinks } from './documentation'; diff --git a/x-pack/plugins/runtime_fields/public/load_editor.tsx b/x-pack/plugins/runtime_fields/public/load_editor.tsx new file mode 100644 index 0000000000000..f1b9c495f0336 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/load_editor.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { CoreSetup, OverlayRef } from 'src/core/public'; + +import { toMountPoint, createKibanaReactContext } from './shared_imports'; +import { LoadEditorResponse, RuntimeField } from './types'; + +export interface OpenRuntimeFieldEditorProps { + onSave(field: RuntimeField): void; + defaultValue?: RuntimeField; +} + +export const getRuntimeFieldEditorLoader = (coreSetup: CoreSetup) => async (): Promise< + LoadEditorResponse +> => { + const { RuntimeFieldEditorFlyoutContent } = await import('./components'); + const [core] = await coreSetup.getStartServices(); + const { uiSettings, overlays, docLinks } = core; + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings }); + + let overlayRef: OverlayRef | null = null; + + const openEditor = ({ onSave, defaultValue }: OpenRuntimeFieldEditorProps) => { + const closeEditor = () => { + overlayRef?.close(); + overlayRef = null; + }; + + const onSaveField = (field: RuntimeField) => { + closeEditor(); + onSave(field); + }; + + overlayRef = overlays.openFlyout( + toMountPoint( + + overlayRef?.close()} + docLinks={docLinks} + defaultValue={defaultValue} + /> + + ) + ); + + return closeEditor; + }; + + return { + openEditor, + }; +}; diff --git a/x-pack/plugins/runtime_fields/public/plugin.test.ts b/x-pack/plugins/runtime_fields/public/plugin.test.ts new file mode 100644 index 0000000000000..07f7a3553d0d3 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/plugin.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { coreMock } from 'src/core/public/mocks'; + +jest.mock('../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../src/plugins/kibana_react/public'); + + return { + ...original, + toMountPoint: (node: React.ReactNode) => node, + }; +}); + +import { StartPlugins, PluginStart } from './types'; +import { RuntimeFieldEditorFlyoutContent } from './components'; +import { RuntimeFieldsPlugin } from './plugin'; + +const noop = () => {}; + +describe('RuntimeFieldsPlugin', () => { + let coreSetup: CoreSetup; + let plugin: RuntimeFieldsPlugin; + + beforeEach(() => { + plugin = new RuntimeFieldsPlugin(); + coreSetup = coreMock.createSetup(); + }); + + test('should return a handler to load the runtime field editor', async () => { + const setupApi = await plugin.setup(coreSetup, {}); + expect(setupApi.loadEditor).toBeDefined(); + }); + + test('once it is loaded it should expose a handler to open the editor', async () => { + const setupApi = await plugin.setup(coreSetup, {}); + const response = await setupApi.loadEditor(); + expect(response.openEditor).toBeDefined(); + }); + + test('should call core.overlays.openFlyout when opening the editor', async () => { + const openFlyout = jest.fn(); + const onSaveSpy = jest.fn(); + + const mockCore = { + overlays: { + openFlyout, + }, + uiSettings: {}, + }; + coreSetup.getStartServices = async () => [mockCore] as any; + const setupApi = await plugin.setup(coreSetup, {}); + const { openEditor } = await setupApi.loadEditor(); + + openEditor({ onSave: onSaveSpy }); + + expect(openFlyout).toHaveBeenCalled(); + + const [[arg]] = openFlyout.mock.calls; + expect(arg.props.children.type).toBe(RuntimeFieldEditorFlyoutContent); + + // We force call the "onSave" prop from the component + // and make sure that the the spy is being called. + // Note: we are testing implementation details, if we change or rename the "onSave" prop on + // the component, we will need to update this test accordingly. + expect(arg.props.children.props.onSave).toBeDefined(); + arg.props.children.props.onSave(); + expect(onSaveSpy).toHaveBeenCalled(); + }); + + test('should return a handler to close the flyout', async () => { + const setupApi = await plugin.setup(coreSetup, {}); + const { openEditor } = await setupApi.loadEditor(); + + const closeEditorHandler = openEditor({ onSave: noop }); + expect(typeof closeEditorHandler).toBe('function'); + }); +}); diff --git a/x-pack/plugins/runtime_fields/public/plugin.ts b/x-pack/plugins/runtime_fields/public/plugin.ts new file mode 100644 index 0000000000000..ebc8b98db66ba --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/plugin.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; + +import { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types'; +import { getRuntimeFieldEditorLoader } from './load_editor'; + +export class RuntimeFieldsPlugin + implements Plugin { + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { + return { + loadEditor: getRuntimeFieldEditorLoader(core), + }; + } + + public start(core: CoreStart, plugins: StartPlugins) { + return {}; + } + + public stop() { + return {}; + } +} diff --git a/x-pack/plugins/runtime_fields/public/shared_imports.ts b/x-pack/plugins/runtime_fields/public/shared_imports.ts new file mode 100644 index 0000000000000..200a68ab71031 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/shared_imports.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + useForm, + Form, + FormSchema, + UseField, + FormHook, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { TextField } from '../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { + CodeEditor, + toMountPoint, + createKibanaReactContext, +} from '../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/runtime_fields/public/test_utils.ts b/x-pack/plugins/runtime_fields/public/test_utils.ts new file mode 100644 index 0000000000000..966db01ef1532 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/test_utils.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerTestBed, TestBed } from '@kbn/test/jest'; diff --git a/x-pack/plugins/runtime_fields/public/types.ts b/x-pack/plugins/runtime_fields/public/types.ts new file mode 100644 index 0000000000000..4172061540af8 --- /dev/null +++ b/x-pack/plugins/runtime_fields/public/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DataPublicPluginStart } from 'src/plugins/data/public'; + +import { RUNTIME_FIELD_TYPES } from './constants'; +import { OpenRuntimeFieldEditorProps } from './load_editor'; + +export interface LoadEditorResponse { + openEditor(props: OpenRuntimeFieldEditorProps): () => void; +} + +export interface PluginSetup { + loadEditor(): Promise; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginStart {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SetupPlugins {} + +export interface StartPlugins { + data: DataPublicPluginStart; +} + +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export interface RuntimeField { + name: string; + type: RuntimeType; + script: string; +} + +export interface ComboBoxOption { + label: string; + value?: T; +} From 6ff61c003dc4850045ff8fd0c24fc8fb0f1e0cbc Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Wed, 18 Nov 2020 11:13:42 +0300 Subject: [PATCH 54/99] Added eventBus to trigger and listen plotHandler event (#83435) --- .../components/timelion_vis_component.tsx | 32 +++++++++++++++++-- .../public/helpers/panel_utils.ts | 4 +++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx index a448b58afe8a4..baf3365a514a6 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx @@ -33,6 +33,8 @@ import { SERIES_ID_ATTR, colors, Axis, + ACTIVE_CURSOR, + eventBus, } from '../helpers/panel_utils'; import { Series, Sheet } from '../helpers/timelion_request_handler'; @@ -338,16 +340,40 @@ function TimelionVisComponent({ }); }, [legendCaption, legendValueNumbers]); + const plotHover = useCallback( + (pos: Position) => { + (plot as CrosshairPlot).setCrosshair(pos); + debouncedSetLegendNumbers(pos); + }, + [plot, debouncedSetLegendNumbers] + ); + const plotHoverHandler = useCallback( (event: JQuery.TriggeredEvent, pos: Position) => { if (!plot) { return; } - (plot as CrosshairPlot).setCrosshair(pos); - debouncedSetLegendNumbers(pos); + plotHover(pos); + eventBus.trigger(ACTIVE_CURSOR, [event, pos]); }, - [plot, debouncedSetLegendNumbers] + [plot, plotHover] ); + + useEffect(() => { + const updateCursor = (_: any, event: JQuery.TriggeredEvent, pos: Position) => { + if (!plot) { + return; + } + plotHover(pos); + }; + + eventBus.on(ACTIVE_CURSOR, updateCursor); + + return () => { + eventBus.off(ACTIVE_CURSOR, updateCursor); + }; + }, [plot, plotHover]); + const mouseLeaveHandler = useCallback(() => { if (!plot) { return; diff --git a/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts b/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts index 860b4e9f2dbde..ba363cf30a079 100644 --- a/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts +++ b/src/plugins/vis_type_timelion/public/helpers/panel_utils.ts @@ -18,6 +18,7 @@ */ import { cloneDeep, defaults, mergeWith, compact } from 'lodash'; +import $ from 'jquery'; import moment, { Moment } from 'moment-timezone'; import { TimefilterContract } from 'src/plugins/data/public'; @@ -50,6 +51,9 @@ interface TimeRangeBounds { max: Moment | undefined; } +export const ACTIVE_CURSOR = 'ACTIVE_CURSOR_TIMELION'; +export const eventBus = $({}); + const colors = [ '#01A4A4', '#C66', From 484437f66dcf056e57b7b8be40cf0e397458a7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 18 Nov 2020 09:32:33 +0100 Subject: [PATCH 55/99] [UsageCollection] Expose `KibanaRequest` to explicitly opted-in collectors (#83413) --- .../telemetry_collectors/nested_collector.ts | 2 +- src/plugins/data/server/server.api.md | 8 +- ...emetry_application_usage_collector.test.ts | 6 +- .../server/collectors/core/index.test.ts | 10 +- .../collectors/csp/csp_collector.test.ts | 17 +- .../server/collectors/kibana/index.test.ts | 13 +- .../collectors/management/index.test.ts | 10 +- .../server/collectors/ops_stats/index.test.ts | 9 +- .../server/collectors/ui_metric/index.test.ts | 10 +- .../server/telemetry_collection/get_kibana.ts | 11 +- .../get_local_stats.test.ts | 3 +- .../telemetry_collection/get_local_stats.ts | 14 +- .../server/plugin.ts | 5 +- .../server/types.ts | 1 + src/plugins/usage_collection/README.md | 10 +- .../server/collector/collector.ts | 126 ++++++-- .../server/collector/collector_set.test.ts | 279 +++++++++++++++++- .../server/collector/collector_set.ts | 82 +++-- .../server/collector/usage_collector.ts | 32 +- src/plugins/usage_collection/server/config.ts | 2 +- src/plugins/usage_collection/server/index.ts | 2 +- src/plugins/usage_collection/server/mocks.ts | 2 +- src/plugins/usage_collection/server/plugin.ts | 2 +- .../server/report/store_report.ts | 2 +- .../usage_collection/server/routes/index.ts | 2 +- .../server/routes/report_metrics.ts | 2 +- .../server/routes/stats/stats.ts | 13 +- .../server/usage_collection.mock.ts | 28 +- .../register_vega_collector.test.ts | 4 +- .../register_visualizations_collector.test.ts | 4 +- .../collectors/get_settings_collector.ts | 14 +- .../collectors/get_usage_collector.test.ts | 33 ++- .../collectors/get_usage_collector.ts | 14 +- .../kibana_monitoring/collectors/index.ts | 6 +- x-pack/plugins/monitoring/server/plugin.ts | 2 +- .../get_all_stats.test.ts | 2 + .../get_cluster_uuids.test.ts | 5 +- .../usage/reporting_usage_collector.test.ts | 28 +- .../spaces_usage_collector.test.ts | 32 +- 39 files changed, 667 insertions(+), 180 deletions(-) diff --git a/src/fixtures/telemetry_collectors/nested_collector.ts b/src/fixtures/telemetry_collectors/nested_collector.ts index bde89fe4a7060..ce563b46b0c4e 100644 --- a/src/fixtures/telemetry_collectors/nested_collector.ts +++ b/src/fixtures/telemetry_collectors/nested_collector.ts @@ -29,7 +29,7 @@ interface Usage { } export class NestedInside { - collector?: UsageCollector; + collector?: UsageCollector; createMyCollector() { this.collector = collectorSet.makeUsageCollector({ type: 'my_nested_collector', diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index b2db4f5c74729..ce66610edf880 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -27,15 +27,15 @@ import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; -import { ISavedObjectsRepository } from 'kibana/server'; +import { ISavedObjectsRepository } from 'src/core/server'; import { IScopedClusterClient } from 'src/core/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'src/core/server'; import { KibanaRequest } from 'src/core/server'; -import { LegacyAPICaller } from 'kibana/server'; -import { Logger } from 'kibana/server'; -import { Logger as Logger_2 } from 'src/core/server'; +import { LegacyAPICaller } from 'src/core/server'; +import { Logger } from 'src/core/server'; +import { Logger as Logger_2 } from 'kibana/server'; import { LoggerFactory } from '@kbn/logging'; import { Moment } from 'moment'; import moment from 'moment'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts index c1457c64080a6..6cb104416ef58 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts @@ -19,7 +19,7 @@ import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; import { - CollectorOptions, + Collector, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; @@ -40,11 +40,11 @@ describe('telemetry_application_usage', () => { const logger = loggingSystemMock.createLogger(); - let collector: CollectorOptions; + let collector: Collector; const usageCollectionMock = createUsageCollectionSetupMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = config; + collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts index e8efa9997c459..e31437a744e29 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts @@ -18,20 +18,22 @@ */ import { - CollectorOptions, + Collector, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { registerCoreUsageCollector } from '.'; -import { coreUsageDataServiceMock } from '../../../../../core/server/mocks'; +import { coreUsageDataServiceMock, loggingSystemMock } from '../../../../../core/server/mocks'; import { CoreUsageData } from 'src/core/server/'; +const logger = loggingSystemMock.createLogger(); + describe('telemetry_core', () => { - let collector: CollectorOptions; + let collector: Collector; const usageCollectionMock = createUsageCollectionSetupMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = config; + collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts index 03184d7385861..2851382f7559a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts @@ -19,8 +19,13 @@ import { CspConfig, ICspConfig } from '../../../../../core/server'; import { createCspCollector } from './csp_collector'; -import { httpServiceMock } from '../../../../../core/server/mocks'; -import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; +import { httpServiceMock, loggingSystemMock } from '../../../../../core/server/mocks'; +import { + Collector, + createCollectorFetchContextMock, +} from 'src/plugins/usage_collection/server/mocks'; + +const logger = loggingSystemMock.createLogger(); describe('csp collector', () => { let httpMock: ReturnType; @@ -36,7 +41,7 @@ describe('csp collector', () => { }); test('fetches whether strict mode is enabled', async () => { - const collector = createCspCollector(httpMock); + const collector = new Collector(logger, createCspCollector(httpMock)); expect((await collector.fetch(mockedFetchContext)).strict).toEqual(true); @@ -45,7 +50,7 @@ describe('csp collector', () => { }); test('fetches whether the legacy browser warning is enabled', async () => { - const collector = createCspCollector(httpMock); + const collector = new Collector(logger, createCspCollector(httpMock)); expect((await collector.fetch(mockedFetchContext)).warnLegacyBrowsers).toEqual(true); @@ -54,7 +59,7 @@ describe('csp collector', () => { }); test('fetches whether the csp rules have been changed or not', async () => { - const collector = createCspCollector(httpMock); + const collector = new Collector(logger, createCspCollector(httpMock)); expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(false); @@ -63,7 +68,7 @@ describe('csp collector', () => { }); test('does not include raw csp rules under any property names', async () => { - const collector = createCspCollector(httpMock); + const collector = new Collector(logger, createCspCollector(httpMock)); // It's important that we do not send the value of csp.rules here as it // can be customized with values that can be identifiable to given diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts index 88ccb2016d420..16a60eca60f47 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts @@ -17,20 +17,25 @@ * under the License. */ -import { pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; import { - CollectorOptions, + loggingSystemMock, + pluginInitializerContextConfigMock, +} from '../../../../../core/server/mocks'; +import { + Collector, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { createCollectorFetchContextMock } from '../../../../usage_collection/server/mocks'; import { registerKibanaUsageCollector } from './'; +const logger = loggingSystemMock.createLogger(); + describe('telemetry_kibana', () => { - let collector: CollectorOptions; + let collector: Collector; const usageCollectionMock = createUsageCollectionSetupMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = config; + collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts index e671f739ee083..0aafee01cf49d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts @@ -17,21 +17,23 @@ * under the License. */ -import { uiSettingsServiceMock } from '../../../../../core/server/mocks'; +import { loggingSystemMock, uiSettingsServiceMock } from '../../../../../core/server/mocks'; import { - CollectorOptions, + Collector, createUsageCollectionSetupMock, createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerManagementUsageCollector } from './'; +const logger = loggingSystemMock.createLogger(); + describe('telemetry_application_usage_collector', () => { - let collector: CollectorOptions; + let collector: Collector; const usageCollectionMock = createUsageCollectionSetupMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = config; + collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts index 61990730812cc..8db7010e64026 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts @@ -19,20 +19,23 @@ import { Subject } from 'rxjs'; import { - CollectorOptions, + Collector, createUsageCollectionSetupMock, createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerOpsStatsCollector } from './'; import { OpsMetrics } from '../../../../../core/server'; +import { loggingSystemMock } from '../../../../../core/server/mocks'; + +const logger = loggingSystemMock.createLogger(); describe('telemetry_ops_stats', () => { - let collector: CollectorOptions; + let collector: Collector; const usageCollectionMock = createUsageCollectionSetupMock(); usageCollectionMock.makeStatsCollector.mockImplementation((config) => { - collector = config; + collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeStatsCollector(config); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts index 48e4e0d99d3cd..90e3b7110e1dc 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts @@ -17,21 +17,23 @@ * under the License. */ -import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; +import { loggingSystemMock, savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; import { - CollectorOptions, + Collector, createUsageCollectionSetupMock, createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerUiMetricUsageCollector } from './'; +const logger = loggingSystemMock.createLogger(); + describe('telemetry_ui_metric', () => { - let collector: CollectorOptions; + let collector: Collector; const usageCollectionMock = createUsageCollectionSetupMock(); usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = config; + collector = new Collector(logger, config); return createUsageCollectionSetupMock().makeUsageCollector(config); }); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 654c5435650cf..207a467ca5fd0 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -21,6 +21,7 @@ import { omit } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ISavedObjectsRepository, + KibanaRequest, LegacyAPICaller, SavedObjectsClientContract, } from 'kibana/server'; @@ -89,8 +90,14 @@ export async function getKibana( usageCollection: UsageCollectionSetup, callWithInternalUser: LegacyAPICaller, asInternalUser: ElasticsearchClient, - soClient: SavedObjectsClientContract | ISavedObjectsRepository + soClient: SavedObjectsClientContract | ISavedObjectsRepository, + kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter ): Promise { - const usage = await usageCollection.bulkFetch(callWithInternalUser, asInternalUser, soClient); + const usage = await usageCollection.bulkFetch( + callWithInternalUser, + asInternalUser, + soClient, + kibanaRequest + ); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 9298b36ac3ea6..6231fd29e7d3d 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -24,7 +24,7 @@ import { usageCollectionPluginMock, createCollectorFetchContextMock, } from '../../../usage_collection/server/mocks'; -import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; function mockUsageCollection(kibanaUsage = {}) { const usageCollection = usageCollectionPluginMock.createSetupContract(); @@ -87,6 +87,7 @@ function mockStatsCollectionConfig(clusterInfo: any, clusterStats: any, kibana: ...createCollectorFetchContextMock(), esClient: mockGetLocalStats(clusterInfo, clusterStats), usageCollection: mockUsageCollection(kibana), + kibanaRequest: httpServerMock.createKibanaRequest(), timestamp: Date.now(), }; } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 4aeefb1d81d6a..a3666683a05a1 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -62,16 +62,16 @@ export type TelemetryLocalStats = ReturnType; /** * Get statistics for all products joined by Elasticsearch cluster. - * @param {Array} cluster uuids - * @param {Object} config contains the new esClient already scoped contains usageCollection, callCluster, esClient, start, end + * @param {Array} cluster uuids array of cluster uuid's + * @param {Object} config contains the usageCollection, callCluster (deprecated), the esClient and Saved Objects client scoped to the request or the internal repository, and the kibana request * @param {Object} StatsCollectionContext contains logger and version (string) */ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( - clustersDetails, // array of cluster uuid's - config, // contains the new esClient already scoped contains usageCollection, callCluster, esClient, start, end and the saved objects client scoped to the request or the internal repository - context // StatsCollectionContext contains logger and version (string) + clustersDetails, + config, + context ) => { - const { callCluster, usageCollection, esClient, soClient } = config; + const { callCluster, usageCollection, esClient, soClient, kibanaRequest } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { @@ -79,7 +79,7 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( getClusterInfo(esClient), // cluster info getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) getNodesUsage(esClient), // nodes_usage info - getKibana(usageCollection, callCluster, esClient, soClient), + getKibana(usageCollection, callCluster, esClient, soClient, kibanaRequest), getDataTelemetry(esClient), ]); return handleLocalStats( diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index e1e1379097adf..2cd06f13a8855 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -157,7 +157,10 @@ export class TelemetryCollectionManagerPlugin const soClient = config.unencrypted ? collectionSoService.getScopedClient(config.request) : collectionSoService.createInternalRepository(); - return { callCluster, timestamp, usageCollection, esClient, soClient }; + // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted + const kibanaRequest = config.unencrypted ? request : void 0; + + return { callCluster, timestamp, usageCollection, esClient, soClient, kibanaRequest }; } private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) { diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 0100fdbbb3970..7d25b8c8261c4 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -79,6 +79,7 @@ export interface StatsCollectionConfig { timestamp: number; esClient: ElasticsearchClient; soClient: SavedObjectsClientContract | ISavedObjectsRepository; + kibanaRequest: KibanaRequest | undefined; // intentionally `| undefined` to enforce providing the parameter } export interface BasicStatsPayload { diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 2ac3de510f8ae..5e6ed901c7647 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -31,7 +31,7 @@ Then you need to make the Telemetry service aware of the collector by registerin ```ts // server/plugin.ts import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; - import { CoreSetup, CoreStart } from 'kibana/server'; + import { CoreSetup, CoreStart } from 'src/core/server'; class Plugin { public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { @@ -46,7 +46,7 @@ Then you need to make the Telemetry service aware of the collector by registerin ```ts // server/collectors/register.ts import { UsageCollectionSetup, CollectorFetchContext } from 'src/plugins/usage_collection/server'; - import { APICluster } from 'kibana/server'; + import { APICluster } from 'src/core/server'; interface Usage { my_objects: { @@ -95,8 +95,8 @@ Some background: - `isReady` (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the `fetch` method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its `fetch` method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns `true` for `isReady` and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your `fetch` method can run without the need of any previous dependencies, then you can return true for `isReady` as shown in the example below. -- The `fetch` method needs to support multiple contexts in which it is called. For example, when stats are pulled from a Kibana Metricbeat module, the Beat calls Kibana's stats API to invoke usage collection. -In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest` or `esClient` wraps `asCurrentUser`, where the request headers are expected to have read privilege on the entire `.kibana' index. The `fetch` method also exposes the saved objects client that will have the correct scope when the collectors' `fetch` method is called. +- The `fetch` method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the **Kibana>Advanced Settings>Usage data** section, the clients provided in the context of the function (`CollectorFetchContext`) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, the `fetch` method receives the clients `callCluster`, `esClient` and `soClient` scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (`kibana_system`). Please, mind it might have lower-level access than the default super-admin `elastic` test user. +In some scenarios, your collector might need to maintain its own client. An example of that is the `monitoring` plugin, that maintains a connection to the Remote Monitoring Cluster to push its monitoring data. If that's the case, your plugin can opt-in to receive the additional `kibanaRequest` parameter by adding `extendFetchContext.kibanaRequest: true` to the collector's config: it will be appended to the context of the `fetch` method only if the request needs to be scoped to a user other than Kibana Internal, so beware that your collector will need to work for both scenarios (especially for the scenario when `kibanaRequest` is missing). Note: there will be many cases where you won't need to use the `callCluster`, `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. @@ -105,7 +105,7 @@ In the case of using a custom SavedObjects client, it is up to the plugin to ini ```ts // server/plugin.ts import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { CoreSetup, CoreStart } from 'kibana/server'; +import { CoreSetup, CoreStart } from 'src/core/server'; class Plugin { private savedObjectsRepository?: ISavedObjectsRepository; diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index c04b087d4adf5..797fdaa06a620 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -23,7 +23,8 @@ import { ElasticsearchClient, ISavedObjectsRepository, SavedObjectsClientContract, -} from 'kibana/server'; + KibanaRequest, +} from 'src/core/server'; export type CollectorFormatForBulkUpload = (result: T) => { type: string; payload: U }; @@ -46,26 +47,71 @@ export type MakeSchemaFrom = { : RecursiveMakeSchemaFrom[Key]>; }; -export interface CollectorFetchContext { +/** + * The context for the `fetch` method: It includes the most commonly used clients in the collectors (ES and SO clients). + * Both are scoped based on the request and the context: + * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they shouldn't read + * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user + * + * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster. + */ +export type CollectorFetchContext = { /** - * @depricated Scoped Legacy Elasticsearch client: use esClient instead + * @deprecated Scoped Legacy Elasticsearch client: use esClient instead */ callCluster: LegacyAPICaller; /** - * Request-scoped Elasticsearch client: - * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they should't read - * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user + * Request-scoped Elasticsearch client + * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) */ esClient: ElasticsearchClient; /** - * Request-scoped Saved Objects client: - * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they should't read - * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user + * Request-scoped Saved Objects client + * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) */ soClient: SavedObjectsClientContract | ISavedObjectsRepository; +} & (WithKibanaRequest extends true + ? { + /** + * The KibanaRequest that can be used to scope the requests: + * It is provided only when your custom clients need to be scoped. If not available, you should use the Internal Client. + * More information about when scoping is needed: {@link CollectorFetchContext} + * @remark You should only use this if you implement your collector to deal with both scenarios: when provided and, especially, when not provided. When telemetry payload is sent to the remote service the `kibanaRequest` will not be provided. + */ + kibanaRequest?: KibanaRequest; + } + : {}); + +export type CollectorFetchMethod< + WithKibanaRequest extends boolean | undefined, + TReturn, + ExtraOptions extends object = {} +> = ( + this: Collector & ExtraOptions, // Specify the context of `this` for this.log and others to become available + context: CollectorFetchContext +) => Promise | TReturn; + +export interface ICollectorOptionsFetchExtendedContext { + /** + * Set to `true` if your `fetch` method requires the `KibanaRequest` object to be added in its context {@link CollectorFetchContextWithRequest}. + * @remark You should fully understand acknowledge that by using the `KibanaRequest` in your collector, you need to ensure it should specially work without it because it won't be provided when building the telemetry payload actually sent to the remote telemetry service. + */ + kibanaRequest?: WithKibanaRequest; } -export interface CollectorOptions { +export type CollectorOptionsFetchExtendedContext< + WithKibanaRequest extends boolean +> = ICollectorOptionsFetchExtendedContext & + (WithKibanaRequest extends true // If enforced to true via Types, the config must be expected + ? Required, 'kibanaRequest'>> + : {}); + +export type CollectorOptions< + TFetchReturn = unknown, + UFormatBulkUploadPayload = TFetchReturn, // TODO: Once we remove bulk_uploader's dependency on usageCollection, we'll be able to remove this type + WithKibanaRequest extends boolean = boolean, + ExtraOptions extends object = {} +> = { /** * Unique string identifier for the collector */ @@ -78,23 +124,42 @@ export interface CollectorOptions { /** * Schema definition of the output of the `fetch` method. */ - schema?: MakeSchemaFrom; - fetch: (collectorFetchContext: CollectorFetchContext) => Promise | T; - /* + schema?: MakeSchemaFrom; + /** + * The method that will collect and return the data in the final format. + * @param collectorFetchContext {@link CollectorFetchContext} + */ + fetch: CollectorFetchMethod; + /** * A hook for allowing the fetched data payload to be organized into a typed * data model for internal bulk upload. See defaultFormatterForBulkUpload for * a generic example. * @deprecated Used only by the Legacy Monitoring collection (to be removed in 8.0) */ - formatForBulkUpload?: CollectorFormatForBulkUpload; -} + formatForBulkUpload?: CollectorFormatForBulkUpload; +} & ExtraOptions & + (WithKibanaRequest extends true // If enforced to true via Types, the config must be enforced + ? { + extendFetchContext: CollectorOptionsFetchExtendedContext; + } + : { + extendFetchContext?: CollectorOptionsFetchExtendedContext; + }); -export class Collector { - public readonly type: CollectorOptions['type']; - public readonly init?: CollectorOptions['init']; - public readonly fetch: CollectorOptions['fetch']; - private readonly _formatForBulkUpload?: CollectorFormatForBulkUpload; - public readonly isReady: CollectorOptions['isReady']; +export class Collector< + TFetchReturn, + UFormatBulkUploadPayload = TFetchReturn, + ExtraOptions extends object = {} +> { + public readonly extendFetchContext: CollectorOptionsFetchExtendedContext; + public readonly type: CollectorOptions['type']; + public readonly init?: CollectorOptions['init']; + public readonly fetch: CollectorFetchMethod; + public readonly isReady: CollectorOptions['isReady']; + private readonly _formatForBulkUpload?: CollectorFormatForBulkUpload< + TFetchReturn, + UFormatBulkUploadPayload + >; /* * @param {Object} logger - logger object * @param {String} options.type - property name as the key for the data @@ -105,8 +170,16 @@ export class Collector { * @param {Function} options.rest - optional other properties */ constructor( - protected readonly log: Logger, - { type, init, fetch, formatForBulkUpload, isReady, ...options }: CollectorOptions + public readonly log: Logger, + { + type, + init, + fetch, + formatForBulkUpload, + isReady, + extendFetchContext = {}, + ...options + }: CollectorOptions ) { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); @@ -126,10 +199,11 @@ export class Collector { this.init = init; this.fetch = fetch; this.isReady = typeof isReady === 'function' ? isReady : () => true; + this.extendFetchContext = extendFetchContext; this._formatForBulkUpload = formatForBulkUpload; } - public formatForBulkUpload(result: T) { + public formatForBulkUpload(result: TFetchReturn) { if (this._formatForBulkUpload) { return this._formatForBulkUpload(result); } else { @@ -137,10 +211,10 @@ export class Collector { } } - protected defaultFormatterForBulkUpload(result: T) { + protected defaultFormatterForBulkUpload(result: TFetchReturn) { return { type: this.type, - payload: (result as unknown) as U, + payload: (result as unknown) as UFormatBulkUploadPayload, }; } } diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 359a2d214f991..fc17ce131430c 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -47,6 +47,7 @@ describe('CollectorSet', () => { const mockCallCluster = jest.fn().mockResolvedValue({ passTest: 1000 }); const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const mockSoClient = savedObjectsRepositoryMock.create(); + const req = void 0; // No need to instantiate any KibanaRequest in these tests it('should throw an error if non-Collector type of object is registered', () => { const collectors = new CollectorSet({ logger }); @@ -93,7 +94,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); expect(loggerSpies.debug).toHaveBeenCalledTimes(1); expect(loggerSpies.debug).toHaveBeenCalledWith( 'Fetching data from MY_TEST_COLLECTOR collector' @@ -118,7 +119,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); + result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); } catch (err) { // Do nothing } @@ -136,7 +137,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -154,7 +155,7 @@ describe('CollectorSet', () => { } as any) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -177,7 +178,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient, req); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -274,4 +275,272 @@ describe('CollectorSet', () => { expect(collectors.isUsageCollector(void 0)).toEqual(false); }); }); + + describe('makeStatsCollector', () => { + const collectorSet = new CollectorSet({ logger }); + test('TS should hide kibanaRequest when not opted-in', () => { + collectorSet.makeStatsCollector({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + }); + }); + + test('TS should hide kibanaRequest when not opted-in (explicit false)', () => { + collectorSet.makeStatsCollector({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + kibanaRequest: false, + }, + }); + }); + + test('TS should allow using kibanaRequest when opted-in (explicit true)', () => { + collectorSet.makeStatsCollector({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + kibanaRequest: true, + }, + }); + }); + + test('fetch can use the logger (TS allows it)', () => { + const collector = collectorSet.makeStatsCollector({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch() { + this.log.info("I can use the Collector's class logger!"); + return { test: 1 }; + }, + }); + expect( + collector.fetch( + // @ts-expect-error: the test implementation is not using it + {} + ) + ).toStrictEqual({ test: 1 }); + }); + }); + + describe('makeUsageCollector', () => { + const collectorSet = new CollectorSet({ logger }); + describe('TS validations', () => { + describe('when types are inferred', () => { + test('TS should hide kibanaRequest when not opted-in', () => { + collectorSet.makeUsageCollector({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + }); + }); + + test('TS should hide kibanaRequest when not opted-in (explicit false)', () => { + collectorSet.makeUsageCollector({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + kibanaRequest: false, + }, + }); + }); + + test('TS should allow using kibanaRequest when opted-in (explicit true)', () => { + collectorSet.makeUsageCollector({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + kibanaRequest: true, + }, + }); + }); + }); + + describe('when types are explicit', () => { + test('TS should hide `kibanaRequest` from ctx when undefined or false', () => { + collectorSet.makeUsageCollector<{ test: number }>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + }); + collectorSet.makeUsageCollector<{ test: number }, unknown, false>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + kibanaRequest: false, + }, + }); + collectorSet.makeUsageCollector<{ test: number }, unknown, false>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + }); + }); + test('TS should not allow `true` when types declare false', () => { + // false is the default when at least 1 type is specified + collectorSet.makeUsageCollector<{ test: number }>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + // @ts-expect-error + kibanaRequest: true, + }, + }); + collectorSet.makeUsageCollector<{ test: number }, unknown, false>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + // @ts-expect-error + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + // @ts-expect-error + kibanaRequest: true, + }, + }); + }); + + test('TS should allow `true` when types explicitly declare `true` and do not allow `false` or undefined', () => { + // false is the default when at least 1 type is specified + collectorSet.makeUsageCollector<{ test: number }, unknown, true>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + kibanaRequest: true, + }, + }); + collectorSet.makeUsageCollector<{ test: number }, unknown, true>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + // @ts-expect-error + kibanaRequest: false, + }, + }); + collectorSet.makeUsageCollector<{ test: number }, unknown, true>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + extendFetchContext: { + // @ts-expect-error + kibanaRequest: undefined, + }, + }); + collectorSet.makeUsageCollector<{ test: number }, unknown, true>({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + // @ts-expect-error + extendFetchContext: {}, + }); + collectorSet.makeUsageCollector<{ test: number }, unknown, true>( + // @ts-expect-error + { + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch: (ctx) => { + const { kibanaRequest } = ctx; + return { test: kibanaRequest ? 1 : 0 }; + }, + } + ); + }); + }); + }); + + test('fetch can use the logger (TS allows it)', () => { + const collector = collectorSet.makeUsageCollector({ + type: 'MY_TEST_COLLECTOR', + isReady: () => true, + schema: { test: { type: 'long' } }, + fetch() { + this.log.info("I can use the Collector's class logger!"); + return { test: 1 }; + }, + }); + expect( + collector.fetch( + // @ts-expect-error: the test implementation is not using it + {} + ) + ).toStrictEqual({ test: 1 }); + }); + }); }); diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index c52830cda6513..fe4f3536ffed6 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -24,50 +24,79 @@ import { ElasticsearchClient, ISavedObjectsRepository, SavedObjectsClientContract, -} from 'kibana/server'; + KibanaRequest, +} from 'src/core/server'; import { Collector, CollectorOptions } from './collector'; import { UsageCollector, UsageCollectorOptions } from './usage_collector'; +type AnyCollector = Collector; +type AnyUsageCollector = UsageCollector; + interface CollectorSetConfig { logger: Logger; maximumWaitTimeForAllCollectorsInS?: number; - collectors?: Array>; + collectors?: AnyCollector[]; } export class CollectorSet { private _waitingForAllCollectorsTimestamp?: number; private readonly logger: Logger; private readonly maximumWaitTimeForAllCollectorsInS: number; - private readonly collectors: Map>; + private readonly collectors: Map; constructor({ logger, maximumWaitTimeForAllCollectorsInS, collectors = [] }: CollectorSetConfig) { this.logger = logger; this.collectors = new Map(collectors.map((collector) => [collector.type, collector])); this.maximumWaitTimeForAllCollectorsInS = maximumWaitTimeForAllCollectorsInS || 60; } + /** + * Instantiates a stats collector with the definition provided in the options + * @param options Definition of the collector {@link CollectorOptions} + */ public makeStatsCollector = < - T, - U, - O extends CollectorOptions = CollectorOptions // Used to allow extra properties (the Collector constructor extends the class with the additional options provided) + TFetchReturn, + TFormatForBulkUpload, + WithKibanaRequest extends boolean, + ExtraOptions extends object = {} >( - options: O + options: CollectorOptions ) => { - return new Collector(this.logger, options); + return new Collector(this.logger, options); }; + + /** + * Instantiates an usage collector with the definition provided in the options + * @param options Definition of the collector {@link CollectorOptions} + */ public makeUsageCollector = < - T, - U = T, - O extends UsageCollectorOptions = UsageCollectorOptions + TFetchReturn, + TFormatForBulkUpload = { usage: { [key: string]: TFetchReturn } }, + // TODO: Right now, users will need to explicitly claim `true` for TS to allow `kibanaRequest` usage. + // If we improve `telemetry-check-tools` so plugins do not need to specify TFetchReturn, + // we'll be able to remove the type defaults and TS will successfully infer the config value as provided in JS. + WithKibanaRequest extends boolean = false, + ExtraOptions extends object = {} >( - options: O + options: UsageCollectorOptions< + TFetchReturn, + TFormatForBulkUpload, + WithKibanaRequest, + ExtraOptions + > ) => { - return new UsageCollector(this.logger, options); + return new UsageCollector( + this.logger, + options + ); }; - /* - * @param collector {Collector} collector object + /** + * Registers a collector to be used when collecting all the usage and stats data + * @param collector Collector to be added to the set (previously created via `makeUsageCollector` or `makeStatsCollector`) */ - public registerCollector = (collector: Collector) => { + public registerCollector = ( + collector: Collector + ) => { // check instanceof if (!(collector instanceof Collector)) { throw new Error('CollectorSet can only have Collector instances registered'); @@ -89,7 +118,7 @@ export class CollectorSet { return [...this.collectors.values()].find((c) => c.type === type); }; - public isUsageCollector = (x: UsageCollector | any): x is UsageCollector => { + public isUsageCollector = (x: AnyUsageCollector | any): x is AnyUsageCollector => { return x instanceof UsageCollector; }; @@ -144,15 +173,22 @@ export class CollectorSet { callCluster: LegacyAPICaller, esClient: ElasticsearchClient, soClient: SavedObjectsClientContract | ISavedObjectsRepository, - collectors: Map> = this.collectors + kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter + collectors: Map = this.collectors ) => { const responses = await Promise.all( [...collectors.values()].map(async (collector) => { this.logger.debug(`Fetching data from ${collector.type} collector`); try { + const context = { + callCluster, + esClient, + soClient, + ...(collector.extendFetchContext.kibanaRequest && { kibanaRequest }), + }; return { type: collector.type, - result: await collector.fetch({ callCluster, esClient, soClient }), + result: await collector.fetch(context), }; } catch (err) { this.logger.warn(err); @@ -169,7 +205,7 @@ export class CollectorSet { /* * @return {new CollectorSet} */ - public getFilteredCollectorSet = (filter: (col: Collector) => boolean) => { + public getFilteredCollectorSet = (filter: (col: AnyCollector) => boolean) => { const filtered = [...this.collectors.values()].filter(filter); return this.makeCollectorSetFromArray(filtered); }; @@ -177,13 +213,15 @@ export class CollectorSet { public bulkFetchUsage = async ( callCluster: LegacyAPICaller, esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository + savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository, + kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter ) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); return await this.bulkFetch( callCluster, esClient, savedObjectsClient, + kibanaRequest, usageCollectors.collectors ); }; @@ -239,7 +277,7 @@ export class CollectorSet { return [...this.collectors.values()].some(someFn); }; - private makeCollectorSetFromArray = (collectors: Collector[]) => { + private makeCollectorSetFromArray = (collectors: AnyCollector[]) => { return new CollectorSet({ logger: this.logger, maximumWaitTimeForAllCollectorsInS: this.maximumWaitTimeForAllCollectorsInS, diff --git a/src/plugins/usage_collection/server/collector/usage_collector.ts b/src/plugins/usage_collection/server/collector/usage_collector.ts index 5bfc36537e0b0..a042ea113d5cc 100644 --- a/src/plugins/usage_collection/server/collector/usage_collector.ts +++ b/src/plugins/usage_collection/server/collector/usage_collector.ts @@ -22,25 +22,39 @@ import { KIBANA_STATS_TYPE } from '../../common/constants'; import { Collector, CollectorOptions } from './collector'; // Enforce the `schema` property for UsageCollectors -export type UsageCollectorOptions = CollectorOptions & - Required, 'schema'>>; +export type UsageCollectorOptions< + TFetchReturn = unknown, + UFormatBulkUploadPayload = { usage: { [key: string]: TFetchReturn } }, + WithKibanaRequest extends boolean = false, + ExtraOptions extends object = {} +> = CollectorOptions & + Required, 'schema'>>; -export class UsageCollector extends Collector< - T, - U -> { - constructor(protected readonly log: Logger, collectorOptions: UsageCollectorOptions) { +export class UsageCollector< + TFetchReturn, + UFormatBulkUploadPayload = { usage: { [key: string]: TFetchReturn } }, + ExtraOptions extends object = {} +> extends Collector { + constructor( + public readonly log: Logger, + collectorOptions: UsageCollectorOptions< + TFetchReturn, + UFormatBulkUploadPayload, + any, + ExtraOptions + > + ) { super(log, collectorOptions); } - protected defaultFormatterForBulkUpload(result: T) { + protected defaultFormatterForBulkUpload(result: TFetchReturn) { return { type: KIBANA_STATS_TYPE, payload: ({ usage: { [this.type]: result, }, - } as unknown) as U, + } as unknown) as UFormatBulkUploadPayload, }; } } diff --git a/src/plugins/usage_collection/server/config.ts b/src/plugins/usage_collection/server/config.ts index 76379d9385cff..09b0e05025e63 100644 --- a/src/plugins/usage_collection/server/config.ts +++ b/src/plugins/usage_collection/server/config.ts @@ -18,7 +18,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginConfigDescriptor } from 'kibana/server'; +import { PluginConfigDescriptor } from 'src/core/server'; import { DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S } from '../common/constants'; export const configSchema = schema.object({ diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index f7a08fdb5e9dd..48ea9afa13976 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/server'; +import { PluginInitializerContext } from 'src/core/server'; import { UsageCollectionPlugin } from './plugin'; export { diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index d08db1eaec0e1..3d89380f629dc 100644 --- a/src/plugins/usage_collection/server/mocks.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -20,7 +20,7 @@ import { loggingSystemMock } from '../../../core/server/mocks'; import { UsageCollectionSetup } from './plugin'; import { CollectorSet } from './collector'; -export { createCollectorFetchContextMock } from './usage_collection.mock'; +export { Collector, createCollectorFetchContextMock } from './usage_collection.mock'; const createSetupContract = () => { return { diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts index 74e70d5ea9d35..9a8876446d01e 100644 --- a/src/plugins/usage_collection/server/plugin.ts +++ b/src/plugins/usage_collection/server/plugin.ts @@ -25,7 +25,7 @@ import { CoreStart, ISavedObjectsRepository, Plugin, -} from 'kibana/server'; +} from 'src/core/server'; import { ConfigType } from './config'; import { CollectorSet } from './collector'; import { setupRoutes } from './routes'; diff --git a/src/plugins/usage_collection/server/report/store_report.ts b/src/plugins/usage_collection/server/report/store_report.ts index c40622831eeee..d9aac23fd1ff0 100644 --- a/src/plugins/usage_collection/server/report/store_report.ts +++ b/src/plugins/usage_collection/server/report/store_report.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ISavedObjectsRepository, SavedObject } from 'kibana/server'; +import { ISavedObjectsRepository, SavedObject } from 'src/core/server'; import { ReportSchemaType } from './schema'; export async function storeReport( diff --git a/src/plugins/usage_collection/server/routes/index.ts b/src/plugins/usage_collection/server/routes/index.ts index b367ddc184be7..15d408ff3723b 100644 --- a/src/plugins/usage_collection/server/routes/index.ts +++ b/src/plugins/usage_collection/server/routes/index.ts @@ -22,7 +22,7 @@ import { ISavedObjectsRepository, MetricsServiceSetup, ServiceStatus, -} from 'kibana/server'; +} from 'src/core/server'; import { Observable } from 'rxjs'; import { CollectorSet } from '../collector'; import { registerUiMetricRoute } from './report_metrics'; diff --git a/src/plugins/usage_collection/server/routes/report_metrics.ts b/src/plugins/usage_collection/server/routes/report_metrics.ts index a72222968eabf..590c3340697b8 100644 --- a/src/plugins/usage_collection/server/routes/report_metrics.ts +++ b/src/plugins/usage_collection/server/routes/report_metrics.ts @@ -18,7 +18,7 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, ISavedObjectsRepository } from 'kibana/server'; +import { IRouter, ISavedObjectsRepository } from 'src/core/server'; import { storeReport, reportSchema } from '../report'; export function registerUiMetricRoute( diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index d38250067053c..16a1c2c742f04 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -27,6 +27,7 @@ import { ElasticsearchClient, IRouter, ISavedObjectsRepository, + KibanaRequest, LegacyAPICaller, MetricsServiceSetup, SavedObjectsClientContract, @@ -67,9 +68,15 @@ export function registerStatsRoute({ const getUsage = async ( callCluster: LegacyAPICaller, esClient: ElasticsearchClient, - savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository + savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository, + kibanaRequest: KibanaRequest ): Promise => { - const usage = await collectorSet.bulkFetchUsage(callCluster, esClient, savedObjectsClient); + const usage = await collectorSet.bulkFetchUsage( + callCluster, + esClient, + savedObjectsClient, + kibanaRequest + ); return collectorSet.toObject(usage); }; @@ -115,7 +122,7 @@ export function registerStatsRoute({ } const usagePromise = shouldGetUsage - ? getUsage(callCluster, asCurrentUser, savedObjectsClient) + ? getUsage(callCluster, asCurrentUser, savedObjectsClient, req) : Promise.resolve({}); const [usage, clusterUuid] = await Promise.all([ usagePromise, diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts index c31756c60e32d..05dae8fa85164 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -19,13 +19,17 @@ import { elasticsearchServiceMock, + httpServerMock, + loggingSystemMock, savedObjectsRepositoryMock, } from '../../../../src/core/server/mocks'; -import { CollectorOptions } from './collector/collector'; +import { CollectorOptions, Collector, UsageCollector } from './collector'; import { UsageCollectionSetup, CollectorFetchContext } from './index'; -export { CollectorOptions }; +export { CollectorOptions, Collector }; + +const logger = loggingSystemMock.createLogger(); export const createUsageCollectionSetupMock = () => { const usageCollectionSetupMock: jest.Mocked = { @@ -37,13 +41,13 @@ export const createUsageCollectionSetupMock = () => { // @ts-ignore jest.fn doesn't play nice with type guards isUsageCollector: jest.fn(), makeCollectorSetFromArray: jest.fn(), - makeStatsCollector: jest.fn(), map: jest.fn(), maximumWaitTimeForAllCollectorsInS: 0, some: jest.fn(), toApiFieldNames: jest.fn(), toObject: jest.fn(), - makeUsageCollector: jest.fn(), + makeStatsCollector: jest.fn().mockImplementation((cfg) => new Collector(logger, cfg)), + makeUsageCollector: jest.fn().mockImplementation((cfg) => new UsageCollector(logger, cfg)), registerCollector: jest.fn(), }; @@ -51,11 +55,23 @@ export const createUsageCollectionSetupMock = () => { return usageCollectionSetupMock; }; -export function createCollectorFetchContextMock(): jest.Mocked { - const collectorFetchClientsMock: jest.Mocked = { +export function createCollectorFetchContextMock(): jest.Mocked> { + const collectorFetchClientsMock: jest.Mocked> = { + callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, + esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, + soClient: savedObjectsRepositoryMock.create(), + }; + return collectorFetchClientsMock; +} + +export function createCollectorFetchContextWithKibanaMock(): jest.Mocked< + CollectorFetchContext +> { + const collectorFetchClientsMock: jest.Mocked> = { callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, soClient: savedObjectsRepositoryMock.create(), + kibanaRequest: httpServerMock.createKibanaRequest(), }; return collectorFetchClientsMock; } diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index e092fc8acfd71..fbef55df39719 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -59,9 +59,9 @@ describe('registerVegaUsageCollector', () => { it('makeUsageCollector config.fetch calls getStats', async () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); - const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; const mockedCollectorFetchContext = createCollectorFetchContextMock(); - const fetchResult = await usageCollectorConfig.fetch(mockedCollectorFetchContext); + const fetchResult = await usageCollector.fetch(mockedCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); expect(mockGetStats).toBeCalledWith( mockedCollectorFetchContext.callCluster, diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts index 7789e3de13e5a..380a86e15aa51 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts @@ -58,9 +58,9 @@ describe('registerVisualizationsCollector', () => { it('makeUsageCollector config.fetch calls getStats', async () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVisualizationsCollector(mockCollectorSet, mockConfig); - const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; const mockCollectorFetchContext = createCollectorFetchContextMock(); - const fetchResult = await usageCollectorConfig.fetch(mockCollectorFetchContext); + const fetchResult = await usageCollector.fetch(mockCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.callCluster, mockIndex); expect(fetchResult).toBe(mockStats); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts index 0dd5ce291f972..2b81f1078ad0a 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.ts @@ -44,15 +44,23 @@ interface EmailSettingData { xpack: { default_admin_email: string | null }; } -export interface KibanaSettingsCollector extends Collector { +export interface KibanaSettingsCollectorExtraOptions { getEmailValueStructure(email: string | null): EmailSettingData; } +export type KibanaSettingsCollector = Collector & + KibanaSettingsCollectorExtraOptions; + export function getSettingsCollector( usageCollection: UsageCollectionSetup, config: MonitoringConfig ) { - return usageCollection.makeStatsCollector({ + return usageCollection.makeStatsCollector< + EmailSettingData | undefined, + unknown, + false, + KibanaSettingsCollectorExtraOptions + >({ type: KIBANA_SETTINGS_TYPE, isReady: () => true, schema: { @@ -60,7 +68,7 @@ export function getSettingsCollector( default_admin_email: { type: 'text' }, }, }, - async fetch(this: KibanaSettingsCollector) { + async fetch() { let kibanaSettingsData; const defaultAdminEmail = await checkForEmailValue(config); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts index 2f63a878b0cde..8a2283420ac95 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts @@ -6,6 +6,7 @@ import { getMonitoringUsageCollector } from './get_usage_collector'; import { fetchClusters } from '../../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; jest.mock('../../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn().mockImplementation(() => { @@ -57,7 +58,7 @@ jest.mock('./lib/fetch_license_type', () => ({ })); describe('getMonitoringUsageCollector', () => { - const callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createLegacyClusterClient(); const config: any = { ui: { ccs: { @@ -70,7 +71,7 @@ describe('getMonitoringUsageCollector', () => { const usageCollection: any = { makeUsageCollector: jest.fn(), }; - await getMonitoringUsageCollector(usageCollection, config, callCluster); + await getMonitoringUsageCollector(usageCollection, config, esClient); const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; @@ -120,11 +121,11 @@ describe('getMonitoringUsageCollector', () => { makeUsageCollector: jest.fn(), }; - await getMonitoringUsageCollector(usageCollection, config, callCluster); + await getMonitoringUsageCollector(usageCollection, config, esClient); const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; const args = mock.calls[0]; - const result = await args[0].fetch(); + const result = await args[0].fetch({}); expect(result).toStrictEqual({ hasMonitoringData: true, clusters: [ @@ -147,7 +148,7 @@ describe('getMonitoringUsageCollector', () => { makeUsageCollector: jest.fn(), }; - await getMonitoringUsageCollector(usageCollection, config, callCluster); + await getMonitoringUsageCollector(usageCollection, config, esClient); const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; const args = mock.calls[0]; @@ -155,7 +156,27 @@ describe('getMonitoringUsageCollector', () => { return []; }); - const result = await args[0].fetch(); + const result = await args[0].fetch({}); + expect(result).toStrictEqual({ + hasMonitoringData: false, + clusters: [], + }); + }); + + it('should handle scoped data', async () => { + const usageCollection: any = { + makeUsageCollector: jest.fn(), + }; + + await getMonitoringUsageCollector(usageCollection, config, esClient); + const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; + const args = mock.calls[0]; + + (fetchClusters as jest.Mock).mockImplementation(() => { + return []; + }); + + const result = await args[0].fetch({ kibanaRequest: {} }); expect(result).toStrictEqual({ hasMonitoringData: false, clusters: [], diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index 278a6c163c0ad..038042f109817 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -5,7 +5,7 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { LegacyAPICaller } from 'src/core/server'; +import { ILegacyClusterClient } from 'src/core/server'; import { MonitoringConfig } from '../../config'; import { fetchAvailableCcs } from '../../lib/alerts/fetch_available_ccs'; import { getStackProductsUsage } from './lib/get_stack_products_usage'; @@ -18,9 +18,9 @@ import { fetchClusters } from '../../lib/alerts/fetch_clusters'; export function getMonitoringUsageCollector( usageCollection: UsageCollectionSetup, config: MonitoringConfig, - callCluster: LegacyAPICaller + legacyEsClient: ILegacyClusterClient ) { - return usageCollection.makeUsageCollector({ + return usageCollection.makeUsageCollector({ type: 'monitoring', isReady: () => true, schema: { @@ -97,7 +97,13 @@ export function getMonitoringUsageCollector( }, }, }, - fetch: async () => { + extendFetchContext: { + kibanaRequest: true, + }, + fetch: async ({ kibanaRequest }) => { + const callCluster = kibanaRequest + ? legacyEsClient.asScoped(kibanaRequest).callAsCurrentUser + : legacyEsClient.callAsInternalUser; const usageClusters: MonitoringClusterStackProductUsage[] = []; const availableCcs = config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : []; const elasticsearchIndex = getCcsIndexPattern(INDEX_PATTERN_ELASTICSEARCH, availableCcs); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts index 47ad78b29962c..25e243656898c 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'src/core/server'; +import { ILegacyClusterClient } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { getSettingsCollector } from './get_settings_collector'; import { getMonitoringUsageCollector } from './get_usage_collector'; @@ -15,10 +15,10 @@ export { KibanaSettingsCollector } from './get_settings_collector'; export function registerCollectors( usageCollection: UsageCollectionSetup, config: MonitoringConfig, - callCluster: LegacyAPICaller + legacyEsClient: ILegacyClusterClient ) { usageCollection.registerCollector(getSettingsCollector(usageCollection, config)); usageCollection.registerCollector( - getMonitoringUsageCollector(usageCollection, config, callCluster) + getMonitoringUsageCollector(usageCollection, config, legacyEsClient) ); } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index d3e5028d72fcc..41b501d88af99 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -173,7 +173,7 @@ export class Plugin { }, }); - registerCollectors(plugins.usageCollection, config, cluster.callAsInternalUser); + registerCollectors(plugins.usageCollection, config, cluster); } // Always create the bulk uploader diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index 099f6915611cb..a119686afe663 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -180,6 +180,7 @@ describe('get_all_stats', () => { esClient: esClient as any, soClient: soClient as any, usageCollection: {} as any, + kibanaRequest: undefined, timestamp, }, { @@ -206,6 +207,7 @@ describe('get_all_stats', () => { esClient: esClient as any, soClient: soClient as any, usageCollection: {} as any, + kibanaRequest: undefined, timestamp, }, { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts index 0acdb9968bc03..b296ff090aedd 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts @@ -13,6 +13,7 @@ import { } from './get_cluster_uuids'; describe('get_cluster_uuids', () => { + const kibanaRequest = undefined; const callCluster = sinon.stub(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const soClient = savedObjectsRepositoryMock.create(); @@ -33,7 +34,7 @@ describe('get_cluster_uuids', () => { callCluster.withArgs('search').returns(Promise.resolve(response)); expect( await getClusterUuids( - { callCluster, esClient, soClient, timestamp, usageCollection: {} as any }, + { callCluster, esClient, soClient, timestamp, kibanaRequest, usageCollection: {} as any }, { maxBucketSize: 1, } as any @@ -47,7 +48,7 @@ describe('get_cluster_uuids', () => { callCluster.returns(Promise.resolve(response)); expect( await fetchClusterUuids( - { callCluster, esClient, soClient, timestamp, usageCollection: {} as any }, + { callCluster, esClient, soClient, timestamp, kibanaRequest, usageCollection: {} as any }, { maxBucketSize: 1, } as any diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 4cecc2e24867f..fff18353c58b0 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -74,7 +74,7 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'basic' }); - const { fetch } = getReportingUsageCollector( + const collector = getReportingUsageCollector( mockCore, plugins.usageCollection, getLicenseMock('basic'), @@ -83,7 +83,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(getMockFetchClients(getResponseMock())); + usageStats = await collector.fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -103,7 +103,7 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'none' }); - const { fetch } = getReportingUsageCollector( + const collector = getReportingUsageCollector( mockCore, plugins.usageCollection, getLicenseMock('none'), @@ -112,7 +112,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(getMockFetchClients(getResponseMock())); + usageStats = await collector.fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -132,7 +132,7 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'platinum' }); - const { fetch } = getReportingUsageCollector( + const collector = getReportingUsageCollector( mockCore, plugins.usageCollection, getLicenseMock('platinum'), @@ -141,7 +141,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(getMockFetchClients(getResponseMock())); + usageStats = await collector.fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -161,7 +161,7 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'basic' }); - const { fetch } = getReportingUsageCollector( + const collector = getReportingUsageCollector( mockCore, plugins.usageCollection, getLicenseMock('basic'), @@ -170,7 +170,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(getMockFetchClients({})); + usageStats = await collector.fetch(getMockFetchClients({})); }); test('sets enables to true', async () => { @@ -193,7 +193,7 @@ describe('data modeling', () => { }); test('with normal looking usage data', async () => { const plugins = getPluginsMock(); - const { fetch } = getReportingUsageCollector( + const collector = getReportingUsageCollector( mockCore, plugins.usageCollection, getLicenseMock(), @@ -237,13 +237,13 @@ describe('data modeling', () => { }, } as SearchResponse) // prettier-ignore ); - const usageStats = await fetch(collectorFetchContext); + const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); test('with sparse data', async () => { const plugins = getPluginsMock(); - const { fetch } = getReportingUsageCollector( + const collector = getReportingUsageCollector( mockCore, plugins.usageCollection, getLicenseMock(), @@ -287,13 +287,13 @@ describe('data modeling', () => { }, } as SearchResponse) // prettier-ignore ); - const usageStats = await fetch(collectorFetchContext); + const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); test('with empty data', async () => { const plugins = getPluginsMock(); - const { fetch } = getReportingUsageCollector( + const collector = getReportingUsageCollector( mockCore, plugins.usageCollection, getLicenseMock(), @@ -337,7 +337,7 @@ describe('data modeling', () => { }, } as SearchResponse) // prettier-ignore ); - const usageStats = await fetch(collectorFetchContext); + const usageStats = await collector.fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index 864c91c583e82..1a377d2f801a0 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -44,7 +44,7 @@ function setup({ return { licensing, features: featuresSetup, - usageCollecion: { + usageCollection: { makeUsageCollector: (options: any) => new MockUsageCollector(options), }, }; @@ -77,23 +77,23 @@ const getMockFetchContext = (mockedCallCluster: jest.Mock) => { describe('error handling', () => { it('handles a 404 when searching for space usage', async () => { - const { features, licensing, usageCollecion } = setup({ + const { features, licensing, usageCollection } = setup({ license: { isAvailable: true, type: 'basic' }, }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { + const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }), features, licensing, }); - await getSpacesUsage(getMockFetchContext(jest.fn().mockRejectedValue({ status: 404 }))); + await collector.fetch(getMockFetchContext(jest.fn().mockRejectedValue({ status: 404 }))); }); it('throws error for a non-404', async () => { - const { features, licensing, usageCollecion } = setup({ + const { features, licensing, usageCollection } = setup({ license: { isAvailable: true, type: 'basic' }, }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { + const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }), features, licensing, @@ -103,7 +103,7 @@ describe('error handling', () => { for (const statusCode of statusCodes) { const error = { status: statusCode }; await expect( - getSpacesUsage(getMockFetchContext(jest.fn().mockRejectedValue(error))) + collector.fetch(getMockFetchContext(jest.fn().mockRejectedValue(error))) ).rejects.toBe(error); } }); @@ -112,15 +112,15 @@ describe('error handling', () => { describe('with a basic license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usageCollecion } = setup({ + const { features, licensing, usageCollection } = setup({ license: { isAvailable: true, type: 'basic' }, }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { + const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); - usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); + usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); expect(defaultCallClusterMock).toHaveBeenCalledWith('search', { body: { @@ -162,13 +162,13 @@ describe('with a basic license', () => { describe('with no license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usageCollecion } = setup({ license: { isAvailable: false } }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { + const { features, licensing, usageCollection } = setup({ license: { isAvailable: false } }); + const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); - usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); + usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); }); test('sets enabled to false', () => { @@ -191,15 +191,15 @@ describe('with no license', () => { describe('with platinum license', () => { let usageStats: UsageStats; beforeAll(async () => { - const { features, licensing, usageCollecion } = setup({ + const { features, licensing, usageCollection } = setup({ license: { isAvailable: true, type: 'platinum' }, }); - const { fetch: getSpacesUsage } = getSpacesUsageCollector(usageCollecion as any, { + const collector = getSpacesUsageCollector(usageCollection as any, { kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$, features, licensing, }); - usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); + usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock)); }); test('sets enabled to true', () => { From 27125bce309c2d181d859be43be6edb292484254 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 18 Nov 2020 10:24:13 +0100 Subject: [PATCH 56/99] [code coverage] adding plugin to flush coverage data (#83447) * [test/common] adding code coverage plugin * start plugin only when CODE_COVERAGE flag is set * build coverage plugin * replace casting with custom Window interface --- test/common/config.js | 4 ++ .../fixtures/plugins/coverage/kibana.json | 6 +++ .../fixtures/plugins/coverage/public/index.ts | 24 +++++++++++ .../plugins/coverage/public/plugin.ts | 43 +++++++++++++++++++ test/scripts/jenkins_build_plugins.sh | 1 + test/scripts/jenkins_xpack_build_plugins.sh | 1 + 6 files changed, 79 insertions(+) create mode 100644 test/common/fixtures/plugins/coverage/kibana.json create mode 100644 test/common/fixtures/plugins/coverage/public/index.ts create mode 100644 test/common/fixtures/plugins/coverage/public/plugin.ts diff --git a/test/common/config.js b/test/common/config.js index 9d6d531ae4b37..6c7d64e3e0bc0 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -61,6 +61,10 @@ export default function () { `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'newsfeed')}`, `--newsfeed.service.urlRoot=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`, `--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/v{VERSION}.json`, + // code coverage reporting plugin + ...(!!process.env.CODE_COVERAGE + ? [`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'coverage')}`] + : []), ], }, services, diff --git a/test/common/fixtures/plugins/coverage/kibana.json b/test/common/fixtures/plugins/coverage/kibana.json new file mode 100644 index 0000000000000..d80432534d746 --- /dev/null +++ b/test/common/fixtures/plugins/coverage/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "coverage-fixtures", + "version": "kibana", + "server": false, + "ui": true +} \ No newline at end of file diff --git a/test/common/fixtures/plugins/coverage/public/index.ts b/test/common/fixtures/plugins/coverage/public/index.ts new file mode 100644 index 0000000000000..ed164c2d6bb94 --- /dev/null +++ b/test/common/fixtures/plugins/coverage/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CodeCoverageReportingPlugin } from './plugin'; + +export function plugin() { + return new CodeCoverageReportingPlugin(); +} diff --git a/test/common/fixtures/plugins/coverage/public/plugin.ts b/test/common/fixtures/plugins/coverage/public/plugin.ts new file mode 100644 index 0000000000000..e4da6b257de9a --- /dev/null +++ b/test/common/fixtures/plugins/coverage/public/plugin.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin } from 'kibana/server'; + +declare global { + interface Window { + __coverage__: any; + flushCoverageToLog: any; + } +} + +export class CodeCoverageReportingPlugin implements Plugin { + constructor() {} + + public start() {} + + public setup() { + window.flushCoverageToLog = function () { + if (window.__coverage__) { + // eslint-disable-next-line no-console + console.log('coveragejson:' + btoa(JSON.stringify(window.__coverage__))); + } + }; + window.addEventListener('beforeunload', window.flushCoverageToLog); + } +} diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh index 59df02d401167..cfb829ea2db54 100755 --- a/test/scripts/jenkins_build_plugins.sh +++ b/test/scripts/jenkins_build_plugins.sh @@ -7,5 +7,6 @@ node scripts/build_kibana_platform_plugins \ --oss \ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \ --workers 6 \ --verbose diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh index 289e64f66c89b..37b6398598788 100755 --- a/test/scripts/jenkins_xpack_build_plugins.sh +++ b/test/scripts/jenkins_xpack_build_plugins.sh @@ -5,6 +5,7 @@ source src/dev/ci_setup/setup_env.sh echo " -> building kibana platform plugins" node scripts/build_kibana_platform_plugins \ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/common/fixtures/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ From a7e5f074122ae445354b1a58e49edcb49edb0f51 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 18 Nov 2020 10:27:11 +0100 Subject: [PATCH 57/99] Add tag bulk action context menu (#82816) * add the delete tag bulk action * add unit tests for bulk delete * fix duplicate i18n key * add RBAC test on bulk delete * add functional tests * self review * design nits * add maxWidth option for confirm modal and add missing doc * change bulk delete confirm modal max width * add more missing doc * only show loading state when performing the bulk delete * use spacer instead of custom margin on horizontal rule * use link instead of button to remove custom styles * remove spacers, just use styles * add divider when action menu is displayed * set max-width for single delete confirm * a11y fixes * address nits * add aria-label to delete action Co-authored-by: Michail Yasonik --- .../core/public/kibana-plugin-core-public.md | 5 + ...erlayflyoutopenoptions._data-test-subj_.md | 11 ++ ...blic.overlayflyoutopenoptions.classname.md | 11 ++ ...yflyoutopenoptions.closebuttonarialabel.md | 11 ++ ...in-core-public.overlayflyoutopenoptions.md | 22 ++++ ...ublic.overlayflyoutopenoptions.ownfocus.md | 11 ++ ...a-plugin-core-public.overlayflyoutstart.md | 20 +++ ...gin-core-public.overlayflyoutstart.open.md | 25 ++++ ...laymodalconfirmoptions._data-test-subj_.md | 11 ++ ....overlaymodalconfirmoptions.buttoncolor.md | 11 ++ ...laymodalconfirmoptions.cancelbuttontext.md | 11 ++ ...ic.overlaymodalconfirmoptions.classname.md | 11 ++ ...odalconfirmoptions.closebuttonarialabel.md | 11 ++ ...aymodalconfirmoptions.confirmbuttontext.md | 11 ++ ...odalconfirmoptions.defaultfocusedbutton.md | 11 ++ ...lic.overlaymodalconfirmoptions.maxwidth.md | 13 ++ ...-core-public.overlaymodalconfirmoptions.md | 27 ++++ ...public.overlaymodalconfirmoptions.title.md | 11 ++ ...verlaymodalopenoptions._data-test-subj_.md | 11 ++ ...ublic.overlaymodalopenoptions.classname.md | 11 ++ ...aymodalopenoptions.closebuttonarialabel.md | 11 ++ ...gin-core-public.overlaymodalopenoptions.md | 21 +++ ...na-plugin-core-public.overlaymodalstart.md | 21 +++ ...ugin-core-public.overlaymodalstart.open.md | 25 ++++ ...re-public.overlaymodalstart.openconfirm.md | 25 ++++ src/core/public/index.ts | 11 +- src/core/public/overlays/index.ts | 2 +- src/core/public/overlays/modal/index.ts | 7 +- .../public/overlays/modal/modal_service.tsx | 8 ++ src/core/public/public.api.md | 58 ++++++++- .../common/test_utils/index.ts | 11 ++ .../edition_modal/create_or_edit_modal.tsx | 11 +- .../management/actions/bulk_delete.test.ts | 67 ++++++++++ .../public/management/actions/bulk_delete.ts | 92 +++++++++++++ .../management/actions/clear_selection.ts | 28 ++++ .../public/management/actions/index.test.ts | 48 +++++++ .../public/management/actions/index.ts | 43 ++++++ .../management/components/_action_bar.scss | 17 +++ .../management/components/action_bar.tsx | 122 ++++++++++++++++++ .../public/management/components/index.ts | 1 + .../public/management/components/table.tsx | 46 +++++-- .../public/management/tag_management_page.tsx | 67 +++++++++- .../public/management/types.ts | 37 ++++++ .../public/tags/tags_client.mock.ts | 25 ++++ .../public/tags/tags_client.test.ts | 98 ++++++++++---- .../public/tags/tags_client.ts | 17 +++ .../server/routes/index.ts | 3 +- .../server/routes/internal/bulk_delete.ts | 33 +++++ .../server/routes/internal/index.ts | 1 + .../page_objects/tag_management_page.ts | 91 +++++++++++++ .../security_and_spaces/apis/_bulk_delete.ts | 87 +++++++++++++ .../security_and_spaces/apis/index.ts | 1 + .../functional/tests/bulk_actions.ts | 54 ++++++++ .../functional/tests/feature_control.ts | 12 ++ .../functional/tests/index.ts | 1 + .../functional/tests/visualize_integration.ts | 2 +- 56 files changed, 1419 insertions(+), 51 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.classname.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.title.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.classname.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md create mode 100644 x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.test.ts create mode 100644 x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.ts create mode 100644 x-pack/plugins/saved_objects_tagging/public/management/actions/clear_selection.ts create mode 100644 x-pack/plugins/saved_objects_tagging/public/management/actions/index.test.ts create mode 100644 x-pack/plugins/saved_objects_tagging/public/management/actions/index.ts create mode 100644 x-pack/plugins/saved_objects_tagging/public/management/components/_action_bar.scss create mode 100644 x-pack/plugins/saved_objects_tagging/public/management/components/action_bar.tsx create mode 100644 x-pack/plugins/saved_objects_tagging/public/management/types.ts create mode 100644 x-pack/plugins/saved_objects_tagging/public/tags/tags_client.mock.ts create mode 100644 x-pack/plugins/saved_objects_tagging/server/routes/internal/bulk_delete.ts create mode 100644 x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_bulk_delete.ts create mode 100644 x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b8b1bdcdee3be..6a90fd49f1d66 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -82,6 +82,11 @@ The plugin integrates with the core system via lifecycle events: `setup` | [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | | | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | | | [OverlayBannersStart](./kibana-plugin-core-public.overlaybannersstart.md) | | +| [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) | | +| [OverlayFlyoutStart](./kibana-plugin-core-public.overlayflyoutstart.md) | APIs to open and manage fly-out dialogs. | +| [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) | | +| [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) | | +| [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) | APIs to open and manage modal dialogs. | | [OverlayRef](./kibana-plugin-core-public.overlayref.md) | Returned by [OverlayStart](./kibana-plugin-core-public.overlaystart.md) methods for closing a mounted overlay. | | [OverlayStart](./kibana-plugin-core-public.overlaystart.md) | | | [Plugin](./kibana-plugin-core-public.plugin.md) | The interface that should be returned by a PluginInitializer. | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md new file mode 100644 index 0000000000000..d583aae0e0b19 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) + +## OverlayFlyoutOpenOptions."data-test-subj" property + +Signature: + +```typescript +'data-test-subj'?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.classname.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.classname.md new file mode 100644 index 0000000000000..26f6db77cccea --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.classname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) + +## OverlayFlyoutOpenOptions.className property + +Signature: + +```typescript +className?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md new file mode 100644 index 0000000000000..44014b7f0d816 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) + +## OverlayFlyoutOpenOptions.closeButtonAriaLabel property + +Signature: + +```typescript +closeButtonAriaLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md new file mode 100644 index 0000000000000..5945bca01f55f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) + +## OverlayFlyoutOpenOptions interface + + +Signature: + +```typescript +export interface OverlayFlyoutOpenOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| ["data-test-subj"](./kibana-plugin-core-public.overlayflyoutopenoptions._data-test-subj_.md) | string | | +| [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | string | | +| [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | string | | +| [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | boolean | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md new file mode 100644 index 0000000000000..337ce2c48e1d9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) + +## OverlayFlyoutOpenOptions.ownFocus property + +Signature: + +```typescript +ownFocus?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.md new file mode 100644 index 0000000000000..790fd57320413 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutStart](./kibana-plugin-core-public.overlayflyoutstart.md) + +## OverlayFlyoutStart interface + +APIs to open and manage fly-out dialogs. + +Signature: + +```typescript +export interface OverlayFlyoutStart +``` + +## Methods + +| Method | Description | +| --- | --- | +| [open(mount, options)](./kibana-plugin-core-public.overlayflyoutstart.open.md) | Opens a flyout panel with the given mount point inside. You can use close() on the returned FlyoutRef to close the flyout. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md new file mode 100644 index 0000000000000..1f740410ca282 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutstart.open.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutStart](./kibana-plugin-core-public.overlayflyoutstart.md) > [open](./kibana-plugin-core-public.overlayflyoutstart.open.md) + +## OverlayFlyoutStart.open() method + +Opens a flyout panel with the given mount point inside. You can use `close()` on the returned FlyoutRef to close the flyout. + +Signature: + +```typescript +open(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| mount | MountPoint | | +| options | OverlayFlyoutOpenOptions | | + +Returns: + +`OverlayRef` + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md new file mode 100644 index 0000000000000..3569b2153c3da --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > ["data-test-subj"](./kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md) + +## OverlayModalConfirmOptions."data-test-subj" property + +Signature: + +```typescript +'data-test-subj'?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md new file mode 100644 index 0000000000000..5c827e19e42e1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [buttonColor](./kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md) + +## OverlayModalConfirmOptions.buttonColor property + +Signature: + +```typescript +buttonColor?: EuiConfirmModalProps['buttonColor']; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md new file mode 100644 index 0000000000000..0c0b9fd48dbd6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [cancelButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md) + +## OverlayModalConfirmOptions.cancelButtonText property + +Signature: + +```typescript +cancelButtonText?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md new file mode 100644 index 0000000000000..0a622aeaac418 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [className](./kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md) + +## OverlayModalConfirmOptions.className property + +Signature: + +```typescript +className?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md new file mode 100644 index 0000000000000..8a321a0b07b4c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md) + +## OverlayModalConfirmOptions.closeButtonAriaLabel property + +Signature: + +```typescript +closeButtonAriaLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md new file mode 100644 index 0000000000000..f84d834369f5b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [confirmButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md) + +## OverlayModalConfirmOptions.confirmButtonText property + +Signature: + +```typescript +confirmButtonText?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md new file mode 100644 index 0000000000000..c5edf48b54ea8 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [defaultFocusedButton](./kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md) + +## OverlayModalConfirmOptions.defaultFocusedButton property + +Signature: + +```typescript +defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md new file mode 100644 index 0000000000000..488b4eb3794fb --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [maxWidth](./kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md) + +## OverlayModalConfirmOptions.maxWidth property + +Sets the max-width of the modal. Set to `true` to use the default (`euiBreakpoints 'm'`), set to `false` to not restrict the width, set to a number for a custom width in px, set to a string for a custom width in custom measurement. + +Signature: + +```typescript +maxWidth?: boolean | number | string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md new file mode 100644 index 0000000000000..83405a151a372 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) + +## OverlayModalConfirmOptions interface + + +Signature: + +```typescript +export interface OverlayModalConfirmOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalconfirmoptions._data-test-subj_.md) | string | | +| [buttonColor](./kibana-plugin-core-public.overlaymodalconfirmoptions.buttoncolor.md) | EuiConfirmModalProps['buttonColor'] | | +| [cancelButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.cancelbuttontext.md) | string | | +| [className](./kibana-plugin-core-public.overlaymodalconfirmoptions.classname.md) | string | | +| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalconfirmoptions.closebuttonarialabel.md) | string | | +| [confirmButtonText](./kibana-plugin-core-public.overlaymodalconfirmoptions.confirmbuttontext.md) | string | | +| [defaultFocusedButton](./kibana-plugin-core-public.overlaymodalconfirmoptions.defaultfocusedbutton.md) | EuiConfirmModalProps['defaultFocusedButton'] | | +| [maxWidth](./kibana-plugin-core-public.overlaymodalconfirmoptions.maxwidth.md) | boolean | number | string | Sets the max-width of the modal. Set to true to use the default (euiBreakpoints 'm'), set to false to not restrict the width, set to a number for a custom width in px, set to a string for a custom width in custom measurement. | +| [title](./kibana-plugin-core-public.overlaymodalconfirmoptions.title.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.title.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.title.md new file mode 100644 index 0000000000000..cfbe41e0a7e9f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalconfirmoptions.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalConfirmOptions](./kibana-plugin-core-public.overlaymodalconfirmoptions.md) > [title](./kibana-plugin-core-public.overlaymodalconfirmoptions.title.md) + +## OverlayModalConfirmOptions.title property + +Signature: + +```typescript +title?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md new file mode 100644 index 0000000000000..f0eba659dc62b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) > ["data-test-subj"](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) + +## OverlayModalOpenOptions."data-test-subj" property + +Signature: + +```typescript +'data-test-subj'?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.classname.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.classname.md new file mode 100644 index 0000000000000..769387b8c35ff --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.classname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) > [className](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) + +## OverlayModalOpenOptions.className property + +Signature: + +```typescript +className?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md new file mode 100644 index 0000000000000..4e685055b9e17 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) > [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) + +## OverlayModalOpenOptions.closeButtonAriaLabel property + +Signature: + +```typescript +closeButtonAriaLabel?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md new file mode 100644 index 0000000000000..5c0ef8fb1ec86 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalopenoptions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalOpenOptions](./kibana-plugin-core-public.overlaymodalopenoptions.md) + +## OverlayModalOpenOptions interface + + +Signature: + +```typescript +export interface OverlayModalOpenOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| ["data-test-subj"](./kibana-plugin-core-public.overlaymodalopenoptions._data-test-subj_.md) | string | | +| [className](./kibana-plugin-core-public.overlaymodalopenoptions.classname.md) | string | | +| [closeButtonAriaLabel](./kibana-plugin-core-public.overlaymodalopenoptions.closebuttonarialabel.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.md new file mode 100644 index 0000000000000..1d8fe1a92dd90 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) + +## OverlayModalStart interface + +APIs to open and manage modal dialogs. + +Signature: + +```typescript +export interface OverlayModalStart +``` + +## Methods + +| Method | Description | +| --- | --- | +| [open(mount, options)](./kibana-plugin-core-public.overlaymodalstart.open.md) | Opens a modal panel with the given mount point inside. You can use close() on the returned OverlayRef to close the modal. | +| [openConfirm(message, options)](./kibana-plugin-core-public.overlaymodalstart.openconfirm.md) | Opens a confirmation modal with the given text or mountpoint as a message. Returns a Promise resolving to true if user confirmed or false otherwise. | + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md new file mode 100644 index 0000000000000..1c6b82e37a624 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.open.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) > [open](./kibana-plugin-core-public.overlaymodalstart.open.md) + +## OverlayModalStart.open() method + +Opens a modal panel with the given mount point inside. You can use `close()` on the returned OverlayRef to close the modal. + +Signature: + +```typescript +open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| mount | MountPoint | | +| options | OverlayModalOpenOptions | | + +Returns: + +`OverlayRef` + diff --git a/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md new file mode 100644 index 0000000000000..b0052c0f6460e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlaymodalstart.openconfirm.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayModalStart](./kibana-plugin-core-public.overlaymodalstart.md) > [openConfirm](./kibana-plugin-core-public.overlaymodalstart.openconfirm.md) + +## OverlayModalStart.openConfirm() method + +Opens a confirmation modal with the given text or mountpoint as a message. Returns a Promise resolving to `true` if user confirmed or `false` otherwise. + +Signature: + +```typescript +openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| message | MountPoint | string | | +| options | OverlayModalConfirmOptions | | + +Returns: + +`Promise` + diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 1393e69d55e51..564bbd712c535 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -167,7 +167,16 @@ export { IHttpResponseInterceptorOverrides, } from './http'; -export { OverlayStart, OverlayBannersStart, OverlayRef } from './overlays'; +export { + OverlayStart, + OverlayBannersStart, + OverlayRef, + OverlayFlyoutStart, + OverlayFlyoutOpenOptions, + OverlayModalOpenOptions, + OverlayModalConfirmOptions, + OverlayModalStart, +} from './overlays'; export { Toast, diff --git a/src/core/public/overlays/index.ts b/src/core/public/overlays/index.ts index 417486f78f719..31b524d85abbe 100644 --- a/src/core/public/overlays/index.ts +++ b/src/core/public/overlays/index.ts @@ -20,5 +20,5 @@ export { OverlayRef } from './types'; export { OverlayBannersStart } from './banners'; export { OverlayFlyoutStart, OverlayFlyoutOpenOptions } from './flyout'; -export { OverlayModalStart, OverlayModalOpenOptions } from './modal'; +export { OverlayModalStart, OverlayModalOpenOptions, OverlayModalConfirmOptions } from './modal'; export { OverlayService, OverlayStart } from './overlay_service'; diff --git a/src/core/public/overlays/modal/index.ts b/src/core/public/overlays/modal/index.ts index 9ef4492af3a3a..4e270838eae44 100644 --- a/src/core/public/overlays/modal/index.ts +++ b/src/core/public/overlays/modal/index.ts @@ -17,4 +17,9 @@ * under the License. */ -export { ModalService, OverlayModalStart, OverlayModalOpenOptions } from './modal_service'; +export { + ModalService, + OverlayModalStart, + OverlayModalOpenOptions, + OverlayModalConfirmOptions, +} from './modal_service'; diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index f3bbd5c94bdb4..4c0c205ae5438 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -70,6 +70,14 @@ export interface OverlayModalConfirmOptions { 'data-test-subj'?: string; defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; buttonColor?: EuiConfirmModalProps['buttonColor']; + /** + * Sets the max-width of the modal. + * Set to `true` to use the default (`euiBreakpoints 'm'`), + * set to `false` to not restrict the width, + * set to a number for a custom width in px, + * set to a string for a custom width in custom measurement. + */ + maxWidth?: boolean | number | string; } /** diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 28a20845426d9..37e57a9ee606e 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -862,6 +862,60 @@ export interface OverlayBannersStart { replace(id: string | undefined, mount: MountPoint, priority?: number): string; } +// @public (undocumented) +export interface OverlayFlyoutOpenOptions { + // (undocumented) + 'data-test-subj'?: string; + // (undocumented) + className?: string; + // (undocumented) + closeButtonAriaLabel?: string; + // (undocumented) + ownFocus?: boolean; +} + +// @public +export interface OverlayFlyoutStart { + open(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; +} + +// @public (undocumented) +export interface OverlayModalConfirmOptions { + // (undocumented) + 'data-test-subj'?: string; + // (undocumented) + buttonColor?: EuiConfirmModalProps['buttonColor']; + // (undocumented) + cancelButtonText?: string; + // (undocumented) + className?: string; + // (undocumented) + closeButtonAriaLabel?: string; + // (undocumented) + confirmButtonText?: string; + // (undocumented) + defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; + maxWidth?: boolean | number | string; + // (undocumented) + title?: string; +} + +// @public (undocumented) +export interface OverlayModalOpenOptions { + // (undocumented) + 'data-test-subj'?: string; + // (undocumented) + className?: string; + // (undocumented) + closeButtonAriaLabel?: string; +} + +// @public +export interface OverlayModalStart { + open(mount: MountPoint, options?: OverlayModalOpenOptions): OverlayRef; + openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise; +} + // @public export interface OverlayRef { close(): Promise; @@ -874,12 +928,8 @@ export interface OverlayStart { banners: OverlayBannersStart; // (undocumented) openConfirm: OverlayModalStart['openConfirm']; - // Warning: (ae-forgotten-export) The symbol "OverlayFlyoutStart" needs to be exported by the entry point index.d.ts - // // (undocumented) openFlyout: OverlayFlyoutStart['open']; - // Warning: (ae-forgotten-export) The symbol "OverlayModalStart" needs to be exported by the entry point index.d.ts - // // (undocumented) openModal: OverlayModalStart['open']; } diff --git a/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts b/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts index 80d2dbc0b1566..7f6e2a12d9e53 100644 --- a/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts +++ b/x-pack/plugins/saved_objects_tagging/common/test_utils/index.ts @@ -6,6 +6,7 @@ import { SavedObject, SavedObjectReference } from 'src/core/types'; import { Tag, TagAttributes } from '../types'; +import { TagsCapabilities } from '../capabilities'; export const createTagReference = (id: string): SavedObjectReference => ({ type: 'tag', @@ -35,3 +36,13 @@ export const createTagAttributes = (parts: Partial = {}): TagAttr color: '#FF00CC', ...parts, }); + +export const createTagCapabilities = (parts: Partial = {}): TagsCapabilities => ({ + view: true, + create: true, + edit: true, + delete: true, + assign: true, + viewConnections: true, + ...parts, +}); diff --git a/x-pack/plugins/saved_objects_tagging/public/components/edition_modal/create_or_edit_modal.tsx b/x-pack/plugins/saved_objects_tagging/public/components/edition_modal/create_or_edit_modal.tsx index 7baebdae2493e..1a80c0598f97a 100644 --- a/x-pack/plugins/saved_objects_tagging/public/components/edition_modal/create_or_edit_modal.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/components/edition_modal/create_or_edit_modal.tsx @@ -22,6 +22,7 @@ import { EuiTextArea, EuiSpacer, EuiText, + htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -52,6 +53,7 @@ export const CreateOrEditModal: FC = ({ tag, mode, }) => { + const optionalMessageId = htmlIdGenerator()(); const ifMounted = useIfMounted(); const [submitting, setSubmitting] = useState(false); @@ -139,6 +141,12 @@ export const CreateOrEditModal: FC = ({ onClick={() => setColor(getRandomColor())} size="xs" style={{ height: '18px', fontSize: '0.75rem' }} + aria-label={i18n.translate( + 'xpack.savedObjectsTagging.management.createModal.color.randomizeAriaLabel', + { + defaultMessage: 'Randomize tag color', + } + )} > = ({ defaultMessage: 'Description', })} labelAppend={ - + = ({ resize="none" fullWidth={true} compressed={true} + aria-describedby={optionalMessageId} /> diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.test.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.test.ts new file mode 100644 index 0000000000000..42a4e628bef4e --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + overlayServiceMock, + notificationServiceMock, +} from '../../../../../../src/core/public/mocks'; +import { tagClientMock } from '../../tags/tags_client.mock'; +import { TagBulkAction } from '../types'; +import { getBulkDeleteAction } from './bulk_delete'; + +describe('bulkDeleteAction', () => { + let tagClient: ReturnType; + let overlays: ReturnType; + let notifications: ReturnType; + let setLoading: jest.MockedFunction<(loading: boolean) => void>; + let action: TagBulkAction; + + const tagIds = ['id-1', 'id-2', 'id-3']; + + beforeEach(() => { + tagClient = tagClientMock.create(); + overlays = overlayServiceMock.createStartContract(); + notifications = notificationServiceMock.createStartContract(); + setLoading = jest.fn(); + + action = getBulkDeleteAction({ tagClient, overlays, notifications, setLoading }); + }); + + it('performs the operation if the confirmation is accepted', async () => { + overlays.openConfirm.mockResolvedValue(true); + + await action.execute(tagIds); + + expect(overlays.openConfirm).toHaveBeenCalledTimes(1); + + expect(tagClient.bulkDelete).toHaveBeenCalledTimes(1); + expect(tagClient.bulkDelete).toHaveBeenCalledWith(tagIds); + + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + }); + + it('does not perform the operation if the confirmation is rejected', async () => { + overlays.openConfirm.mockResolvedValue(false); + + await action.execute(tagIds); + + expect(overlays.openConfirm).toHaveBeenCalledTimes(1); + + expect(tagClient.bulkDelete).not.toHaveBeenCalled(); + expect(notifications.toasts.addSuccess).not.toHaveBeenCalled(); + }); + + it('does not show notification if `client.bulkDelete` rejects ', async () => { + overlays.openConfirm.mockResolvedValue(true); + tagClient.bulkDelete.mockRejectedValue(new Error('error calling bulkDelete')); + + await expect(action.execute(tagIds)).rejects.toMatchInlineSnapshot( + `[Error: error calling bulkDelete]` + ); + + expect(notifications.toasts.addSuccess).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.ts new file mode 100644 index 0000000000000..6d9c14d330007 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/bulk_delete.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OverlayStart, NotificationsStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import { ITagInternalClient } from '../../tags'; +import { TagBulkAction } from '../types'; + +interface GetBulkDeleteActionOptions { + overlays: OverlayStart; + notifications: NotificationsStart; + tagClient: ITagInternalClient; + setLoading: (loading: boolean) => void; +} + +export const getBulkDeleteAction = ({ + overlays, + notifications, + tagClient, + setLoading, +}: GetBulkDeleteActionOptions): TagBulkAction => { + return { + id: 'delete', + label: i18n.translate('xpack.savedObjectsTagging.management.actions.bulkDelete.label', { + defaultMessage: 'Delete', + }), + 'aria-label': i18n.translate( + 'xpack.savedObjectsTagging.management.actions.bulkDelete.ariaLabel', + { + defaultMessage: 'Delete selected tags', + } + ), + icon: 'trash', + refreshAfterExecute: true, + execute: async (tagIds) => { + const confirmed = await overlays.openConfirm( + i18n.translate('xpack.savedObjectsTagging.management.actions.bulkDelete.confirm.text', { + defaultMessage: + 'By deleting {count, plural, one {this tag} other {these tags}}, you will no longer be able to assign {count, plural, one {it} other {them}} to saved objects. ' + + '{count, plural, one {This tag} other {These tags}} will be removed from any saved objects that currently use {count, plural, one {it} other {them}}. ' + + 'Are you sure you wish to proceed?', + values: { + count: tagIds.length, + }, + }), + { + title: i18n.translate( + 'xpack.savedObjectsTagging.management.actions.bulkDelete.confirm.title', + { + defaultMessage: 'Delete {count, plural, one {1 tag} other {# tags}}', + values: { + count: tagIds.length, + }, + } + ), + confirmButtonText: i18n.translate( + 'xpack.savedObjectsTagging.management.actions.bulkDelete.confirm.confirmButtonText', + { + defaultMessage: 'Delete {count, plural, one {tag} other {tags}}', + values: { + count: tagIds.length, + }, + } + ), + buttonColor: 'danger', + maxWidth: 560, + } + ); + + if (confirmed) { + setLoading(true); + await tagClient.bulkDelete(tagIds); + setLoading(false); + + notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.savedObjectsTagging.management.actions.bulkDelete.notification.successTitle', + { + defaultMessage: 'Deleted {count, plural, one {1 tag} other {# tags}}', + values: { + count: tagIds.length, + }, + } + ), + }); + } + }, + }; +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/clear_selection.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/clear_selection.ts new file mode 100644 index 0000000000000..79212be98236c --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/clear_selection.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { TagBulkAction } from '../types'; + +interface GetClearSelectionActionOptions { + clearSelection: () => void; +} + +export const getClearSelectionAction = ({ + clearSelection, +}: GetClearSelectionActionOptions): TagBulkAction => { + return { + id: 'clear_selection', + label: i18n.translate('xpack.savedObjectsTagging.management.actions.clearSelection.label', { + defaultMessage: 'Clear selection', + }), + icon: 'cross', + refreshAfterExecute: true, + execute: async () => { + clearSelection(); + }, + }; +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/index.test.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/index.test.ts new file mode 100644 index 0000000000000..5325d4ee97cf8 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/index.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { createTagCapabilities } from '../../../common/test_utils'; +import { TagsCapabilities } from '../../../common/capabilities'; +import { tagClientMock } from '../../tags/tags_client.mock'; +import { TagBulkAction } from '../types'; + +import { getBulkActions } from './index'; + +describe('getBulkActions', () => { + let core: ReturnType; + let tagClient: ReturnType; + let clearSelection: jest.MockedFunction<() => void>; + let setLoading: jest.MockedFunction<(loading: boolean) => void>; + + beforeEach(() => { + core = coreMock.createStart(); + tagClient = tagClientMock.create(); + clearSelection = jest.fn(); + setLoading = jest.fn(); + }); + + const getActions = (caps: Partial) => + getBulkActions({ + core, + tagClient, + clearSelection, + setLoading, + capabilities: createTagCapabilities(caps), + }); + + const getIds = (actions: TagBulkAction[]) => actions.map((action) => action.id); + + it('only returns the `delete` action if user got `delete` permission', () => { + let actions = getActions({ delete: true }); + + expect(getIds(actions)).toContain('delete'); + + actions = getActions({ delete: false }); + + expect(getIds(actions)).not.toContain('delete'); + }); +}); diff --git a/x-pack/plugins/saved_objects_tagging/public/management/actions/index.ts b/x-pack/plugins/saved_objects_tagging/public/management/actions/index.ts new file mode 100644 index 0000000000000..182f0013251df --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/actions/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from 'src/core/public'; +import { TagsCapabilities } from '../../../common'; +import { ITagInternalClient } from '../../tags'; +import { TagBulkAction } from '../types'; +import { getBulkDeleteAction } from './bulk_delete'; +import { getClearSelectionAction } from './clear_selection'; + +interface GetBulkActionOptions { + core: CoreStart; + capabilities: TagsCapabilities; + tagClient: ITagInternalClient; + clearSelection: () => void; + setLoading: (loading: boolean) => void; +} + +export const getBulkActions = ({ + core: { notifications, overlays }, + capabilities, + tagClient, + clearSelection, + setLoading, +}: GetBulkActionOptions): TagBulkAction[] => { + const actions: TagBulkAction[] = []; + + if (capabilities.delete) { + actions.push(getBulkDeleteAction({ notifications, overlays, tagClient, setLoading })); + } + + // only add clear selection if user has permission to perform any other action + // as having at least one action will show the bulk action menu, and the selection column on the table + // and we want to avoid doing that only for the 'unselect' action. + if (actions.length > 0) { + actions.push(getClearSelectionAction({ clearSelection })); + } + + return actions; +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/_action_bar.scss b/x-pack/plugins/saved_objects_tagging/public/management/components/_action_bar.scss new file mode 100644 index 0000000000000..6858e70e49e8f --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/_action_bar.scss @@ -0,0 +1,17 @@ +.tagMgt__actionBar + .euiSpacer { + display: none; +} + +.tagMgt__actionBarDivider { + height: $euiSize; + border-right: $euiBorderThin; +} + +.tagMgt__actionBar { + border-bottom: $euiBorderThin; + padding-bottom: $euiSizeS; +} + +.tagMgt__actionBarIcon { + margin-left: $euiSizeXS; +} diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/action_bar.tsx b/x-pack/plugins/saved_objects_tagging/public/management/components/action_bar.tsx new file mode 100644 index 0000000000000..15d8f155f6246 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/action_bar.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useMemo, FC } from 'react'; +import { + EuiPopover, + EuiFlexItem, + EuiFlexGroup, + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + EuiText, + EuiLink, + EuiIcon, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TagBulkAction } from '../types'; + +import './_action_bar.scss'; + +export interface ActionBarProps { + actions: TagBulkAction[]; + totalCount: number; + selectedCount: number; + onActionSelected: (action: TagBulkAction) => void; +} + +const actionToMenuItem = ( + action: TagBulkAction, + onActionSelected: (action: TagBulkAction) => void, + closePopover: () => void +): EuiContextMenuPanelItemDescriptor => { + return { + name: action.label, + icon: action.icon, + onClick: () => { + closePopover(); + onActionSelected(action); + }, + 'data-test-subj': `actionBar-button-${action.id}`, + }; +}; + +export const ActionBar: FC = ({ + actions, + onActionSelected, + selectedCount, + totalCount, +}) => { + const [isPopoverOpened, setPopOverOpened] = useState(false); + + const closePopover = useCallback(() => { + setPopOverOpened(false); + }, [setPopOverOpened]); + + const togglePopover = useCallback(() => { + setPopOverOpened((opened) => !opened); + }, [setPopOverOpened]); + + const contextMenuPanels = useMemo(() => { + return [ + { + id: 0, + items: actions.map((action) => actionToMenuItem(action, onActionSelected, closePopover)), + }, + ]; + }, [actions, onActionSelected, closePopover]); + + return ( +
+ + + + + + + {selectedCount > 0 && ( + <> + +
+ + + + + + + + + } + > + + + + + )} + +
+ ); +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/index.ts b/x-pack/plugins/saved_objects_tagging/public/management/components/index.ts index 8435aa0431c23..a28e3523d7af6 100644 --- a/x-pack/plugins/saved_objects_tagging/public/management/components/index.ts +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/index.ts @@ -6,3 +6,4 @@ export { Header } from './header'; export { TagTable } from './table'; +export { ActionBar } from './action_bar'; diff --git a/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx b/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx index e86977c60ade1..ed1903fca2495 100644 --- a/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/management/components/table.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useRef, useEffect, FC } from 'react'; -import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; +import React, { useRef, useEffect, FC, ReactNode } from 'react'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiLink, Query } from '@elastic/eui'; import { Action as EuiTableAction } from '@elastic/eui/src/components/basic_table/action_types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -16,12 +16,16 @@ interface TagTableProps { loading: boolean; capabilities: TagsCapabilities; tags: TagWithRelations[]; + initialQuery?: Query; + allowSelection: boolean; + onQueryChange: (query?: Query) => void; selectedTags: TagWithRelations[]; onSelectionChange: (selection: TagWithRelations[]) => void; onEdit: (tag: TagWithRelations) => void; onDelete: (tag: TagWithRelations) => void; getTagRelationUrl: (tag: TagWithRelations) => string; onShowRelations: (tag: TagWithRelations) => void; + actionBar: ReactNode; } const tablePagination = { @@ -43,11 +47,16 @@ export const TagTable: FC = ({ loading, capabilities, tags, + initialQuery, + allowSelection, + onQueryChange, selectedTags, + onSelectionChange, onEdit, onDelete, onShowRelations, getTagRelationUrl, + actionBar, }) => { const tableRef = useRef>(null); @@ -60,9 +69,11 @@ export const TagTable: FC = ({ const actions: Array> = []; if (capabilities.edit) { actions.push({ - name: i18n.translate('xpack.savedObjectsTagging.management.table.actions.edit.title', { - defaultMessage: 'Edit', - }), + name: ({ name }) => + i18n.translate('xpack.savedObjectsTagging.management.table.actions.edit.title', { + defaultMessage: 'Edit {name} tag', + values: { name }, + }), description: i18n.translate( 'xpack.savedObjectsTagging.management.table.actions.edit.description', { @@ -77,9 +88,11 @@ export const TagTable: FC = ({ } if (capabilities.delete) { actions.push({ - name: i18n.translate('xpack.savedObjectsTagging.management.table.actions.delete.title', { - defaultMessage: 'Delete', - }), + name: ({ name }) => + i18n.translate('xpack.savedObjectsTagging.management.table.actions.delete.title', { + defaultMessage: 'Delete {name} tag', + values: { name }, + }), description: i18n.translate( 'xpack.savedObjectsTagging.management.table.actions.delete.description', { @@ -171,13 +184,30 @@ export const TagTable: FC = ({ { + onQueryChange(query || undefined); + }, box: { 'data-test-subj': 'tagsManagementSearchBar', incremental: true, diff --git a/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx b/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx index 4afb15bec6243..6b0e17a945c06 100644 --- a/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx +++ b/x-pack/plugins/saved_objects_tagging/public/management/tag_management_page.tsx @@ -6,13 +6,15 @@ import React, { useEffect, useCallback, useState, useMemo, FC } from 'react'; import useMount from 'react-use/lib/useMount'; -import { EuiPageContent } from '@elastic/eui'; +import { EuiPageContent, Query } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ChromeBreadcrumb, CoreStart } from 'src/core/public'; import { TagWithRelations, TagsCapabilities } from '../../common'; import { getCreateModalOpener, getEditModalOpener } from '../components/edition_modal'; import { ITagInternalClient } from '../tags'; -import { Header, TagTable } from './components'; +import { TagBulkAction } from './types'; +import { Header, TagTable, ActionBar } from './components'; +import { getBulkActions } from './actions'; import { getTagConnectionsUrl } from './utils'; interface TagManagementPageParams { @@ -32,6 +34,21 @@ export const TagManagementPage: FC = ({ const [loading, setLoading] = useState(false); const [allTags, setAllTags] = useState([]); const [selectedTags, setSelectedTags] = useState([]); + const [query, setQuery] = useState(); + + const filteredTags = useMemo(() => { + return query ? Query.execute(query, allTags) : allTags; + }, [allTags, query]); + + const bulkActions = useMemo(() => { + return getBulkActions({ + core, + capabilities, + tagClient, + setLoading, + clearSelection: () => setSelectedTags([]), + }); + }, [core, capabilities, tagClient]); const createModalOpener = useMemo(() => getCreateModalOpener({ overlays, tagClient }), [ overlays, @@ -140,13 +157,12 @@ export const TagManagementPage: FC = ({ } ), buttonColor: 'danger', + maxWidth: 560, } ); if (confirmed) { await tagClient.delete(tag.id); - fetchTags(); - notifications.toasts.addSuccess({ title: i18n.translate('xpack.savedObjectsTagging.notifications.deleteTagSuccessTitle', { defaultMessage: 'Deleted "{name}" tag', @@ -155,18 +171,59 @@ export const TagManagementPage: FC = ({ }, }), }); + + await fetchTags(); } }, [overlays, notifications, fetchTags, tagClient] ); + const executeBulkAction = useCallback( + async (action: TagBulkAction) => { + try { + await action.execute(selectedTags.map(({ id }) => id)); + } catch (e) { + notifications.toasts.addError(e, { + title: i18n.translate('xpack.savedObjectsTagging.notifications.bulkActionError', { + defaultMessage: 'An error occurred', + }), + }); + } finally { + setLoading(false); + } + if (action.refreshAfterExecute) { + await fetchTags(); + } + }, + [selectedTags, fetchTags, notifications] + ); + + const actionBar = useMemo( + () => ( + + ), + [selectedTags, filteredTags, bulkActions, executeBulkAction] + ); + return (
{ + setQuery(newQuery); + setSelectedTags([]); + }} + allowSelection={bulkActions.length > 0} selectedTags={selectedTags} onSelectionChange={(tags) => { setSelectedTags(tags); diff --git a/x-pack/plugins/saved_objects_tagging/public/management/types.ts b/x-pack/plugins/saved_objects_tagging/public/management/types.ts new file mode 100644 index 0000000000000..fc15785142431 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/management/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; + +/** + * Represents a tag `bulk action` + */ +export interface TagBulkAction { + /** + * The unique identifier for this action. + */ + id: string; + /** + * The label displayed in the bulk action context menu. + */ + label: string; + /** + * Optional aria-label if the visual label isn't descriptive enough. + */ + 'aria-label'?: string; + /** + * An optional icon to display before the label in the context menu. + */ + icon?: EuiIconType; + /** + * Handler to execute this action against the given list of selected tag ids. + */ + execute: (tagIds: string[]) => void | Promise; + /** + * If true, the list of tags will be reloaded after the action's execution. Defaults to false. + */ + refreshAfterExecute?: boolean; +} diff --git a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.mock.ts b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.mock.ts new file mode 100644 index 0000000000000..4ef0e89ae4866 --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ITagInternalClient } from './tags_client'; + +const createInternalClientMock = () => { + const mock: jest.Mocked = { + create: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + find: jest.fn(), + bulkDelete: jest.fn(), + }; + + return mock; +}; + +export const tagClientMock = { + create: createInternalClientMock, +}; diff --git a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.test.ts b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.test.ts index ac73880e52949..576f89b796010 100644 --- a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.test.ts +++ b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.test.ts @@ -216,41 +216,83 @@ describe('TagsClient', () => { }); }); - ///// + describe('internal APIs', () => { + describe('#find', () => { + const findOptions: FindTagsOptions = { + search: 'for, you know.', + }; + let expectedTags: Tag[]; + + beforeEach(() => { + expectedTags = [ + createTag({ id: 'tag-1' }), + createTag({ id: 'tag-2' }), + createTag({ id: 'tag-3' }), + ]; + http.get.mockResolvedValue({ tags: expectedTags, total: expectedTags.length }); + }); - describe('#find', () => { - const findOptions: FindTagsOptions = { - search: 'for, you know.', - }; - let expectedTags: Tag[]; + it('calls `http.get` with the correct parameters', async () => { + await tagsClient.find(findOptions); - beforeEach(() => { - expectedTags = [ - createTag({ id: 'tag-1' }), - createTag({ id: 'tag-2' }), - createTag({ id: 'tag-3' }), - ]; - http.get.mockResolvedValue({ tags: expectedTags, total: expectedTags.length }); + expect(http.get).toHaveBeenCalledTimes(1); + expect(http.get).toHaveBeenCalledWith(`/internal/saved_objects_tagging/tags/_find`, { + query: findOptions, + }); + }); + it('returns the tag objects from the response', async () => { + const { tags, total } = await tagsClient.find(findOptions); + expect(tags).toEqual(expectedTags); + expect(total).toEqual(3); + }); + it('forwards the error from the http call if any', async () => { + const error = new Error('something when wrong'); + http.get.mockRejectedValue(error); + + await expect(tagsClient.find(findOptions)).rejects.toThrowError(error); + }); }); - it('calls `http.get` with the correct parameters', async () => { - await tagsClient.find(findOptions); + describe('#bulkDelete', () => { + const tagIds = ['id-to-delete-1', 'id-to-delete-2']; - expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(`/internal/saved_objects_tagging/tags/_find`, { - query: findOptions, + beforeEach(() => { + http.post.mockResolvedValue({}); }); - }); - it('returns the tag objects from the response', async () => { - const { tags, total } = await tagsClient.find(findOptions); - expect(tags).toEqual(expectedTags); - expect(total).toEqual(3); - }); - it('forwards the error from the http call if any', async () => { - const error = new Error('something when wrong'); - http.get.mockRejectedValue(error); - await expect(tagsClient.find(findOptions)).rejects.toThrowError(error); + it('calls `http.post` with the correct parameters', async () => { + await tagsClient.bulkDelete(tagIds); + + expect(http.post).toHaveBeenCalledTimes(1); + expect(http.post).toHaveBeenCalledWith( + `/internal/saved_objects_tagging/tags/_bulk_delete`, + { + body: JSON.stringify({ + ids: tagIds, + }), + } + ); + }); + it('forwards the error from the http call if any', async () => { + const error = new Error('something when wrong'); + http.post.mockRejectedValue(error); + + await expect(tagsClient.bulkDelete(tagIds)).rejects.toThrowError(error); + }); + it('notifies its changeListener if the http call succeed', async () => { + await tagsClient.bulkDelete(tagIds); + + expect(changeListener.onDelete).toHaveBeenCalledTimes(2); + expect(changeListener.onDelete).toHaveBeenCalledWith(tagIds[0]); + expect(changeListener.onDelete).toHaveBeenCalledWith(tagIds[1]); + }); + it('ignores potential errors when calling `changeListener.onDelete`', async () => { + changeListener.onDelete.mockImplementation(() => { + throw new Error('error in onCreate'); + }); + + await expect(tagsClient.bulkDelete(tagIds)).resolves.toBeUndefined(); + }); }); }); }); diff --git a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.ts b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.ts index 3169babb2bae8..a866ae82f9702 100644 --- a/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.ts +++ b/x-pack/plugins/saved_objects_tagging/public/tags/tags_client.ts @@ -34,6 +34,7 @@ const trapErrors = (fn: () => void) => { export interface ITagInternalClient extends ITagsClient { find(options: FindTagsOptions): Promise; + bulkDelete(ids: string[]): Promise; } export class TagsClient implements ITagInternalClient { @@ -114,4 +115,20 @@ export class TagsClient implements ITagInternalClient { }, }); } + + public async bulkDelete(tagIds: string[]) { + await this.http.post<{}>('/internal/saved_objects_tagging/tags/_bulk_delete', { + body: JSON.stringify({ + ids: tagIds, + }), + }); + + trapErrors(() => { + if (this.changeListener) { + tagIds.forEach((tagId) => { + this.changeListener!.onDelete(tagId); + }); + } + }); + } } diff --git a/x-pack/plugins/saved_objects_tagging/server/routes/index.ts b/x-pack/plugins/saved_objects_tagging/server/routes/index.ts index 9519f54e01693..facfb3f690a28 100644 --- a/x-pack/plugins/saved_objects_tagging/server/routes/index.ts +++ b/x-pack/plugins/saved_objects_tagging/server/routes/index.ts @@ -10,7 +10,7 @@ import { registerDeleteTagRoute } from './delete_tag'; import { registerGetAllTagsRoute } from './get_all_tags'; import { registerGetTagRoute } from './get_tag'; import { registerUpdateTagRoute } from './update_tag'; -import { registerInternalFindTagsRoute } from './internal'; +import { registerInternalFindTagsRoute, registerInternalBulkDeleteRoute } from './internal'; export const registerRoutes = ({ router }: { router: IRouter }) => { // public API @@ -21,4 +21,5 @@ export const registerRoutes = ({ router }: { router: IRouter }) => { registerGetTagRoute(router); // internal API registerInternalFindTagsRoute(router); + registerInternalBulkDeleteRoute(router); }; diff --git a/x-pack/plugins/saved_objects_tagging/server/routes/internal/bulk_delete.ts b/x-pack/plugins/saved_objects_tagging/server/routes/internal/bulk_delete.ts new file mode 100644 index 0000000000000..bade81678543d --- /dev/null +++ b/x-pack/plugins/saved_objects_tagging/server/routes/internal/bulk_delete.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export const registerInternalBulkDeleteRoute = (router: IRouter) => { + router.post( + { + path: '/internal/saved_objects_tagging/tags/_bulk_delete', + validate: { + body: schema.object({ + ids: schema.arrayOf(schema.string()), + }), + }, + }, + router.handleLegacyErrors(async (ctx, req, res) => { + const { ids: tagIds } = req.body; + const client = ctx.tags!.tagsClient; + + for (const tagId of tagIds) { + await client.delete(tagId); + } + + return res.ok({ + body: {}, + }); + }) + ); +}; diff --git a/x-pack/plugins/saved_objects_tagging/server/routes/internal/index.ts b/x-pack/plugins/saved_objects_tagging/server/routes/internal/index.ts index 9d427cfe5831c..e20403af1f59b 100644 --- a/x-pack/plugins/saved_objects_tagging/server/routes/internal/index.ts +++ b/x-pack/plugins/saved_objects_tagging/server/routes/internal/index.ts @@ -5,3 +5,4 @@ */ export { registerInternalFindTagsRoute } from './find_tags'; +export { registerInternalBulkDeleteRoute } from './bulk_delete'; diff --git a/x-pack/test/functional/page_objects/tag_management_page.ts b/x-pack/test/functional/page_objects/tag_management_page.ts index 8b354e9d0e1c4..7d40bf5600da4 100644 --- a/x-pack/test/functional/page_objects/tag_management_page.ts +++ b/x-pack/test/functional/page_objects/tag_management_page.ts @@ -156,6 +156,13 @@ export function TagManagementPageProvider({ getService, getPageObjects }: FtrPro } } + /** + * Tag management page object. + * + * @remarks All the table manipulation helpers makes the assumption + * that all tags are displayed on a single page. Pagination + * and finding / interacting with a tag on another page is not supported. + */ class TagManagementPage { public readonly tagModal = new TagModal(this); @@ -272,6 +279,90 @@ export function TagManagementPageProvider({ getService, getPageObjects }: FtrPro await connectionLink.click(); } + /** + * Return true if the selection column is displayed on the table, false otherwise. + */ + async isSelectionColumnDisplayed() { + const firstRow = await testSubjects.find('tagsTableRow'); + const checkbox = await firstRow.findAllByCssSelector( + '.euiTableRowCellCheckbox .euiCheckbox__input' + ); + return Boolean(checkbox.length); + } + + /** + * Click on the selection checkbox of the tag matching given tag name. + */ + async selectTagByName(tagName: string) { + const tagRow = await this.getRowByName(tagName); + const checkbox = await tagRow.findByCssSelector( + '.euiTableRowCellCheckbox .euiCheckbox__input' + ); + await checkbox.click(); + } + + /** + * Returns true if the tag bulk action menu is displayed, false otherwise. + */ + async isActionMenuButtonDisplayed() { + return testSubjects.exists('actionBar-contextMenuButton'); + } + + /** + * Open the bulk action menu if not already opened. + */ + async openActionMenu() { + if (!(await this.isActionMenuOpened())) { + await this.toggleActionMenu(); + } + } + + /** + * Check if the action for given `actionId` is present in the bulk action menu. + * + * The menu will automatically be opened if not already, but the test must still + * select tags to make the action menu button appear. + */ + async isActionPresent(actionId: string) { + if (!(await this.isActionMenuButtonDisplayed())) { + return false; + } + const menuWasOpened = await this.isActionMenuOpened(); + if (!menuWasOpened) { + await this.openActionMenu(); + } + + const actionExists = await testSubjects.exists(`actionBar-button-${actionId}`); + + if (!menuWasOpened) { + await this.toggleActionMenu(); + } + + return actionExists; + } + + /** + * Click on given bulk action button + */ + async clickOnAction(actionId: string) { + await this.openActionMenu(); + await testSubjects.click(`actionBar-button-${actionId}`); + } + + /** + * Toggle (close if opened, open if closed) the bulk action menu. + */ + async toggleActionMenu() { + await testSubjects.click('actionBar-contextMenuButton'); + } + + /** + * Return true if the bulk action menu is opened, false otherwise. + */ + async isActionMenuOpened() { + return testSubjects.exists('actionBar-contextMenuPopover'); + } + /** * Return the info of all the tags currently displayed in the table (in table's order) */ diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_bulk_delete.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_bulk_delete.ts new file mode 100644 index 0000000000000..0c9a06bd58828 --- /dev/null +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_bulk_delete.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { USERS, User, ExpectedResponse } from '../../../common/lib'; +import { FtrProviderContext } from '../services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + + describe('POST /internal/saved_objects_tagging/tags/_bulk_delete', () => { + beforeEach(async () => { + await esArchiver.load('rbac_tags'); + }); + + afterEach(async () => { + await esArchiver.unload('rbac_tags'); + }); + + const responses: Record = { + authorized: { + httpCode: 200, + expectResponse: ({ body }) => { + expect(body).to.eql({}); + }, + }, + unauthorized: { + httpCode: 403, + expectResponse: ({ body }) => { + expect(body).to.eql({ + statusCode: 403, + error: 'Forbidden', + message: 'Unable to delete tag', + }); + }, + }, + }; + + const expectedResults: Record = { + authorized: [ + USERS.SUPERUSER, + USERS.DEFAULT_SPACE_SO_MANAGEMENT_WRITE_USER, + USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, + ], + unauthorized: [ + USERS.DEFAULT_SPACE_READ_USER, + USERS.DEFAULT_SPACE_SO_TAGGING_READ_USER, + USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, + USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.NOT_A_KIBANA_USER, + ], + }; + + const createUserTest = ( + { username, password, description }: User, + { httpCode, expectResponse }: ExpectedResponse + ) => { + it(`returns expected ${httpCode} response for ${description ?? username}`, async () => { + await supertest + .post(`/internal/saved_objects_tagging/tags/_bulk_delete`) + .send({ + ids: ['default-space-tag-1', 'default-space-tag-2'], + }) + .auth(username, password) + .expect(httpCode) + .then(expectResponse); + }); + }; + + const createTestSuite = () => { + Object.entries(expectedResults).forEach(([responseId, users]) => { + const response: ExpectedResponse = responses[responseId]; + users.forEach((user) => { + createUserTest(user, response); + }); + }); + }; + + createTestSuite(); + }); +} diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts index 5f3d1cf854f82..727479546431c 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/index.ts @@ -22,5 +22,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./_find')); + loadTestFile(require.resolve('./_bulk_delete')); }); } diff --git a/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts b/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts new file mode 100644 index 0000000000000..556130bed7931 --- /dev/null +++ b/x-pack/test/saved_object_tagging/functional/tests/bulk_actions.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'security', 'savedObjects', 'tagManagement']); + const tagManagementPage = PageObjects.tagManagement; + + describe('table bulk actions', () => { + beforeEach(async () => { + await esArchiver.load('functional_base'); + await tagManagementPage.navigateTo(); + }); + afterEach(async () => { + await esArchiver.unload('functional_base'); + }); + + describe('bulk delete', () => { + it('deletes multiple tags', async () => { + await tagManagementPage.selectTagByName('tag-1'); + await tagManagementPage.selectTagByName('tag-3'); + + await tagManagementPage.clickOnAction('delete'); + + await PageObjects.common.clickConfirmOnModal(); + await tagManagementPage.waitUntilTableIsLoaded(); + + const displayedTags = await tagManagementPage.getDisplayedTagNames(); + expect(displayedTags.length).to.be(3); + expect(displayedTags).to.eql(['my-favorite-tag', 'tag with whitespace', 'tag-2']); + }); + }); + + describe('clear selection', () => { + it('clears the current selection', async () => { + await tagManagementPage.selectTagByName('tag-1'); + await tagManagementPage.selectTagByName('tag-3'); + + await tagManagementPage.clickOnAction('clear_selection'); + + await tagManagementPage.waitUntilTableIsLoaded(); + + expect(await tagManagementPage.isActionMenuButtonDisplayed()).to.be(false); + }); + }); + }); +} diff --git a/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts b/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts index 72beabca59f5c..65443fb517edf 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/feature_control.ts @@ -35,6 +35,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }; + const selectSomeTags = async () => { + if (await tagManagementPage.isSelectionColumnDisplayed()) { + await tagManagementPage.selectTagByName('tag-1'); + await tagManagementPage.selectTagByName('tag-3'); + } + }; + const addFeatureControlSuite = ({ user, description, privileges }: FeatureControlUserSuite) => { const testPrefix = (allowed: boolean) => (allowed ? `can` : `can't`); @@ -57,6 +64,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await tagManagementPage.isDeleteButtonVisible()).to.be(privileges.delete); }); + it(`${testPrefix(privileges.delete)} bulk delete tags`, async () => { + await selectSomeTags(); + expect(await tagManagementPage.isActionPresent('delete')).to.be(privileges.delete); + }); + it(`${testPrefix(privileges.create)} create tag`, async () => { expect(await tagManagementPage.isCreateButtonVisible()).to.be(privileges.create); }); diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index 0ddfa64d682a8..7fd0605c34d76 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -17,6 +17,7 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { }); loadTestFile(require.resolve('./listing')); + loadTestFile(require.resolve('./bulk_actions')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./edit')); loadTestFile(require.resolve('./som_integration')); diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index d8bd39ac7dc53..b938591543196 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -102,7 +102,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(itemNames).to.contain('My new markdown viz'); }); - it('allows to assign tags to the new visualization', async () => { + it('allows to create a tag from the tag selector', async () => { const { tagModal } = PageObjects.tagManagement; await PageObjects.visualize.navigateToNewVisualization(); From 00c053594682a0f66160150e61df91296aa87312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Wed, 18 Nov 2020 11:44:14 +0100 Subject: [PATCH 58/99] [Logs UI] Update internal state when its props change (#83302) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../infra/public/components/log_stream/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index 62a4d7ffc3d81..43d84497af9e9 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useEffect } from 'react'; import { noop } from 'lodash'; -import useMount from 'react-use/lib/useMount'; import { euiStyled } from '../../../../observability/public'; import { LogEntriesCursor } from '../../../common/http_api'; @@ -100,10 +99,13 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re const parsedHeight = typeof height === 'number' ? `${height}px` : height; // Component lifetime - useMount(() => { + useEffect(() => { loadSourceConfiguration(); + }, [loadSourceConfiguration]); + + useEffect(() => { fetchEntries(); - }); + }, [fetchEntries]); // Pagination handler const handlePagination = useCallback( From 957882a479bd3416d1f4f8d2085b5a78d4cee001 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Wed, 18 Nov 2020 15:58:53 +0300 Subject: [PATCH 59/99] [TSVB] Y-axis has number formatting not considering all series formatters in the group (#83438) * [TSVB] Y-axis has number formatting not considering all series formatters in the group * Replace check for percent with a check for same formatters in common * Remove unnecessary series check --- .../application/components/vis_types/timeseries/vis.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index c12e518a9dcd3..f936710bf2b81 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -161,6 +161,10 @@ export class TimeseriesVisualization extends Component { const yAxis = []; let mainDomainAdded = false; + const allSeriesHaveSameFormatters = seriesModel.every( + (seriesGroup) => seriesGroup.formatter === seriesModel[0].formatter + ); + this.showToastNotification = null; seriesModel.forEach((seriesGroup) => { @@ -211,7 +215,7 @@ export class TimeseriesVisualization extends Component { }); } else if (!mainDomainAdded) { TimeseriesVisualization.addYAxis(yAxis, { - tickFormatter: series.length === 1 ? undefined : (val) => val, + tickFormatter: allSeriesHaveSameFormatters ? seriesGroupTickFormatter : (val) => val, id: yAxisIdGenerator('main'), groupId: mainAxisGroupId, position: model.axis_position, From 7114db3b1de955eb0e224e321ef4d647a1e69436 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 18 Nov 2020 16:59:40 +0300 Subject: [PATCH 60/99] [TSVB] use new Search API for rollup search (#83275) * [TSVB] use new Search API for rollup search Closes: #82710 * remove unused code * rollup_search_strategy.test.js -> rollup_search_strategy.test.ts * default_search_capabilities.test.js -> default_search_capabilities.test.ts * remove getRollupService * fix CI * fix some types * update types * update codeowners * fix PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + docs/developer/plugin-list.asciidoc | 4 + .../vis_type_timeseries/server/index.ts | 4 +- .../server/lib/get_fields.ts | 2 +- .../server/lib/get_vis_data.ts | 2 +- ...js => default_search_capabilities.test.ts} | 12 +- ...ties.js => default_search_capabilities.ts} | 37 ++--- .../search_strategies_registry.test.ts | 8 +- .../strategies/abstract_search_strategy.ts | 36 ++--- ...est.js => default_search_strategy.test.ts} | 12 +- ...strategy.js => default_search_strategy.ts} | 9 +- .../lib/vis_data/helpers/get_bucket_size.js | 10 +- ...econds.test.js => unit_to_seconds.test.ts} | 29 +--- ...{unit_to_seconds.js => unit_to_seconds.ts} | 57 ++++--- .../register_rollup_search_strategy.test.js | 22 --- .../register_rollup_search_strategy.ts | 28 ---- .../rollup_search_capabilities.ts | 115 -------------- .../rollup_search_strategy.ts | 94 ------------ x-pack/plugins/rollup/server/plugin.ts | 12 +- .../vis_type_timeseries_enhanced/README.md | 10 ++ .../vis_type_timeseries_enhanced/kibana.json | 10 ++ .../server}/index.ts | 6 +- .../server/plugin.ts | 33 ++++ .../lib/interval_helper.test.ts} | 0 .../search_strategies/lib/interval_helper.ts | 0 .../rollup_search_capabilities.test.ts} | 37 ++--- .../rollup_search_capabilities.ts | 123 +++++++++++++++ .../rollup_search_strategy.test.ts} | 142 ++++++++---------- .../rollup_search_strategy.ts | 79 ++++++++++ 29 files changed, 453 insertions(+), 481 deletions(-) rename src/plugins/vis_type_timeseries/server/lib/search_strategies/{default_search_capabilities.test.js => default_search_capabilities.test.ts} (90%) rename src/plugins/vis_type_timeseries/server/lib/search_strategies/{default_search_capabilities.js => default_search_capabilities.ts} (69%) rename src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/{default_search_strategy.test.js => default_search_strategy.test.ts} (79%) rename src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/{default_search_strategy.js => default_search_strategy.ts} (82%) rename src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/{unit_to_seconds.test.js => unit_to_seconds.test.ts} (86%) rename src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/{unit_to_seconds.js => unit_to_seconds.ts} (60%) delete mode 100644 x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js delete mode 100644 x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts delete mode 100644 x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts delete mode 100644 x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts create mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/README.md create mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/kibana.json rename x-pack/plugins/{rollup/server/lib/search_strategies => vis_type_timeseries_enhanced/server}/index.ts (50%) create mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/server/plugin.ts rename x-pack/plugins/{rollup/server/lib/search_strategies/lib/interval_helper.test.js => vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.test.ts} (100%) rename x-pack/plugins/{rollup/server/lib => vis_type_timeseries_enhanced/server}/search_strategies/lib/interval_helper.ts (100%) rename x-pack/plugins/{rollup/server/lib/search_strategies/rollup_search_capabilities.test.js => vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.test.ts} (77%) create mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.ts rename x-pack/plugins/{rollup/server/lib/search_strategies/rollup_search_strategy.test.js => vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.test.ts} (56%) create mode 100644 x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index af010089e4892..d92725b233e3e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,6 +9,7 @@ /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app +/x-pack/plugins/vis_type_timeseries_enhanced/ @elastic/kibana-app /src/plugins/advanced_settings/ @elastic/kibana-app /src/plugins/charts/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 1436399a03dbc..198b0372d9254 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -558,6 +558,10 @@ in their infrastructure. |NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to ui_actions_enhanced plugin. +|{kib-repo}blob/{branch}/x-pack/plugins/vis_type_timeseries_enhanced/README.md[visTypeTimeseriesEnhanced] +|The vis_type_timeseries_enhanced plugin is the x-pack counterpart to the OSS vis_type_timeseries plugin. + + |{kib-repo}blob/{branch}/x-pack/plugins/watcher/README.md[watcher] |This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts index 333ed0ff64fdb..1037dc81b2b17 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -43,7 +43,9 @@ export { AbstractSearchStrategy, ReqFacade, } from './lib/search_strategies/strategies/abstract_search_strategy'; -// @ts-ignore + +export { VisPayload } from '../common/types'; + export { DefaultSearchCapabilities } from './lib/search_strategies/default_search_capabilities'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index dc49e280a2bb7..8f87318222f2b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -38,7 +38,7 @@ export async function getFields( // removes the need to refactor many layers of dependencies on "req", and instead just augments the top // level object passed from here. The layers should be refactored fully at some point, but for now // this works and we are still using the New Platform services for these vis data portions. - const reqFacade: ReqFacade = { + const reqFacade: ReqFacade<{}> = { requestContext, ...request, framework, diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts index 5eef2b53e2431..fcb66d2e12fd1 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts @@ -64,7 +64,7 @@ export function getVisData( // removes the need to refactor many layers of dependencies on "req", and instead just augments the top // level object passed from here. The layers should be refactored fully at some point, but for now // this works and we are still using the New Platform services for these vis data portions. - const reqFacade: ReqFacade = { + const reqFacade: ReqFacade = { requestContext, ...request, framework, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts similarity index 90% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts index b9b7759711567..a570e02ada8d1 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.test.ts @@ -17,13 +17,15 @@ * under the License. */ import { DefaultSearchCapabilities } from './default_search_capabilities'; +import { ReqFacade } from './strategies/abstract_search_strategy'; +import { VisPayload } from '../../../common/types'; describe('DefaultSearchCapabilities', () => { - let defaultSearchCapabilities; - let req; + let defaultSearchCapabilities: DefaultSearchCapabilities; + let req: ReqFacade; beforeEach(() => { - req = {}; + req = {} as ReqFacade; defaultSearchCapabilities = new DefaultSearchCapabilities(req); }); @@ -45,13 +47,13 @@ describe('DefaultSearchCapabilities', () => { }); test('should return Search Timezone', () => { - defaultSearchCapabilities.request = { + defaultSearchCapabilities.request = ({ payload: { timerange: { timezone: 'UTC', }, }, - }; + } as unknown) as ReqFacade; expect(defaultSearchCapabilities.searchTimezone).toEqual('UTC'); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts similarity index 69% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts index 02a710fef897f..73b701379aee0 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/default_search_capabilities.ts @@ -16,40 +16,43 @@ * specific language governing permissions and limitations * under the License. */ +import { Unit } from '@elastic/datemath'; import { convertIntervalToUnit, parseInterval, getSuitableUnit, } from '../vis_data/helpers/unit_to_seconds'; import { RESTRICTIONS_KEYS } from '../../../common/ui_restrictions'; +import { ReqFacade } from './strategies/abstract_search_strategy'; +import { VisPayload } from '../../../common/types'; -const getTimezoneFromRequest = (request) => { +const getTimezoneFromRequest = (request: ReqFacade) => { return request.payload.timerange.timezone; }; export class DefaultSearchCapabilities { - constructor(request, fieldsCapabilities = {}) { - this.request = request; - this.fieldsCapabilities = fieldsCapabilities; - } + constructor( + public request: ReqFacade, + public fieldsCapabilities: Record = {} + ) {} - get defaultTimeInterval() { + public get defaultTimeInterval() { return null; } - get whiteListedMetrics() { + public get whiteListedMetrics() { return this.createUiRestriction(); } - get whiteListedGroupByFields() { + public get whiteListedGroupByFields() { return this.createUiRestriction(); } - get whiteListedTimerangeModes() { + public get whiteListedTimerangeModes() { return this.createUiRestriction(); } - get uiRestrictions() { + public get uiRestrictions() { return { [RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics, [RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields, @@ -57,36 +60,36 @@ export class DefaultSearchCapabilities { }; } - get searchTimezone() { + public get searchTimezone() { return getTimezoneFromRequest(this.request); } - createUiRestriction(restrictionsObject) { + createUiRestriction(restrictionsObject?: Record) { return { '*': !restrictionsObject, ...(restrictionsObject || {}), }; } - parseInterval(interval) { + parseInterval(interval: string) { return parseInterval(interval); } - getSuitableUnit(intervalInSeconds) { + getSuitableUnit(intervalInSeconds: string | number) { return getSuitableUnit(intervalInSeconds); } - convertIntervalToUnit(intervalString, unit) { + convertIntervalToUnit(intervalString: string, unit: Unit) { const parsedInterval = this.parseInterval(intervalString); - if (parsedInterval.unit !== unit) { + if (parsedInterval?.unit !== unit) { return convertIntervalToUnit(intervalString, unit); } return parsedInterval; } - getValidTimeInterval(intervalString) { + getValidTimeInterval(intervalString: string) { // Default search capabilities doesn't have any restrictions for the interval string return intervalString; } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts index 66ea4f017dd90..4c3dcbd17bbd9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts @@ -27,10 +27,10 @@ import { DefaultSearchCapabilities } from './default_search_capabilities'; class MockSearchStrategy extends AbstractSearchStrategy { checkForViability() { - return { + return Promise.resolve({ isViable: true, capabilities: {}, - }; + }); } } @@ -65,7 +65,7 @@ describe('SearchStrategyRegister', () => { }); test('should add a strategy if it is an instance of AbstractSearchStrategy', () => { - const anotherSearchStrategy = new MockSearchStrategy('es'); + const anotherSearchStrategy = new MockSearchStrategy(); const addedStrategies = registry.addStrategy(anotherSearchStrategy); expect(addedStrategies.length).toEqual(2); @@ -75,7 +75,7 @@ describe('SearchStrategyRegister', () => { test('should return a MockSearchStrategy instance', async () => { const req = {}; const indexPattern = '*'; - const anotherSearchStrategy = new MockSearchStrategy('es'); + const anotherSearchStrategy = new MockSearchStrategy(); registry.addStrategy(anotherSearchStrategy); const { searchStrategy, capabilities } = (await registry.getViableStrategy(req, indexPattern))!; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index b1e21edf8b588..71461d319f2b6 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -46,16 +46,8 @@ export interface ReqFacade extends FakeRequest { getEsShardTimeout: () => Promise; } -export class AbstractSearchStrategy { - public indexType?: string; - public additionalParams: any; - - constructor(type?: string, additionalParams: any = {}) { - this.indexType = type; - this.additionalParams = additionalParams; - } - - async search(req: ReqFacade, bodies: any[], options = {}) { +export abstract class AbstractSearchStrategy { + async search(req: ReqFacade, bodies: any[], indexType?: string) { const requests: any[] = []; const { sessionId } = req.payload; @@ -64,15 +56,13 @@ export class AbstractSearchStrategy { req.requestContext .search!.search( { + indexType, params: { ...body, - ...this.additionalParams, }, - indexType: this.indexType, }, { sessionId, - ...options, } ) .toPromise() @@ -81,7 +71,18 @@ export class AbstractSearchStrategy { return Promise.all(requests); } - async getFieldsForWildcard(req: ReqFacade, indexPattern: string, capabilities: any) { + checkForViability( + req: ReqFacade, + indexPattern: string + ): Promise<{ isViable: boolean; capabilities: unknown }> { + throw new TypeError('Must override method'); + } + + async getFieldsForWildcard( + req: ReqFacade, + indexPattern: string, + capabilities?: unknown + ) { const { indexPatternsService } = req.pre; return await indexPatternsService!.getFieldsForWildcard({ @@ -89,11 +90,4 @@ export class AbstractSearchStrategy { fieldCapsOptions: { allow_no_indices: true }, }); } - - checkForViability( - req: ReqFacade, - indexPattern: string - ): { isViable: boolean; capabilities: any } { - throw new TypeError('Must override method'); - } } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts similarity index 79% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts index a9994ba3e1f75..d8ea6c9c8a526 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts @@ -17,13 +17,15 @@ * under the License. */ import { DefaultSearchStrategy } from './default_search_strategy'; +import { ReqFacade } from './abstract_search_strategy'; +import { VisPayload } from '../../../../common/types'; describe('DefaultSearchStrategy', () => { - let defaultSearchStrategy; - let req; + let defaultSearchStrategy: DefaultSearchStrategy; + let req: ReqFacade; beforeEach(() => { - req = {}; + req = {} as ReqFacade; defaultSearchStrategy = new DefaultSearchStrategy(); }); @@ -34,8 +36,8 @@ describe('DefaultSearchStrategy', () => { expect(defaultSearchStrategy.getFieldsForWildcard).toBeDefined(); }); - test('should check a strategy for viability', () => { - const value = defaultSearchStrategy.checkForViability(req); + test('should check a strategy for viability', async () => { + const value = await defaultSearchStrategy.checkForViability(req); expect(value.isViable).toBe(true); expect(value.capabilities).toEqual({ diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts similarity index 82% rename from src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js rename to src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts index 8e57c117637bf..e1f519456d373 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts @@ -17,16 +17,17 @@ * under the License. */ -import { AbstractSearchStrategy } from './abstract_search_strategy'; +import { AbstractSearchStrategy, ReqFacade } from './abstract_search_strategy'; import { DefaultSearchCapabilities } from '../default_search_capabilities'; +import { VisPayload } from '../../../../common/types'; export class DefaultSearchStrategy extends AbstractSearchStrategy { name = 'default'; - checkForViability(req) { - return { + checkForViability(req: ReqFacade) { + return Promise.resolve({ isViable: true, capabilities: new DefaultSearchCapabilities(req), - }; + }); } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js index 53f0b84b8ec3b..c021ba3cebc66 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js @@ -42,14 +42,18 @@ const calculateBucketData = (timeInterval, capabilities) => { } // Check decimal - if (parsedInterval.value % 1 !== 0) { + if (parsedInterval && parsedInterval.value % 1 !== 0) { if (parsedInterval.unit !== 'ms') { - const { value, unit } = convertIntervalToUnit( + const converted = convertIntervalToUnit( intervalString, ASCENDING_UNIT_ORDER[ASCENDING_UNIT_ORDER.indexOf(parsedInterval.unit) - 1] ); - intervalString = value + unit; + if (converted) { + intervalString = converted.value + converted.unit; + } + + intervalString = undefined; } else { intervalString = '1ms'; } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts similarity index 86% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.js rename to src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts index 5b533178949f1..278e557209a21 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.test.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { Unit } from '@elastic/datemath'; import { getUnitValue, @@ -51,22 +52,13 @@ describe('unit_to_seconds', () => { })); test('should not parse "gm" interval (negative)', () => - expect(parseInterval('gm')).toEqual({ - value: undefined, - unit: undefined, - })); + expect(parseInterval('gm')).toBeUndefined()); test('should not parse "-1d" interval (negative)', () => - expect(parseInterval('-1d')).toEqual({ - value: undefined, - unit: undefined, - })); + expect(parseInterval('-1d')).toBeUndefined()); test('should not parse "M" interval (negative)', () => - expect(parseInterval('M')).toEqual({ - value: undefined, - unit: undefined, - })); + expect(parseInterval('M')).toBeUndefined()); }); describe('convertIntervalToUnit()', () => { @@ -95,16 +87,10 @@ describe('unit_to_seconds', () => { })); test('should not convert "30m" interval to "0" unit (positive)', () => - expect(convertIntervalToUnit('30m', 'o')).toEqual({ - value: undefined, - unit: undefined, - })); + expect(convertIntervalToUnit('30m', 'o' as Unit)).toBeUndefined()); test('should not convert "m" interval to "s" unit (positive)', () => - expect(convertIntervalToUnit('m', 's')).toEqual({ - value: undefined, - unit: undefined, - })); + expect(convertIntervalToUnit('m', 's')).toBeUndefined()); }); describe('getSuitableUnit()', () => { @@ -155,8 +141,5 @@ describe('unit_to_seconds', () => { expect(getSuitableUnit(stringValue)).toBeUndefined(); }); - - test('should return "undefined" in case of no input value(negative)', () => - expect(getSuitableUnit()).toBeUndefined()); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts similarity index 60% rename from src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.js rename to src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts index be8f1741627ba..8950e05c85d4f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts @@ -16,12 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import { INTERVAL_STRING_RE } from '../../../../common/interval_regexp'; import { sortBy, isNumber } from 'lodash'; +import { Unit } from '@elastic/datemath'; + +/** @ts-ignore */ +import { INTERVAL_STRING_RE } from '../../../../common/interval_regexp'; export const ASCENDING_UNIT_ORDER = ['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y']; -const units = { +const units: Record = { ms: 0.001, s: 1, m: 60, @@ -32,49 +35,53 @@ const units = { y: 86400 * 7 * 4 * 12, // Leap year? }; -const sortedUnits = sortBy(Object.keys(units), (key) => units[key]); +const sortedUnits = sortBy(Object.keys(units), (key: Unit) => units[key]); -export const parseInterval = (intervalString) => { - let value; - let unit; +interface ParsedInterval { + value: number; + unit: Unit; +} +export const parseInterval = (intervalString: string): ParsedInterval | undefined => { if (intervalString) { const matches = intervalString.match(INTERVAL_STRING_RE); if (matches) { - value = Number(matches[1]); - unit = matches[2]; + return { + value: Number(matches[1]), + unit: matches[2] as Unit, + }; } } - - return { value, unit }; }; -export const convertIntervalToUnit = (intervalString, newUnit) => { +export const convertIntervalToUnit = ( + intervalString: string, + newUnit: Unit +): ParsedInterval | undefined => { const parsedInterval = parseInterval(intervalString); - let value; - let unit; - if (parsedInterval.value && units[newUnit]) { - value = Number( - ((parsedInterval.value * units[parsedInterval.unit]) / units[newUnit]).toFixed(2) - ); - unit = newUnit; + if (parsedInterval && units[newUnit]) { + return { + value: Number( + ((parsedInterval.value * units[parsedInterval.unit!]) / units[newUnit]).toFixed(2) + ), + unit: newUnit, + }; } - - return { value, unit }; }; -export const getSuitableUnit = (intervalInSeconds) => +export const getSuitableUnit = (intervalInSeconds: string | number) => sortedUnits.find((key, index, array) => { - const nextUnit = array[index + 1]; + const nextUnit = array[index + 1] as Unit; const isValidInput = isNumber(intervalInSeconds) && intervalInSeconds > 0; const isLastItem = index + 1 === array.length; return ( isValidInput && - ((intervalInSeconds >= units[key] && intervalInSeconds < units[nextUnit]) || isLastItem) + ((intervalInSeconds >= units[key as Unit] && intervalInSeconds < units[nextUnit]) || + isLastItem) ); - }); + }) as Unit; -export const getUnitValue = (unit) => units[unit]; +export const getUnitValue = (unit: Unit) => units[unit]; diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js deleted file mode 100644 index 8672a8b8f6849..0000000000000 --- a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js +++ /dev/null @@ -1,22 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { registerRollupSearchStrategy } from './register_rollup_search_strategy'; - -describe('Register Rollup Search Strategy', () => { - let addSearchStrategy; - let getRollupService; - - beforeEach(() => { - addSearchStrategy = jest.fn().mockName('addSearchStrategy'); - getRollupService = jest.fn().mockName('getRollupService'); - }); - - test('should run initialization', () => { - registerRollupSearchStrategy(addSearchStrategy, getRollupService); - - expect(addSearchStrategy).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts deleted file mode 100644 index 22dafbb71d802..0000000000000 --- a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts +++ /dev/null @@ -1,28 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ILegacyScopedClusterClient } from 'src/core/server'; -import { - DefaultSearchCapabilities, - AbstractSearchStrategy, - ReqFacade, -} from '../../../../../../src/plugins/vis_type_timeseries/server'; -import { getRollupSearchStrategy } from './rollup_search_strategy'; -import { getRollupSearchCapabilities } from './rollup_search_capabilities'; - -export const registerRollupSearchStrategy = ( - addSearchStrategy: (searchStrategy: any) => void, - getRollupService: (reg: ReqFacade) => Promise -) => { - const RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities); - const RollupSearchStrategy = getRollupSearchStrategy( - AbstractSearchStrategy, - RollupSearchCapabilities, - getRollupService - ); - - addSearchStrategy(new RollupSearchStrategy()); -}; diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts deleted file mode 100644 index 354bf641114c7..0000000000000 --- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts +++ /dev/null @@ -1,115 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { get, has } from 'lodash'; -import { KibanaRequest } from 'src/core/server'; -import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper'; - -export const getRollupSearchCapabilities = (DefaultSearchCapabilities: any) => - class RollupSearchCapabilities extends DefaultSearchCapabilities { - constructor( - req: KibanaRequest, - fieldsCapabilities: { [key: string]: any }, - rollupIndex: string - ) { - super(req, fieldsCapabilities); - - this.rollupIndex = rollupIndex; - this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {}); - } - - public get dateHistogram() { - const [dateHistogram] = Object.values(this.availableMetrics.date_histogram); - - return dateHistogram; - } - - public get defaultTimeInterval() { - return ( - this.dateHistogram.fixed_interval || - this.dateHistogram.calendar_interval || - /* - Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future. - We can remove the following line only for versions > 8.x - */ - this.dateHistogram.interval || - null - ); - } - - public get searchTimezone() { - return get(this.dateHistogram, 'time_zone', null); - } - - public get whiteListedMetrics() { - const baseRestrictions = this.createUiRestriction({ - count: this.createUiRestriction(), - }); - - const getFields = (fields: { [key: string]: any }) => - Object.keys(fields).reduce( - (acc, item) => ({ - ...acc, - [item]: true, - }), - this.createUiRestriction({}) - ); - - return Object.keys(this.availableMetrics).reduce( - (acc, item) => ({ - ...acc, - [item]: getFields(this.availableMetrics[item]), - }), - baseRestrictions - ); - } - - public get whiteListedGroupByFields() { - return this.createUiRestriction({ - everything: true, - terms: has(this.availableMetrics, 'terms'), - }); - } - - public get whiteListedTimerangeModes() { - return this.createUiRestriction({ - last_value: true, - }); - } - - getValidTimeInterval(userIntervalString: string) { - const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval); - const inRollupJobUnit = this.convertIntervalToUnit( - userIntervalString, - parsedRollupJobInterval.unit - ); - - const getValidCalendarInterval = () => { - let unit = parsedRollupJobInterval.unit; - - if (inRollupJobUnit.value > parsedRollupJobInterval.value) { - const inSeconds = this.convertIntervalToUnit(userIntervalString, 's'); - - unit = this.getSuitableUnit(inSeconds.value); - } - - return { - value: 1, - unit, - }; - }; - - const getValidFixedInterval = () => ({ - value: leastCommonInterval(inRollupJobUnit.value, parsedRollupJobInterval.value), - unit: parsedRollupJobInterval.unit, - }); - - const { value, unit } = (isCalendarInterval(parsedRollupJobInterval) - ? getValidCalendarInterval - : getValidFixedInterval)(); - - return `${value}${unit}`; - } - }; diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts deleted file mode 100644 index dcf6629d35397..0000000000000 --- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts +++ /dev/null @@ -1,94 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { keyBy, isString } from 'lodash'; -import { ILegacyScopedClusterClient } from 'src/core/server'; -import { ReqFacade } from '../../../../../../src/plugins/vis_type_timeseries/server'; - -import { - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, -} from '../../../../../../src/plugins/data/server'; - -const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData); - -const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*'); -const isIndexPatternValid = (indexPattern: string) => - indexPattern && isString(indexPattern) && !isIndexPatternContainsWildcard(indexPattern); - -export const getRollupSearchStrategy = ( - AbstractSearchStrategy: any, - RollupSearchCapabilities: any, - getRollupService: (reg: ReqFacade) => Promise -) => - class RollupSearchStrategy extends AbstractSearchStrategy { - name = 'rollup'; - - constructor() { - super('rollup', { rest_total_hits_as_int: true }); - } - - async search(req: ReqFacade, bodies: any[], options = {}) { - const rollupService = await getRollupService(req); - const requests: any[] = []; - bodies.forEach((body) => { - requests.push( - rollupService.callAsCurrentUser('rollup.search', { - ...body, - rest_total_hits_as_int: true, - }) - ); - }); - return Promise.all(requests); - } - - async getRollupData(req: ReqFacade, indexPattern: string) { - const rollupService = await getRollupService(req); - return rollupService - .callAsCurrentUser('rollup.rollupIndexCapabilities', { - indexPattern, - }) - .catch(() => Promise.resolve({})); - } - - async checkForViability(req: ReqFacade, indexPattern: string) { - let isViable = false; - let capabilities = null; - - if (isIndexPatternValid(indexPattern)) { - const rollupData = await this.getRollupData(req, indexPattern); - const rollupIndices = getRollupIndices(rollupData); - - isViable = rollupIndices.length === 1; - - if (isViable) { - const [rollupIndex] = rollupIndices; - const fieldsCapabilities = getCapabilitiesForRollupIndices(rollupData); - - capabilities = new RollupSearchCapabilities(req, fieldsCapabilities, rollupIndex); - } - } - - return { - isViable, - capabilities, - }; - } - - async getFieldsForWildcard( - req: ReqFacade, - indexPattern: string, - { - fieldsCapabilities, - rollupIndex, - }: { fieldsCapabilities: { [key: string]: any }; rollupIndex: string } - ) { - const fields = await super.getFieldsForWildcard(req, indexPattern); - const fieldsFromFieldCapsApi = keyBy(fields, 'name'); - const rollupIndexCapabilities = fieldsCapabilities[rollupIndex].aggs; - - return mergeCapabilitiesWithFields(rollupIndexCapabilities, fieldsFromFieldCapsApi); - } - }; diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 51920af7c8cbc..3c670f56c7d8f 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -24,7 +24,6 @@ import { import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { ReqFacade } from '../../../../src/plugins/vis_type_timeseries/server'; import { PLUGIN, CONFIG_ROLLUPS } from '../common'; import { Dependencies } from './types'; import { registerApiRoutes } from './routes'; @@ -32,7 +31,6 @@ import { License } from './services'; import { registerRollupUsageCollector } from './collectors'; import { rollupDataEnricher } from './rollup_data_enricher'; import { IndexPatternsFetcher } from './shared_imports'; -import { registerRollupSearchStrategy } from './lib/search_strategies'; import { elasticsearchJsPlugin } from './client/elasticsearch_rollup'; import { isEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; @@ -45,6 +43,7 @@ async function getCustomEsClient(getStartServices: CoreSetup['getStartServices'] const [core] = await getStartServices(); // Extend the elasticsearchJs client with additional endpoints. const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + return core.elasticsearch.legacy.createClient('rollup', esClientConfig); } @@ -128,15 +127,6 @@ export class RollupPlugin implements Plugin { }, }); - if (visTypeTimeseries) { - const getRollupService = async (request: ReqFacade) => { - this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices)); - return this.rollupEsClient.asScoped(request); - }; - const { addSearchStrategy } = visTypeTimeseries; - registerRollupSearchStrategy(addSearchStrategy, getRollupService); - } - if (usageCollection) { this.globalConfig$ .pipe(first()) diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/README.md b/x-pack/plugins/vis_type_timeseries_enhanced/README.md new file mode 100644 index 0000000000000..33aa16d8574ae --- /dev/null +++ b/x-pack/plugins/vis_type_timeseries_enhanced/README.md @@ -0,0 +1,10 @@ +# vis_type_timeseries_enhanced + +The `vis_type_timeseries_enhanced` plugin is the x-pack counterpart to the OSS `vis_type_timeseries` plugin. + +It exists to provide Elastic-licensed services, or parts of services, which +enhance existing OSS functionality from `vis_type_timeseries`. + +Currently the `vis_type_timeseries_enhanced` plugin doesn't return any APIs which you can +consume directly. + diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/kibana.json b/x-pack/plugins/vis_type_timeseries_enhanced/kibana.json new file mode 100644 index 0000000000000..4b296856c3f97 --- /dev/null +++ b/x-pack/plugins/vis_type_timeseries_enhanced/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "visTypeTimeseriesEnhanced", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false, + "requiredPlugins": [ + "visTypeTimeseries" + ] +} diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/index.ts b/x-pack/plugins/vis_type_timeseries_enhanced/server/index.ts similarity index 50% rename from x-pack/plugins/rollup/server/lib/search_strategies/index.ts rename to x-pack/plugins/vis_type_timeseries_enhanced/server/index.ts index 7db0b38ea29dd..d2665ec1e2813 100644 --- a/x-pack/plugins/rollup/server/lib/search_strategies/index.ts +++ b/x-pack/plugins/vis_type_timeseries_enhanced/server/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerRollupSearchStrategy } from './register_rollup_search_strategy'; +import { PluginInitializerContext } from 'src/core/server'; +import { VisTypeTimeseriesEnhanced } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new VisTypeTimeseriesEnhanced(initializerContext); diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/plugin.ts b/x-pack/plugins/vis_type_timeseries_enhanced/server/plugin.ts new file mode 100644 index 0000000000000..0598a691ab7c5 --- /dev/null +++ b/x-pack/plugins/vis_type_timeseries_enhanced/server/plugin.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, PluginInitializerContext, Logger, CoreSetup } from 'src/core/server'; +import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; +import { RollupSearchStrategy } from './search_strategies/rollup_search_strategy'; + +interface VisTypeTimeseriesEnhancedSetupDependencies { + visTypeTimeseries: VisTypeTimeseriesSetup; +} + +export class VisTypeTimeseriesEnhanced + implements Plugin { + private logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get('vis_type_timeseries_enhanced'); + } + + public async setup( + core: CoreSetup, + { visTypeTimeseries }: VisTypeTimeseriesEnhancedSetupDependencies + ) { + this.logger.debug('Starting plugin'); + + visTypeTimeseries.addSearchStrategy(new RollupSearchStrategy()); + } + + public start() {} +} diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.test.ts similarity index 100% rename from x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js rename to x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.test.ts diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.ts similarity index 100% rename from x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts rename to x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/lib/interval_helper.ts diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.test.ts similarity index 77% rename from x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js rename to x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.test.ts index 977601247594f..6c30895635fe5 100644 --- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js +++ b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.test.ts @@ -3,27 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getRollupSearchCapabilities } from './rollup_search_capabilities'; +import { Unit } from '@elastic/datemath'; +import { RollupSearchCapabilities } from './rollup_search_capabilities'; -class DefaultSearchCapabilities { - constructor(request, fieldsCapabilities = {}) { - this.fieldsCapabilities = fieldsCapabilities; - this.parseInterval = jest.fn((interval) => interval); - } -} +import { ReqFacade, VisPayload } from '../../../../../src/plugins/vis_type_timeseries/server'; describe('Rollup Search Capabilities', () => { const testTimeZone = 'time_zone'; const testInterval = '10s'; const rollupIndex = 'rollupIndex'; - const request = {}; + const request = ({} as unknown) as ReqFacade; - let RollupSearchCapabilities; - let fieldsCapabilities; - let rollupSearchCaps; + let fieldsCapabilities: Record; + let rollupSearchCaps: RollupSearchCapabilities; beforeEach(() => { - RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities); fieldsCapabilities = { [rollupIndex]: { aggs: { @@ -41,7 +35,6 @@ describe('Rollup Search Capabilities', () => { }); test('should create instance of RollupSearchRequest', () => { - expect(rollupSearchCaps).toBeInstanceOf(DefaultSearchCapabilities); expect(rollupSearchCaps.fieldsCapabilities).toBe(fieldsCapabilities); expect(rollupSearchCaps.rollupIndex).toBe(rollupIndex); }); @@ -55,9 +48,9 @@ describe('Rollup Search Capabilities', () => { }); describe('getValidTimeInterval', () => { - let rollupJobInterval; - let userInterval; - let getSuitableUnit; + let rollupJobInterval: { value: number; unit: Unit }; + let userInterval: { value: number; unit: Unit }; + let getSuitableUnit: Unit; beforeEach(() => { rollupSearchCaps.parseInterval = jest @@ -81,7 +74,7 @@ describe('Rollup Search Capabilities', () => { getSuitableUnit = 'd'; - expect(rollupSearchCaps.getValidTimeInterval()).toBe('1d'); + expect(rollupSearchCaps.getValidTimeInterval('')).toBe('1d'); }); test('should return 1w as common interval for 7d(user interval) and 1d(rollup interval) - calendar intervals', () => { @@ -96,7 +89,7 @@ describe('Rollup Search Capabilities', () => { getSuitableUnit = 'w'; - expect(rollupSearchCaps.getValidTimeInterval()).toBe('1w'); + expect(rollupSearchCaps.getValidTimeInterval('')).toBe('1w'); }); test('should return 1w as common interval for 1d(user interval) and 1w(rollup interval) - calendar intervals', () => { @@ -111,7 +104,7 @@ describe('Rollup Search Capabilities', () => { getSuitableUnit = 'w'; - expect(rollupSearchCaps.getValidTimeInterval()).toBe('1w'); + expect(rollupSearchCaps.getValidTimeInterval('')).toBe('1w'); }); test('should return 2y as common interval for 0.1y(user interval) and 2y(rollup interval) - fixed intervals', () => { @@ -124,7 +117,7 @@ describe('Rollup Search Capabilities', () => { unit: 'y', }; - expect(rollupSearchCaps.getValidTimeInterval()).toBe('2y'); + expect(rollupSearchCaps.getValidTimeInterval('')).toBe('2y'); }); test('should return 3h as common interval for 2h(user interval) and 3h(rollup interval) - fixed intervals', () => { @@ -137,7 +130,7 @@ describe('Rollup Search Capabilities', () => { unit: 'h', }; - expect(rollupSearchCaps.getValidTimeInterval()).toBe('3h'); + expect(rollupSearchCaps.getValidTimeInterval('')).toBe('3h'); }); test('should return 6m as common interval for 4m(user interval) and 3m(rollup interval) - fixed intervals', () => { @@ -150,7 +143,7 @@ describe('Rollup Search Capabilities', () => { unit: 'm', }; - expect(rollupSearchCaps.getValidTimeInterval()).toBe('6m'); + expect(rollupSearchCaps.getValidTimeInterval('')).toBe('6m'); }); }); }); diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.ts b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.ts new file mode 100644 index 0000000000000..015a371bd2a35 --- /dev/null +++ b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_capabilities.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get, has } from 'lodash'; +import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper'; + +import { + ReqFacade, + DefaultSearchCapabilities, + VisPayload, +} from '../../../../../src/plugins/vis_type_timeseries/server'; + +export class RollupSearchCapabilities extends DefaultSearchCapabilities { + rollupIndex: string; + availableMetrics: Record; + + constructor( + req: ReqFacade, + fieldsCapabilities: Record, + rollupIndex: string + ) { + super(req, fieldsCapabilities); + + this.rollupIndex = rollupIndex; + this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {}); + } + + public get dateHistogram() { + const [dateHistogram] = Object.values(this.availableMetrics.date_histogram); + + return dateHistogram; + } + + public get defaultTimeInterval() { + return ( + this.dateHistogram.fixed_interval || + this.dateHistogram.calendar_interval || + /* + Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future. + We can remove the following line only for versions > 8.x + */ + this.dateHistogram.interval || + null + ); + } + + public get searchTimezone() { + return get(this.dateHistogram, 'time_zone', null); + } + + public get whiteListedMetrics() { + const baseRestrictions = this.createUiRestriction({ + count: this.createUiRestriction(), + }); + + const getFields = (fields: { [key: string]: any }) => + Object.keys(fields).reduce( + (acc, item) => ({ + ...acc, + [item]: true, + }), + this.createUiRestriction({}) + ); + + return Object.keys(this.availableMetrics).reduce( + (acc, item) => ({ + ...acc, + [item]: getFields(this.availableMetrics[item]), + }), + baseRestrictions + ); + } + + public get whiteListedGroupByFields() { + return this.createUiRestriction({ + everything: true, + terms: has(this.availableMetrics, 'terms'), + }); + } + + public get whiteListedTimerangeModes() { + return this.createUiRestriction({ + last_value: true, + }); + } + + getValidTimeInterval(userIntervalString: string) { + const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval); + const inRollupJobUnit = this.convertIntervalToUnit( + userIntervalString, + parsedRollupJobInterval!.unit + ); + + const getValidCalendarInterval = () => { + let unit = parsedRollupJobInterval!.unit; + + if (inRollupJobUnit!.value > parsedRollupJobInterval!.value) { + const inSeconds = this.convertIntervalToUnit(userIntervalString, 's'); + if (inSeconds?.value) { + unit = this.getSuitableUnit(inSeconds.value); + } + } + + return { + value: 1, + unit, + }; + }; + + const getValidFixedInterval = () => ({ + value: leastCommonInterval(inRollupJobUnit?.value, parsedRollupJobInterval?.value), + unit: parsedRollupJobInterval!.unit, + }); + + const { value, unit } = (isCalendarInterval(parsedRollupJobInterval!) + ? getValidCalendarInterval + : getValidFixedInterval)(); + + return `${value}${unit}`; + } +} diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.test.ts similarity index 56% rename from x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js rename to x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.test.ts index f3da7ed3fdd17..ec6c91b616f5b 100644 --- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js +++ b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.test.ts @@ -3,15 +3,35 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getRollupSearchStrategy } from './rollup_search_strategy'; +import { RollupSearchStrategy } from './rollup_search_strategy'; +import type { ReqFacade, VisPayload } from '../../../../../src/plugins/vis_type_timeseries/server'; + +jest.mock('../../../../../src/plugins/vis_type_timeseries/server', () => { + const actual = jest.requireActual('../../../../../src/plugins/vis_type_timeseries/server'); + class AbstractSearchStrategyMock { + getFieldsForWildcard() { + return [ + { + name: 'day_of_week.terms.value', + type: 'object', + esTypes: ['object'], + searchable: false, + aggregatable: false, + }, + ]; + } + } + + return { + ...actual, + AbstractSearchStrategy: AbstractSearchStrategyMock, + }; +}); describe('Rollup Search Strategy', () => { - let RollupSearchStrategy; - let RollupSearchCapabilities; - let callWithRequest; - let rollupResolvedData; + let rollupResolvedData: Promise; - const request = { + const request = ({ requestContext: { core: { elasticsearch: { @@ -25,41 +45,9 @@ describe('Rollup Search Strategy', () => { }, }, }, - }; - const getRollupService = jest.fn().mockImplementation(() => { - return { - callAsCurrentUser: async () => { - return rollupResolvedData; - }, - }; - }); - const indexPattern = 'indexPattern'; + } as unknown) as ReqFacade; - beforeEach(() => { - class AbstractSearchStrategy { - getCallWithRequestInstance = jest.fn(() => callWithRequest); - - getFieldsForWildcard() { - return [ - { - name: 'day_of_week.terms.value', - type: 'object', - esTypes: ['object'], - searchable: false, - aggregatable: false, - }, - ]; - } - } - - RollupSearchCapabilities = jest.fn(() => 'capabilities'); - - RollupSearchStrategy = getRollupSearchStrategy( - AbstractSearchStrategy, - RollupSearchCapabilities, - getRollupService - ); - }); + const indexPattern = 'indexPattern'; test('should create instance of RollupSearchRequest', () => { const rollupSearchStrategy = new RollupSearchStrategy(); @@ -68,68 +56,66 @@ describe('Rollup Search Strategy', () => { }); describe('checkForViability', () => { - let rollupSearchStrategy; + let rollupSearchStrategy: RollupSearchStrategy; const rollupIndex = 'rollupIndex'; beforeEach(() => { rollupSearchStrategy = new RollupSearchStrategy(); - rollupSearchStrategy.getRollupData = jest.fn(() => ({ - [rollupIndex]: { - rollup_jobs: [ - { - job_id: 'test', - rollup_index: rollupIndex, - index_pattern: 'kibana*', - fields: { - order_date: [ - { - agg: 'date_histogram', - delay: '1m', - interval: '1m', - time_zone: 'UTC', - }, - ], - day_of_week: [ - { - agg: 'terms', - }, - ], + rollupSearchStrategy.getRollupData = jest.fn(() => + Promise.resolve({ + [rollupIndex]: { + rollup_jobs: [ + { + job_id: 'test', + rollup_index: rollupIndex, + index_pattern: 'kibana*', + fields: { + order_date: [ + { + agg: 'date_histogram', + delay: '1m', + interval: '1m', + time_zone: 'UTC', + }, + ], + day_of_week: [ + { + agg: 'terms', + }, + ], + }, }, - }, - ], - }, - })); + ], + }, + }) + ); }); test('isViable should be false for invalid index', async () => { - const result = await rollupSearchStrategy.checkForViability(request, null); + const result = await rollupSearchStrategy.checkForViability( + request, + (null as unknown) as string + ); expect(result).toEqual({ isViable: false, capabilities: null, }); }); - - test('should get RollupSearchCapabilities for valid rollup index ', async () => { - await rollupSearchStrategy.checkForViability(request, rollupIndex); - - expect(RollupSearchCapabilities).toHaveBeenCalled(); - }); }); describe('getRollupData', () => { - let rollupSearchStrategy; + let rollupSearchStrategy: RollupSearchStrategy; beforeEach(() => { rollupSearchStrategy = new RollupSearchStrategy(); }); test('should return rollup data', async () => { - rollupResolvedData = Promise.resolve('data'); + rollupResolvedData = Promise.resolve({ body: 'data' }); const rollupData = await rollupSearchStrategy.getRollupData(request, indexPattern); - expect(getRollupService).toHaveBeenCalled(); expect(rollupData).toBe('data'); }); @@ -143,8 +129,8 @@ describe('Rollup Search Strategy', () => { }); describe('getFieldsForWildcard', () => { - let rollupSearchStrategy; - let fieldsCapabilities; + let rollupSearchStrategy: RollupSearchStrategy; + let fieldsCapabilities: Record; const rollupIndex = 'rollupIndex'; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.ts new file mode 100644 index 0000000000000..f1c20c318d109 --- /dev/null +++ b/x-pack/plugins/vis_type_timeseries_enhanced/server/search_strategies/rollup_search_strategy.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { keyBy, isString } from 'lodash'; +import { + AbstractSearchStrategy, + ReqFacade, + VisPayload, +} from '../../../../../src/plugins/vis_type_timeseries/server'; + +import { + mergeCapabilitiesWithFields, + getCapabilitiesForRollupIndices, +} from '../../../../../src/plugins/data/server'; + +import { RollupSearchCapabilities } from './rollup_search_capabilities'; + +const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData); +const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*'); +const isIndexPatternValid = (indexPattern: string) => + indexPattern && isString(indexPattern) && !isIndexPatternContainsWildcard(indexPattern); + +export class RollupSearchStrategy extends AbstractSearchStrategy { + name = 'rollup'; + + async search(req: ReqFacade, bodies: any[]) { + return super.search(req, bodies, 'rollup'); + } + + async getRollupData(req: ReqFacade, indexPattern: string) { + return req.requestContext.core.elasticsearch.client.asCurrentUser.rollup + .getRollupIndexCaps({ + index: indexPattern, + }) + .then((data) => data.body) + .catch(() => Promise.resolve({})); + } + + async checkForViability(req: ReqFacade, indexPattern: string) { + let isViable = false; + let capabilities = null; + + if (isIndexPatternValid(indexPattern)) { + const rollupData = await this.getRollupData(req, indexPattern); + const rollupIndices = getRollupIndices(rollupData); + + isViable = rollupIndices.length === 1; + + if (isViable) { + const [rollupIndex] = rollupIndices; + const fieldsCapabilities = getCapabilitiesForRollupIndices(rollupData); + + capabilities = new RollupSearchCapabilities(req, fieldsCapabilities, rollupIndex); + } + } + + return { + isViable, + capabilities, + }; + } + + async getFieldsForWildcard( + req: ReqFacade, + indexPattern: string, + { + fieldsCapabilities, + rollupIndex, + }: { fieldsCapabilities: { [key: string]: any }; rollupIndex: string } + ) { + const fields = await super.getFieldsForWildcard(req, indexPattern); + const fieldsFromFieldCapsApi = keyBy(fields, 'name'); + const rollupIndexCapabilities = fieldsCapabilities[rollupIndex].aggs; + + return mergeCapabilitiesWithFields(rollupIndexCapabilities, fieldsFromFieldCapsApi); + } +} From b9fc45bb5dedd5587db6d3f6e9d35d8b98fffee0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 18 Nov 2020 15:22:20 +0100 Subject: [PATCH 61/99] update chromedriver dependency to 87 (#83624) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a2c085c0424b1..2560be4f55d08 100644 --- a/package.json +++ b/package.json @@ -598,7 +598,7 @@ "broadcast-channel": "^3.0.3", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^86.0.0", + "chromedriver": "^87.0.0", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compare-versions": "3.5.1", diff --git a/yarn.lock b/yarn.lock index f2b4147fc3c9b..3bfa72cc50aeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9367,10 +9367,10 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^86.0.0: - version "86.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-86.0.0.tgz#4b9504d5bbbcd4c6bd6d6fd1dd8247ab8cdeca67" - integrity sha512-byLJWhAfuYOmzRYPDf4asJgGDbI4gJGHa+i8dnQZGuv+6WW1nW1Fg+8zbBMOfLvGn7sKL41kVdmCEVpQHn9oyg== +chromedriver@^87.0.0: + version "87.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-87.0.0.tgz#e8390deed8ada791719a67ad6bf1116614f1ba2d" + integrity sha512-PY7FnHOQKfH0oPfSdhpLx5nEy5g4dGYySf2C/WZGkAaCaldYH8/3lPPucZ8MlOCi4bCSGoKoCUTeG6+wYWavvw== dependencies: "@testim/chrome-version" "^1.0.7" axios "^0.19.2" From 69e3ceb4743535b4a500a137ba769dbfd4e6db15 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Wed, 18 Nov 2020 09:57:55 -0500 Subject: [PATCH 62/99] [Security Solution][Endpoint][Admin] Malware user notification is a platinum tiered feature (#82894) --- .../common/license/license.ts | 60 +++++++++++++++++++ .../public/common/hooks/use_license.ts | 13 ++++ .../pages/policy/view/policy_details.test.tsx | 47 +++++++++++++++ .../view/policy_forms/protections/malware.tsx | 47 +++++++++------ .../security_solution/public/plugin.tsx | 2 + .../plugins/security_solution/public/types.ts | 2 + .../server/lib/license/license.ts | 9 +++ .../security_solution/server/plugin.ts | 9 ++- 8 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/license/license.ts create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_license.ts create mode 100644 x-pack/plugins/security_solution/server/lib/license/license.ts diff --git a/x-pack/plugins/security_solution/common/license/license.ts b/x-pack/plugins/security_solution/common/license/license.ts new file mode 100644 index 0000000000000..96c1a14ceb1f4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/license/license.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Observable, Subscription } from 'rxjs'; +import { ILicense } from '../../../licensing/common/types'; + +// Generic license service class that works with the license observable +// Both server and client plugins instancates a singleton version of this class +export class LicenseService { + private observable: Observable | null = null; + private subscription: Subscription | null = null; + private licenseInformation: ILicense | null = null; + + private updateInformation(licenseInformation: ILicense) { + this.licenseInformation = licenseInformation; + } + + public start(license$: Observable) { + this.observable = license$; + this.subscription = this.observable.subscribe(this.updateInformation.bind(this)); + } + + public stop() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public getLicenseInformation() { + return this.licenseInformation; + } + + public getLicenseInformation$() { + return this.observable; + } + + public isGoldPlus() { + return ( + this.licenseInformation?.isAvailable && + this.licenseInformation?.isActive && + this.licenseInformation?.hasAtLeast('gold') + ); + } + public isPlatinumPlus() { + return ( + this.licenseInformation?.isAvailable && + this.licenseInformation?.isActive && + this.licenseInformation?.hasAtLeast('platinum') + ); + } + public isEnterprise() { + return ( + this.licenseInformation?.isAvailable && + this.licenseInformation?.isActive && + this.licenseInformation?.hasAtLeast('enterprise') + ); + } +} diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_license.ts b/x-pack/plugins/security_solution/public/common/hooks/use_license.ts new file mode 100644 index 0000000000000..db4d588bf293f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_license.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LicenseService } from '../../../common/license/license'; + +export const licenseService = new LicenseService(); + +export function useLicense() { + return licenseService; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index abc9a2cbd027c..bfa592b1f9c8e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -13,8 +13,20 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_da import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { getPolicyDetailPath, getEndpointListPath } from '../../../common/routing'; import { policyListApiPathHandlers } from '../store/policy_list/test_mock_utils'; +import { licenseService } from '../../../../common/hooks/use_license'; jest.mock('../../../../common/components/link_to'); +jest.mock('../../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); describe('Policy Details', () => { type FindReactWrapperResponse = ReturnType['find']>; @@ -275,5 +287,40 @@ describe('Policy Details', () => { }); }); }); + describe('when the subscription tier is platinum or higher', () => { + beforeEach(() => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); + policyView = render(); + }); + + it('malware popup and message customization options are shown', () => { + // use query for finding stuff, if it doesn't find it, just returns null + const userNotificationCheckbox = policyView.find( + 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' + ); + const userNotificationCustomMessageTextArea = policyView.find( + 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' + ); + expect(userNotificationCheckbox).toHaveLength(1); + expect(userNotificationCustomMessageTextArea).toHaveLength(1); + }); + }); + describe('when the subscription tier is gold or lower', () => { + beforeEach(() => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + policyView = render(); + }); + + it('malware popup and message customization options are hidden', () => { + const userNotificationCheckbox = policyView.find( + 'EuiCheckbox[data-test-subj="malwareUserNotificationCheckbox"]' + ); + const userNotificationCustomMessageTextArea = policyView.find( + 'EuiTextArea[data-test-subj="malwareUserNotificationCustomMessage"]' + ); + expect(userNotificationCheckbox).toHaveLength(0); + expect(userNotificationCustomMessageTextArea).toHaveLength(0); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 7259b2ec19ee2..c72093552f551 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -30,6 +30,7 @@ import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; import { popupVersionsMap } from './popup_options_to_versions'; +import { useLicense } from '../../../../../../common/hooks/use_license'; const ProtectionRadioGroup = styled.div` display: flex; @@ -116,6 +117,7 @@ export const MalwareProtections = React.memo(() => { policyDetailsConfig && policyDetailsConfig.windows.popup.malware.enabled; const userNotificationMessage = policyDetailsConfig && policyDetailsConfig.windows.popup.malware.message; + const isPlatinumPlus = useLicense().isPlatinumPlus(); const radios: Immutable { ); })} - - - - - - - - {userNotificationSelected && ( + {isPlatinumPlus && ( + <> + + + + + + + + )} + {isPlatinumPlus && userNotificationSelected && ( <> @@ -256,6 +265,7 @@ export const MalwareProtections = React.memo(() => { value={userNotificationMessage} onChange={handleCustomUserNotification} fullWidth={true} + data-test-subj="malwareUserNotificationCustomMessage" /> )} @@ -263,6 +273,7 @@ export const MalwareProtections = React.memo(() => { ); }, [ radios, + isPlatinumPlus, handleUserNotificationCheckbox, userNotificationSelected, userNotificationMessage, diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 5895880adb26a..5cc0d79a3f9a3 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -60,6 +60,7 @@ import { } from '../common/search_strategy/index_fields'; import { SecurityAppStore } from './common/store/store'; import { getCaseConnectorUI } from './common/lib/connectors'; +import { licenseService } from './common/hooks/use_license'; import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; @@ -345,6 +346,7 @@ export class Plugin implements IPlugin; private manifestTask: ManifestTask | undefined; private exceptionsCache: LRU; @@ -364,6 +366,8 @@ export class Plugin implements IPlugin Date: Wed, 18 Nov 2020 08:28:16 -0700 Subject: [PATCH 63/99] [Metrics UI] Converting legend key to optional (#83495) * [Metrics UI] Converting legend key to optional * Adding check and default to legend component --- .../metrics/inventory_view/components/layout.tsx | 8 ++++++-- .../inventory_view/components/waffle/legend.tsx | 8 ++++++-- .../inventory_view/hooks/use_waffle_options.ts | 15 ++++++++------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 76512b8a366c5..92aa015113b2a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -16,7 +16,7 @@ import { PageContent } from '../../../../components/page'; import { useSnapshot } from '../hooks/use_snaphot'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; -import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; +import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; import { useSourceContext } from '../../../../containers/source'; import { InfraFormatterType } from '../../../../lib/lib'; import { euiStyled } from '../../../../../../observability/public'; @@ -62,10 +62,14 @@ export const Layout = () => { false ); + const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette; + const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps; + const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors; + const options = { formatter: InfraFormatterType.percent, formatTemplate: '{{value}}', - legend: createLegend(legend.palette, legend.steps, legend.reverseColors), + legend: createLegend(legendPalette, legendSteps, legendReverseColors), metric, sort, fields: source?.configuration?.fields, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx index c211de8fd3d21..ea7bb66e689d9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx @@ -17,7 +17,11 @@ import { import { GradientLegend } from './gradient_legend'; import { LegendControls } from './legend_controls'; import { StepLegend } from './steps_legend'; -import { useWaffleOptionsContext, WaffleLegendOptions } from '../../hooks/use_waffle_options'; +import { + DEFAULT_LEGEND, + useWaffleOptionsContext, + WaffleLegendOptions, +} from '../../hooks/use_waffle_options'; import { SteppedGradientLegend } from './stepped_gradient_legend'; interface Props { legend: InfraWaffleMapLegend; @@ -52,7 +56,7 @@ export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter return ( ; From b3eefb97da8e712789b5c5d2eeae65c886ed8f64 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 18 Nov 2020 16:43:12 +0100 Subject: [PATCH 64/99] SO Tagging: fix flaky test and re-enable it (#82930) * fix flaky test and re-enable it * wait for table to load before to perform operations * move everything out of ciGroup2 for flaky test runner * add debug block for flaky runner * use correct vis name * remove test sync * Revert "move everything out of ciGroup2 for flaky test runner" This reverts commit db86c3b5 --- test/functional/services/listing_table.ts | 14 ++++++++++++++ .../functional/tests/dashboard_integration.ts | 13 ++++++++++++- .../functional/tests/visualize_integration.ts | 17 +++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts index 53b45697136ed..c9f2b8369783c 100644 --- a/test/functional/services/listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -62,6 +62,20 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider return visualizationNames; } + public async waitUntilTableIsLoaded() { + return retry.try(async () => { + const isLoaded = await find.existsByDisplayedByCssSelector( + '[data-test-subj="itemsInMemTable"]:not(.euiBasicTable-loading)' + ); + + if (isLoaded) { + return true; + } else { + throw new Error('Waiting'); + } + }); + } + /** * Navigates through all pages on Landing page and returns array of items names */ diff --git a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts index 081fa1feb1c33..42ef8de2eb0c2 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts @@ -13,7 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); const find = getService('find'); - const PageObjects = getPageObjects(['dashboard', 'tagManagement', 'common']); + const PageObjects = getPageObjects(['dashboard', 'tagManagement', 'common', 'header']); /** * Select tags in the searchbar's tag filter. @@ -31,6 +31,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // click elsewhere to close the filter dropdown const searchFilter = await find.byCssSelector('main .euiFieldSearch'); await searchFilter.click(); + // wait until the table refreshes + await listingTable.waitUntilTableIsLoaded(); }; describe('dashboard integration', () => { @@ -47,6 +49,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.waitUntilTableIsLoaded(); }); it('allows to manually type tag filter query', async () => { @@ -96,6 +99,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.waitUntilTableIsLoaded(); + await selectFilterTags('tag-1'); const itemNames = await listingTable.getAllItemsNames(); expect(itemNames).to.contain('my-new-dashboard'); @@ -128,8 +133,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await tagModal.isOpened()).to.be(false); await PageObjects.dashboard.clickSave(); + await PageObjects.common.waitForSaveModalToClose(); await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.waitUntilTableIsLoaded(); + await selectFilterTags('my-new-tag'); const itemNames = await listingTable.getAllItemsNames(); expect(itemNames).to.contain('dashboard-with-new-tag'); @@ -140,6 +148,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { beforeEach(async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.waitUntilTableIsLoaded(); }); it('allows to select tags for an existing dashboard', async () => { @@ -152,6 +161,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); await PageObjects.dashboard.gotoDashboardLandingPage(); + await listingTable.waitUntilTableIsLoaded(); + await selectFilterTags('tag-3'); const itemNames = await listingTable.getAllItemsNames(); expect(itemNames).to.contain('dashboard 4 with real data (tag-1)'); diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index b938591543196..834c3083071df 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -13,7 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); const find = getService('find'); - const PageObjects = getPageObjects(['visualize', 'tagManagement', 'visEditor']); + const PageObjects = getPageObjects(['visualize', 'tagManagement', 'visEditor', 'common']); /** * Select tags in the searchbar's tag filter. @@ -31,6 +31,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // click elsewhere to close the filter dropdown const searchFilter = await find.byCssSelector('main .euiFieldSearch'); await searchFilter.click(); + // wait until the table refreshes + await listingTable.waitUntilTableIsLoaded(); }; const selectSavedObjectTags = async (...tagNames: string[]) => { @@ -56,6 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('listing', () => { beforeEach(async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.waitUntilTableIsLoaded(); }); it('allows to manually type tag filter query', async () => { @@ -83,7 +86,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('creating', () => { - it.skip('allows to assign tags to the new visualization', async () => { + it('allows to assign tags to the new visualization', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickMarkdownWidget(); @@ -95,7 +98,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await selectSavedObjectTags('tag-1'); await testSubjects.click('confirmSaveSavedObjectButton'); + await PageObjects.common.waitForSaveModalToClose(); + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.waitUntilTableIsLoaded(); await selectFilterTags('tag-1'); const itemNames = await listingTable.getAllItemsNames(); @@ -133,7 +139,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(await tagModal.isOpened()).to.be(false); await testSubjects.click('confirmSaveSavedObjectButton'); + await PageObjects.common.waitForSaveModalToClose(); + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.waitUntilTableIsLoaded(); await selectFilterTags('my-new-tag'); const itemNames = await listingTable.getAllItemsNames(); @@ -144,6 +153,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('editing', () => { beforeEach(async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.waitUntilTableIsLoaded(); }); it('allows to assign tags to an existing visualization', async () => { @@ -153,7 +163,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await selectSavedObjectTags('tag-2'); await testSubjects.click('confirmSaveSavedObjectButton'); + await PageObjects.common.waitForSaveModalToClose(); + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.waitUntilTableIsLoaded(); await selectFilterTags('tag-2'); const itemNames = await listingTable.getAllItemsNames(); From 62e06aee9b99219d2d37bbf69c160665f04ea2fd Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Wed, 18 Nov 2020 09:11:05 -0700 Subject: [PATCH 65/99] [esaggs][inspector]: Refactor to prep for esaggs move to server. (#83199) --- ...plugins-embeddable-public.adapters.data.md | 11 + ...ugin-plugins-embeddable-public.adapters.md | 8 + ...ins-embeddable-public.adapters.requests.md | 11 + .../data/common/search/aggs/agg_type.ts | 4 +- .../data/common/search/aggs/buckets/terms.ts | 42 ++- .../autocomplete/autocomplete_service.ts | 5 +- .../value_suggestion_provider.test.ts | 39 +-- .../providers/value_suggestion_provider.ts | 17 +- .../index_patterns/index_pattern.stub.ts | 11 - src/plugins/data/public/plugin.ts | 24 +- src/plugins/data/public/public.api.md | 3 +- .../data/public/search/expressions/esaggs.ts | 323 ------------------ .../build_tabular_inspector_data.ts | 46 +-- .../search/expressions/esaggs/esaggs_fn.ts | 155 +++++++++ .../public/search/expressions/esaggs/index.ts | 20 ++ .../expressions/esaggs/request_handler.ts | 213 ++++++++++++ src/plugins/data/public/services.ts | 9 - .../embeddable/search_embeddable.ts | 12 +- src/plugins/embeddable/public/public.api.md | 11 +- .../common/adapters/data/data_adapter.ts | 4 +- .../adapters/data/data_adapters.test.ts | 16 +- .../common/adapters/data/formatted_data.ts | 4 +- .../inspector/common/adapters/data/index.ts | 5 +- .../inspector/common/adapters/data/types.ts | 21 +- .../inspector/common/adapters/index.ts | 12 +- .../adapters/request/request_adapter.ts | 4 +- .../inspector/common/adapters/types.ts | 5 + src/plugins/inspector/common/index.ts | 15 +- .../public/test/is_available.test.ts | 3 +- .../views/data/components/data_view.tsx | 14 +- .../inspector/public/views/data/types.ts | 15 +- .../requests/components/requests_view.tsx | 28 +- src/plugins/ui_actions/public/public.api.md | 1 + .../classes/sources/es_source/es_source.ts | 4 +- 34 files changed, 629 insertions(+), 486 deletions(-) create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.requests.md delete mode 100644 src/plugins/data/public/search/expressions/esaggs.ts rename src/plugins/data/public/search/expressions/{ => esaggs}/build_tabular_inspector_data.ts (78%) create mode 100644 src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts create mode 100644 src/plugins/data/public/search/expressions/esaggs/index.ts create mode 100644 src/plugins/data/public/search/expressions/esaggs/request_handler.ts diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md new file mode 100644 index 0000000000000..0ddbcb3546d1e --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.data.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Adapters](./kibana-plugin-plugins-embeddable-public.adapters.md) > [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md) + +## Adapters.data property + +Signature: + +```typescript +data?: DataAdapter; +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md index 9635b36cdf05a..47484dc79d88c 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.md @@ -11,3 +11,11 @@ The interface that the adapters used to open an inspector have to fullfill. ```typescript export interface Adapters ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [data](./kibana-plugin-plugins-embeddable-public.adapters.data.md) | DataAdapter | | +| [requests](./kibana-plugin-plugins-embeddable-public.adapters.requests.md) | RequestAdapter | | + diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.requests.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.requests.md new file mode 100644 index 0000000000000..2954ad86138ff --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.adapters.requests.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [Adapters](./kibana-plugin-plugins-embeddable-public.adapters.md) > [requests](./kibana-plugin-plugins-embeddable-public.adapters.requests.md) + +## Adapters.requests property + +Signature: + +```typescript +requests?: RequestAdapter; +``` diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 3ffac0c12eb22..4f4a593764b1e 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -54,7 +54,7 @@ export interface AggTypeConfig< aggConfigs: IAggConfigs, aggConfig: TAggConfig, searchSource: ISearchSource, - inspectorRequestAdapter: RequestAdapter, + inspectorRequestAdapter?: RequestAdapter, abortSignal?: AbortSignal ) => Promise; getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; @@ -189,7 +189,7 @@ export class AggType< aggConfigs: IAggConfigs, aggConfig: TAggConfig, searchSource: ISearchSource, - inspectorRequestAdapter: RequestAdapter, + inspectorRequestAdapter?: RequestAdapter, abortSignal?: AbortSignal ) => Promise; /** diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 3d543e6c5f574..ac65e7fa813b3 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -19,6 +19,7 @@ import { noop } from 'lodash'; import { i18n } from '@kbn/i18n'; +import type { RequestAdapter } from 'src/plugins/inspector/common'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -111,27 +112,32 @@ export const getTermsBucketAgg = () => nestedSearchSource.setField('aggs', filterAgg); - const request = inspectorRequestAdapter.start( - i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', { - defaultMessage: 'Other bucket', - }), - { - description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', { - defaultMessage: - 'This request counts the number of documents that fall ' + - 'outside the criterion of the data buckets.', + let request: ReturnType | undefined; + if (inspectorRequestAdapter) { + request = inspectorRequestAdapter.start( + i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', { + defaultMessage: 'Other bucket', }), - } - ); - nestedSearchSource.getSearchRequestBody().then((body) => { - request.json(body); - }); - request.stats(getRequestInspectorStats(nestedSearchSource)); + { + description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', { + defaultMessage: + 'This request counts the number of documents that fall ' + + 'outside the criterion of the data buckets.', + }), + } + ); + nestedSearchSource.getSearchRequestBody().then((body) => { + request!.json(body); + }); + request.stats(getRequestInspectorStats(nestedSearchSource)); + } const response = await nestedSearchSource.fetch({ abortSignal }); - request - .stats(getResponseInspectorStats(response, nestedSearchSource)) - .ok({ json: response }); + if (request) { + request + .stats(getResponseInspectorStats(response, nestedSearchSource)) + .ok({ json: response }); + } resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg()); } if (aggConfig.params.missingBucket) { diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index 2136a405baad6..5e9aede0760fe 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -18,6 +18,7 @@ */ import { CoreSetup, PluginInitializerContext } from 'src/core/public'; +import { TimefilterSetup } from '../query'; import { QuerySuggestionGetFn } from './providers/query_suggestion_provider'; import { getEmptyValueSuggestions, @@ -57,9 +58,9 @@ export class AutocompleteService { private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language); /** @public **/ - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { timefilter }: { timefilter: TimefilterSetup }) { this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled - ? setupValueSuggestionProvider(core) + ? setupValueSuggestionProvider(core, { timefilter }) : getEmptyValueSuggestions; return { diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts index 0ef5b7db958e4..4e1745ffcabb2 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -18,29 +18,10 @@ */ import { stubIndexPattern, stubFields } from '../../stubs'; +import { TimefilterSetup } from '../../query'; import { setupValueSuggestionProvider, ValueSuggestionsGetFn } from './value_suggestion_provider'; import { IUiSettingsClient, CoreSetup } from 'kibana/public'; -jest.mock('../../services', () => ({ - getQueryService: () => ({ - timefilter: { - timefilter: { - createFilter: () => { - return { - time: 'fake', - }; - }, - getTime: () => { - return { - to: 'now', - from: 'now-15m', - }; - }, - }, - }, - }), -})); - describe('FieldSuggestions', () => { let getValueSuggestions: ValueSuggestionsGetFn; let http: any; @@ -50,7 +31,23 @@ describe('FieldSuggestions', () => { const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient; http = { fetch: jest.fn() }; - getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup); + getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, { + timefilter: ({ + timefilter: { + createFilter: () => { + return { + time: 'fake', + }; + }, + getTime: () => { + return { + to: 'now', + from: 'now-15m', + }; + }, + }, + } as unknown) as TimefilterSetup, + }); }); describe('with value suggestions disabled', () => { diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index fe9f939a0261d..ee92fce02dda5 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -21,7 +21,7 @@ import dateMath from '@elastic/datemath'; import { memoize } from 'lodash'; import { CoreSetup } from 'src/core/public'; import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common'; -import { getQueryService } from '../../services'; +import { TimefilterSetup } from '../../query'; function resolver(title: string, field: IFieldType, query: string, filters: any[]) { // Only cache results for a minute @@ -40,8 +40,10 @@ interface ValueSuggestionsGetFnArgs { signal?: AbortSignal; } -const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => { - const { timefilter } = getQueryService().timefilter; +const getAutocompleteTimefilter = ( + { timefilter }: TimefilterSetup, + indexPattern: IIndexPattern +) => { const timeRange = timefilter.getTime(); // Use a rounded timerange so that memoizing works properly @@ -54,7 +56,10 @@ const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => { export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSuggestionsGetFn; -export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => { +export const setupValueSuggestionProvider = ( + core: CoreSetup, + { timefilter }: { timefilter: TimefilterSetup } +): ValueSuggestionsGetFn => { const requestSuggestions = memoize( (index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => core.http.fetch(`/api/kibana/suggestions/values/${index}`, { @@ -86,7 +91,9 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG return []; } - const timeFilter = useTimeRange ? getAutocompleteTimefilter(indexPattern) : undefined; + const timeFilter = useTimeRange + ? getAutocompleteTimefilter(timefilter, indexPattern) + : undefined; const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : []; const filters = [...(boolFilter ? boolFilter : []), ...filterQuery]; return await requestSuggestions(title, field, query, filters, signal); diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts index e5c6c008e3e28..804f0d7d89225 100644 --- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -20,20 +20,9 @@ import sinon from 'sinon'; import { CoreSetup } from 'src/core/public'; -import { FieldFormat as FieldFormatImpl } from '../../common/field_formats'; import { IFieldType, FieldSpec } from '../../common/index_patterns'; -import { FieldFormatsStart } from '../field_formats'; import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../'; import { getFieldFormatsRegistry } from '../test_utils'; -import { setFieldFormats } from '../services'; - -setFieldFormats(({ - getDefaultInstance: () => - ({ - getConverterFor: () => (value: any) => value, - convert: (value: any) => JSON.stringify(value), - } as FieldFormatImpl), -} as unknown) as FieldFormatsStart); export function getStubIndexPattern( pattern: string, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index afa8d935f367b..7e8283476ffc5 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -41,16 +41,14 @@ import { UiSettingsPublicToCommon, } from './index_patterns'; import { - setFieldFormats, setIndexPatterns, setNotifications, setOverlays, - setQueryService, setSearchService, setUiSettings, } from './services'; import { createSearchBar } from './ui/search_bar/create_search_bar'; -import { esaggs } from './search/expressions'; +import { getEsaggs } from './search/expressions'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, @@ -111,8 +109,22 @@ export class DataPublicPlugin ): DataPublicPluginSetup { const startServices = createStartServicesGetter(core.getStartServices); - expressions.registerFunction(esaggs); expressions.registerFunction(indexPatternLoad); + expressions.registerFunction( + getEsaggs({ + getStartDependencies: async () => { + const [, , self] = await core.getStartServices(); + const { fieldFormats, indexPatterns, query, search } = self; + return { + addFilters: query.filterManager.addFilters.bind(query.filterManager), + aggs: search.aggs, + deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats), + indexPatterns, + searchSource: search.searchSource, + }; + }, + }) + ); this.usageCollection = usageCollection; @@ -145,7 +157,7 @@ export class DataPublicPlugin }); return { - autocomplete: this.autocomplete.setup(core), + autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }), search: searchService, fieldFormats: this.fieldFormatsService.setup(core), query: queryService, @@ -162,7 +174,6 @@ export class DataPublicPlugin setUiSettings(uiSettings); const fieldFormats = this.fieldFormatsService.start(); - setFieldFormats(fieldFormats); const indexPatterns = new IndexPatternsService({ uiSettings: new UiSettingsPublicToCommon(uiSettings), @@ -186,7 +197,6 @@ export class DataPublicPlugin savedObjectsClient: savedObjects.client, uiSettings, }); - setQueryService(query); const search = this.searchService.start(core, { fieldFormats, indexPatterns }); setSearchService(search); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0768658e40299..165e11517311c 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -27,6 +27,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; +import { EventEmitter } from 'events'; import { ExclusiveUnion } from '@elastic/eui'; import { ExecutionContext } from 'src/plugins/expressions/common'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; @@ -66,7 +67,7 @@ import * as React_2 from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Reporter } from '@kbn/analytics'; import { RequestAdapter } from 'src/plugins/inspector/common'; -import { RequestStatistics } from 'src/plugins/inspector/common'; +import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts deleted file mode 100644 index 3932484801fa8..0000000000000 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { get, hasIn } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; -import { PersistedState } from '../../../../../plugins/visualizations/public'; -import { Adapters } from '../../../../../plugins/inspector/public'; - -import { - calculateBounds, - EsaggsExpressionFunctionDefinition, - Filter, - getTime, - IIndexPattern, - isRangeFilter, - Query, - TimeRange, -} from '../../../common'; -import { - getRequestInspectorStats, - getResponseInspectorStats, - IAggConfigs, - ISearchSource, - tabifyAggResponse, -} from '../../../common/search'; - -import { FilterManager } from '../../query'; -import { - getFieldFormats, - getIndexPatterns, - getQueryService, - getSearchService, -} from '../../services'; -import { buildTabularInspectorData } from './build_tabular_inspector_data'; - -export interface RequestHandlerParams { - searchSource: ISearchSource; - aggs: IAggConfigs; - timeRange?: TimeRange; - timeFields?: string[]; - indexPattern?: IIndexPattern; - query?: Query; - filters?: Filter[]; - filterManager: FilterManager; - uiState?: PersistedState; - partialRows?: boolean; - inspectorAdapters: Adapters; - metricsAtAllLevels?: boolean; - visParams?: any; - abortSignal?: AbortSignal; - searchSessionId?: string; -} - -const name = 'esaggs'; - -const handleCourierRequest = async ({ - searchSource, - aggs, - timeRange, - timeFields, - indexPattern, - query, - filters, - partialRows, - metricsAtAllLevels, - inspectorAdapters, - filterManager, - abortSignal, - searchSessionId, -}: RequestHandlerParams) => { - // Create a new search source that inherits the original search source - // but has the appropriate timeRange applied via a filter. - // This is a temporary solution until we properly pass down all required - // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641). - // Using callParentStartHandlers: true we make sure, that the parent searchSource - // onSearchRequestStart will be called properly even though we use an inherited - // search source. - const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); - const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true }); - - aggs.setTimeRange(timeRange as TimeRange); - - // For now we need to mirror the history of the passed search source, since - // the request inspector wouldn't work otherwise. - Object.defineProperty(requestSearchSource, 'history', { - get() { - return searchSource.history; - }, - set(history) { - return (searchSource.history = history); - }, - }); - - requestSearchSource.setField('aggs', function () { - return aggs.toDsl(metricsAtAllLevels); - }); - - requestSearchSource.onRequestStart((paramSearchSource, options) => { - return aggs.onSearchRequestStart(paramSearchSource, options); - }); - - // If timeFields have been specified, use the specified ones, otherwise use primary time field of index - // pattern if it's available. - const defaultTimeField = indexPattern?.getTimeField?.(); - const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; - const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; - - // If a timeRange has been specified and we had at least one timeField available, create range - // filters for that those time fields - if (timeRange && allTimeFields.length > 0) { - timeFilterSearchSource.setField('filter', () => { - return allTimeFields - .map((fieldName) => getTime(indexPattern, timeRange, { fieldName })) - .filter(isRangeFilter); - }); - } - - requestSearchSource.setField('filter', filters); - requestSearchSource.setField('query', query); - - inspectorAdapters.requests.reset(); - const request = inspectorAdapters.requests.start( - i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { - defaultMessage: 'Data', - }), - { - description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { - defaultMessage: - 'This request queries Elasticsearch to fetch the data for the visualization.', - }), - searchSessionId, - } - ); - request.stats(getRequestInspectorStats(requestSearchSource)); - - try { - const response = await requestSearchSource.fetch({ - abortSignal, - sessionId: searchSessionId, - }); - - request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); - - (searchSource as any).rawResponse = response; - } catch (e) { - // Log any error during request to the inspector - request.error({ json: e }); - throw e; - } finally { - // Add the request body no matter if things went fine or not - requestSearchSource.getSearchRequestBody().then((req: unknown) => { - request.json(req); - }); - } - - // Note that rawResponse is not deeply cloned here, so downstream applications using courier - // must take care not to mutate it, or it could have unintended side effects, e.g. displaying - // response data incorrectly in the inspector. - let resp = (searchSource as any).rawResponse; - for (const agg of aggs.aggs) { - if (hasIn(agg, 'type.postFlightRequest')) { - resp = await agg.type.postFlightRequest( - resp, - aggs, - agg, - requestSearchSource, - inspectorAdapters.requests, - abortSignal - ); - } - } - - (searchSource as any).finalResponse = resp; - - const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; - const tabifyParams = { - metricsAtAllLevels, - partialRows, - timeRange: parsedTimeRange - ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } - : undefined, - }; - - const response = tabifyAggResponse(aggs, (searchSource as any).finalResponse, tabifyParams); - - (searchSource as any).tabifiedResponse = response; - - inspectorAdapters.data.setTabularLoader( - () => - buildTabularInspectorData((searchSource as any).tabifiedResponse, { - queryFilter: filterManager, - deserializeFieldFormat: getFieldFormats().deserialize, - }), - { returnsFormattedValues: true } - ); - - return response; -}; - -export const esaggs = (): EsaggsExpressionFunctionDefinition => ({ - name, - type: 'datatable', - inputTypes: ['kibana_context', 'null'], - help: i18n.translate('data.functions.esaggs.help', { - defaultMessage: 'Run AggConfig aggregation', - }), - args: { - index: { - types: ['string'], - help: '', - }, - metricsAtAllLevels: { - types: ['boolean'], - default: false, - help: '', - }, - partialRows: { - types: ['boolean'], - default: false, - help: '', - }, - includeFormatHints: { - types: ['boolean'], - default: false, - help: '', - }, - aggConfigs: { - types: ['string'], - default: '""', - help: '', - }, - timeFields: { - types: ['string'], - help: '', - multi: true, - }, - }, - async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { - const indexPatterns = getIndexPatterns(); - const { filterManager } = getQueryService(); - const searchService = getSearchService(); - - const aggConfigsState = JSON.parse(args.aggConfigs); - const indexPattern = await indexPatterns.get(args.index); - const aggs = searchService.aggs.createAggConfigs(indexPattern, aggConfigsState); - - // we should move searchSource creation inside courier request handler - const searchSource = await searchService.searchSource.create(); - - searchSource.setField('index', indexPattern); - searchSource.setField('size', 0); - - const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange); - - const response = await handleCourierRequest({ - searchSource, - aggs, - indexPattern, - timeRange: get(input, 'timeRange', undefined), - query: get(input, 'query', undefined) as any, - filters: get(input, 'filters', undefined), - timeFields: args.timeFields, - metricsAtAllLevels: args.metricsAtAllLevels, - partialRows: args.partialRows, - inspectorAdapters: inspectorAdapters as Adapters, - filterManager, - abortSignal: (abortSignal as unknown) as AbortSignal, - searchSessionId: getSearchSessionId(), - }); - - const table: Datatable = { - type: 'datatable', - rows: response.rows, - columns: response.columns.map((column) => { - const cleanedColumn: DatatableColumn = { - id: column.id, - name: column.name, - meta: { - type: column.aggConfig.params.field?.type || 'number', - field: column.aggConfig.params.field?.name, - index: indexPattern.title, - params: column.aggConfig.toSerializedFieldFormat(), - source: 'esaggs', - sourceParams: { - indexPatternId: indexPattern.id, - appliedTimeRange: - column.aggConfig.params.field?.name && - input?.timeRange && - args.timeFields && - args.timeFields.includes(column.aggConfig.params.field?.name) - ? { - from: resolvedTimeRange?.min?.toISOString(), - to: resolvedTimeRange?.max?.toISOString(), - } - : undefined, - ...column.aggConfig.serialize(), - }, - }, - }; - return cleanedColumn; - }), - }; - - return table; - }, -}); diff --git a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts similarity index 78% rename from src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts rename to src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts index 7eff6f25fd828..79dedf4131764 100644 --- a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/plugins/data/public/search/expressions/esaggs/build_tabular_inspector_data.ts @@ -18,35 +18,41 @@ */ import { set } from '@elastic/safer-lodash-set'; -import { FormattedData } from '../../../../../plugins/inspector/public'; -import { TabbedTable } from '../../../common'; -import { FormatFactory } from '../../../common/field_formats/utils'; -import { createFilter } from './create_filter'; +import { + FormattedData, + TabularData, + TabularDataValue, +} from '../../../../../../plugins/inspector/common'; +import { Filter, TabbedTable } from '../../../../common'; +import { FormatFactory } from '../../../../common/field_formats/utils'; +import { createFilter } from '../create_filter'; /** - * @deprecated + * Type borrowed from the client-side FilterManager['addFilters']. * - * Do not use this function. - * - * @todo This function is used only by Courier. Courier will - * soon be removed, and this function will be deleted, too. If Courier is not removed, - * move this function inside Courier. - * - * --- + * We need to use a custom type to make this isomorphic since FilterManager + * doesn't exist on the server. * + * @internal + */ +export type AddFilters = (filters: Filter[] | Filter, pinFilterStatus?: boolean) => void; + +/** * This function builds tabular data from the response and attaches it to the * inspector. It will only be called when the data view in the inspector is opened. + * + * @internal */ export async function buildTabularInspectorData( table: TabbedTable, { - queryFilter, + addFilters, deserializeFieldFormat, }: { - queryFilter: { addFilters: (filter: any) => void }; + addFilters?: AddFilters; deserializeFieldFormat: FormatFactory; } -) { +): Promise { const aggConfigs = table.columns.map((column) => column.aggConfig); const rows = table.rows.map((row) => { return table.columns.reduce>((prev, cur, colIndex) => { @@ -74,20 +80,22 @@ export async function buildTabularInspectorData( name: col.name, field: `col-${colIndex}-${col.aggConfig.id}`, filter: + addFilters && isCellContentFilterable && - ((value: { raw: unknown }) => { + ((value: TabularDataValue) => { const rowIndex = rows.findIndex( (row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw ); const filter = createFilter(aggConfigs, table, colIndex, rowIndex, value.raw); if (filter) { - queryFilter.addFilters(filter); + addFilters(filter); } }), filterOut: + addFilters && isCellContentFilterable && - ((value: { raw: unknown }) => { + ((value: TabularDataValue) => { const rowIndex = rows.findIndex( (row) => row[`col-${colIndex}-${col.aggConfig.id}`].raw === value.raw ); @@ -101,7 +109,7 @@ export async function buildTabularInspectorData( } else { set(filter, 'meta.negate', notOther && notMissing); } - queryFilter.addFilters(filter); + addFilters(filter); } }), }; diff --git a/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts new file mode 100644 index 0000000000000..ce3bd9bdaee76 --- /dev/null +++ b/src/plugins/data/public/search/expressions/esaggs/esaggs_fn.ts @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { Datatable, DatatableColumn } from 'src/plugins/expressions/common'; +import { Adapters } from 'src/plugins/inspector/common'; + +import { calculateBounds, EsaggsExpressionFunctionDefinition } from '../../../../common'; +import { FormatFactory } from '../../../../common/field_formats/utils'; +import { IndexPatternsContract } from '../../../../common/index_patterns/index_patterns'; +import { ISearchStartSearchSource, AggsStart } from '../../../../common/search'; + +import { AddFilters } from './build_tabular_inspector_data'; +import { handleRequest } from './request_handler'; + +const name = 'esaggs'; + +interface StartDependencies { + addFilters: AddFilters; + aggs: AggsStart; + deserializeFieldFormat: FormatFactory; + indexPatterns: IndexPatternsContract; + searchSource: ISearchStartSearchSource; +} + +export function getEsaggs({ + getStartDependencies, +}: { + getStartDependencies: () => Promise; +}) { + return (): EsaggsExpressionFunctionDefinition => ({ + name, + type: 'datatable', + inputTypes: ['kibana_context', 'null'], + help: i18n.translate('data.functions.esaggs.help', { + defaultMessage: 'Run AggConfig aggregation', + }), + args: { + index: { + types: ['string'], + help: '', + }, + metricsAtAllLevels: { + types: ['boolean'], + default: false, + help: '', + }, + partialRows: { + types: ['boolean'], + default: false, + help: '', + }, + includeFormatHints: { + types: ['boolean'], + default: false, + help: '', + }, + aggConfigs: { + types: ['string'], + default: '""', + help: '', + }, + timeFields: { + types: ['string'], + help: '', + multi: true, + }, + }, + async fn(input, args, { inspectorAdapters, abortSignal, getSearchSessionId }) { + const { + addFilters, + aggs, + deserializeFieldFormat, + indexPatterns, + searchSource, + } = await getStartDependencies(); + + const aggConfigsState = JSON.parse(args.aggConfigs); + const indexPattern = await indexPatterns.get(args.index); + const aggConfigs = aggs.createAggConfigs(indexPattern, aggConfigsState); + + const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange); + + const response = await handleRequest({ + abortSignal: (abortSignal as unknown) as AbortSignal, + addFilters, + aggs: aggConfigs, + deserializeFieldFormat, + filters: get(input, 'filters', undefined), + indexPattern, + inspectorAdapters: inspectorAdapters as Adapters, + metricsAtAllLevels: args.metricsAtAllLevels, + partialRows: args.partialRows, + query: get(input, 'query', undefined) as any, + searchSessionId: getSearchSessionId(), + searchSourceService: searchSource, + timeFields: args.timeFields, + timeRange: get(input, 'timeRange', undefined), + }); + + const table: Datatable = { + type: 'datatable', + rows: response.rows, + columns: response.columns.map((column) => { + const cleanedColumn: DatatableColumn = { + id: column.id, + name: column.name, + meta: { + type: column.aggConfig.params.field?.type || 'number', + field: column.aggConfig.params.field?.name, + index: indexPattern.title, + params: column.aggConfig.toSerializedFieldFormat(), + source: name, + sourceParams: { + indexPatternId: indexPattern.id, + appliedTimeRange: + column.aggConfig.params.field?.name && + input?.timeRange && + args.timeFields && + args.timeFields.includes(column.aggConfig.params.field?.name) + ? { + from: resolvedTimeRange?.min?.toISOString(), + to: resolvedTimeRange?.max?.toISOString(), + } + : undefined, + ...column.aggConfig.serialize(), + }, + }, + }; + return cleanedColumn; + }), + }; + + return table; + }, + }); +} diff --git a/src/plugins/data/public/search/expressions/esaggs/index.ts b/src/plugins/data/public/search/expressions/esaggs/index.ts new file mode 100644 index 0000000000000..cbd3fb9cc5e91 --- /dev/null +++ b/src/plugins/data/public/search/expressions/esaggs/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './esaggs_fn'; diff --git a/src/plugins/data/public/search/expressions/esaggs/request_handler.ts b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts new file mode 100644 index 0000000000000..93b5705b821c0 --- /dev/null +++ b/src/plugins/data/public/search/expressions/esaggs/request_handler.ts @@ -0,0 +1,213 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { Adapters } from 'src/plugins/inspector/common'; + +import { + calculateBounds, + Filter, + getTime, + IndexPattern, + isRangeFilter, + Query, + TimeRange, +} from '../../../../common'; +import { + getRequestInspectorStats, + getResponseInspectorStats, + IAggConfigs, + ISearchStartSearchSource, + tabifyAggResponse, +} from '../../../../common/search'; +import { FormatFactory } from '../../../../common/field_formats/utils'; + +import { AddFilters, buildTabularInspectorData } from './build_tabular_inspector_data'; + +interface RequestHandlerParams { + abortSignal?: AbortSignal; + addFilters?: AddFilters; + aggs: IAggConfigs; + deserializeFieldFormat: FormatFactory; + filters?: Filter[]; + indexPattern?: IndexPattern; + inspectorAdapters: Adapters; + metricsAtAllLevels?: boolean; + partialRows?: boolean; + query?: Query; + searchSessionId?: string; + searchSourceService: ISearchStartSearchSource; + timeFields?: string[]; + timeRange?: TimeRange; +} + +export const handleRequest = async ({ + abortSignal, + addFilters, + aggs, + deserializeFieldFormat, + filters, + indexPattern, + inspectorAdapters, + metricsAtAllLevels, + partialRows, + query, + searchSessionId, + searchSourceService, + timeFields, + timeRange, +}: RequestHandlerParams) => { + const searchSource = await searchSourceService.create(); + + searchSource.setField('index', indexPattern); + searchSource.setField('size', 0); + + // Create a new search source that inherits the original search source + // but has the appropriate timeRange applied via a filter. + // This is a temporary solution until we properly pass down all required + // information for the request to the request handler (https://github.com/elastic/kibana/issues/16641). + // Using callParentStartHandlers: true we make sure, that the parent searchSource + // onSearchRequestStart will be called properly even though we use an inherited + // search source. + const timeFilterSearchSource = searchSource.createChild({ callParentStartHandlers: true }); + const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true }); + + aggs.setTimeRange(timeRange as TimeRange); + + // For now we need to mirror the history of the passed search source, since + // the request inspector wouldn't work otherwise. + Object.defineProperty(requestSearchSource, 'history', { + get() { + return searchSource.history; + }, + set(history) { + return (searchSource.history = history); + }, + }); + + requestSearchSource.setField('aggs', function () { + return aggs.toDsl(metricsAtAllLevels); + }); + + requestSearchSource.onRequestStart((paramSearchSource, options) => { + return aggs.onSearchRequestStart(paramSearchSource, options); + }); + + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; + + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (timeRange && allTimeFields.length > 0) { + timeFilterSearchSource.setField('filter', () => { + return allTimeFields + .map((fieldName) => getTime(indexPattern, timeRange, { fieldName })) + .filter(isRangeFilter); + }); + } + + requestSearchSource.setField('filter', filters); + requestSearchSource.setField('query', query); + + let request; + if (inspectorAdapters.requests) { + inspectorAdapters.requests.reset(); + request = inspectorAdapters.requests.start( + i18n.translate('data.functions.esaggs.inspector.dataRequest.title', { + defaultMessage: 'Data', + }), + { + description: i18n.translate('data.functions.esaggs.inspector.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + searchSessionId, + } + ); + request.stats(getRequestInspectorStats(requestSearchSource)); + } + + try { + const response = await requestSearchSource.fetch({ + abortSignal, + sessionId: searchSessionId, + }); + + if (request) { + request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); + } + + (searchSource as any).rawResponse = response; + } catch (e) { + // Log any error during request to the inspector + if (request) { + request.error({ json: e }); + } + throw e; + } finally { + // Add the request body no matter if things went fine or not + if (request) { + request.json(await requestSearchSource.getSearchRequestBody()); + } + } + + // Note that rawResponse is not deeply cloned here, so downstream applications using courier + // must take care not to mutate it, or it could have unintended side effects, e.g. displaying + // response data incorrectly in the inspector. + let response = (searchSource as any).rawResponse; + for (const agg of aggs.aggs) { + if (typeof agg.type.postFlightRequest === 'function') { + response = await agg.type.postFlightRequest( + response, + aggs, + agg, + requestSearchSource, + inspectorAdapters.requests, + abortSignal + ); + } + } + + const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; + const tabifyParams = { + metricsAtAllLevels, + partialRows, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, + }; + + const tabifiedResponse = tabifyAggResponse(aggs, response, tabifyParams); + + if (inspectorAdapters.data) { + inspectorAdapters.data.setTabularLoader( + () => + buildTabularInspectorData(tabifiedResponse, { + addFilters, + deserializeFieldFormat, + }), + { returnsFormattedValues: true } + ); + } + + return tabifiedResponse; +}; diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index 032bce6d8d2aa..28fb4ff8b53ae 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -18,7 +18,6 @@ */ import { NotificationsStart, CoreStart } from 'src/core/public'; -import { FieldFormatsStart } from './field_formats'; import { createGetterSetter } from '../../kibana_utils/public'; import { IndexPatternsContract } from './index_patterns'; import { DataPublicPluginStart } from './types'; @@ -31,20 +30,12 @@ export const [getUiSettings, setUiSettings] = createGetterSetter( - 'FieldFormats' -); - export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( 'IndexPatterns' ); -export const [getQueryService, setQueryService] = createGetterSetter< - DataPublicPluginStart['query'] ->('Query'); - export const [getSearchService, setSearchService] = createGetterSetter< DataPublicPluginStart['search'] >('Search'); diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 170078076ec6f..980e90d0acf20 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -84,7 +84,7 @@ export class SearchEmbeddable private readonly savedSearch: SavedSearch; private $rootScope: ng.IRootScopeService; private $compile: ng.ICompileService; - private inspectorAdaptors: Adapters; + private inspectorAdapters: Adapters; private searchScope?: SearchScope; private panelTitle: string = ''; private filtersSearchSource?: ISearchSource; @@ -131,7 +131,7 @@ export class SearchEmbeddable this.savedSearch = savedSearch; this.$rootScope = $rootScope; this.$compile = $compile; - this.inspectorAdaptors = { + this.inspectorAdapters = { requests: new RequestAdapter(), }; this.initializeSearchScope(); @@ -150,7 +150,7 @@ export class SearchEmbeddable } public getInspectorAdapters() { - return this.inspectorAdaptors; + return this.inspectorAdapters; } public getSavedSearch() { @@ -195,7 +195,7 @@ export class SearchEmbeddable const searchScope: SearchScope = (this.searchScope = this.$rootScope.$new()); searchScope.description = this.savedSearch.description; - searchScope.inspectorAdapters = this.inspectorAdaptors; + searchScope.inspectorAdapters = this.inspectorAdapters; const { searchSource } = this.savedSearch; const indexPattern = (searchScope.indexPattern = searchSource.getField('index'))!; @@ -287,7 +287,7 @@ export class SearchEmbeddable ); // Log request to inspector - this.inspectorAdaptors.requests.reset(); + this.inspectorAdapters.requests!.reset(); const title = i18n.translate('discover.embeddable.inspectorRequestDataTitle', { defaultMessage: 'Data', }); @@ -295,7 +295,7 @@ export class SearchEmbeddable defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', }); - const inspectorRequest = this.inspectorAdaptors.requests.start(title, { + const inspectorRequest = this.inspectorAdapters.requests!.start(title, { description, searchSessionId, }); diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 6a2565edf2f67..1bdfbe9d01a2f 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -28,6 +28,7 @@ import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; +import { EventEmitter } from 'events'; import { ExclusiveUnion } from '@elastic/eui'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { History } from 'history'; @@ -59,7 +60,7 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; -import { RequestAdapter } from 'src/plugins/inspector/common'; +import { RequestAdapter as RequestAdapter_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject as SavedObject_2 } from 'src/core/server'; @@ -100,6 +101,14 @@ export const ACTION_EDIT_PANEL = "editPanel"; export interface Adapters { // (undocumented) [key: string]: any; + // Warning: (ae-forgotten-export) The symbol "DataAdapter" needs to be exported by the entry point index.d.ts + // + // (undocumented) + data?: DataAdapter; + // Warning: (ae-forgotten-export) The symbol "RequestAdapter" needs to be exported by the entry point index.d.ts + // + // (undocumented) + requests?: RequestAdapter; } // Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/inspector/common/adapters/data/data_adapter.ts b/src/plugins/inspector/common/adapters/data/data_adapter.ts index 34e6c278c693f..a21aa7db39145 100644 --- a/src/plugins/inspector/common/adapters/data/data_adapter.ts +++ b/src/plugins/inspector/common/adapters/data/data_adapter.ts @@ -20,7 +20,7 @@ import { EventEmitter } from 'events'; import { TabularCallback, TabularHolder, TabularLoaderOptions } from './types'; -class DataAdapter extends EventEmitter { +export class DataAdapter extends EventEmitter { private tabular?: TabularCallback; private tabularOptions?: TabularLoaderOptions; @@ -38,5 +38,3 @@ class DataAdapter extends EventEmitter { return Promise.resolve(this.tabular()).then((data) => ({ data, options })); } } - -export { DataAdapter }; diff --git a/src/plugins/inspector/common/adapters/data/data_adapters.test.ts b/src/plugins/inspector/common/adapters/data/data_adapters.test.ts index 287024ca1b59e..7cc52807548f0 100644 --- a/src/plugins/inspector/common/adapters/data/data_adapters.test.ts +++ b/src/plugins/inspector/common/adapters/data/data_adapters.test.ts @@ -35,33 +35,37 @@ describe('DataAdapter', () => { }); it('should call the provided callback and resolve with its value', async () => { - const spy = jest.fn(() => 'foo'); + const data = { columns: [], rows: [] }; + const spy = jest.fn(() => data); adapter.setTabularLoader(spy); expect(spy).not.toBeCalled(); const result = await adapter.getTabular(); expect(spy).toBeCalled(); - expect(result.data).toBe('foo'); + expect(result.data).toBe(data); }); it('should pass through options specified via setTabularLoader', async () => { - adapter.setTabularLoader(() => 'foo', { returnsFormattedValues: true }); + const data = { columns: [], rows: [] }; + adapter.setTabularLoader(() => data, { returnsFormattedValues: true }); const result = await adapter.getTabular(); expect(result.options).toEqual({ returnsFormattedValues: true }); }); it('should return options set when starting loading data', async () => { - adapter.setTabularLoader(() => 'foo', { returnsFormattedValues: true }); + const data = { columns: [], rows: [] }; + adapter.setTabularLoader(() => data, { returnsFormattedValues: true }); const waitForResult = adapter.getTabular(); - adapter.setTabularLoader(() => 'bar', { returnsFormattedValues: false }); + adapter.setTabularLoader(() => data, { returnsFormattedValues: false }); const result = await waitForResult; expect(result.options).toEqual({ returnsFormattedValues: true }); }); }); it('should emit a "tabular" event when a new tabular loader is specified', () => { + const data = { columns: [], rows: [] }; const spy = jest.fn(); adapter.once('change', spy); - adapter.setTabularLoader(() => 42); + adapter.setTabularLoader(() => data); expect(spy).toBeCalled(); }); }); diff --git a/src/plugins/inspector/common/adapters/data/formatted_data.ts b/src/plugins/inspector/common/adapters/data/formatted_data.ts index c752e8670aca3..08c956f27d011 100644 --- a/src/plugins/inspector/common/adapters/data/formatted_data.ts +++ b/src/plugins/inspector/common/adapters/data/formatted_data.ts @@ -17,8 +17,6 @@ * under the License. */ -class FormattedData { +export class FormattedData { constructor(public readonly raw: any, public readonly formatted: any) {} } - -export { FormattedData }; diff --git a/src/plugins/inspector/common/adapters/data/index.ts b/src/plugins/inspector/common/adapters/data/index.ts index 920e298ab455f..a8b1abcd8cd7e 100644 --- a/src/plugins/inspector/common/adapters/data/index.ts +++ b/src/plugins/inspector/common/adapters/data/index.ts @@ -17,5 +17,6 @@ * under the License. */ -export { FormattedData } from './formatted_data'; -export { DataAdapter } from './data_adapter'; +export * from './data_adapter'; +export * from './formatted_data'; +export * from './types'; diff --git a/src/plugins/inspector/common/adapters/data/types.ts b/src/plugins/inspector/common/adapters/data/types.ts index 1c7b17c143eca..040724f4ae36e 100644 --- a/src/plugins/inspector/common/adapters/data/types.ts +++ b/src/plugins/inspector/common/adapters/data/types.ts @@ -17,8 +17,25 @@ * under the License. */ -// TODO: add a more specific TabularData type. -export type TabularData = any; +export interface TabularDataValue { + formatted: string; + raw: unknown; +} + +export interface TabularDataColumn { + name: string; + field: string; + filter?: (value: TabularDataValue) => void; + filterOut?: (value: TabularDataValue) => void; +} + +export type TabularDataRow = Record; + +export interface TabularData { + columns: TabularDataColumn[]; + rows: TabularDataRow[]; +} + export type TabularCallback = () => TabularData | Promise; export interface TabularHolder { diff --git a/src/plugins/inspector/common/adapters/index.ts b/src/plugins/inspector/common/adapters/index.ts index 1e7a44a2c60b1..0c6319a2905a8 100644 --- a/src/plugins/inspector/common/adapters/index.ts +++ b/src/plugins/inspector/common/adapters/index.ts @@ -17,12 +17,6 @@ * under the License. */ -export { Adapters } from './types'; -export { DataAdapter, FormattedData } from './data'; -export { - RequestAdapter, - RequestStatistic, - RequestStatistics, - RequestStatus, - RequestResponder, -} from './request'; +export * from './data'; +export * from './request'; +export * from './types'; diff --git a/src/plugins/inspector/common/adapters/request/request_adapter.ts b/src/plugins/inspector/common/adapters/request/request_adapter.ts index af10d1b77b16d..5f5728e1cf331 100644 --- a/src/plugins/inspector/common/adapters/request/request_adapter.ts +++ b/src/plugins/inspector/common/adapters/request/request_adapter.ts @@ -29,7 +29,7 @@ import { Request, RequestParams, RequestStatus } from './types'; * instead it offers a generic API to log requests of any kind. * @extends EventEmitter */ -class RequestAdapter extends EventEmitter { +export class RequestAdapter extends EventEmitter { private requests: Map; constructor() { @@ -78,5 +78,3 @@ class RequestAdapter extends EventEmitter { this.emit('change'); } } - -export { RequestAdapter }; diff --git a/src/plugins/inspector/common/adapters/types.ts b/src/plugins/inspector/common/adapters/types.ts index 362c69e299c9d..b51c3e56c749f 100644 --- a/src/plugins/inspector/common/adapters/types.ts +++ b/src/plugins/inspector/common/adapters/types.ts @@ -17,9 +17,14 @@ * under the License. */ +import type { DataAdapter } from './data'; +import type { RequestAdapter } from './request'; + /** * The interface that the adapters used to open an inspector have to fullfill. */ export interface Adapters { + data?: DataAdapter; + requests?: RequestAdapter; [key: string]: any; } diff --git a/src/plugins/inspector/common/index.ts b/src/plugins/inspector/common/index.ts index 06ab36a577d98..c5755b22095dc 100644 --- a/src/plugins/inspector/common/index.ts +++ b/src/plugins/inspector/common/index.ts @@ -17,4 +17,17 @@ * under the License. */ -export * from './adapters'; +export { + Adapters, + DataAdapter, + FormattedData, + RequestAdapter, + RequestStatistic, + RequestStatistics, + RequestStatus, + RequestResponder, + TabularData, + TabularDataColumn, + TabularDataRow, + TabularDataValue, +} from './adapters'; diff --git a/src/plugins/inspector/public/test/is_available.test.ts b/src/plugins/inspector/public/test/is_available.test.ts index 0604129a0734a..c38d9d7a3f825 100644 --- a/src/plugins/inspector/public/test/is_available.test.ts +++ b/src/plugins/inspector/public/test/is_available.test.ts @@ -18,8 +18,7 @@ */ import { inspectorPluginMock } from '../mocks'; -import { DataAdapter } from '../../common/adapters/data/data_adapter'; -import { RequestAdapter } from '../../common/adapters/request/request_adapter'; +import { DataAdapter, RequestAdapter } from '../../common/adapters'; const adapter1 = new DataAdapter(); const adapter2 = new RequestAdapter(); diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx index 100fa7787321c..324094d8f93d0 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.tsx @@ -35,7 +35,7 @@ import { Adapters } from '../../../../common'; import { TabularLoaderOptions, TabularData, - TabularCallback, + TabularHolder, } from '../../../../common/adapters/data/types'; import { IUiSettingsClient } from '../../../../../../core/public'; import { withKibana, KibanaReactContextValue } from '../../../../../kibana_react/public'; @@ -44,7 +44,7 @@ interface DataViewComponentState { tabularData: TabularData | null; tabularOptions: TabularLoaderOptions; adapters: Adapters; - tabularPromise: TabularCallback | null; + tabularPromise: Promise | null; } interface DataViewComponentProps extends InspectorViewProps { @@ -73,7 +73,7 @@ class DataViewComponent extends Component string; + export interface DataViewColumn { name: string; field: string; - sortable: (item: DataViewRow) => string | number; + sortable: (item: TabularDataRow) => string | number; render: DataViewColumnRender; } -type DataViewColumnRender = (value: string, _item: DataViewRow) => string; - -export interface DataViewRow { - [fields: string]: { - formatted: string; - raw: any; - }; -} +export type DataViewRow = TabularDataRow; diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx index 7762689daf4e6..e1879f7a6b6c8 100644 --- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx @@ -31,7 +31,7 @@ import { RequestDetails } from './request_details'; interface RequestSelectorState { requests: Request[]; - request: Request; + request: Request | null; } export class RequestsViewComponent extends Component { @@ -43,9 +43,9 @@ export class RequestsViewComponent extends Component { - const requests = this.props.adapters.requests.getRequests(); + const requests = this.props.adapters.requests!.getRequests(); const newState = { requests } as RequestSelectorState; - if (!requests.includes(this.state.request)) { + if (!this.state.request || !requests.includes(this.state.request)) { newState.request = requests.length ? requests[0] : null; } this.setState(newState); @@ -69,7 +69,7 @@ export class RequestsViewComponent extends Component - - + {this.state.request && ( + <> + + + + )} {this.state.request && this.state.request.description && ( diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index 3e40c94e116fb..3a14f49169e09 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -8,6 +8,7 @@ import { CoreSetup } from 'src/core/public'; import { CoreStart } from 'src/core/public'; import { EnvironmentMode } from '@kbn/config'; import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { EventEmitter } from 'events'; import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; import { Plugin } from 'src/core/public'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index bef0b8c6ea7af..103fd11263330 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -134,7 +134,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource destroy() { const inspectorAdapters = this.getInspectorAdapters(); - if (inspectorAdapters) { + if (inspectorAdapters?.requests) { inspectorAdapters.requests.resetRequest(this.getId()); } } @@ -164,7 +164,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource const inspectorAdapters = this.getInspectorAdapters(); let inspectorRequest: RequestResponder | undefined; - if (inspectorAdapters) { + if (inspectorAdapters?.requests) { inspectorRequest = inspectorAdapters.requests.start(requestName, { id: requestId, description: requestDescription, From 7d9f460a9ceaa5f48e84fc0122bc5bb3883ac0b1 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Wed, 18 Nov 2020 11:17:41 -0500 Subject: [PATCH 66/99] [CI] Build docker image during packer_cache (#82145) --- .ci/build_docker.sh | 10 ++++++++++ .ci/packer_cache_for_branch.sh | 2 ++ vars/kibanaPipeline.groovy | 7 +------ 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100755 .ci/build_docker.sh diff --git a/.ci/build_docker.sh b/.ci/build_docker.sh new file mode 100755 index 0000000000000..1f45182aad840 --- /dev/null +++ b/.ci/build_docker.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(dirname "${0}")" + +cp /usr/local/bin/runbld ./ +cp /usr/local/bin/bash_standard_lib.sh ./ + +docker build -t kibana-ci -f ./Dockerfile . diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh index b8b5f7d3c3f0e..0d9b22b04dbd0 100755 --- a/.ci/packer_cache_for_branch.sh +++ b/.ci/packer_cache_for_branch.sh @@ -53,6 +53,8 @@ tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ echo "created $HOME/.kibana/bootstrap_cache/$branch.tar" +.ci/build_docker.sh + if [[ "$branch" != "master" ]]; then rm --preserve-root -rf "$checkoutDir" fi diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index eea3ff18f3453..0051293704717 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -390,12 +390,7 @@ def scriptTaskDocker(description, script) { def buildDocker() { sh( - script: """ - cp /usr/local/bin/runbld .ci/ - cp /usr/local/bin/bash_standard_lib.sh .ci/ - cd .ci - docker build -t kibana-ci -f ./Dockerfile . - """, + script: "./.ci/build_docker.sh", label: 'Build CI Docker image' ) } From e07d6d0b389eb8dcd553778aea570c6a082843f9 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Wed, 18 Nov 2020 08:19:41 -0800 Subject: [PATCH 67/99] Derive the port from the protocol in cases where it's not explicitly stated (#83583) --- .../chromium/driver/chromium_driver.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 500185cd7e14f..5a1cdfe867590 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -333,13 +333,32 @@ export class HeadlessChromiumDriver { private _shouldUseCustomHeaders(conditions: ConditionalHeadersConditions, url: string) { const { hostname, protocol, port, pathname } = parseUrl(url); - if (port === null) throw new Error(`URL missing port: ${url}`); + // `port` is null in URLs that don't explicitly state it, + // however we can derive the port from the protocol (http/https) + // IE: https://feeds-staging.elastic.co/kibana/v8.0.0.json + const derivedPort = (() => { + if (port) { + return port; + } + + if (protocol === 'http:') { + return '80'; + } + + if (protocol === 'https:') { + return '443'; + } + + return null; + })(); + + if (derivedPort === null) throw new Error(`URL missing port: ${url}`); if (pathname === null) throw new Error(`URL missing pathname: ${url}`); return ( hostname === conditions.hostname && protocol === `${conditions.protocol}:` && - this._shouldUseCustomHeadersForPort(conditions, port) && + this._shouldUseCustomHeadersForPort(conditions, derivedPort) && pathname.startsWith(`${conditions.basePath}/`) ); } From 21c0258e6bec3d0834cf3126b3b012208a9ae0cc Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 18 Nov 2020 10:41:28 -0600 Subject: [PATCH 68/99] [Metrics UI] Add Process tab to Enhanced Node Details (#83477) --- .../common/http_api/host_details/index.ts | 7 + .../http_api/host_details/process_list.ts | 20 ++ x-pack/plugins/infra/common/http_api/index.ts | 1 + .../components/bottom_drawer.tsx | 6 +- .../components/node_details/overlay.tsx | 66 ++-- .../node_details/tabs/processes.tsx | 21 -- .../node_details/tabs/processes/index.tsx | 102 +++++++ .../tabs/processes/parse_process_list.ts | 55 ++++ .../tabs/processes/process_row.tsx | 267 ++++++++++++++++ .../tabs/processes/processes_table.tsx | 288 ++++++++++++++++++ .../tabs/processes/state_badge.tsx | 28 ++ .../node_details/tabs/processes/states.ts | 33 ++ .../tabs/processes/summary_table.tsx | 81 +++++ .../node_details/tabs/processes/types.ts | 22 ++ .../components/node_details/tabs/shared.tsx | 9 +- .../inventory_view/hooks/use_process_list.ts | 55 ++++ x-pack/plugins/infra/server/infra_server.ts | 2 + .../server/lib/host_details/process_list.ts | 64 ++++ .../infra/server/routes/process_list/index.ts | 50 +++ .../server/utils/get_all_metrics_data.ts | 34 +++ 20 files changed, 1163 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/infra/common/http_api/host_details/index.ts create mode 100644 x-pack/plugins/infra/common/http_api/host_details/process_list.ts delete mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/state_badge.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/states.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/types.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts create mode 100644 x-pack/plugins/infra/server/lib/host_details/process_list.ts create mode 100644 x-pack/plugins/infra/server/routes/process_list/index.ts create mode 100644 x-pack/plugins/infra/server/utils/get_all_metrics_data.ts diff --git a/x-pack/plugins/infra/common/http_api/host_details/index.ts b/x-pack/plugins/infra/common/http_api/host_details/index.ts new file mode 100644 index 0000000000000..b323ed8e9e327 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/host_details/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './process_list'; diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts new file mode 100644 index 0000000000000..4b4a0a54b9d13 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { MetricsAPITimerangeRT, MetricsAPISeriesRT } from '../metrics_api'; + +export const ProcessListAPIRequestRT = rt.type({ + hostTerm: rt.record(rt.string, rt.string), + timerange: MetricsAPITimerangeRT, + indexPattern: rt.string, +}); + +export const ProcessListAPIResponseRT = rt.array(MetricsAPISeriesRT); + +export type ProcessListAPIRequest = rt.TypeOf; + +export type ProcessListAPIResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts index 4c729d11ba8c1..914011454a732 100644 --- a/x-pack/plugins/infra/common/http_api/index.ts +++ b/x-pack/plugins/infra/common/http_api/index.ts @@ -11,3 +11,4 @@ export * from './metrics_explorer'; export * from './metrics_api'; export * from './log_alerts'; export * from './snapshot_api'; +export * from './host_details'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx index 7c6e58125b48b..5c6e124914f39 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx @@ -38,7 +38,11 @@ export const BottomDrawer: React.FC<{ - + {isOpen ? hideHistory : showHistory} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index dd0060f773b49..af712c0611577 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTabbedContent } from '@elastic/eui'; +import { EuiPortal, EuiTabs, EuiTab, EuiPanel, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPanel } from '@elastic/eui'; -import React, { CSSProperties, useMemo } from 'react'; -import { EuiText } from '@elastic/eui'; +import React, { CSSProperties, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { euiStyled } from '../../../../../../../observability/public'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; @@ -17,6 +15,7 @@ import { MetricsTab } from './tabs/metrics'; import { LogsTab } from './tabs/logs'; import { ProcessesTab } from './tabs/processes'; import { PropertiesTab } from './tabs/properties'; +import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN, OVERLAY_HEADER_SIZE } from './tabs/shared'; interface Props { isOpen: boolean; @@ -48,46 +47,63 @@ export const NodeContextPopover = ({ }); }, [tabConfigs, node, nodeType, currentTime, options]); + const [selectedTab, setSelectedTab] = useState(0); + if (!isOpen) { return null; } return ( - - - - - -

{node.name}

-
-
- - - - - -
-
- -
+ + + + + + +

{node.name}

+
+
+ + + + + +
+ + {tabs.map((tab, i) => ( + setSelectedTab(i)}> + {tab.name} + + ))} + +
+ {tabs[selectedTab].content} +
+
); }; const OverlayHeader = euiStyled.div` border-color: ${(props) => props.theme.eui.euiBorderColor}; border-bottom-width: ${(props) => props.theme.eui.euiBorderWidthThick}; - padding: ${(props) => props.theme.eui.euiSizeS}; padding-bottom: 0; overflow: hidden; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + height: ${OVERLAY_HEADER_SIZE}px; +`; + +const OverlayHeaderTitleWrapper = euiStyled(EuiFlexGroup).attrs({ alignItems: 'center' })` + padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) => + props.theme.eui.paddingSizes.m} 0; `; const panelStyle: CSSProperties = { position: 'absolute', right: 10, - top: -100, + top: OVERLAY_Y_START, width: '50%', - maxWidth: 600, + maxWidth: 730, zIndex: 2, - height: '50vh', + height: `calc(100vh - ${OVERLAY_Y_START + OVERLAY_BOTTOM_MARGIN}px)`, overflow: 'hidden', }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx deleted file mode 100644 index 94ba1150c20dd..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx +++ /dev/null @@ -1,21 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { TabContent, TabProps } from './shared'; - -const TabComponent = (props: TabProps) => { - return Processes Placeholder; -}; - -export const ProcessesTab = { - id: 'processes', - name: i18n.translate('xpack.infra.nodeDetails.tabs.processes', { - defaultMessage: 'Processes', - }), - content: TabComponent, -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx new file mode 100644 index 0000000000000..836d491e6210e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton, Query } from '@elastic/eui'; +import { useProcessList } from '../../../../hooks/use_process_list'; +import { TabContent, TabProps } from '../shared'; +import { STATE_NAMES } from './states'; +import { SummaryTable } from './summary_table'; +import { ProcessesTable } from './processes_table'; + +const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { + const [searchFilter, setSearchFilter] = useState(EuiSearchBar.Query.MATCH_ALL); + + const hostTerm = useMemo(() => { + const field = + options.fields && Reflect.has(options.fields, nodeType) + ? Reflect.get(options.fields, nodeType) + : nodeType; + return { [field]: node.name }; + }, [options, node, nodeType]); + + const { loading, error, response, makeRequest: reload } = useProcessList( + hostTerm, + 'metricbeat-*', + options.fields!.timestamp, + currentTime + ); + + if (error) { + return ( + + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { + defaultMessage: 'Unable to show process data', + })} + + } + actions={ + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', { + defaultMessage: 'Try again', + })} + + } + /> + + ); + } + + return ( + + + + setSearchFilter(query ?? EuiSearchBar.Query.MATCH_ALL)} + box={{ + incremental: true, + placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', { + defaultMessage: 'Search for processes…', + }), + }} + filters={[ + { + type: 'field_value_selection', + field: 'state', + name: 'State', + operator: 'exact', + multiSelect: false, + options: Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({ + value, + view, + })), + }, + ]} + /> + + + + ); +}; + +export const ProcessesTab = { + id: 'processes', + name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { + defaultMessage: 'Processes', + }), + content: TabComponent, +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts new file mode 100644 index 0000000000000..88584ef2987e1 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; +import { Process } from './types'; + +export const parseProcessList = (processList: ProcessListAPIResponse) => + processList.map((process) => { + const command = process.id; + let mostRecentPoint; + for (let i = process.rows.length - 1; i >= 0; i--) { + const point = process.rows[i]; + if (point && Array.isArray(point.meta) && point.meta?.length) { + mostRecentPoint = point; + break; + } + } + if (!mostRecentPoint) return { command, cpu: null, memory: null, startTime: null, state: null }; + + const { cpu, memory } = mostRecentPoint; + const { system, process: processMeta, user } = (mostRecentPoint.meta as any[])[0]; + const startTime = system.process.cpu.start_time; + const state = system.process.state; + + const timeseries = { + cpu: pickTimeseries(process.rows, 'cpu'), + memory: pickTimeseries(process.rows, 'memory'), + }; + + return { + command, + cpu, + memory, + startTime, + state, + pid: processMeta.pid, + user: user.name, + timeseries, + } as Process; + }); + +const pickTimeseries = (rows: any[], metricID: string) => ({ + rows: rows.map((row) => ({ + timestamp: row.timestamp, + metric_0: row[metricID], + })), + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + id: metricID, +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx new file mode 100644 index 0000000000000..bbf4a25fc49a7 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -0,0 +1,267 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useMemo } from 'react'; +import moment from 'moment'; +import { first, last } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiTableRow, + EuiTableRowCell, + EuiButtonEmpty, + EuiCode, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; +import { Axis, Chart, Settings, Position, TooltipValue, niceTimeFormatter } from '@elastic/charts'; +import { AutoSizer } from '../../../../../../../components/auto_sizer'; +import { createFormatter } from '../../../../../../../../common/formatters'; +import { useUiSetting } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { getChartTheme } from '../../../../../metrics_explorer/components/helpers/get_chart_theme'; +import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain'; +import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart'; +import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api'; +import { Color } from '../../../../../../../../common/color_palette'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { Process } from './types'; + +interface Props { + cells: React.ReactNode[]; + item: Process; +} + +export const ProcessRow = ({ cells, item }: Props) => { + const [isExpanded, setIsExpanded] = useState(false); + + return ( + <> + + + setIsExpanded(!isExpanded)} + /> + + {cells} + + + {isExpanded && ( + + {({ measureRef, bounds: { height = 0 } }) => ( + + + + + +
+ + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCommand', + { + defaultMessage: 'Command', + } + )} + + + {item.command} + +
+
+ {item.apmTrace && ( + + + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.viewTraceInAPM', + { + defaultMessage: 'View trace in APM', + } + )} + + + )} +
+ + + + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelPID', + { + defaultMessage: 'PID', + } + )} + + + {item.pid} + + + + + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelUser', + { + defaultMessage: 'User', + } + )} + + + {item.user} + + + + {cpuMetricLabel} + + + + + + {memoryMetricLabel} + + + + + +
+
+ )} +
+ )} +
+ + ); +}; + +interface ProcessChartProps { + timeseries: Process['timeseries']['x']; + color: Color; + label: string; +} +const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => { + const chartMetric = { + color, + aggregation: 'avg' as MetricsExplorerAggregation, + label, + }; + const isDarkMode = useUiSetting('theme:darkMode'); + + const dateFormatter = useMemo(() => { + if (!timeseries) return () => ''; + const firstTimestamp = first(timeseries.rows)?.timestamp; + const lastTimestamp = last(timeseries.rows)?.timestamp; + + if (firstTimestamp == null || lastTimestamp == null) { + return (value: number) => `${value}`; + } + + return niceTimeFormatter([firstTimestamp, lastTimestamp]); + }, [timeseries]); + + const yAxisFormatter = createFormatter('percent'); + + const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), + }; + + const dataDomain = calculateDomain(timeseries, [chartMetric], false); + const domain = dataDomain + ? { + max: dataDomain.max * 1.1, // add 10% headroom. + min: dataDomain.min, + } + : { max: 0, min: 0 }; + + return ( + + + + + + + + + ); +}; + +export const CodeLine = euiStyled(EuiCode).attrs({ + transparentBackground: true, +})` + text-overflow: ellipsis; + overflow: hidden; + padding: 0 !important; + & code.euiCodeBlock__code { + white-space: nowrap !important; + vertical-align: middle; + } +`; + +const ExpandedCommandLine = euiStyled(EuiCode).attrs({ + transparentBackground: true, +})` + padding: 0 !important; + margin-bottom: ${(props) => props.theme.eui.euiSizeS}; +`; + +const ExpandedRowCell = euiStyled(EuiTableRowCell).attrs({ + textOnly: false, + colSpan: 6, +})<{ commandHeight: number }>` + height: ${(props) => props.commandHeight + 240}px; + padding: 0 ${(props) => props.theme.eui.paddingSizes.m}; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; +`; + +const ChartContainer = euiStyled.div` + width: 300px; + height: 140px; +`; + +const cpuMetricLabel = i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCPU', + { + defaultMessage: 'CPU', + } +); + +const memoryMetricLabel = i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelMemory', + { + defaultMessage: 'Memory', + } +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx new file mode 100644 index 0000000000000..43f3a333fda83 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -0,0 +1,288 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useState, useEffect, useCallback } from 'react'; +import { omit } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiTable, + EuiTableHeader, + EuiTableBody, + EuiTableHeaderCell, + EuiTableRowCell, + EuiSpacer, + EuiTablePagination, + EuiLoadingChart, + Query, + SortableProperties, + LEFT_ALIGNMENT, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; +import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; +import { FORMATTERS } from '../../../../../../../../common/formatters'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { Process } from './types'; +import { ProcessRow, CodeLine } from './process_row'; +import { parseProcessList } from './parse_process_list'; +import { StateBadge } from './state_badge'; +import { STATE_ORDER } from './states'; + +interface TableProps { + processList: ProcessListAPIResponse; + currentTime: number; + isLoading: boolean; + searchFilter: Query; +} + +function useSortableProperties( + sortablePropertyItems: Array<{ + name: string; + getValue: (obj: T) => any; + isAscending: boolean; + }>, + defaultSortProperty: string +) { + const [sortableProperties] = useState>( + new SortableProperties(sortablePropertyItems, defaultSortProperty) + ); + const [sortedColumn, setSortedColumn] = useState( + omit(sortableProperties.getSortedProperty(), 'getValue') + ); + + return { + setSortedColumn: useCallback( + (property) => { + sortableProperties.sortOn(property); + setSortedColumn(omit(sortableProperties.getSortedProperty(), 'getValue')); + }, + [sortableProperties] + ), + sortedColumn, + sortItems: (items: T[]) => sortableProperties.sortItems(items), + }; +} + +export const ProcessesTable = ({ + processList, + currentTime, + isLoading, + searchFilter, +}: TableProps) => { + const [currentPage, setCurrentPage] = useState(0); + const [itemsPerPage, setItemsPerPage] = useState(10); + useEffect(() => setCurrentPage(0), [processList, searchFilter, itemsPerPage]); + + const { sortedColumn, sortItems, setSortedColumn } = useSortableProperties( + [ + { + name: 'state', + getValue: (item: any) => STATE_ORDER.indexOf(item.state), + isAscending: true, + }, + { + name: 'command', + getValue: (item: any) => item.command.toLowerCase(), + isAscending: true, + }, + { + name: 'startTime', + getValue: (item: any) => Date.parse(item.startTime), + isAscending: false, + }, + { + name: 'cpu', + getValue: (item: any) => item.cpu, + isAscending: false, + }, + { + name: 'memory', + getValue: (item: any) => item.memory, + isAscending: false, + }, + ], + 'state' + ); + + const currentItems = useMemo(() => { + const filteredItems = Query.execute(searchFilter, parseProcessList(processList)) as Process[]; + if (!filteredItems.length) return []; + const sortedItems = sortItems(filteredItems); + return sortedItems; + }, [processList, searchFilter, sortItems]); + + const pageCount = useMemo(() => Math.ceil(currentItems.length / itemsPerPage), [ + itemsPerPage, + currentItems, + ]); + + const pageStartIdx = useMemo(() => currentPage * itemsPerPage + (currentPage > 0 ? 1 : 0), [ + currentPage, + itemsPerPage, + ]); + const currentItemsPage = useMemo( + () => currentItems.slice(pageStartIdx, pageStartIdx + itemsPerPage), + [pageStartIdx, currentItems, itemsPerPage] + ); + + if (isLoading) return ; + + return ( + <> + + + + {columns.map((column) => ( + setSortedColumn(column.field) : undefined} + isSorted={sortedColumn.name === column.field} + isSortAscending={sortedColumn.name === column.field && sortedColumn.isAscending} + > + {column.name} + + ))} + + + + + + + + + ); +}; + +const LoadingPlaceholder = () => { + return ( +
+ +
+ ); +}; + +interface TableBodyProps { + items: Process[]; + currentTime: number; +} +const ProcessesTableBody = ({ items, currentTime }: TableBodyProps) => ( + <> + {items.map((item, i) => { + const cells = columns.map((column) => ( + + {column.render ? column.render(item[column.field], currentTime) : item[column.field]} + + )); + return ; + })} + +); + +const StyledTableBody = euiStyled(EuiTableBody)` + & .euiTableCellContent { + padding-top: 0; + padding-bottom: 0; + + } +`; + +const ONE_MINUTE = 60 * 1000; +const ONE_HOUR = ONE_MINUTE * 60; +const RuntimeCell = ({ startTime, currentTime }: { startTime: string; currentTime: number }) => { + const runtimeLength = currentTime - Date.parse(startTime); + let remainingRuntimeMS = runtimeLength; + const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR); + remainingRuntimeMS -= runtimeHours * ONE_HOUR; + const runtimeMinutes = Math.floor(remainingRuntimeMS / ONE_MINUTE); + remainingRuntimeMS -= runtimeMinutes * ONE_MINUTE; + const runtimeSeconds = Math.floor(remainingRuntimeMS / 1000); + remainingRuntimeMS -= runtimeSeconds * 1000; + + const runtimeDisplayHours = runtimeHours ? `${runtimeHours}:` : ''; + const runtimeDisplayMinutes = runtimeMinutes < 10 ? `0${runtimeMinutes}:` : `${runtimeMinutes}:`; + const runtimeDisplaySeconds = runtimeSeconds < 10 ? `0${runtimeSeconds}` : runtimeSeconds; + + return <>{`${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}`}; +}; + +const columns: Array<{ + field: keyof Process; + name: string; + sortable: boolean; + render?: Function; + width?: string | number; + textOnly?: boolean; + align?: typeof RIGHT_ALIGNMENT | typeof LEFT_ALIGNMENT; +}> = [ + { + field: 'state', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', { + defaultMessage: 'State', + }), + sortable: true, + render: (state: string) => , + width: 84, + textOnly: false, + }, + { + field: 'command', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', { + defaultMessage: 'Command', + }), + sortable: true, + width: '40%', + render: (command: string) => {command}, + }, + { + field: 'startTime', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelTime', { + defaultMessage: 'Time', + }), + align: RIGHT_ALIGNMENT, + sortable: true, + render: (startTime: string, currentTime: number) => ( + + ), + }, + { + field: 'cpu', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', { + defaultMessage: 'CPU', + }), + sortable: true, + render: (value: number) => FORMATTERS.percent(value), + }, + { + field: 'memory', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelMemory', { + defaultMessage: 'Mem.', + }), + sortable: true, + render: (value: number) => FORMATTERS.percent(value), + }, +]; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/state_badge.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/state_badge.tsx new file mode 100644 index 0000000000000..17306abdb60a3 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/state_badge.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { STATE_NAMES } from './states'; + +export const StateBadge = ({ state }: { state: string }) => { + switch (state) { + case 'running': + return {STATE_NAMES.running}; + case 'sleeping': + return {STATE_NAMES.sleeping}; + case 'dead': + return {STATE_NAMES.dead}; + case 'stopped': + return {STATE_NAMES.stopped}; + case 'idle': + return {STATE_NAMES.idle}; + case 'zombie': + return {STATE_NAMES.zombie}; + default: + return {STATE_NAMES.unknown}; + } +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/states.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/states.ts new file mode 100644 index 0000000000000..b5e32420709eb --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/states.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STATE_NAMES = { + running: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateRunning', { + defaultMessage: 'Running', + }), + sleeping: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateSleeping', { + defaultMessage: 'Sleeping', + }), + dead: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateDead', { + defaultMessage: 'Dead', + }), + stopped: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateStopped', { + defaultMessage: 'Stopped', + }), + idle: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateIdle', { + defaultMessage: 'Idle', + }), + zombie: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateZombie', { + defaultMessage: 'Zombie', + }), + unknown: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateUnknown', { + defaultMessage: 'Unknown', + }), +}; + +export const STATE_ORDER = ['running', 'sleeping', 'stopped', 'idle', 'dead', 'zombie', 'unknown']; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx new file mode 100644 index 0000000000000..59becb0bf534d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { mapValues, countBy } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTable, EuiLoadingSpinner, EuiBasicTableColumn } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; +import { parseProcessList } from './parse_process_list'; +import { STATE_NAMES } from './states'; + +interface Props { + processList: ProcessListAPIResponse; + isLoading: boolean; +} + +type SummaryColumn = { + total: number; +} & Record; + +export const SummaryTable = ({ processList, isLoading }: Props) => { + const parsedList = parseProcessList(processList); + const processCount = useMemo( + () => + [ + { + total: isLoading ? -1 : parsedList.length, + ...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)), + ...(isLoading ? [] : countBy(parsedList, 'state')), + }, + ] as SummaryColumn[], + [parsedList, isLoading] + ); + return ( + + + + ); +}; + +const loadingRenderer = (value: number) => (value === -1 ? : value); + +const columns = [ + { + field: 'total', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.headingTotalProcesses', { + defaultMessage: 'Total processes', + }), + width: 125, + render: loadingRenderer, + }, + ...Object.entries(STATE_NAMES).map(([field, name]) => ({ field, name, render: loadingRenderer })), +] as Array>; + +const LoadingSpinner = euiStyled(EuiLoadingSpinner).attrs({ size: 'm' })` + margin-top: 2px; + margin-bottom: 3px; +`; + +const StyleWrapper = euiStyled.div` + & .euiTableHeaderCell { + border-bottom: none; + & .euiTableCellContent { + padding-bottom: 0; + } + & .euiTableCellContent__text { + font-size: ${(props) => props.theme.eui.euiFontSizeS}; + } + } + + & .euiTableRowCell { + border-top: none; + & .euiTableCellContent { + padding-top: 0; + } + } +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/types.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/types.ts new file mode 100644 index 0000000000000..d483fe510c944 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsExplorerSeries } from '../../../../../../../../common/http_api'; +import { STATE_NAMES } from './states'; + +export interface Process { + command: string; + cpu: number; + memory: number; + startTime: number; + state: keyof typeof STATE_NAMES; + pid: number; + user: string; + timeseries: { + [x: string]: MetricsExplorerSeries; + }; + apmTrace?: string; // Placeholder +} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx index 241ad7104836e..7386fa64aca9c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx @@ -15,6 +15,13 @@ export interface TabProps { nodeType: InventoryItemType; } +export const OVERLAY_Y_START = 266; +export const OVERLAY_BOTTOM_MARGIN = 16; +export const OVERLAY_HEADER_SIZE = 96; +const contentHeightOffset = OVERLAY_Y_START + OVERLAY_BOTTOM_MARGIN + OVERLAY_HEADER_SIZE; export const TabContent = euiStyled.div` - padding: ${(props) => props.theme.eui.paddingSizes.l}; + padding: ${(props) => props.theme.eui.paddingSizes.s}; + height: calc(100vh - ${contentHeightOffset}px); + overflow-y: auto; + overflow-x: hidden; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts new file mode 100644 index 0000000000000..8e0843fe8b278 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { useEffect } from 'react'; +import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../../common/http_api'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; + +export function useProcessList( + hostTerm: Record, + indexPattern: string, + timefield: string, + to: number +) { + const decodeResponse = (response: any) => { + return pipe( + ProcessListAPIResponseRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); + }; + + const timerange = { + field: timefield, + interval: 'modules', + to, + from: to - 15 * 60 * 1000, // 15 minutes + }; + + const { error, loading, response, makeRequest } = useHTTPRequest( + '/api/metrics/process_list', + 'POST', + JSON.stringify({ + hostTerm, + timerange, + indexPattern, + }), + decodeResponse + ); + + useEffect(() => { + makeRequest(); + }, [makeRequest]); + + return { + error, + loading, + response, + makeRequest, + }; +} diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 49fe55e3dee01..2bf5687da7e08 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -41,6 +41,7 @@ import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './r import { initSourceRoute } from './routes/source'; import { initAlertPreviewRoute } from './routes/alerting'; import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; +import { initProcessListRoute } from './routes/process_list'; export const initInfraServer = (libs: InfraBackendLibs) => { const schema = makeExecutableSchema({ @@ -82,4 +83,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initLogSourceStatusRoutes(libs); initAlertPreviewRoute(libs); initGetLogAlertsChartPreviewDataRoute(libs); + initProcessListRoute(libs); }; diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts new file mode 100644 index 0000000000000..99e8b2e8f6ab1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ProcessListAPIRequest, MetricsAPIRequest } from '../../../common/http_api'; +import { getAllMetricsData } from '../../utils/get_all_metrics_data'; +import { query } from '../metrics'; +import { ESSearchClient } from '../metrics/types'; + +export const getProcessList = async ( + client: ESSearchClient, + { hostTerm, timerange, indexPattern }: ProcessListAPIRequest +) => { + const queryBody = { + timerange, + modules: ['system.cpu', 'system.memory'], + groupBy: ['system.process.cmdline'], + filters: [{ term: hostTerm }], + indexPattern, + limit: 9, + metrics: [ + { + id: 'cpu', + aggregations: { + cpu: { + avg: { + field: 'system.process.cpu.total.norm.pct', + }, + }, + }, + }, + { + id: 'memory', + aggregations: { + memory: { + avg: { + field: 'system.process.memory.rss.pct', + }, + }, + }, + }, + { + id: 'meta', + aggregations: { + meta: { + top_hits: { + size: 1, + sort: [{ [timerange.field]: { order: 'desc' } }], + _source: [ + 'system.process.cpu.start_time', + 'system.process.state', + 'process.pid', + 'user.name', + ], + }, + }, + }, + }, + ], + } as MetricsAPIRequest; + return await getAllMetricsData((body: MetricsAPIRequest) => query(client, body), queryBody); +}; diff --git a/x-pack/plugins/infra/server/routes/process_list/index.ts b/x-pack/plugins/infra/server/routes/process_list/index.ts new file mode 100644 index 0000000000000..9851613255d8d --- /dev/null +++ b/x-pack/plugins/infra/server/routes/process_list/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from '@hapi/boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { throwErrors } from '../../../common/runtime_types'; +import { createSearchClient } from '../../lib/create_search_client'; +import { getProcessList } from '../../lib/host_details/process_list'; +import { ProcessListAPIRequestRT, ProcessListAPIResponseRT } from '../../../common/http_api'; + +const escapeHatch = schema.object({}, { unknowns: 'allow' }); + +export const initProcessListRoute = (libs: InfraBackendLibs) => { + const { framework } = libs; + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/process_list', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { + try { + const options = pipe( + ProcessListAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessList(client, options); + + return response.ok({ + body: ProcessListAPIResponseRT.encode(processListResponse), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/utils/get_all_metrics_data.ts b/x-pack/plugins/infra/server/utils/get_all_metrics_data.ts new file mode 100644 index 0000000000000..cec58494d1b98 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/get_all_metrics_data.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAPIResponse, MetricsAPISeries } from '../../common/http_api/metrics_api'; + +export const getAllMetricsData = async ( + query: (options: Options) => Promise, + options: Options, + previousBuckets: MetricsAPISeries[] = [] +): Promise => { + const response = await query(options); + + // Nothing available, return the previous buckets. + if (response.series.length === 0) { + return previousBuckets; + } + + const currentBuckets = response.series; + + // if there are no currentBuckets then we are finished paginating through the results + if (!response.info.afterKey) { + return previousBuckets.concat(currentBuckets); + } + + // There is possibly more data, concat previous and current buckets and call ourselves recursively. + const newOptions = { + ...options, + afterKey: response.info.afterKey, + }; + return getAllMetricsData(query, newOptions, previousBuckets.concat(currentBuckets)); +}; From 7a7057eba7270042a230ff4ac4f2404145357312 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 18 Nov 2020 10:49:26 -0600 Subject: [PATCH 69/99] [ML] Performance improvements to annotations editing in Single Metric Viewer & buttons placement (#83216) --- .../__snapshots__/index.test.tsx.snap | 3 - .../annotation_flyout/index.test.tsx | 122 ++++++++++++------ .../annotations/annotation_flyout/index.tsx | 61 +++++---- .../annotations_table.test.js.snap | 6 +- .../annotations_table/annotations_table.js | 22 ++-- .../ml/ml_annotation_updates_context.ts | 14 ++ .../application/routing/routes/explorer.tsx | 9 +- .../application/routing/routes/jobs_list.tsx | 9 +- .../routing/routes/timeseriesexplorer.tsx | 16 ++- .../services/annotations_service.test.tsx | 19 ++- .../services/annotations_service.tsx | 26 +++- .../timeseries_chart/timeseries_chart.d.ts | 8 +- .../timeseries_chart/timeseries_chart.js | 27 ++-- .../timeseries_chart_annotations.ts | 14 +- .../timeseries_chart_with_tooltip.tsx | 6 +- .../timeseriesexplorer/timeseriesexplorer.js | 1 - 16 files changed, 244 insertions(+), 119 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/ml/public/application/contexts/ml/ml_annotation_updates_context.ts diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap deleted file mode 100644 index dba73c246c3d0..0000000000000 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AnnotationFlyout Initialization. 1`] = `""`; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx index a4d2cd6b091a8..5ad175e2792b7 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.test.tsx @@ -5,58 +5,102 @@ */ import useObservable from 'react-use/lib/useObservable'; - import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json'; - import React from 'react'; -import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; import { Annotation } from '../../../../../common/types/annotations'; -import { annotation$ } from '../../../services/annotations_service'; +import { AnnotationUpdatesService } from '../../../services/annotations_service'; import { AnnotationFlyout } from './index'; +import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; + +jest.mock('../../../util/dependency_cache', () => ({ + getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }), +})); + +const MlAnnotationUpdatesContextProvider = ({ + annotationUpdatesService, + children, +}: { + annotationUpdatesService: AnnotationUpdatesService; + children: React.ReactElement; +}) => { + return ( + + {children} + + ); +}; + +const ObservableComponent = (props: any) => { + const { annotationUpdatesService } = props; + const annotationProp = useObservable(annotationUpdatesService!.isAnnotationInitialized$()); + if (annotationProp === undefined) { + return null; + } + return ( + + ); +}; describe('AnnotationFlyout', () => { - test('Initialization.', () => { - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); + let annotationUpdatesService: AnnotationUpdatesService | null = null; + beforeEach(() => { + annotationUpdatesService = new AnnotationUpdatesService(); }); - test('Update button is disabled with empty annotation', () => { + test('Update button is disabled with empty annotation', async () => { const annotation = mockAnnotations[1] as Annotation; - annotation$.next(annotation); - - // useObservable wraps the observable in a new component - const ObservableComponent = (props: any) => { - const annotationProp = useObservable(annotation$); - if (annotationProp === undefined) { - return null; - } - return ; - }; - - const wrapper = mountWithIntl(); - const updateBtn = wrapper.find('EuiButton').first(); - expect(updateBtn.prop('isDisabled')).toEqual(true); + + annotationUpdatesService!.setValue(annotation); + + const { getByTestId } = render( + + + + ); + const updateBtn = getByTestId('annotationFlyoutUpdateButton'); + expect(updateBtn).toBeDisabled(); }); - test('Error displayed and update button displayed if annotation text is longer than max chars', () => { + test('Error displayed and update button displayed if annotation text is longer than max chars', async () => { const annotation = mockAnnotations[2] as Annotation; - annotation$.next(annotation); - - // useObservable wraps the observable in a new component - const ObservableComponent = (props: any) => { - const annotationProp = useObservable(annotation$); - if (annotationProp === undefined) { - return null; - } - return ; - }; - - const wrapper = mountWithIntl(); - const updateBtn = wrapper.find('EuiButton').first(); - expect(updateBtn.prop('isDisabled')).toEqual(true); - - expect(wrapper.find('EuiFormErrorText')).toHaveLength(1); + annotationUpdatesService!.setValue(annotation); + + const { getByTestId } = render( + + + + ); + const updateBtn = getByTestId('annotationFlyoutUpdateButton'); + expect(updateBtn).toBeDisabled(); + await waitFor(() => { + const errorText = screen.queryByText(/characters above maximum length/); + expect(errorText).not.toBe(undefined); + }); + }); + + test('Flyout disappears when annotation is updated', async () => { + const annotation = mockAnnotations[0] as Annotation; + + annotationUpdatesService!.setValue(annotation); + + const { getByTestId } = render( + + + + ); + const updateBtn = getByTestId('annotationFlyoutUpdateButton'); + expect(updateBtn).not.toBeDisabled(); + expect(screen.queryByTestId('mlAnnotationFlyout')).toBeInTheDocument(); + + await fireEvent.click(updateBtn); + expect(screen.queryByTestId('mlAnnotationFlyout')).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx index 84abe3ed8a821..88996772f49d6 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, FC, ReactNode, useCallback } from 'react'; +import React, { Component, FC, ReactNode, useCallback, useContext } from 'react'; import useObservable from 'react-use/lib/useObservable'; import * as Rx from 'rxjs'; import { cloneDeep } from 'lodash'; @@ -28,15 +28,14 @@ import { import { CommonProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { distinctUntilChanged } from 'rxjs/operators'; import { ANNOTATION_MAX_LENGTH_CHARS, ANNOTATION_EVENT_USER, } from '../../../../../common/constants/annotations'; import { - annotation$, annotationsRefreshed, AnnotationState, + AnnotationUpdatesService, } from '../../../services/annotations_service'; import { AnnotationDescriptionList } from '../annotation_description_list'; import { DeleteAnnotationModal } from '../delete_annotation_modal'; @@ -48,6 +47,7 @@ import { } from '../../../../../common/types/annotations'; import { PartitionFieldsType } from '../../../../../common/types/anomalies'; import { PARTITION_FIELDS } from '../../../../../common/constants/anomalies'; +import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; interface ViewableDetector { index: number; @@ -67,6 +67,7 @@ interface Props { }; detectorIndex: number; detectors: ViewableDetector[]; + annotationUpdatesService: AnnotationUpdatesService; } interface State { @@ -85,7 +86,8 @@ export class AnnotationFlyoutUI extends Component { public annotationSub: Rx.Subscription | null = null; componentDidMount() { - this.annotationSub = annotation$.subscribe((v) => { + const { annotationUpdatesService } = this.props; + this.annotationSub = annotationUpdatesService.update$().subscribe((v) => { this.setState({ annotationState: v, }); @@ -100,15 +102,17 @@ export class AnnotationFlyoutUI extends Component { if (this.state.annotationState === null) { return; } + const { annotationUpdatesService } = this.props; - annotation$.next({ + annotationUpdatesService.setValue({ ...this.state.annotationState, annotation: e.target.value, }); }; public cancelEditingHandler = () => { - annotation$.next(null); + const { annotationUpdatesService } = this.props; + annotationUpdatesService.setValue(null); }; public deleteConfirmHandler = () => { @@ -148,7 +152,10 @@ export class AnnotationFlyoutUI extends Component { } this.closeDeleteModal(); - annotation$.next(null); + + const { annotationUpdatesService } = this.props; + + annotationUpdatesService.setValue(null); annotationsRefreshed(); }; @@ -193,7 +200,8 @@ export class AnnotationFlyoutUI extends Component { public saveOrUpdateAnnotation = () => { const { annotationState: originalAnnotation } = this.state; - const { chartDetails, detectorIndex } = this.props; + const { chartDetails, detectorIndex, annotationUpdatesService } = this.props; + if (originalAnnotation === null) { return; } @@ -218,8 +226,7 @@ export class AnnotationFlyoutUI extends Component { } // Mark the annotation created by `user` if and only if annotation is being created, not updated annotation.event = annotation.event ?? ANNOTATION_EVENT_USER; - - annotation$.next(null); + annotationUpdatesService.setValue(null); ml.annotations .indexAnnotation(annotation) @@ -356,16 +363,16 @@ export class AnnotationFlyoutUI extends Component { - + - + - + {isExistingAnnotation && ( { )} - + {isExistingAnnotation ? ( { } export const AnnotationFlyout: FC = (props) => { - const annotationProp = useObservable( - annotation$.pipe( - distinctUntilChanged((prev, curr) => { - // prevent re-rendering - return prev !== null && curr !== null; - }) - ) - ); + const annotationUpdatesService = useContext(MlAnnotationUpdatesContext); + const annotationProp = useObservable(annotationUpdatesService.isAnnotationInitialized$()); const cancelEditingHandler = useCallback(() => { - annotation$.next(null); + annotationUpdatesService.setValue(null); }, []); if (annotationProp === undefined || annotationProp === null) { @@ -423,7 +429,12 @@ export const AnnotationFlyout: FC = (props) => { const isExistingAnnotation = typeof annotationProp._id !== 'undefined'; return ( - +

@@ -441,7 +452,7 @@ export const AnnotationFlyout: FC = (props) => {

- +
); }; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap index 114a6b235d1ad..0c6fa6669c2eb 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` - annotation$.next(originalAnnotation ?? annotation)} + onClick={() => annotationUpdatesService.setValue(originalAnnotation ?? annotation)} iconType="pencil" aria-label={editAnnotationsTooltipAriaLabelText} /> @@ -693,4 +694,7 @@ class AnnotationsTableUI extends Component { } } -export const AnnotationsTable = withKibana(AnnotationsTableUI); +export const AnnotationsTable = withKibana((props) => { + const annotationUpdatesService = useContext(MlAnnotationUpdatesContext); + return ; +}); diff --git a/x-pack/plugins/ml/public/application/contexts/ml/ml_annotation_updates_context.ts b/x-pack/plugins/ml/public/application/contexts/ml/ml_annotation_updates_context.ts new file mode 100644 index 0000000000000..37dea3029c8ad --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/ml/ml_annotation_updates_context.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createContext } from 'react'; +import { AnnotationUpdatesService } from '../../services/annotations_service'; + +export type MlAnnotationUpdatesContextValue = AnnotationUpdatesService; + +export const MlAnnotationUpdatesContext = createContext( + new AnnotationUpdatesService() +); diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index cb6944e0ecf05..b91a5bd4a1aa4 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useState, useCallback } from 'react'; +import React, { FC, useEffect, useState, useCallback, useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; @@ -34,6 +34,8 @@ import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; import { isViewBySwimLaneData } from '../../explorer/swimlane_container'; import { JOB_ID } from '../../../../common/constants/anomalies'; +import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context'; +import { AnnotationUpdatesService } from '../../services/annotations_service'; export const explorerRouteFactory = ( navigateToPath: NavigateToPath, @@ -59,10 +61,13 @@ const PageWrapper: FC = ({ deps }) => { jobs: mlJobService.loadJobsWrapper, jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()), }); + const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []); return ( - + + + ); }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx index 2863e59508e35..d91ec27d9a505 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, FC } from 'react'; +import React, { useEffect, FC, useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; @@ -19,6 +19,8 @@ import { basicResolvers } from '../resolvers'; import { JobsPage } from '../../jobs/jobs_list'; import { useTimefilter } from '../../contexts/kibana'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; +import { AnnotationUpdatesService } from '../../services/annotations_service'; +import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context'; export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ path: '/jobs', @@ -57,10 +59,13 @@ const PageWrapper: FC = ({ deps }) => { setGlobalState({ refreshInterval }); timefilter.setRefreshInterval(refreshInterval); }, []); + const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []); return ( - + + + ); }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 9331fdc04b7bb..2653781ce1a30 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -5,7 +5,7 @@ */ import { isEqual } from 'lodash'; -import React, { FC, useCallback, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import moment from 'moment'; @@ -39,7 +39,8 @@ import { basicResolvers } from '../resolvers'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; import { useToastNotificationService } from '../../services/toast_notification_service'; - +import { AnnotationUpdatesService } from '../../services/annotations_service'; +import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context'; export const timeSeriesExplorerRouteFactory = ( navigateToPath: NavigateToPath, basePath: string @@ -64,13 +65,16 @@ const PageWrapper: FC = ({ deps }) => { jobs: mlJobService.loadJobsWrapper, jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()), }); + const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []); return ( - + + + ); }; diff --git a/x-pack/plugins/ml/public/application/services/annotations_service.test.tsx b/x-pack/plugins/ml/public/application/services/annotations_service.test.tsx index 2ba54d243ed1b..969748acc6af8 100644 --- a/x-pack/plugins/ml/public/application/services/annotations_service.test.tsx +++ b/x-pack/plugins/ml/public/application/services/annotations_service.test.tsx @@ -7,20 +7,29 @@ import mockAnnotations from '../components/annotations/annotations_table/__mocks__/mock_annotations.json'; import { Annotation } from '../../../common/types/annotations'; -import { annotation$, annotationsRefresh$, annotationsRefreshed } from './annotations_service'; - +import { + annotationsRefresh$, + annotationsRefreshed, + AnnotationUpdatesService, +} from './annotations_service'; describe('annotations_service', () => { - test('annotation$', () => { + let annotationUpdatesService: AnnotationUpdatesService | null = null; + + beforeEach(() => { + annotationUpdatesService = new AnnotationUpdatesService(); + }); + + test('annotationUpdatesService', () => { const subscriber = jest.fn(); - annotation$.subscribe(subscriber); + annotationUpdatesService!.update$().subscribe(subscriber); // the subscriber should have been triggered with the initial value of null expect(subscriber.mock.calls).toHaveLength(1); expect(subscriber.mock.calls[0][0]).toBe(null); const annotation = mockAnnotations[0] as Annotation; - annotation$.next(annotation); + annotationUpdatesService!.setValue(annotation); // the subscriber should have been triggered with the updated annotation value expect(subscriber.mock.calls).toHaveLength(2); diff --git a/x-pack/plugins/ml/public/application/services/annotations_service.tsx b/x-pack/plugins/ml/public/application/services/annotations_service.tsx index 6493770156cb8..208c3b6ca5827 100644 --- a/x-pack/plugins/ml/public/application/services/annotations_service.tsx +++ b/x-pack/plugins/ml/public/application/services/annotations_service.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BehaviorSubject } from 'rxjs'; - +import { BehaviorSubject, Observable } from 'rxjs'; +import { distinctUntilChanged } from 'rxjs/operators'; import { Annotation } from '../../../common/types/annotations'; /* @@ -79,3 +79,25 @@ export const annotation$ = new BehaviorSubject(null); */ export const annotationsRefresh$ = new BehaviorSubject(Date.now()); export const annotationsRefreshed = () => annotationsRefresh$.next(Date.now()); + +export class AnnotationUpdatesService { + private _annotation$: BehaviorSubject = new BehaviorSubject( + null + ); + + public update$() { + return this._annotation$.asObservable(); + } + public isAnnotationInitialized$(): Observable { + return this._annotation$.asObservable().pipe( + distinctUntilChanged((prev, curr) => { + // prevent re-rendering + return prev !== null && curr !== null; + }) + ); + } + + public setValue(annotation: AnnotationState) { + this._annotation$.next(annotation); + } +} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts index 04b666b4fc684..f58a399f5e3de 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts @@ -10,6 +10,7 @@ import React from 'react'; import { Annotation } from '../../../../../common/types/annotations'; import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; import { ChartTooltipService } from '../../../components/chart_tooltip'; +import { AnnotationState, AnnotationUpdatesService } from '../../../services/annotations_service'; interface Props { selectedJob: CombinedJob; @@ -47,6 +48,11 @@ interface TimeseriesChartProps { tooltipService: object; } -declare class TimeseriesChart extends React.Component { +interface TimeseriesChartIntProps { + annotationUpdatesService: AnnotationUpdatesService; + annotationProps: AnnotationState; +} + +declare class TimeseriesChart extends React.Component { focusXScale: d3.scale.Ordinal<{}, number>; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index b2d054becbb1a..6f2beb8fe9067 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -10,7 +10,7 @@ */ import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, useContext } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { isEqual, reduce, each, get } from 'lodash'; import d3 from 'd3'; @@ -21,7 +21,6 @@ import { getSeverityWithLow, getMultiBucketImpactLabel, } from '../../../../../common/util/anomaly_utils'; -import { annotation$ } from '../../../services/annotations_service'; import { formatValue } from '../../../formatters/format_value'; import { LINE_CHART_ANOMALY_RADIUS, @@ -51,7 +50,7 @@ import { unhighlightFocusChartAnnotation, ANNOTATION_MIN_WIDTH, } from './timeseries_chart_annotations'; -import { distinctUntilChanged } from 'rxjs/operators'; +import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; const focusZoomPanelHeight = 25; const focusChartHeight = 310; @@ -571,7 +570,6 @@ class TimeseriesChartIntl extends Component { } renderFocusChart() { - console.log('renderFocusChart'); const { focusAggregationInterval, focusAnnotationData: focusAnnotationDataOriginalPropValue, @@ -742,7 +740,8 @@ class TimeseriesChartIntl extends Component { this.focusXScale, showAnnotations, showFocusChartTooltip, - hideFocusChartTooltip + hideFocusChartTooltip, + this.props.annotationUpdatesService ); // disable brushing (creation of annotations) when annotations aren't shown @@ -1800,17 +1799,17 @@ class TimeseriesChartIntl extends Component { } export const TimeseriesChart = (props) => { - const annotationProp = useObservable( - annotation$.pipe( - distinctUntilChanged((prev, curr) => { - // prevent re-rendering - return prev !== null && curr !== null; - }) - ) - ); + const annotationUpdatesService = useContext(MlAnnotationUpdatesContext); + const annotationProp = useObservable(annotationUpdatesService.isAnnotationInitialized$()); if (annotationProp === undefined) { return null; } - return ; + return ( + + ); }; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts index bd86d07dcd8b7..8757fbad19df3 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts @@ -13,13 +13,14 @@ import { Dictionary } from '../../../../../common/types/common'; import { TimeseriesChart } from './timeseries_chart'; -import { annotation$ } from '../../../services/annotations_service'; +import { AnnotationUpdatesService } from '../../../services/annotations_service'; export const ANNOTATION_MASK_ID = 'mlAnnotationMask'; // getAnnotationBrush() is expected to be called like getAnnotationBrush.call(this) // so it gets passed on the context of the component it gets called from. export function getAnnotationBrush(this: TimeseriesChart) { + const { annotationUpdatesService } = this.props; const focusXScale = this.focusXScale; const annotateBrush = d3.svg.brush().x(focusXScale).on('brushend', brushend.bind(this)); @@ -35,7 +36,7 @@ export function getAnnotationBrush(this: TimeseriesChart) { const endTimestamp = extent[1].getTime(); if (timestamp === endTimestamp) { - annotation$.next(null); + annotationUpdatesService.setValue(null); return; } @@ -47,7 +48,7 @@ export function getAnnotationBrush(this: TimeseriesChart) { type: ANNOTATION_TYPE.ANNOTATION, }; - annotation$.next(annotation); + annotationUpdatesService.setValue(annotation); } return annotateBrush; @@ -105,7 +106,8 @@ export function renderAnnotations( focusXScale: TimeseriesChart['focusXScale'], showAnnotations: boolean, showFocusChartTooltip: (d: Annotation, t: object) => {}, - hideFocusChartTooltip: () => void + hideFocusChartTooltip: () => void, + annotationUpdatesService: AnnotationUpdatesService ) { const upperRectMargin = ANNOTATION_UPPER_RECT_MARGIN; const upperTextMargin = ANNOTATION_UPPER_TEXT_MARGIN; @@ -153,9 +155,9 @@ export function renderAnnotations( // clear a possible existing annotation set up for editing before setting the new one. // this needs to be done explicitly here because a new annotation created using the brush tool // could still be present in the chart. - annotation$.next(null); + annotationUpdatesService.setValue(null); // set the actual annotation and trigger the flyout - annotation$.next(d); + annotationUpdatesService.setValue(d); }); rects diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx index 89e7d292dbdf2..23e7740dd048e 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useEffect, useState, useCallback } from 'react'; +import React, { FC, useEffect, useState, useCallback, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { MlTooltipComponent } from '../../../components/chart_tooltip'; import { TimeseriesChart } from './timeseries_chart'; @@ -16,6 +16,7 @@ import { useMlKibana, useNotifications } from '../../../contexts/kibana'; import { getBoundsRoundedToInterval } from '../../../util/time_buckets'; import { ANNOTATION_EVENT_USER } from '../../../../../common/constants/annotations'; import { getControlsForDetector } from '../../get_controls_for_detector'; +import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context'; interface TimeSeriesChartWithTooltipsProps { bounds: any; @@ -50,6 +51,8 @@ export const TimeSeriesChartWithTooltips: FC = }, } = useMlKibana(); + const annotationUpdatesService = useContext(MlAnnotationUpdatesContext); + const [annotationData, setAnnotationData] = useState([]); const showAnnotationErrorToastNotification = useCallback((error?: string) => { @@ -123,6 +126,7 @@ export const TimeSeriesChartWithTooltips: FC = {(tooltipService) => ( {fieldNamesWithEmptyValues.length > 0 && ( From 77da781144c9d694da4b605c36364237191f1167 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Wed, 18 Nov 2020 10:57:22 -0600 Subject: [PATCH 70/99] [ML] Persist URL state for Anomaly detection jobs using metric function (#83507) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/common/types/ml_url_generator.ts | 2 + .../routing/routes/timeseriesexplorer.tsx | 15 +++ .../plot_function_controls.tsx | 66 +++++++++++- .../series_controls/series_controls.tsx | 5 +- .../get_function_description.ts | 19 +++- .../timeseriesexplorer/timeseriesexplorer.js | 100 ++++++------------ .../timeseriesexplorer_constants.ts | 1 + .../get_viewable_detectors.ts | 29 +++++ .../anomaly_detection_urls_generator.ts | 5 + 9 files changed, 167 insertions(+), 75 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors.ts diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index b188ac0a87571..9a3d8fc4a4f02 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -132,6 +132,7 @@ export interface TimeSeriesExplorerAppState { forecastId?: string; detectorIndex?: number; entities?: Record; + functionDescription?: string; }; query?: any; } @@ -145,6 +146,7 @@ export interface TimeSeriesExplorerPageState entities?: Record; forecastId?: string; globalState?: MlCommonGlobalState; + functionDescription?: string; } export type TimeSeriesExplorerUrlState = MLPageState< diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 2653781ce1a30..f0fb4558bcfa9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -161,6 +161,11 @@ export const TimeSeriesExplorerUrlStateManager: FC ); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx index 0356c20fecb9a..8e26a912a6051 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/plot_function_controls/plot_function_controls.tsx @@ -3,9 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { mlJobService } from '../../../services/job_service'; +import { getFunctionDescription, isMetricDetector } from '../../get_function_description'; +import { useToastNotificationService } from '../../../services/toast_notification_service'; +import { ML_JOB_AGGREGATION } from '../../../../../common/constants/aggregation_types'; +import type { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; const plotByFunctionOptions = [ { @@ -30,11 +35,70 @@ const plotByFunctionOptions = [ export const PlotByFunctionControls = ({ functionDescription, setFunctionDescription, + selectedDetectorIndex, + selectedJobId, + selectedEntities, }: { functionDescription: undefined | string; setFunctionDescription: (func: string) => void; + selectedDetectorIndex: number; + selectedJobId: string; + selectedEntities: Record; }) => { + const toastNotificationService = useToastNotificationService(); + + const getFunctionDescriptionToPlot = useCallback( + async ( + _selectedDetectorIndex: number, + _selectedEntities: Record, + _selectedJobId: string, + _selectedJob: CombinedJob + ) => { + const functionToPlot = await getFunctionDescription( + { + selectedDetectorIndex: _selectedDetectorIndex, + selectedEntities: _selectedEntities, + selectedJobId: _selectedJobId, + selectedJob: _selectedJob, + }, + toastNotificationService + ); + setFunctionDescription(functionToPlot); + }, + [setFunctionDescription, toastNotificationService] + ); + + useEffect(() => { + if (functionDescription !== undefined) { + return; + } + const selectedJob = mlJobService.getJob(selectedJobId); + if ( + // set if only entity controls are picked + selectedEntities !== undefined && + functionDescription === undefined && + isMetricDetector(selectedJob, selectedDetectorIndex) + ) { + const detector = selectedJob.analysis_config.detectors[selectedDetectorIndex]; + if (detector?.function === ML_JOB_AGGREGATION.METRIC) { + getFunctionDescriptionToPlot( + selectedDetectorIndex, + selectedEntities, + selectedJobId, + selectedJob + ); + } + } + }, [ + setFunctionDescription, + selectedDetectorIndex, + selectedEntities, + selectedJobId, + functionDescription, + ]); + if (functionDescription === undefined) return null; + return ( = selectedDetectorIndex) { + const detector = selectedJob.analysis_config.detectors[selectedDetectorIndex]; + if (detector?.function === ML_JOB_AGGREGATION.METRIC) { + return true; + } + } + return false; +} /** * Get the function description from the record with the highest anomaly score @@ -31,11 +43,7 @@ export const getFunctionDescription = async ( ) => { // if the detector's function is metric, fetch the highest scoring anomaly record // and set to plot the function_description (avg/min/max) of that record by default - if ( - selectedJob?.analysis_config?.detectors[selectedDetectorIndex]?.function !== - ML_JOB_AGGREGATION.METRIC - ) - return; + if (!isMetricDetector(selectedJob, selectedDetectorIndex)) return; const entityControls = getControlsForDetector( selectedDetectorIndex, @@ -43,6 +51,7 @@ export const getFunctionDescription = async ( selectedJobId ); const criteriaFields = getCriteriaFields(selectedDetectorIndex, entityControls); + try { const resp = await mlResultsService .getRecordsForCriteria([selectedJob.job_id], criteriaFields, 0, null, null, 1) diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index e3b6e38f47bab..f22cc191ef844 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -8,7 +8,7 @@ * React component for rendering Single Metric Viewer. */ -import { each, find, get, has, isEqual } from 'lodash'; +import { find, get, has, isEqual } from 'lodash'; import moment from 'moment-timezone'; import { Subject, Subscription, forkJoin } from 'rxjs'; import { map, debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators'; @@ -40,7 +40,6 @@ import { isModelPlotEnabled, isModelPlotChartableForDetector, isSourceDataChartableForDetector, - isTimeSeriesViewDetector, mlFunctionToESAggregation, } from '../../../common/util/job_utils'; @@ -84,7 +83,8 @@ import { SeriesControls } from './components/series_controls'; import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/timeseries_chart_with_tooltip'; import { PlotByFunctionControls } from './components/plot_function_controls'; import { aggregationTypeTransform } from '../../../common/util/anomaly_utils'; -import { getFunctionDescription } from './get_function_description'; +import { isMetricDetector } from './get_function_description'; +import { getViewableDetectors } from './timeseriesexplorer_utils/get_viewable_detectors'; // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be @@ -93,20 +93,6 @@ const allValuesLabel = i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionV defaultMessage: 'all', }); -export function getViewableDetectors(selectedJob) { - const jobDetectors = selectedJob.analysis_config.detectors; - const viewableDetectors = []; - each(jobDetectors, (dtr, index) => { - if (isTimeSeriesViewDetector(selectedJob, index)) { - viewableDetectors.push({ - index, - detector_description: dtr.detector_description, - }); - } - }); - return viewableDetectors; -} - function getTimeseriesexplorerDefaultState() { return { chartDetails: undefined, @@ -143,8 +129,6 @@ function getTimeseriesexplorerDefaultState() { zoomTo: undefined, zoomFromFocusLoaded: undefined, zoomToFocusLoaded: undefined, - // Sets function to plot by if original function is metric - functionDescription: undefined, }; } @@ -223,9 +207,7 @@ export class TimeSeriesExplorer extends React.Component { }; setFunctionDescription = (selectedFuction) => { - this.setState({ - functionDescription: selectedFuction, - }); + this.props.appStateHandler(APP_STATE_ACTION.SET_FUNCTION_DESCRIPTION, selectedFuction); }; previousChartProps = {}; @@ -280,9 +262,17 @@ export class TimeSeriesExplorer extends React.Component { * Gets focus data for the current component state/ */ getFocusData(selection) { - const { selectedJobId, selectedForecastId, selectedDetectorIndex } = this.props; - const { modelPlotEnabled, functionDescription } = this.state; + const { + selectedJobId, + selectedForecastId, + selectedDetectorIndex, + functionDescription, + } = this.props; + const { modelPlotEnabled } = this.state; const selectedJob = mlJobService.getJob(selectedJobId); + if (isMetricDetector(selectedJob, selectedDetectorIndex) && functionDescription === undefined) { + return; + } const entityControls = this.getControlsForDetector(); // Calculate the aggregation interval for the focus chart. @@ -333,8 +323,8 @@ export class TimeSeriesExplorer extends React.Component { selectedJobId, tableInterval, tableSeverity, + functionDescription, } = this.props; - const { functionDescription } = this.state; const selectedJob = mlJobService.getJob(selectedJobId); const entityControls = this.getControlsForDetector(); @@ -394,24 +384,6 @@ export class TimeSeriesExplorer extends React.Component { ); }; - getFunctionDescription = async () => { - const { selectedDetectorIndex, selectedEntities, selectedJobId } = this.props; - const selectedJob = mlJobService.getJob(selectedJobId); - - const functionDescriptionToPlot = await getFunctionDescription( - { - selectedDetectorIndex, - selectedEntities, - selectedJobId, - selectedJob, - }, - this.props.toastNotificationService - ); - if (!this.unmounted) { - this.setFunctionDescription(functionDescriptionToPlot); - } - }; - setForecastId = (forecastId) => { this.props.appStateHandler(APP_STATE_ACTION.SET_FORECAST_ID, forecastId); }; @@ -424,14 +396,22 @@ export class TimeSeriesExplorer extends React.Component { selectedForecastId, selectedJobId, zoom, + functionDescription, } = this.props; - const { loadCounter: currentLoadCounter, functionDescription } = this.state; + const { loadCounter: currentLoadCounter } = this.state; const currentSelectedJob = mlJobService.getJob(selectedJobId); if (currentSelectedJob === undefined) { return; } + if ( + isMetricDetector(currentSelectedJob, selectedDetectorIndex) && + functionDescription === undefined + ) { + return; + } + const functionToPlotByIfMetric = aggregationTypeTransform.toES(functionDescription); this.contextChartSelectedInitCallDone = false; @@ -845,7 +825,7 @@ export class TimeSeriesExplorer extends React.Component { this.componentDidUpdate(); } - componentDidUpdate(previousProps, previousState) { + componentDidUpdate(previousProps) { if (previousProps === undefined || previousProps.selectedJobId !== this.props.selectedJobId) { this.contextChartSelectedInitCallDone = false; this.setState({ fullRefresh: false, loading: true }, () => { @@ -853,15 +833,6 @@ export class TimeSeriesExplorer extends React.Component { }); } - if ( - previousProps === undefined || - previousProps.selectedJobId !== this.props.selectedJobId || - previousProps.selectedDetectorIndex !== this.props.selectedDetectorIndex || - !isEqual(previousProps.selectedEntities, this.props.selectedEntities) - ) { - this.getFunctionDescription(); - } - if ( previousProps === undefined || previousProps.selectedForecastId !== this.props.selectedForecastId @@ -885,7 +856,7 @@ export class TimeSeriesExplorer extends React.Component { !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || previousProps.selectedForecastId !== this.props.selectedForecastId || previousProps.selectedJobId !== this.props.selectedJobId || - previousState.functionDescription !== this.state.functionDescription + previousProps.functionDescription !== this.props.functionDescription ) { const fullRefresh = previousProps === undefined || @@ -894,7 +865,7 @@ export class TimeSeriesExplorer extends React.Component { !isEqual(previousProps.selectedEntities, this.props.selectedEntities) || previousProps.selectedForecastId !== this.props.selectedForecastId || previousProps.selectedJobId !== this.props.selectedJobId || - previousState.functionDescription !== this.state.functionDescription; + previousProps.functionDescription !== this.props.functionDescription; this.loadSingleMetricData(fullRefresh); } @@ -965,7 +936,6 @@ export class TimeSeriesExplorer extends React.Component { zoomTo, zoomFromFocusLoaded, zoomToFocusLoaded, - functionDescription, } = this.state; const chartProps = { modelPlotEnabled, @@ -1044,15 +1014,13 @@ export class TimeSeriesExplorer extends React.Component { selectedEntities={this.props.selectedEntities} bounds={bounds} > - {functionDescription && ( - - )} + {arePartitioningFieldsProvided && ( diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts index a801a1c5ce6f5..6cd58f42f929a 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_constants.ts @@ -15,6 +15,7 @@ export const APP_STATE_ACTION = { SET_FORECAST_ID: 'SET_FORECAST_ID', SET_ZOOM: 'SET_ZOOM', UNSET_ZOOM: 'UNSET_ZOOM', + SET_FUNCTION_DESCRIPTION: 'SET_FUNCTION_DESCRIPTION', }; export const CHARTS_POINT_TARGET = 500; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors.ts new file mode 100644 index 0000000000000..25d7751da8277 --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CombinedJob } from '../../../../common/types/anomaly_detection_jobs'; +import { isTimeSeriesViewDetector } from '../../../../common/util/job_utils'; + +interface ViewableDetector { + index: number; + detector_description: string | undefined; + function: string; +} +export function getViewableDetectors(selectedJob: CombinedJob): ViewableDetector[] { + const jobDetectors = selectedJob.analysis_config.detectors; + const viewableDetectors: ViewableDetector[] = []; + jobDetectors.forEach((dtr, index) => { + if (isTimeSeriesViewDetector(selectedJob, index)) { + viewableDetectors.push({ + index, + detector_description: dtr.detector_description, + function: dtr.function, + }); + } + }); + + return viewableDetectors; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index 6d7e286a29476..d53dfa8fd19c9 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -163,6 +163,7 @@ export function createSingleMetricViewerUrl( forecastId, entities, globalState, + functionDescription, } = params; let queryState: Partial = {}; @@ -189,6 +190,10 @@ export function createSingleMetricViewerUrl( if (entities !== undefined) { mlTimeSeriesExplorer.entities = entities; } + if (functionDescription !== undefined) { + mlTimeSeriesExplorer.functionDescription = functionDescription; + } + appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; if (zoom) appState.zoom = zoom; From 4917df30b93769f21a2cdba9faa6fddd25b2344d Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 18 Nov 2020 20:23:08 +0300 Subject: [PATCH 71/99] Update typescript eslint to v4.8 (#83520) * update deps * update rules use type-aware @typescript-eslint/no-shadow instead of no-shadow. do not use no-undef, rely on TypeScript instead * fix or mute all lint errors * react-hooks eslint plugin fails on ? syntax * fix wrong typings in viz * remove React as a global type * fix eslint errors * update version to 4.8.1 * fix a new error --- .eslintrc.js | 6 +- .../public/todo/todo.tsx | 2 +- .../with_data_services/components/app.tsx | 1 + package.json | 6 +- .../typescript.js | 6 +- src/core/public/utils/crypto/sha256.ts | 2 +- .../server/http/router/validator/validator.ts | 4 +- .../forms/hook_form_lib/hooks/use_field.ts | 6 +- ...ate_state_container_react_helpers.test.tsx | 6 +- .../kibana_utils/demos/state_sync/url.ts | 4 +- .../public/state_sync/state_sync.test.ts | 2 +- .../controls/has_extended_bounds.tsx | 1 - src/plugins/visualizations/public/vis.ts | 4 +- tsconfig.base.json | 1 - .../AgentConfigurationCreateEdit/index.tsx | 1 - .../TransactionActionMenu.tsx | 1 - x-pack/plugins/apm/typings/common.d.ts | 6 +- .../components/__tests__/app.test.tsx | 2 +- .../settings/__tests__/settings.test.tsx | 2 +- .../package_policy_input_config.tsx | 1 + .../package_policy_input_stream.tsx | 1 + .../step_select_agent_policy.tsx | 2 + .../sections/agents/agent_list_page/index.tsx | 1 + .../agent_reassign_policy_flyout/index.tsx | 1 + .../epm/components/package_list_grid.tsx | 2 +- .../template_form/template_form.tsx | 1 + .../inventory/components/expression.tsx | 1 - .../alerting/inventory/components/metric.tsx | 16 +-- .../components/expression_editor/editor.tsx | 2 - .../components/expression.tsx | 5 - .../infra/public/components/header/header.tsx | 3 +- .../saved_views/view_list_modal.tsx | 2 +- .../logs/log_filter/log_filter_state.ts | 1 - .../containers/logs/log_source/log_source.ts | 1 - .../containers/source/use_source_via_http.ts | 1 - .../hooks/use_bulk_get_saved_object.tsx | 1 - .../public/hooks/use_create_saved_object.tsx | 1 - .../public/hooks/use_delete_saved_object.tsx | 1 - .../public/hooks/use_find_saved_object.tsx | 1 - .../public/hooks/use_get_saved_object.tsx | 1 - .../public/hooks/use_update_saved_object.tsx | 1 - .../page_results_content.tsx | 1 - .../source_configuration_settings.tsx | 1 - .../components/node_details/overlay.tsx | 1 + .../metric_detail/components/sub_section.tsx | 1 - .../lens/public/app_plugin/app.test.tsx | 2 +- .../editor_frame/suggestion_panel.tsx | 1 + .../public/application/index.tsx | 1 + .../matrix_histogram/index.ts | 2 +- .../draggable_wrapper_hover_content.tsx | 2 - .../common/components/header_page/types.ts | 2 +- .../components/matrix_histogram/types.ts | 2 +- .../common/components/toasters/utils.ts | 2 +- .../public/common/store/types.ts | 2 +- .../alerts_histogram_panel/index.tsx | 1 - .../timeline_actions/alert_context_menu.tsx | 5 + .../detection_engine/rules/use_rules.tsx | 4 +- .../detection_engine/rules/all/reducer.ts | 2 +- .../detection_engine/rules/details/index.tsx | 1 - .../pages/endpoint_hosts/view/index.tsx | 1 + .../public/overview/components/types.ts | 2 +- .../public/resolver/store/data/selectors.ts | 22 ++-- .../public/resolver/store/selectors.ts | 4 +- .../public/resolver/store/ui/selectors.ts | 5 +- .../public/resolver/types.ts | 2 +- .../components/open_timeline/types.ts | 6 +- .../components/timeline/body/helpers.tsx | 2 +- .../body/renderers/column_renderer.ts | 2 +- .../expressions/boundary_index_expression.tsx | 2 + .../expressions/entity_by_expression.tsx | 1 + .../connector_add_modal.tsx | 1 + .../sections/alert_form/alert_add.tsx | 1 + .../common/lib/saved_object_test_utils.ts | 2 +- yarn.lock | 111 +++++++++--------- 74 files changed, 152 insertions(+), 155 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ad9de04251e4c..5ac1a79d03274 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -863,7 +863,8 @@ module.exports = { 'no-shadow-restricted-names': 'error', 'no-sparse-arrays': 'error', 'no-this-before-super': 'error', - 'no-undef': 'error', + // rely on typescript + 'no-undef': 'off', 'no-unreachable': 'error', 'no-unsafe-finally': 'error', 'no-useless-call': 'error', @@ -998,7 +999,8 @@ module.exports = { 'no-shadow-restricted-names': 'error', 'no-sparse-arrays': 'error', 'no-this-before-super': 'error', - 'no-undef': 'error', + // rely on typescript + 'no-undef': 'off', 'no-unreachable': 'error', 'no-unsafe-finally': 'error', 'no-useless-call': 'error', diff --git a/examples/state_containers_examples/public/todo/todo.tsx b/examples/state_containers_examples/public/todo/todo.tsx index b6f4f6550026b..fe597042d38c7 100644 --- a/examples/state_containers_examples/public/todo/todo.tsx +++ b/examples/state_containers_examples/public/todo/todo.tsx @@ -313,7 +313,7 @@ export const TodoAppPage: React.FC<{ function withDefaultState( stateContainer: BaseStateContainer, - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow defaultState: State ): INullableBaseStateContainer { return { diff --git a/examples/state_containers_examples/public/with_data_services/components/app.tsx b/examples/state_containers_examples/public/with_data_services/components/app.tsx index d007cfd97edca..8f444b96524c1 100644 --- a/examples/state_containers_examples/public/with_data_services/components/app.tsx +++ b/examples/state_containers_examples/public/with_data_services/components/app.tsx @@ -180,6 +180,7 @@ function useGlobalStateSyncing( }, [query, kbnUrlStateStorage]); } +// eslint-disable-next-line @typescript-eslint/no-shadow function useAppStateSyncing( appStateContainer: BaseStateContainer, query: DataPublicPluginStart['query'], diff --git a/package.json b/package.json index 2560be4f55d08..87e51abe49be3 100644 --- a/package.json +++ b/package.json @@ -567,8 +567,8 @@ "@types/xml2js": "^0.4.5", "@types/yauzl": "^2.9.1", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^3.10.0", - "@typescript-eslint/parser": "^3.10.0", + "@typescript-eslint/eslint-plugin": "^4.8.1", + "@typescript-eslint/parser": "^4.8.1", "@welldone-software/why-did-you-render": "^5.0.0", "@yarnpkg/lockfile": "^1.1.0", "abab": "^1.0.4", @@ -644,7 +644,7 @@ "eslint-plugin-prefer-object-spread": "^1.2.1", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.20.3", - "eslint-plugin-react-hooks": "^4.0.4", + "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-perf": "^3.2.3", "expose-loader": "^0.7.5", "faker": "1.1.0", diff --git a/packages/elastic-eslint-config-kibana/typescript.js b/packages/elastic-eslint-config-kibana/typescript.js index d3e80b7448151..b439f5297032b 100644 --- a/packages/elastic-eslint-config-kibana/typescript.js +++ b/packages/elastic-eslint-config-kibana/typescript.js @@ -189,6 +189,11 @@ module.exports = { '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-misused-new': 'error', '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-shadow': 'error', + // rely on typescript + '@typescript-eslint/no-undef': 'off', + 'no-undef': 'off', + '@typescript-eslint/triple-slash-reference': ['error', { path: 'never', types: 'never', @@ -218,7 +223,6 @@ module.exports = { 'no-eval': 'error', 'no-new-wrappers': 'error', 'no-script-url': 'error', - 'no-shadow': 'error', 'no-throw-literal': 'error', 'no-undef-init': 'error', 'no-unsafe-finally': 'error', diff --git a/src/core/public/utils/crypto/sha256.ts b/src/core/public/utils/crypto/sha256.ts index eaa057d604689..13e0d405a706b 100644 --- a/src/core/public/utils/crypto/sha256.ts +++ b/src/core/public/utils/crypto/sha256.ts @@ -130,7 +130,7 @@ type BufferEncoding = | 'binary' | 'hex'; -/* eslint-disable no-bitwise, no-shadow */ +/* eslint-disable no-bitwise, @typescript-eslint/no-shadow */ export class Sha256 { private _a: number; private _b: number; diff --git a/src/core/server/http/router/validator/validator.ts b/src/core/server/http/router/validator/validator.ts index babca87495a4e..be7781fdacbe0 100644 --- a/src/core/server/http/router/validator/validator.ts +++ b/src/core/server/http/router/validator/validator.ts @@ -143,8 +143,8 @@ export type RouteValidatorFullConfig = RouteValidatorConfig & * @internal */ export class RouteValidator

{ - public static from

( - opts: RouteValidator | RouteValidatorFullConfig + public static from<_P = {}, _Q = {}, _B = {}>( + opts: RouteValidator<_P, _Q, _B> | RouteValidatorFullConfig<_P, _Q, _B> ) { if (opts instanceof RouteValidator) { return opts; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index f4f13a698ee30..eb67842bff833 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -118,16 +118,16 @@ export const useField = ( * updating the "value" state. */ const formatInputValue = useCallback( - (inputValue: unknown): T => { + (inputValue: unknown): U => { const isEmptyString = typeof inputValue === 'string' && inputValue.trim() === ''; if (isEmptyString || !formatters) { - return inputValue as T; + return inputValue as U; } const formData = __getFormData$().value; - return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as T; + return formatters.reduce((output, formatter) => formatter(output, formData), inputValue) as U; }, [formatters, __getFormData$] ); diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx index 81101f3180738..48e5ee3c87e37 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx @@ -97,11 +97,11 @@ test('context receives stateContainer', () => { const { Provider, context } = createStateContainerReactHelpers(); ReactDOM.render( - /* eslint-disable no-shadow */ + /* eslint-disable @typescript-eslint/no-shadow */ {(stateContainer) => stateContainer.get().foo} , - /* eslint-enable no-shadow */ + /* eslint-enable @typescript-eslint/no-shadow */ container ); @@ -116,7 +116,7 @@ describe('hooks', () => { const stateContainer = createStateContainer({ foo: 'bar' }); const { Provider, useContainer } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow const stateContainer = useContainer(); return <>{stateContainer.get().foo}; }; diff --git a/src/plugins/kibana_utils/demos/state_sync/url.ts b/src/plugins/kibana_utils/demos/state_sync/url.ts index e8e63eefe866c..f7a66e79b8170 100644 --- a/src/plugins/kibana_utils/demos/state_sync/url.ts +++ b/src/plugins/kibana_utils/demos/state_sync/url.ts @@ -56,9 +56,9 @@ export const result = Promise.resolve() }); function withDefaultState( - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow stateContainer: BaseStateContainer, - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow defaultState: State ): INullableBaseStateContainer { return { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index 4b2b2bd99911b..f96c243e82f89 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -354,7 +354,7 @@ describe('state_sync', () => { function withDefaultState( stateContainer: BaseStateContainer, - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow defaultState: State ): INullableBaseStateContainer { return { diff --git a/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx b/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx index ae3da8e203a57..a316a087c8bcb 100644 --- a/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx @@ -38,7 +38,6 @@ function HasExtendedBoundsParamEditor(props: AggParamEditorProps) { setValue(value && agg.params.min_doc_count); } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [agg.params.min_doc_count, setValue, value]); return ( diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index cae9058071b6c..75c889af3d5c9 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -97,13 +97,13 @@ export class Vis { public readonly uiState: PersistedState; constructor(visType: string, visState: SerializedVis = {} as any) { - this.type = this.getType(visType); + this.type = this.getType(visType); this.params = this.getParams(visState.params); this.uiState = new PersistedState(visState.uiState); this.id = visState.id; } - private getType(visType: string) { + private getType(visType: string) { const type = getTypes().get(visType); if (!type) { const errorMessage = i18n.translate('visualizations.visualizationTypeInvalidMessage', { diff --git a/tsconfig.base.json b/tsconfig.base.json index 0aad8d6b9c124..111c9dbc949de 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -53,7 +53,6 @@ "types": [ "node", "jest", - "react", "flot", "jest-styled-components", "@testing-library/jest-dom" diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx index 1c42f146b867a..4f94f255a4e4c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -81,7 +81,6 @@ export function AgentConfigurationCreateEdit({ ..._newConfig, settings: existingConfig?.settings || {}, })); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [existingConfig]); // update newConfig when existingConfig has loaded diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 4a548b44cf361..3f72f07b2a7d2 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -65,7 +65,6 @@ export function TransactionActionMenu({ transaction }: Props) { { key: 'transaction.name', value: transaction?.transaction.name }, { key: 'transaction.type', value: transaction?.transaction.type }, ].filter((filter): filter is Filter => typeof filter.value === 'string'), - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [transaction] ); diff --git a/x-pack/plugins/apm/typings/common.d.ts b/x-pack/plugins/apm/typings/common.d.ts index 9133315c4c16a..fd5b5c8ea0876 100644 --- a/x-pack/plugins/apm/typings/common.d.ts +++ b/x-pack/plugins/apm/typings/common.d.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type { UnwrapPromise } from '@kbn/utility-types'; import '../../../typings/rison_node'; import '../../infra/types/eui'; // EUIBasicTable @@ -21,8 +21,6 @@ type AllowUnknownObjectProperties = T extends object } : T; -export type PromiseValueType = Value extends Promise - ? Value - : Value; +export type PromiseValueType> = UnwrapPromise; export type Maybe = T | null | undefined; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx index eaf45db0a0b93..755f6907a4d5b 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx @@ -39,7 +39,7 @@ jest.mock('../../supported_renderers'); jest.mock('@elastic/eui/lib/components/portal/portal', () => { // Local constants are not supported in Jest mocks-- they must be // imported within the mock. - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow const React = jest.requireActual('react'); return { EuiPortal: (props: any) =>

{props.children}
, diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx index 28aa6ef90aedb..a4f2aca9bd79c 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx @@ -25,7 +25,7 @@ jest.mock('@elastic/eui/lib/services/accessibility', () => { }; }); jest.mock('@elastic/eui/lib/components/portal/portal', () => { - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow const React = jest.requireActual('react'); return { EuiPortal: (props: any) =>
{props.children}
, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx index 177354dad14dc..75000ad7e1d3b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_config.tsx @@ -47,6 +47,7 @@ export const PackagePolicyInputConfig: React.FunctionComponent<{ const hasErrors = forceShowErrors && validationHasErrors(inputVarsValidationResults); const requiredVars: RegistryVarsEntry[] = []; + // eslint-disable-next-line react-hooks/exhaustive-deps const advancedVars: RegistryVarsEntry[] = []; if (packageInputVars) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx index 963d0da50ce7f..11d11ed33d5d9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_stream.tsx @@ -49,6 +49,7 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{ const hasErrors = forceShowErrors && validationHasErrors(inputStreamValidationResults); const requiredVars: RegistryVarsEntry[] = []; + // eslint-disable-next-line react-hooks/exhaustive-deps const advancedVars: RegistryVarsEntry[] = []; if (packageInputStream.vars && packageInputStream.vars.length) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx index 525a224146994..9c94bb939cdf8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx @@ -91,6 +91,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ sortOrder: 'asc', full: true, }); + // eslint-disable-next-line react-hooks/exhaustive-deps const agentPolicies = agentPoliciesData?.items || []; const agentPoliciesById = agentPolicies.reduce( (acc: { [key: string]: GetAgentPoliciesResponseItem }, policy) => { @@ -131,6 +132,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ } }, [selectedPolicyId, agentPolicy, updateAgentPolicy, setIsLoadingSecondStep]); + // eslint-disable-next-line react-hooks/exhaustive-deps const agentPolicyOptions: Array> = packageInfoData ? agentPolicies.map((agentConf) => { const alreadyHasLimitedPackage = diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index d46d2aa442745..1d08a1f791976 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -278,6 +278,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { perPage: 1000, }); + // eslint-disable-next-line react-hooks/exhaustive-deps const agentPolicies = agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []; const agentPoliciesIndexedById = useMemo(() => { return agentPolicies.reduce((acc, agentPolicy) => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_flyout/index.tsx index 20c1eb8ff9c50..46e291e73fa78 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_flyout/index.tsx @@ -49,6 +49,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ page: 1, perPage: 1000, }); + // eslint-disable-next-line react-hooks/exhaustive-deps const agentPolicies = agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []; useEffect(() => { if (!selectedAgentPolicyId && agentPolicies[0]) { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx index ef3b94081b1d8..b96fda2c23af1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx @@ -37,7 +37,7 @@ export function PackageListGrid({ isLoading, controls, title, list }: ListProps) const localSearchRef = useLocalSearch(list); const onQueryChange = ({ - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow query, queryText: userInput, error, diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 8e84abb5ce495..2fc0a260103f7 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -105,6 +105,7 @@ export const TemplateForm = ({ aliases: true, }); + // eslint-disable-next-line react-hooks/exhaustive-deps const indexTemplate = defaultValue ?? { name: '', indexPatterns: [], diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 097e0f1f1690b..e16b2aeaacac4 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -147,7 +147,6 @@ export const Expressions: React.FC = (props) => { timeUnit: timeUnit ?? defaultExpression.timeUnit, }); setAlertParams('criteria', exp); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [setAlertParams, alertParams.criteria, timeSize, timeUnit]); const removeExpression = useCallback( diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx index 2dd2938dfd55a..dac9f91c9bd29 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/metric.tsx @@ -91,6 +91,7 @@ export const MetricExpression = ({ const [selectedOption, setSelectedOption] = useState(metric?.value); const [fieldDisplayedCustomLabel, setFieldDisplayedCustomLabel] = useState(customMetric?.label); + // eslint-disable-next-line react-hooks/exhaustive-deps const firstFieldOption = { text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.metric.selectFieldLabel', { defaultMessage: 'Select a metric', @@ -106,16 +107,11 @@ export const MetricExpression = ({ [fields, customMetric?.field] ); - const expressionDisplayValue = useMemo( - () => { - return customMetricTabOpen - ? customMetric?.field && getCustomMetricLabel(customMetric) - : metric?.text || firstFieldOption.text; - }, - // The ?s are confusing eslint here, so... - // eslint-disable-next-line react-hooks/exhaustive-deps - [customMetricTabOpen, metric, customMetric, firstFieldOption] - ); + const expressionDisplayValue = useMemo(() => { + return customMetricTabOpen + ? customMetric?.field && getCustomMetricLabel(customMetric) + : metric?.text || firstFieldOption.text; + }, [customMetricTabOpen, metric, customMetric, firstFieldOption]); const onChangeTab = useCallback( (id) => { diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index 48639f3095d3d..662b7f68f8fec 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -157,7 +157,6 @@ export const Editor: React.FC = (props) => { } else { return []; } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [sourceStatus]); const groupByFields = useMemo(() => { @@ -168,7 +167,6 @@ export const Editor: React.FC = (props) => { } else { return []; } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [sourceStatus]); const updateThreshold = useCallback( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 92c0172703423..48e15e0026ff6 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -96,7 +96,6 @@ export const Expressions: React.FC = (props) => { aggregation: 'avg', }; } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [alertsContext.metadata]); const updateParams = useCallback( @@ -116,7 +115,6 @@ export const Expressions: React.FC = (props) => { timeUnit: timeUnit ?? defaultExpression.timeUnit, }); setAlertParams('criteria', exp); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [setAlertParams, alertParams.criteria, timeSize, timeUnit]); const removeExpression = useCallback( @@ -127,7 +125,6 @@ export const Expressions: React.FC = (props) => { setAlertParams('criteria', exp); } }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [setAlertParams, alertParams.criteria] ); @@ -172,7 +169,6 @@ export const Expressions: React.FC = (props) => { setTimeSize(ts || undefined); setAlertParams('criteria', criteria); }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertParams.criteria, setAlertParams] ); @@ -186,7 +182,6 @@ export const Expressions: React.FC = (props) => { setTimeUnit(tu as Unit); setAlertParams('criteria', criteria); }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [alertParams.criteria, setAlertParams] ); diff --git a/x-pack/plugins/infra/public/components/header/header.tsx b/x-pack/plugins/infra/public/components/header/header.tsx index 47ee1857da591..32ee6658ff1a8 100644 --- a/x-pack/plugins/infra/public/components/header/header.tsx +++ b/x-pack/plugins/infra/public/components/header/header.tsx @@ -17,6 +17,7 @@ interface HeaderProps { export const Header = ({ breadcrumbs = [], readOnlyBadge = false }: HeaderProps) => { const chrome = useKibana().services.chrome; + // eslint-disable-next-line react-hooks/exhaustive-deps const badge = readOnlyBadge ? { text: i18n.translate('xpack.infra.header.badge.readOnly.text', { @@ -31,12 +32,10 @@ export const Header = ({ breadcrumbs = [], readOnlyBadge = false }: HeaderProps) const setBreadcrumbs = useCallback(() => { return chrome?.setBreadcrumbs(breadcrumbs || []); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [breadcrumbs, chrome]); const setBadge = useCallback(() => { return chrome?.setBadge(badge); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [badge, chrome]); useEffect(() => { diff --git a/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx index 4015d64e1097f..374ba23f690e3 100644 --- a/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx @@ -60,7 +60,7 @@ export function SavedViewListModal diff --git a/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts b/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts index d5a43c0d6cffa..7c903f59002dc 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts @@ -82,7 +82,6 @@ export const useLogFilterState: (props: { } return true; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [filterQueryDraft]); const serializedFilterQuery = useMemo(() => (filterQuery ? filterQuery.serializedQuery : null), [ diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index 75c328b829397..f430e6b5e4d90 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -91,7 +91,6 @@ export const useLogSource = ({ sourceId, fetch }: { sourceId: string; fetch: Htt fields: sourceStatus?.logIndexFields ?? [], title: sourceConfiguration?.configuration.name ?? 'unknown', }), - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [sourceConfiguration, sourceStatus] ); diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts index 54d565d9ee223..94e2537a67a2a 100644 --- a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts +++ b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts @@ -76,7 +76,6 @@ export const useSourceViaHttp = ({ title: pickIndexPattern(response?.source, indexType), }; }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [response, type] ); diff --git a/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx index cfa9a711f7743..2a70edc9b9a57 100644 --- a/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx @@ -35,7 +35,6 @@ export const useBulkGetSavedObject = (type: string) => { }; fetchData(); }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [type, kibana.services.savedObjects] ); diff --git a/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx index 0efb862ad2eb4..8313d496a0651 100644 --- a/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_create_saved_object.tsx @@ -40,7 +40,6 @@ export const useCreateSavedObject = (type: string) => { }; save(); }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [type, kibana.services.savedObjects] ); diff --git a/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx index e353a79b19073..3f2d15b3b86aa 100644 --- a/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_delete_saved_object.tsx @@ -29,7 +29,6 @@ export const useDeleteSavedObject = (type: string) => { }; dobj(); }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [type, kibana.services.savedObjects] ); diff --git a/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx index 8aead6adfd0ab..7c179875442d1 100644 --- a/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_find_saved_object.tsx @@ -37,7 +37,6 @@ export const useFindSavedObject = }; fetchData(); }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [type, kibana.services.savedObjects] ); diff --git a/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx b/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx index 4c1e9ef7a6136..f5b51ee869fb7 100644 --- a/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx +++ b/x-pack/plugins/infra/public/hooks/use_update_saved_object.tsx @@ -40,7 +40,6 @@ export const useUpdateSavedObject = (type: string) => { }; save(); }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [type, kibana.services.savedObjects] ); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index 740fc8b7bafcd..98367335d9c2d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -77,7 +77,6 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent { const availableFields = useMemo( () => sourceStatus?.logIndexFields.map((field) => field.name) ?? [], - /* eslint-disable-next-line react-hooks/exhaustive-deps */ [sourceStatus] ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index af712c0611577..8b2140aa196b3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -33,6 +33,7 @@ export const NodeContextPopover = ({ options, onClose, }: Props) => { + // eslint-disable-next-line react-hooks/exhaustive-deps const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab]; const tabs = useMemo(() => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx index 88e7c0c08e441..4c75003616117 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx @@ -23,7 +23,6 @@ export const SubSection: FunctionComponent = ({ isLiveStreaming, stopLiveStreaming, }) => { - /* eslint-disable-next-line react-hooks/exhaustive-deps */ const metric = useMemo(() => metrics?.find((m) => m.id === id), [id, metrics]); if (!children || !metric) { diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 831dd58c373a7..a211416472f48 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -44,7 +44,7 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p jest.mock('../editor_frame_service/editor_frame/expression_helpers'); jest.mock('src/core/public'); jest.mock('../../../../../src/plugins/saved_objects/public', () => { - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow const { SavedObjectSaveModal, SavedObjectSaveModalOrigin } = jest.requireActual( '../../../../../src/plugins/saved_objects/public' ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 97165a8513078..913b396622518 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -273,6 +273,7 @@ export function SuggestionPanel({ return (props: ReactExpressionRendererProps) => ( ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [plugins.data.query.timefilter.timefilter, context]); const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState(-1); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 57a7bba8502d1..585a45cf5279c 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -39,6 +39,7 @@ function App() { const Wrapper = () => { const { core } = usePluginContext(); + // eslint-disable-next-line react-hooks/exhaustive-deps const breadcrumb = [observabilityLabelBreadcrumb, ...route.breadcrumb]; useEffect(() => { core.chrome.setBreadcrumbs(breadcrumb); diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts index 0217c48668fb9..84a5d868c34a9 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts @@ -60,7 +60,7 @@ export interface MatrixHistogramSchema { buildDsl: (options: MatrixHistogramRequestOptions) => {}; aggName: string; parseKey: string; - parser?: (data: MatrixHistogramParseData, keyBucket: string) => MatrixHistogramData[]; + parser?: (data: MatrixHistogramParseData, keyBucket: string) => MatrixHistogramData[]; } export type MatrixHistogramParseData = T extends MatrixHistogramType.alerts diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index 8c68551ddd981..f0eae407eedce 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -99,7 +99,6 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ onFilterAdded(); } } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [closePopOver, field, value, filterManager, onFilterAdded]); const filterOutValue = useCallback(() => { @@ -117,7 +116,6 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ onFilterAdded(); } } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [closePopOver, field, value, filterManager, onFilterAdded]); const handleGoGetTimelineId = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/types.ts b/x-pack/plugins/security_solution/public/common/components/header_page/types.ts index 3c16af83585e9..3c45886e3c702 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/header_page/types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type React from 'react'; export type TitleProp = string | React.ReactNode; export interface DraggableArguments { diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index 828cadd90bb13..327c2fa64997d 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type React from 'react'; import { EuiTitleSize } from '@elastic/eui'; import { ScaleType, Position, TickFormatter } from '@elastic/charts'; import { ActionCreator } from 'redux'; diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts index 47c5588a12830..78509669443ab 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/toasters/utils.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type React from 'react'; import uuid from 'uuid'; import { isError } from 'lodash/fp'; diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index 6903567c752bc..189aa05b91f4b 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -158,7 +158,7 @@ export type CreateStructuredSelector = < >( selectorMap: SelectorMap ) => ( - state: SelectorMap[keyof SelectorMap] extends (state: infer State) => unknown ? State : never + state: SelectorMap[keyof SelectorMap] extends (state: infer S) => unknown ? S : never ) => { [Key in keyof SelectorMap]: ReturnType; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index c96ef570c7e09..8900aa118d1cd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -171,7 +171,6 @@ export const AlertsHistogramPanel = memo( value: bucket.key, })) : NO_LEGEND_DATA, - // eslint-disable-next-line react-hooks/exhaustive-deps [alertsData, selectedStackByOption.value, timelineId] ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 0315d513ee260..fcef88b3f189a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -203,6 +203,7 @@ const AlertContextMenuComponent: React.FC = ({ setEventsLoading, ]); + // eslint-disable-next-line react-hooks/exhaustive-deps const openAlertActionComponent = ( = ({ setEventsLoading, ]); + // eslint-disable-next-line react-hooks/exhaustive-deps const closeAlertActionComponent = ( = ({ setEventsLoading, ]); + // eslint-disable-next-line react-hooks/exhaustive-deps const inProgressAlertActionComponent = ( = ({ setOpenAddExceptionModal('endpoint'); }, [closePopover]); + // eslint-disable-next-line react-hooks/exhaustive-deps const addEndpointExceptionComponent = ( = ({ return !isMlRule(ruleType) && !isThresholdRule(ruleType); }, [ecsRowData]); + // eslint-disable-next-line react-hooks/exhaustive-deps const addExceptionComponent = ( { let isSubscribed = true; const abortCtrl = new AbortController(); @@ -96,8 +97,7 @@ export const useRules = ({ filterOptions.filter, filterOptions.sortField, filterOptions.sortOrder, - // eslint-disable-next-line react-hooks/exhaustive-deps - filterOptions.tags?.sort().join(), + filterTags, filterOptions.showCustomRules, filterOptions.showElasticRules, refetchPrePackagedRulesStatus, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts index d603e5791f5ce..89fa34856a3f9 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/reducer.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type React from 'react'; import { EuiBasicTable } from '@elastic/eui'; import { FilterOptions, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 54aae5c41bd5f..d7cc389507463 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -281,7 +281,6 @@ export const RuleDetailsPageComponent: FC = ({ date={rule?.last_failure_at} /> ) : null, - // eslint-disable-next-line react-hooks/exhaustive-deps [rule, ruleDetailTab] ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 25b012ed68625..a37f256e359b9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -492,6 +492,7 @@ export const EndpointList = () => { ], }, ]; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [formatUrl, queryParams, search, agentPolicies, services?.application?.getUrlForApp]); const renderTableOrEmptyState = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/overview/components/types.ts b/x-pack/plugins/security_solution/public/overview/components/types.ts index e260f2843692d..6aabf78788df0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/types.ts +++ b/x-pack/plugins/security_solution/public/overview/components/types.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type React from 'react'; export type OverviewStatId = | 'auditbeatAuditd' | 'auditbeatFIM' diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 505e6cfc3ee72..a79ffda0bcce9 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -103,9 +103,8 @@ export const terminatedProcesses = createSelector(resolverTreeResponse, function * A function that given an entity id returns a boolean indicating if the id is in the set of terminated processes. */ export const isProcessTerminated = createSelector(terminatedProcesses, function ( - /* eslint-disable no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow terminatedProcesses - /* eslint-enable no-shadow */ ) { return (entityID: string) => { return terminatedProcesses.has(entityID); @@ -137,9 +136,8 @@ export const graphableProcesses = createSelector(resolverTreeResponse, function * The 'indexed process tree' contains the tree data, indexed in helpful ways. Used for O(1) access to stuff during graph layout. */ export const tree = createSelector(graphableProcesses, function indexedTree( - /* eslint-disable no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow graphableProcesses - /* eslint-enable no-shadow */ ) { return indexedProcessTreeModel.factory(graphableProcesses); }); @@ -248,9 +246,8 @@ export const relatedEventsByCategory: ( ) => (node: string, eventCategory: string) => SafeResolverEvent[] = createSelector( relatedEventsByEntityId, function ( - /* eslint-disable no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow relatedEventsByEntityId - /* eslint-enable no-shadow */ ) { // A map of nodeID -> event category -> SafeResolverEvent[] const nodeMap: Map> = new Map(); @@ -351,10 +348,9 @@ export const layout: (state: DataState) => IsometricTaxiLayout = createSelector( tree, originID, function processNodePositionsAndEdgeLineSegments( - /* eslint-disable no-shadow */ indexedProcessTree, + // eslint-disable-next-line @typescript-eslint/no-shadow originID - /* eslint-enable no-shadow */ ) { // use the isometric taxi layout as a base const taxiLayout = isometricTaxiLayoutModel.isometricTaxiLayoutFactory(indexedProcessTree); @@ -650,7 +646,7 @@ export const relatedEventCountOfTypeForNode: ( export const panelViewAndParameters = createSelector( (state: DataState) => state.locationSearch, resolverComponentInstanceID, - /* eslint-disable-next-line no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow (locationSearch, resolverComponentInstanceID) => { return panelViewAndParametersFromLocationSearchAndResolverComponentInstanceID({ locationSearch, @@ -670,7 +666,7 @@ export const nodeEventsInCategory = (state: DataState) => { export const lastRelatedEventResponseContainsCursor = createSelector( (state: DataState) => state.nodeEventsInCategory, panelViewAndParameters, - /* eslint-disable-next-line no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow function (nodeEventsInCategory, panelViewAndParameters) { if ( nodeEventsInCategory !== undefined && @@ -689,7 +685,7 @@ export const lastRelatedEventResponseContainsCursor = createSelector( export const hadErrorLoadingNodeEventsInCategory = createSelector( (state: DataState) => state.nodeEventsInCategory, panelViewAndParameters, - /* eslint-disable-next-line no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow function (nodeEventsInCategory, panelViewAndParameters) { if ( nodeEventsInCategory !== undefined && @@ -708,7 +704,7 @@ export const hadErrorLoadingNodeEventsInCategory = createSelector( export const isLoadingNodeEventsInCategory = createSelector( (state: DataState) => state.nodeEventsInCategory, panelViewAndParameters, - /* eslint-disable-next-line no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow function (nodeEventsInCategory, panelViewAndParameters) { const { panelView } = panelViewAndParameters; return panelView === 'nodeEventsInCategory' && nodeEventsInCategory === undefined; @@ -718,7 +714,7 @@ export const isLoadingNodeEventsInCategory = createSelector( export const isLoadingMoreNodeEventsInCategory = createSelector( (state: DataState) => state.nodeEventsInCategory, panelViewAndParameters, - /* eslint-disable-next-line no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow function (nodeEventsInCategory, panelViewAndParameters) { if ( nodeEventsInCategory !== undefined && diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index e805c16ed9c28..9a2ab53458a9c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -257,10 +257,10 @@ export const relatedEventTotalForProcess = composeSelectors( * animated. So in order to get the currently visible entities, we need to pass in time. */ export const visibleNodesAndEdgeLines = createSelector(nodesAndEdgelines, boundingBox, function ( - /* eslint-disable no-shadow */ + /* eslint-disable @typescript-eslint/no-shadow */ nodesAndEdgelines, boundingBox - /* eslint-enable no-shadow */ + /* eslint-enable @typescript-eslint/no-shadow */ ) { // `boundingBox` and `nodesAndEdgelines` are each memoized. return (time: number) => nodesAndEdgelines(boundingBox(time)); diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts index 6f185db4bd8b6..c60f92e4ba119 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts @@ -16,7 +16,7 @@ import { parameterName } from '../parameter_name'; */ export const ariaActiveDescendant = createSelector( (uiState: ResolverUIState) => uiState, - /* eslint-disable no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow ({ ariaActiveDescendant }) => { return ariaActiveDescendant; } @@ -27,7 +27,7 @@ export const ariaActiveDescendant = createSelector( */ export const selectedNode = createSelector( (uiState: ResolverUIState) => uiState, - /* eslint-disable no-shadow */ + // eslint-disable-next-line @typescript-eslint/no-shadow ({ selectedNode }: ResolverUIState) => { return selectedNode; } @@ -83,6 +83,7 @@ export const relatedEventsRelativeHrefs: ( ) => ( categories: Record | undefined, nodeID: string + // eslint-disable-next-line @typescript-eslint/no-shadow ) => Map = createSelector(relativeHref, (relativeHref) => { return (categories: Record | undefined, nodeID: string) => { const hrefsByCategory = new Map(); diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 7129e3a47120a..6cb25861a7b58 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -5,7 +5,7 @@ */ /* eslint-disable no-duplicate-imports */ - +import type React from 'react'; import { Store } from 'redux'; import { Middleware, Dispatch } from 'redux'; import { BBox } from 'rbush'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index 769a0a1658a46..4e7e99a5d3e49 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SetStateAction, Dispatch } from 'react'; +import type React from 'react'; import { AllTimelinesVariables } from '../../containers/all'; import { TimelineModel } from '../../store/timeline/model'; import { NoteResult } from '../../../graphql/types'; @@ -93,7 +93,9 @@ export type OnOpenTimeline = ({ }) => void; export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void; -export type SetActionTimeline = Dispatch>; +export type SetActionTimeline = React.Dispatch< + React.SetStateAction +>; export type EnableExportTimelineDownloader = (selectedItem: OpenTimelineResult) => void; /** Invoked when the user presses enters to submit the text in the search input */ export type OnQueryChange = (query: EuiSearchBarQuery) => void; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index d4d77d6fd40a0..3ea7b8d471a44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -31,7 +31,7 @@ export const eventHasNotes = (noteIds: string[]): boolean => !isEmpty(noteIds); export const getPinTooltip = ({ isPinned, - // eslint-disable-next-line no-shadow + // eslint-disable-next-line @typescript-eslint/no-shadow eventHasNotes, timelineType, }: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts index c462841f7ea38..7efae14d58a97 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/column_renderer.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import type React from 'react'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx index 55dfc82bdbdb8..6433845370ff7 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx @@ -36,10 +36,12 @@ export const BoundaryIndexExpression: FunctionComponent = ({ setBoundaryGeoField, setBoundaryNameField, }) => { + // eslint-disable-next-line react-hooks/exhaustive-deps const BOUNDARY_NAME_ENTITY_TYPES = ['string', 'number', 'ip']; const { dataUi, dataIndexPatterns, http } = alertsContext; const IndexPatternSelect = (dataUi && dataUi.IndexPatternSelect) || null; const { boundaryGeoField } = alertParams; + // eslint-disable-next-line react-hooks/exhaustive-deps const nothingSelected: IFieldType = { name: '', type: 'string', diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx index f519ad882802c..0cff207e674e5 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx @@ -28,6 +28,7 @@ export const EntityByExpression: FunctionComponent = ({ indexFields, isInvalid, }) => { + // eslint-disable-next-line react-hooks/exhaustive-deps const ENTITY_TYPES = ['string', 'number', 'ip']; const usePrevious = (value: T): T | undefined => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index de27256bf566c..a2a2d1234dbcd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -57,6 +57,7 @@ export const ConnectorAddModal = ({ consumer, }: ConnectorAddModalProps) => { let hasErrors = false; + // eslint-disable-next-line react-hooks/exhaustive-deps const initialConnector = { actionTypeId: actionType.id, config: {}, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 89deb4b26f012..741cbadb07070 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -36,6 +36,7 @@ export const AlertAdd = ({ alertTypeId, initialValues, }: AlertAddProps) => { + // eslint-disable-next-line react-hooks/exhaustive-deps const initialAlert = ({ params: {}, consumer, diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts index 511d183145a30..c9d84d9819c6f 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts @@ -332,7 +332,7 @@ export const getTestScenarios = (modifiers?: T[]) => { }, ]; if (modifiers) { - const addModifier = (list: T[]) => + const addModifier = (list: U[]) => list.map((x) => modifiers.map((modifier) => ({ ...x, modifier }))).flat(); spaces = addModifier(spaces); security = addModifier(security); diff --git a/yarn.lock b/yarn.lock index 3bfa72cc50aeb..337d7600bdb3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4587,11 +4587,6 @@ "@types/cheerio" "*" "@types/react" "*" -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== - "@types/eslint@^6.1.3": version "6.1.3" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-6.1.3.tgz#ec2a66e445a48efaa234020eb3b6e8f06afc9c61" @@ -5965,26 +5960,28 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.0.tgz#40fd53e81639c0d1a515b44e5fdf4c03dfd3cd39" - integrity sha512-Bbeg9JAnSzZ85Y0gpInZscSpifA6SbEgRryaKdP5ZlUjhTKsvZS4GUIE6xAZCjhNTrf4zXXsySo83ZdHL7it0w== +"@typescript-eslint/eslint-plugin@^4.8.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.8.1.tgz#b362abe0ee478a6c6d06c14552a6497f0b480769" + integrity sha512-d7LeQ7dbUrIv5YVFNzGgaW3IQKMmnmKFneRWagRlGYOSfLJVaRbj/FrBNOBC1a3tVO+TgNq1GbHvRtg1kwL0FQ== dependencies: - "@typescript-eslint/experimental-utils" "3.10.0" + "@typescript-eslint/experimental-utils" "4.8.1" + "@typescript-eslint/scope-manager" "4.8.1" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.0.tgz#f97a669a84a78319ab324cd51169d0c52853a360" - integrity sha512-e5ZLSTuXgqC/Gq3QzK2orjlhTZVXzwxDujQmTBOM1NIVBZgW3wiIZjaXuVutk9R4UltFlwC9UD2+bdxsA7yyNg== +"@typescript-eslint/experimental-utils@4.8.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.8.1.tgz#27275c20fa4336df99ebcf6195f7d7aa7aa9f22d" + integrity sha512-WigyLn144R3+lGATXW4nNcDJ9JlTkG8YdBWHkDlN0lC3gUGtDi7Pe3h5GPvFKMcRz8KbZpm9FJV9NTW8CpRHpg== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.10.0" - "@typescript-eslint/typescript-estree" "3.10.0" + "@typescript-eslint/scope-manager" "4.8.1" + "@typescript-eslint/types" "4.8.1" + "@typescript-eslint/typescript-estree" "4.8.1" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -6000,16 +5997,15 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.0.tgz#820322d990a82265a78f4c1fc9aae03ce95b76ac" - integrity sha512-iJyf3f2HVwscvJR7ySGMXw2DJgIAPKEz8TeU17XVKzgJRV4/VgCeDFcqLzueRe7iFI2gv+Tln4AV88ZOnsCNXg== +"@typescript-eslint/parser@^4.8.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.8.1.tgz#4fe2fbdbb67485bafc4320b3ae91e34efe1219d1" + integrity sha512-QND8XSVetATHK9y2Ltc/XBl5Ro7Y62YuZKnPEwnNPB8E379fDsvzJ1dMJ46fg/VOmk0hXhatc+GXs5MaXuL5Uw== dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.10.0" - "@typescript-eslint/types" "3.10.0" - "@typescript-eslint/typescript-estree" "3.10.0" - eslint-visitor-keys "^1.1.0" + "@typescript-eslint/scope-manager" "4.8.1" + "@typescript-eslint/types" "4.8.1" + "@typescript-eslint/typescript-estree" "4.8.1" + debug "^4.1.1" "@typescript-eslint/scope-manager@4.3.0": version "4.3.0" @@ -6019,29 +6015,23 @@ "@typescript-eslint/types" "4.3.0" "@typescript-eslint/visitor-keys" "4.3.0" -"@typescript-eslint/types@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.0.tgz#b81906674eca94a884345ba0bc1aaf6cd4da912a" - integrity sha512-ktUWSa75heQNwH85GRM7qP/UUrXqx9d6yIdw0iLO9/uE1LILW+i+3+B64dUodUS2WFWLzKTlwfi9giqrODibWg== +"@typescript-eslint/scope-manager@4.8.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.8.1.tgz#e343c475f8f1d15801b546cb17d7f309b768fdce" + integrity sha512-r0iUOc41KFFbZdPAdCS4K1mXivnSZqXS5D9oW+iykQsRlTbQRfuFRSW20xKDdYiaCoH+SkSLeIF484g3kWzwOQ== + dependencies: + "@typescript-eslint/types" "4.8.1" + "@typescript-eslint/visitor-keys" "4.8.1" "@typescript-eslint/types@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf" integrity sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw== -"@typescript-eslint/typescript-estree@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.0.tgz#65df13579a5e53c12afb4f1c5309589e3855a5de" - integrity sha512-yjuY6rmVHRhcUKgXaSPNVloRueGWpFNhxR5EQLzxXfiFSl1U/+FBqHhbaGwtPPEgCSt61QNhZgiFjWT27bgAyw== - dependencies: - "@typescript-eslint/types" "3.10.0" - "@typescript-eslint/visitor-keys" "3.10.0" - debug "^4.1.1" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" +"@typescript-eslint/types@4.8.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.8.1.tgz#23829c73c5fc6f4fcd5346a7780b274f72fee222" + integrity sha512-ave2a18x2Y25q5K05K/U3JQIe2Av4+TNi/2YuzyaXLAsDx6UZkz1boZ7nR/N6Wwae2PpudTZmHFXqu7faXfHmA== "@typescript-eslint/typescript-estree@4.3.0": version "4.3.0" @@ -6057,6 +6047,20 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@4.8.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.8.1.tgz#7307e3f2c9e95df7daa8dc0a34b8c43b7ec0dd32" + integrity sha512-bJ6Fn/6tW2g7WIkCWh3QRlaSU7CdUUK52shx36/J7T5oTQzANvi6raoTsbwGM11+7eBbeem8hCCKbyvAc0X3sQ== + dependencies: + "@typescript-eslint/types" "4.8.1" + "@typescript-eslint/visitor-keys" "4.8.1" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + "@typescript-eslint/typescript-estree@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.9.0.tgz#5d6d49be936e96fb0f859673480f89b070a5dd9b" @@ -6065,13 +6069,6 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/visitor-keys@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.0.tgz#6c0cac867e705a42e2c71b359bf6a10a88a28985" - integrity sha512-g4qftk8lWb/rHZe9uEp8oZSvsJhUvR2cfp7F7qE6DyUD2SsovEs8JDQTRP1xHzsD+pERsEpYNqkDgQXW6+ob5A== - dependencies: - eslint-visitor-keys "^1.1.0" - "@typescript-eslint/visitor-keys@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz#0e5ab0a09552903edeae205982e8521e17635ae0" @@ -6080,6 +6077,14 @@ "@typescript-eslint/types" "4.3.0" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.8.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.8.1.tgz#794f68ee292d1b2e3aa9690ebedfcb3a8c90e3c3" + integrity sha512-3nrwXFdEYALQh/zW8rFwP4QltqsanCDz4CwWMPiIZmwlk9GlvBeueEIbq05SEq4ganqM0g9nh02xXgv5XI3PeQ== + dependencies: + "@typescript-eslint/types" "4.8.1" + eslint-visitor-keys "^2.0.0" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -12800,10 +12805,10 @@ eslint-plugin-prettier@^3.1.4: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-react-hooks@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.4.tgz#aed33b4254a41b045818cacb047b81e6df27fa58" - integrity sha512-equAdEIsUETLFNCmmCkiCGq6rkSK5MoJhXFPFYeUebcjKgBmWWcgVOqZyQC8Bv1BwVCnTq9tBxgJFgAJTWoJtA== +eslint-plugin-react-hooks@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" + integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== eslint-plugin-react-perf@^3.2.3: version "3.2.3" From 37636f3e35b40a2be5e25f8586f78e81b7202283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 18 Nov 2020 19:16:38 +0100 Subject: [PATCH 72/99] [Telemetry] Move Monitoring collection strategy to a collector (#82638) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/plugin.ts | 2 +- x-pack/plugins/monitoring/kibana.json | 1 - x-pack/plugins/monitoring/server/plugin.ts | 35 ++---- .../get_all_stats.test.ts | 41 +------ .../telemetry_collection/get_all_stats.ts | 20 ++-- .../get_cluster_uuids.test.ts | 26 +---- .../telemetry_collection/get_cluster_uuids.ts | 31 +++-- .../telemetry_collection/get_licenses.test.ts | 18 +-- .../telemetry_collection/get_licenses.ts | 23 ++-- .../server/telemetry_collection/index.ts | 2 +- .../register_monitoring_collection.ts | 41 ------- ...egister_monitoring_telemetry_collection.ts | 59 ++++++++++ x-pack/plugins/monitoring/server/types.ts | 2 - .../get_stats_with_xpack.test.ts.snap | 86 ++++++++++++++ .../get_stats_with_xpack.test.ts | 108 +++++++++++------- .../get_stats_with_xpack.ts | 27 +++-- .../apis/telemetry/telemetry.js | 104 +++++++++++++---- 17 files changed, 356 insertions(+), 270 deletions(-) delete mode 100644 x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts create mode 100644 x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index 2cd06f13a8855..c9e2f22fa19aa 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -289,9 +289,9 @@ export class TelemetryCollectionManagerPlugin return stats.map((stat) => { const license = licenses[stat.cluster_uuid]; return { + collectionSource: collection.title, ...(license ? { license } : {}), ...stat, - collectionSource: collection.title, }; }); } diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index a1e28985a352f..a3d886b14cdfe 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -13,7 +13,6 @@ ], "optionalPlugins": [ "infra", - "telemetryCollectionManager", "usageCollection", "home", "cloud", diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 41b501d88af99..8a8e6a867c2e2 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -20,8 +20,6 @@ import { CoreStart, CustomHttpResponseOptions, ResponseError, - IClusterClient, - SavedObjectsServiceStart, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -41,7 +39,7 @@ import { initInfraSource } from './lib/logs/init_infra_source'; import { mbSafeQuery } from './lib/mb_safe_query'; import { instantiateClient } from './es_client/instantiate_client'; import { registerCollectors } from './kibana_monitoring/collectors'; -import { registerMonitoringCollection } from './telemetry_collection'; +import { registerMonitoringTelemetryCollection } from './telemetry_collection'; import { LicenseService } from './license_service'; import { AlertsFactory } from './alerts'; import { @@ -76,8 +74,6 @@ export class Plugin { private monitoringCore = {} as MonitoringCore; private legacyShimDependencies = {} as LegacyShimDependencies; private bulkUploader: IBulkUploader = {} as IBulkUploader; - private telemetryElasticsearchClient: IClusterClient | undefined; - private telemetrySavedObjectsService: SavedObjectsServiceStart | undefined; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; @@ -145,19 +141,6 @@ export class Plugin { plugins.alerts?.registerType(alert.getAlertType()); } - // Initialize telemetry - if (plugins.telemetryCollectionManager) { - registerMonitoringCollection({ - telemetryCollectionManager: plugins.telemetryCollectionManager, - esCluster: this.cluster, - esClientGetter: () => this.telemetryElasticsearchClient, - soServiceGetter: () => this.telemetrySavedObjectsService, - customContext: { - maxBucketSize: config.ui.max_bucket_size, - }, - }); - } - // Register collector objects for stats to show up in the APIs if (plugins.usageCollection) { core.savedObjects.registerType({ @@ -174,6 +157,11 @@ export class Plugin { }); registerCollectors(plugins.usageCollection, config, cluster); + registerMonitoringTelemetryCollection( + plugins.usageCollection, + cluster, + config.ui.max_bucket_size + ); } // Always create the bulk uploader @@ -253,16 +241,7 @@ export class Plugin { }; } - start({ elasticsearch, savedObjects }: CoreStart) { - // TODO: For the telemetry plugin to work, we need to provide the new ES client. - // The new client should be inititalized with a similar config to `this.cluster` but, since we're not using - // the new client in Monitoring Telemetry collection yet, setting the local client allows progress for now. - // The usage collector `fetch` method has been refactored to accept a `collectorFetchContext` object, - // exposing both es clients and the saved objects client. - // We will update the client in a follow up PR. - this.telemetryElasticsearchClient = elasticsearch.client; - this.telemetrySavedObjectsService = savedObjects; - } + start() {} stop() { if (this.cluster) { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index a119686afe663..aa2033b649734 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -9,13 +9,10 @@ import { getStackStats, getAllStats, handleAllStats } from './get_all_stats'; import { ESClusterStats } from './get_es_stats'; import { KibanaStats } from './get_kibana_stats'; import { ClustersHighLevelStats } from './get_high_level_stats'; -import { coreMock } from 'src/core/server/mocks'; describe('get_all_stats', () => { const timestamp = Date.now(); const callCluster = sinon.stub(); - const esClient = sinon.stub(); - const soClient = sinon.stub(); const esClusters = [ { cluster_uuid: 'a' }, @@ -172,24 +169,7 @@ describe('get_all_stats', () => { .onCall(4) .returns(Promise.resolve({})); // Beats state - expect( - await getAllStats( - [{ clusterUuid: 'a' }], - { - callCluster: callCluster as any, - esClient: esClient as any, - soClient: soClient as any, - usageCollection: {} as any, - kibanaRequest: undefined, - timestamp, - }, - { - logger: coreMock.createPluginInitializerContext().logger.get('test'), - version: 'version', - maxBucketSize: 1, - } - ) - ).toStrictEqual(allClusters); + expect(await getAllStats(['a'], callCluster, timestamp, 1)).toStrictEqual(allClusters); }); it('returns empty clusters', async () => { @@ -199,24 +179,7 @@ describe('get_all_stats', () => { callCluster.withArgs('search').returns(Promise.resolve(clusterUuidsResponse)); - expect( - await getAllStats( - [], - { - callCluster: callCluster as any, - esClient: esClient as any, - soClient: soClient as any, - usageCollection: {} as any, - kibanaRequest: undefined, - timestamp, - }, - { - logger: coreMock.createPluginInitializerContext().logger.get('test'), - version: 'version', - maxBucketSize: 1, - } - ) - ).toStrictEqual([]); + expect(await getAllStats([], callCluster, timestamp, 1)).toStrictEqual([]); }); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index b6b2023b2af1a..1f194b75e2002 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -7,8 +7,8 @@ import { set } from '@elastic/safer-lodash-set'; import { get, merge } from 'lodash'; -import { StatsGetter } from 'src/plugins/telemetry_collection_manager/server'; import moment from 'moment'; +import { LegacyAPICaller } from 'kibana/server'; import { LOGSTASH_SYSTEM_ID, KIBANA_SYSTEM_ID, @@ -20,24 +20,20 @@ import { getKibanaStats, KibanaStats } from './get_kibana_stats'; import { getBeatsStats, BeatsStatsByClusterUuid } from './get_beats_stats'; import { getHighLevelStats, ClustersHighLevelStats } from './get_high_level_stats'; -export interface CustomContext { - maxBucketSize: number; -} /** * Get statistics for all products joined by Elasticsearch cluster. * Returns the array of clusters joined with the Kibana and Logstash instances. * */ -export const getAllStats: StatsGetter = async ( - clustersDetails, - { callCluster, timestamp }, - { maxBucketSize } -) => { +export async function getAllStats( + clusterUuids: string[], + callCluster: LegacyAPICaller, // TODO: To be changed to the new ES client when the plugin migrates + timestamp: number, + maxBucketSize: number +) { const start = moment(timestamp).subtract(USAGE_FETCH_INTERVAL, 'ms').toISOString(); const end = moment(timestamp).toISOString(); - const clusterUuids = clustersDetails.map((clusterDetails) => clusterDetails.clusterUuid); - const [esClusters, kibana, logstash, beats] = await Promise.all([ getElasticsearchStats(callCluster, clusterUuids, maxBucketSize), // cluster_stats, stack_stats.xpack, cluster_name/uuid, license, version getKibanaStats(callCluster, clusterUuids, start, end, maxBucketSize), // stack_stats.kibana @@ -46,7 +42,7 @@ export const getAllStats: StatsGetter = async ( ]); return handleAllStats(esClusters, { kibana, logstash, beats }); -}; +} /** * Combine the statistics from the stack to create "cluster" stats that associate all products together based on the cluster diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts index b296ff090aedd..18a87296f7868 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts @@ -5,7 +5,6 @@ */ import sinon from 'sinon'; -import { elasticsearchServiceMock, savedObjectsRepositoryMock } from 'src/core/server/mocks'; import { getClusterUuids, fetchClusterUuids, @@ -13,10 +12,7 @@ import { } from './get_cluster_uuids'; describe('get_cluster_uuids', () => { - const kibanaRequest = undefined; const callCluster = sinon.stub(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - const soClient = savedObjectsRepositoryMock.create(); const response = { aggregations: { cluster_uuids: { @@ -24,36 +20,20 @@ describe('get_cluster_uuids', () => { }, }, }; - const expectedUuids = response.aggregations.cluster_uuids.buckets - .map((bucket) => bucket.key) - .map((expectedUuid) => ({ clusterUuid: expectedUuid })); + const expectedUuids = response.aggregations.cluster_uuids.buckets.map((bucket) => bucket.key); const timestamp = Date.now(); describe('getClusterUuids', () => { it('returns cluster UUIDs', async () => { callCluster.withArgs('search').returns(Promise.resolve(response)); - expect( - await getClusterUuids( - { callCluster, esClient, soClient, timestamp, kibanaRequest, usageCollection: {} as any }, - { - maxBucketSize: 1, - } as any - ) - ).toStrictEqual(expectedUuids); + expect(await getClusterUuids(callCluster, timestamp, 1)).toStrictEqual(expectedUuids); }); }); describe('fetchClusterUuids', () => { it('searches for clusters', async () => { callCluster.returns(Promise.resolve(response)); - expect( - await fetchClusterUuids( - { callCluster, esClient, soClient, timestamp, kibanaRequest, usageCollection: {} as any }, - { - maxBucketSize: 1, - } as any - ) - ).toStrictEqual(response); + expect(await fetchClusterUuids(callCluster, timestamp, 1)).toStrictEqual(response); }); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.ts index 5f471851b6621..32cda4ebdac9a 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.ts @@ -6,33 +6,31 @@ import { get } from 'lodash'; import moment from 'moment'; -import { - ClusterDetailsGetter, - StatsCollectionConfig, - ClusterDetails, -} from 'src/plugins/telemetry_collection_manager/server'; +import { LegacyAPICaller } from 'kibana/server'; import { createQuery } from './create_query'; import { INDEX_PATTERN_ELASTICSEARCH, CLUSTER_DETAILS_FETCH_INTERVAL, } from '../../common/constants'; -import { CustomContext } from './get_all_stats'; + /** * Get a list of Cluster UUIDs that exist within the specified timespan. */ -export const getClusterUuids: ClusterDetailsGetter = async ( - config, - { maxBucketSize } -) => { - const response = await fetchClusterUuids(config, maxBucketSize); +export async function getClusterUuids( + callCluster: LegacyAPICaller, // TODO: To be changed to the new ES client when the plugin migrates + timestamp: number, + maxBucketSize: number +) { + const response = await fetchClusterUuids(callCluster, timestamp, maxBucketSize); return handleClusterUuidsResponse(response); -}; +} /** * Fetch the aggregated Cluster UUIDs from the monitoring cluster. */ export async function fetchClusterUuids( - { callCluster, timestamp }: StatsCollectionConfig, + callCluster: LegacyAPICaller, + timestamp: number, maxBucketSize: number ) { const start = moment(timestamp).subtract(CLUSTER_DETAILS_FETCH_INTERVAL, 'ms').toISOString(); @@ -66,10 +64,7 @@ export async function fetchClusterUuids( * @param {Object} response The aggregation response * @return {Array} Strings; each representing a Cluster's UUID. */ -export function handleClusterUuidsResponse(response: any): ClusterDetails[] { +export function handleClusterUuidsResponse(response: any): string[] { const uuidBuckets: any[] = get(response, 'aggregations.cluster_uuids.buckets', []); - - return uuidBuckets.map((uuidBucket) => ({ - clusterUuid: uuidBucket.key as string, - })); + return uuidBuckets.map((uuidBucket) => uuidBucket.key); } diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts index 4812d9522d7ae..8db563cebac03 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.test.ts @@ -19,7 +19,7 @@ describe('get_licenses', () => { }, }; const expectedClusters = response.hits.hits.map((hit) => hit._source); - const clusterUuids = expectedClusters.map((cluster) => ({ clusterUuid: cluster.cluster_uuid })); + const clusterUuids = expectedClusters.map((cluster) => cluster.cluster_uuid); const expectedLicenses = { abc: { type: 'basic' }, xyz: { type: 'basic' }, @@ -30,13 +30,7 @@ describe('get_licenses', () => { it('returns clusters', async () => { callWith.withArgs('search').returns(Promise.resolve(response)); - expect( - await getLicenses( - clusterUuids, - { callCluster: callWith } as any, - { maxBucketSize: 1 } as any - ) - ).toStrictEqual(expectedLicenses); + expect(await getLicenses(clusterUuids, callWith, 1)).toStrictEqual(expectedLicenses); }); }); @@ -44,13 +38,7 @@ describe('get_licenses', () => { it('searches for clusters', async () => { callWith.returns(response); - expect( - await fetchLicenses( - callWith, - clusterUuids.map(({ clusterUuid }) => clusterUuid), - { maxBucketSize: 1 } as any - ) - ).toStrictEqual(response); + expect(await fetchLicenses(callWith, clusterUuids, 1)).toStrictEqual(response); }); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts index a8b68929e84b8..7b1b877c51278 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_licenses.ts @@ -5,26 +5,21 @@ */ import { SearchResponse } from 'elasticsearch'; -import { - ESLicense, - LicenseGetter, - StatsCollectionConfig, -} from 'src/plugins/telemetry_collection_manager/server'; +import { ESLicense } from 'src/plugins/telemetry_collection_manager/server'; +import { LegacyAPICaller } from 'kibana/server'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; -import { CustomContext } from './get_all_stats'; /** * Get statistics for all selected Elasticsearch clusters. */ -export const getLicenses: LicenseGetter = async ( - clustersDetails, - { callCluster }, - { maxBucketSize } -) => { - const clusterUuids = clustersDetails.map(({ clusterUuid }) => clusterUuid); +export async function getLicenses( + clusterUuids: string[], + callCluster: LegacyAPICaller, // TODO: To be changed to the new ES client when the plugin migrates + maxBucketSize: number +): Promise<{ [clusterUuid: string]: ESLicense | undefined }> { const response = await fetchLicenses(callCluster, clusterUuids, maxBucketSize); return handleLicenses(response); -}; +} /** * Fetch the Elasticsearch stats. @@ -36,7 +31,7 @@ export const getLicenses: LicenseGetter = async ( * Returns the response for the aggregations to fetch details for the product. */ export function fetchLicenses( - callCluster: StatsCollectionConfig['callCluster'], + callCluster: LegacyAPICaller, clusterUuids: string[], maxBucketSize: number ) { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/index.ts b/x-pack/plugins/monitoring/server/telemetry_collection/index.ts index 764e080e390c1..8627c741c974b 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/index.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerMonitoringCollection } from './register_monitoring_collection'; +export { registerMonitoringTelemetryCollection } from './register_monitoring_telemetry_collection'; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts deleted file mode 100644 index 109fefd2eb8de..0000000000000 --- a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts +++ /dev/null @@ -1,41 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ILegacyCustomClusterClient, - IClusterClient, - SavedObjectsServiceStart, -} from 'kibana/server'; -import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; -import { getAllStats, CustomContext } from './get_all_stats'; -import { getClusterUuids } from './get_cluster_uuids'; -import { getLicenses } from './get_licenses'; - -export function registerMonitoringCollection({ - telemetryCollectionManager, - esCluster, - esClientGetter, - soServiceGetter, - customContext, -}: { - telemetryCollectionManager: TelemetryCollectionManagerPluginSetup; - esCluster: ILegacyCustomClusterClient; - esClientGetter: () => IClusterClient | undefined; - soServiceGetter: () => SavedObjectsServiceStart | undefined; - customContext: CustomContext; -}) { - telemetryCollectionManager.setCollection({ - esCluster, - esClientGetter, - soServiceGetter, - title: 'monitoring', - priority: 2, - statsGetter: getAllStats, - clusterDetailsGetter: getClusterUuids, - licenseGetter: getLicenses, - customContext, - }); -} diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts new file mode 100644 index 0000000000000..91d6c2374acba --- /dev/null +++ b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ILegacyClusterClient } from 'kibana/server'; +import { UsageStatsPayload } from 'src/plugins/telemetry_collection_manager/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { getAllStats } from './get_all_stats'; +import { getClusterUuids } from './get_cluster_uuids'; +import { getLicenses } from './get_licenses'; + +// TODO: To be removed in https://github.com/elastic/kibana/pull/83546 +interface MonitoringCollectorOptions { + ignoreForInternalUploader: boolean; // Allow the additional property required by bulk_uploader to be filtered out +} + +export function registerMonitoringTelemetryCollection( + usageCollection: UsageCollectionSetup, + legacyEsClient: ILegacyClusterClient, + maxBucketSize: number +) { + const monitoringStatsCollector = usageCollection.makeStatsCollector< + UsageStatsPayload[], + unknown, + true, + MonitoringCollectorOptions + >({ + type: 'monitoringTelemetry', + isReady: () => true, + ignoreForInternalUploader: true, // Used only by monitoring's bulk_uploader to filter out unwanted collectors + extendFetchContext: { kibanaRequest: true }, + fetch: async ({ kibanaRequest }) => { + const timestamp = Date.now(); // Collect the telemetry from the monitoring indices for this moment. + // NOTE: Usually, the monitoring indices index stats for each product every 10s (by default). + // However, some data may be delayed up-to 24h because monitoring only collects extended Kibana stats in that interval + // to avoid overloading of the system when retrieving data from the collectors (that delay is dealt with in the Kibana Stats getter inside the `getAllStats` method). + // By 8.x, we expect to stop collecting the Kibana extended stats and keep only the monitoring-related metrics. + const callCluster = kibanaRequest + ? legacyEsClient.asScoped(kibanaRequest).callAsCurrentUser + : legacyEsClient.callAsInternalUser; + const clusterDetails = await getClusterUuids(callCluster, timestamp, maxBucketSize); + const [licenses, stats] = await Promise.all([ + getLicenses(clusterDetails, callCluster, maxBucketSize), + getAllStats(clusterDetails, callCluster, timestamp, maxBucketSize), + ]); + return stats.map((stat) => { + const license = licenses[stat.cluster_uuid]; + return { + ...(license ? { license } : {}), + ...stat, + collectionSource: 'monitoring', + }; + }); + }, + }); + usageCollection.registerCollector(monitoringStatsCollector); +} diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 543a12fb41356..b25daced50b73 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -6,7 +6,6 @@ import { Observable } from 'rxjs'; import { IRouter, ILegacyClusterClient, Logger } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { LicenseFeature, ILicense } from '../../licensing/server'; import { PluginStartContract as ActionsPluginsStartContact } from '../../actions/server'; import { @@ -35,7 +34,6 @@ export interface MonitoringElasticsearchConfig { export interface PluginsSetup { encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup; - telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; usageCollection?: UsageCollectionSetup; licensing: LicensingPluginSetup; features: FeaturesPluginSetupContract; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap index b9bb206b8056f..b68186c0c343d 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -156,3 +156,89 @@ Object { "version": "8.0.0", } `; + +exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry with appended Monitoring data 1`] = ` +Object { + "cluster_name": "test", + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, + }, + }, + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, + }, + "since": 1588616945163, + "timestamp": 1588617023177, + }, + ], + }, + }, + }, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "data": Array [], + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", + }, + ], + "platforms": Array [ + Object { + "count": 1, + "platform": "rocky", + }, + ], + }, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], + }, + "xpack": Object {}, + }, + "timestamp": Any, + "version": "8.0.0", +} +`; + +exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry with appended Monitoring data 2`] = ` +Object { + "collectionSource": "monitoring", + "timestamp": Any, +} +`; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index a4806cefeef3d..5b3f73f206c6e 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -48,34 +48,54 @@ const getContext = () => ({ logger: coreMock.createPluginInitializerContext().logger.get('test'), }); -const mockUsageCollection = (kibanaUsage = kibana) => ({ +const mockUsageCollection = (kibanaUsage: Record = kibana) => ({ bulkFetch: () => kibanaUsage, toObject: (data: any) => data, }); +/** + * Instantiate the esClient mock with the common requests + */ +function mockEsClient() { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // mock for license should return a basic license + esClient.license.get.mockResolvedValue( + // @ts-ignore we only care about the response body + { body: { license: { type: 'basic' } } } + ); + // mock for xpack usage should return an empty object + esClient.xpack.usage.mockResolvedValue( + // @ts-ignore we only care about the response body + { body: {} } + ); + // mock for nodes usage should resolve for this test + esClient.nodes.usage.mockResolvedValue( + // @ts-ignore we only care about the response body + { body: { cluster_name: 'test cluster', nodes: nodesUsage } } + ); + // mock for info should resolve for this test + esClient.info.mockResolvedValue( + // @ts-ignore we only care about the response body + { + body: { + cluster_uuid: 'test', + cluster_name: 'test', + version: { number: '8.0.0' }, + }, + } + ); + + return esClient; +} + describe('Telemetry Collection: Get Aggregated Stats', () => { test('OSS-like telemetry (no license nor X-Pack telemetry)', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const esClient = mockEsClient(); // mock for xpack.usage should throw a 404 for this test esClient.xpack.usage.mockRejectedValue(new Error('Not Found')); // mock for license should throw a 404 for this test esClient.license.get.mockRejectedValue(new Error('Not Found')); - // mock for nodes usage should resolve for this test - esClient.nodes.usage.mockResolvedValue( - // @ts-ignore we only care about the response body - { body: { cluster_name: 'test cluster', nodes: nodesUsage } } - ); - // mock for info should resolve for this test - esClient.info.mockResolvedValue( - // @ts-ignore we only care about the response body - { - body: { - cluster_uuid: 'test', - cluster_name: 'test', - version: { number: '8.0.0' }, - }, - } - ); + const usageCollection = mockUsageCollection(); const context = getContext(); @@ -95,32 +115,7 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { }); test('X-Pack telemetry (license + X-Pack)', async () => { - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - // mock for license should return a basic license - esClient.license.get.mockResolvedValue( - // @ts-ignore we only care about the response body - { body: { license: { type: 'basic' } } } - ); - // mock for xpack usage should return an empty object - esClient.xpack.usage.mockResolvedValue( - // @ts-ignore we only care about the response body - { body: {} } - ); - // mock for nodes usage should return the cluster name and nodes usage - esClient.nodes.usage.mockResolvedValue( - // @ts-ignore we only care about the response body - { body: { cluster_name: 'test cluster', nodes: nodesUsage } } - ); - esClient.info.mockResolvedValue( - // @ts-ignore we only care about the response body - { - body: { - cluster_uuid: 'test', - cluster_name: 'test', - version: { number: '8.0.0' }, - }, - } - ); + const esClient = mockEsClient(); const usageCollection = mockUsageCollection(); const context = getContext(); @@ -138,4 +133,29 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { }); }); }); + + test('X-Pack telemetry with appended Monitoring data', async () => { + const esClient = mockEsClient(); + const usageCollection = mockUsageCollection({ + ...kibana, + monitoringTelemetry: [ + { collectionSource: 'monitoring', timestamp: new Date().toISOString() }, + ], + }); + const context = getContext(); + + const stats = await getStatsWithXpack( + [{ clusterUuid: '1234' }], + { + esClient, + usageCollection, + } as any, + context + ); + stats.forEach((entry, index) => { + expect(entry).toMatchSnapshot({ + timestamp: expect.any(String), + }); + }); + }); }); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts index 87e3d0a9613da..c0e55274b08df 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts @@ -21,14 +21,23 @@ export const getStatsWithXpack: StatsGetter<{}, TelemetryAggregatedStats> = asyn const clustersLocalStats = await getLocalStats(clustersDetails, config, context); const xpack = await getXPackUsage(esClient).catch(() => undefined); // We want to still report something (and do not lose the license) even when this method fails. - return clustersLocalStats.map((localStats) => { - if (xpack) { - return { - ...localStats, - stack_stats: { ...localStats.stack_stats, xpack }, - }; - } + return clustersLocalStats + .map((localStats) => { + if (xpack) { + return { + ...localStats, + stack_stats: { ...localStats.stack_stats, xpack }, + }; + } - return localStats; - }); + return localStats; + }) + .reduce((acc, stats) => { + // Concatenate the telemetry reported via monitoring as additional payloads instead of reporting it inside of stack_stats.kibana.plugins.monitoringTelemetry + const monitoringTelemetry = stats.stack_stats.kibana?.plugins?.monitoringTelemetry; + if (monitoringTelemetry) { + delete stats.stack_stats.kibana!.plugins.monitoringTelemetry; + } + return [...acc, stats, ...(monitoringTelemetry || [])]; + }, [] as TelemetryAggregatedStats[]); }; diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry.js b/x-pack/test/api_integration/apis/telemetry/telemetry.js index b21ca27167bd0..d0b7b2bbbb7d2 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry.js +++ b/x-pack/test/api_integration/apis/telemetry/telemetry.js @@ -5,48 +5,108 @@ */ import expect from '@kbn/expect'; +import moment from 'moment'; import multiClusterFixture from './fixtures/multicluster'; import basicClusterFixture from './fixtures/basiccluster'; +/** + * Update the .monitoring-* documents loaded via the archiver to the recent `timestamp` + * @param esSupertest The client to send requests to ES + * @param fromTimestamp The lower timestamp limit to query the documents from + * @param toTimestamp The upper timestamp limit to query the documents from + * @param timestamp The new timestamp to be set + */ +function updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp) { + return Promise.all([ + esSupertest + .post('/.monitoring-es-*/_update_by_query?refresh=true') + .send({ + query: { + range: { + timestamp: { + format: 'epoch_millis', + gte: moment(fromTimestamp).valueOf(), + lte: moment(toTimestamp).valueOf(), + }, + }, + }, + script: { + source: `ctx._source.timestamp='${timestamp}'`, + lang: 'painless', + }, + }) + .expect(200), + esSupertest + .post('/.monitoring-kibana-*/_update_by_query?refresh=true') + .send({ + query: { + range: { + timestamp: { + format: 'epoch_millis', + gte: moment(fromTimestamp).valueOf(), + lte: moment(toTimestamp).valueOf(), + }, + }, + }, + script: { + source: `ctx._source.timestamp='${timestamp}'`, + lang: 'painless', + }, + }) + .expect(200), + ]); +} + export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const esSupertest = getService('esSupertest'); describe('/api/telemetry/v2/clusters/_stats', () => { - it('should load multiple trial-license clusters', async () => { + const timestamp = new Date().toISOString(); + describe('monitoring/multicluster', () => { const archive = 'monitoring/multicluster'; - const timestamp = '2017-08-16T00:00:00Z'; - - await esArchiver.load(archive); - - const { body } = await supertest - .post('/api/telemetry/v2/clusters/_stats') - .set('kbn-xsrf', 'xxx') - .send({ timestamp, unencrypted: true }) - .expect(200); - - expect(body).length(3); - expect(body).to.eql(multiClusterFixture); + const fromTimestamp = '2017-08-15T21:00:00.000Z'; + const toTimestamp = '2017-08-16T00:00:00.000Z'; + before(async () => { + await esArchiver.load(archive); + await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp); + }); + after(() => esArchiver.unload(archive)); + it('should load multiple trial-license clusters', async () => { + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timestamp, unencrypted: true }) + .expect(200); - await esArchiver.unload(archive); + expect(body).length(4); + const [localXPack, ...monitoring] = body; + expect(localXPack.collectionSource).to.eql('local_xpack'); + expect(monitoring).to.eql(multiClusterFixture.map((item) => ({ ...item, timestamp }))); + }); }); describe('with basic cluster and reporting and canvas usage info', () => { - it('should load non-expiring basic cluster', async () => { - const archive = 'monitoring/basic_6.3.x'; - const timestamp = '2018-07-23T22:13:00Z'; - + const archive = 'monitoring/basic_6.3.x'; + const fromTimestamp = '2018-07-23T22:54:59.087Z'; + const toTimestamp = '2018-07-23T22:55:05.933Z'; + before(async () => { await esArchiver.load(archive); - + await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp); + }); + after(() => esArchiver.unload(archive)); + it('should load non-expiring basic cluster', async () => { const { body } = await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') .send({ timestamp, unencrypted: true }) .expect(200); - expect(body).to.eql(basicClusterFixture); - - await esArchiver.unload(archive); + expect(body).length(2); + const [localXPack, ...monitoring] = body; + expect(localXPack.collectionSource).to.eql('local_xpack'); + expect(monitoring).to.eql(basicClusterFixture.map((item) => ({ ...item, timestamp }))); }); }); }); From 4786b70cd147274147aa2444ada2de22c999fc56 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 18 Nov 2020 19:45:38 +0100 Subject: [PATCH 73/99] Bump is-my-json-valid to v2.20.5 (#83642) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 337d7600bdb3d..d43c450e0c58f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16809,9 +16809,9 @@ is-my-ip-valid@^1.0.0: integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== is-my-json-valid@^2.10.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz#8fd6e40363cd06b963fa877d444bfb5eddc62175" - integrity sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q== + version "2.20.5" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.20.5.tgz#5eca6a8232a687f68869b7361be1612e7512e5df" + integrity sha512-VTPuvvGQtxvCeghwspQu1rBgjYUT6FGxPlvFKbYuFtgc4ADsX3U5ihZOYN0qyU6u+d4X9xXb0IT5O6QpXKt87A== dependencies: generate-function "^2.0.0" generate-object-property "^1.1.0" From 02b59f25d2366699880c473a0ede385dd5932259 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 18 Nov 2020 19:46:32 +0100 Subject: [PATCH 74/99] Bump jsonpointer to v4.1.0 (#83641) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d43c450e0c58f..a88cad99fca07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18312,9 +18312,9 @@ jsonparse@^1.2.0: integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= jsonpointer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" - integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= + version "4.1.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc" + integrity sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg== jsonwebtoken@^8.3.0, jsonwebtoken@^8.5.1: version "8.5.1" From 19ed71968afa416bc7ed7ddefbbbeff7fad7fe83 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 18 Nov 2020 19:47:14 +0100 Subject: [PATCH 75/99] Bump y18n@5 to v5.0.5 (#83644) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a88cad99fca07..15ce27eeca446 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29797,9 +29797,9 @@ y18n@^4.0.0: integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== y18n@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.1.tgz#1ad2a7eddfa8bce7caa2e1f6b5da96c39d99d571" - integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg== + version "5.0.5" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" + integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== yallist@^2.1.2: version "2.1.2" From 02dfc47be60edf549cc7780dee14a1374639dde1 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 18 Nov 2020 19:47:53 +0100 Subject: [PATCH 76/99] Bump flat to v4.1.1 (#83647) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 15ce27eeca446..2a82e7024a895 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13805,9 +13805,9 @@ flat-cache@^2.0.1: write "1.0.3" flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== dependencies: is-buffer "~2.0.3" From 2a365ff6329544465227e61141ded6fba8bb2c80 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 18 Nov 2020 19:49:58 +0100 Subject: [PATCH 77/99] [APM] Improve router types (#83620) * [APM] Improve router types * Pass processorEvent param to useDynamicIndexPattern --- .../common/runtime_types/merge/index.test.ts | 71 +++++++ .../apm/common/runtime_types/merge/index.ts | 68 ++++++ .../strict_keys_rt/index.test.ts | 106 ++++++++++ .../runtime_types/strict_keys_rt/index.ts | 195 ++++++++++++++++++ .../anomaly_detection_setup_link.tsx | 5 +- .../app/ErrorGroupDetails/index.tsx | 4 +- .../app/ErrorGroupOverview/index.tsx | 4 +- .../route_handlers/agent_configuration.tsx | 2 +- .../app/RumDashboard/ClientMetrics/index.tsx | 2 +- .../ImpactfulMetrics/JSErrors.tsx | 2 +- .../PageLoadDistribution/index.tsx | 2 +- .../PageLoadDistribution/use_breakdowns.ts | 2 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 2 +- .../app/RumDashboard/Panels/MainFilters.tsx | 2 +- .../URLFilter/URLSearch/index.tsx | 2 +- .../RumDashboard/UXMetrics/KeyUXMetrics.tsx | 2 +- .../app/RumDashboard/UXMetrics/index.tsx | 2 +- .../RumDashboard/VisitorBreakdown/index.tsx | 2 +- .../app/RumDashboard/ux_overview_fetchers.ts | 4 +- .../Popover/ServiceStatsFetcher.tsx | 2 +- .../components/app/ServiceMap/index.tsx | 2 +- .../app/ServiceNodeMetrics/index.tsx | 4 +- .../app/ServiceNodeOverview/index.tsx | 2 +- .../ServicePage/ServicePage.tsx | 6 +- .../SettingsPage/saveConfig.ts | 3 +- .../List/ConfirmDeleteModal.tsx | 3 +- .../Settings/AgentConfigurations/index.tsx | 2 +- .../app/Settings/ApmIndices/index.tsx | 5 +- .../CustomLinkFlyout/DeleteButton.tsx | 3 +- .../CustomLinkFlyout/LinkPreview.tsx | 2 +- .../CustomLinkFlyout/link_preview.test.tsx | 4 +- .../CustomLinkFlyout/saveCustomLink.ts | 6 +- .../CustomizeUI/CustomLink/index.test.tsx | 2 +- .../Settings/CustomizeUI/CustomLink/index.tsx | 5 +- .../anomaly_detection/add_environments.tsx | 2 +- .../Settings/anomaly_detection/create_jobs.ts | 3 +- .../app/Settings/anomaly_detection/index.tsx | 7 +- .../public/components/app/TraceLink/index.tsx | 2 +- .../components/app/TraceOverview/index.tsx | 4 +- .../app/service_inventory/index.tsx | 2 +- .../service_overview_errors_table/index.tsx | 2 +- .../TransactionActionMenu.tsx | 2 +- .../__test__/TransactionActionMenu.test.tsx | 2 +- .../transaction_error_rate_chart/index.tsx | 4 +- .../public/context/charts_sync_context.tsx | 2 +- .../plugins/apm/public/hooks/useAgentName.ts | 2 +- .../public/hooks/useAnomalyDetectionJobs.ts | 2 +- .../public/hooks/useDynamicIndexPattern.ts | 2 +- .../apm/public/hooks/useEnvironments.tsx | 2 +- .../public/hooks/useServiceMetricCharts.ts | 2 +- .../hooks/useServiceTransactionTypes.tsx | 2 +- .../public/hooks/useTransactionBreakdown.ts | 4 +- .../apm/public/hooks/useTransactionCharts.ts | 3 +- .../hooks/useTransactionDistribution.ts | 4 +- .../apm/public/hooks/useTransactionList.ts | 4 +- .../plugins/apm/public/hooks/useWaterfall.ts | 2 +- .../apm/public/hooks/use_annotations.ts | 2 +- .../services/__test__/callApmApi.test.ts | 7 +- .../apm_observability_overview_fetchers.ts | 4 +- .../public/services/rest/createCallApmApi.ts | 19 +- .../apm/public/services/rest/index_pattern.ts | 5 +- .../apm/server/lib/helpers/setup_request.ts | 2 - .../plugins/apm/server/routes/correlations.ts | 20 +- .../server/routes/create_api/index.test.ts | 82 +++++--- .../apm/server/routes/create_api/index.ts | 135 ++++++------ .../apm/server/routes/create_apm_api.ts | 3 +- .../plugins/apm/server/routes/create_route.ts | 27 ++- x-pack/plugins/apm/server/routes/errors.ts | 30 +-- .../apm/server/routes/index_pattern.ts | 26 ++- x-pack/plugins/apm/server/routes/metrics.ts | 10 +- .../server/routes/observability_overview.ts | 16 +- .../plugins/apm/server/routes/rum_client.ts | 110 +++++----- .../plugins/apm/server/routes/service_map.ts | 20 +- .../apm/server/routes/service_nodes.ts | 10 +- x-pack/plugins/apm/server/routes/services.ts | 72 +++---- .../routes/settings/agent_configuration.ts | 80 ++++--- .../routes/settings/anomaly_detection.ts | 25 +-- .../apm/server/routes/settings/apm_indices.ts | 25 +-- .../apm/server/routes/settings/custom_link.ts | 53 +++-- x-pack/plugins/apm/server/routes/traces.ts | 20 +- .../plugins/apm/server/routes/transaction.ts | 10 +- .../apm/server/routes/transaction_groups.ts | 61 +++--- x-pack/plugins/apm/server/routes/typings.ts | 160 ++++++-------- .../plugins/apm/server/routes/ui_filters.ts | 42 ++-- 84 files changed, 1044 insertions(+), 623 deletions(-) create mode 100644 x-pack/plugins/apm/common/runtime_types/merge/index.test.ts create mode 100644 x-pack/plugins/apm/common/runtime_types/merge/index.ts create mode 100644 x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts create mode 100644 x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts b/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts new file mode 100644 index 0000000000000..0e0cb4a349c83 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { isLeft } from 'fp-ts/lib/Either'; +import { merge } from './'; +import { jsonRt } from '../json_rt'; + +describe('merge', () => { + it('fails on one or more errors', () => { + const type = merge([t.type({ foo: t.string }), t.type({ bar: t.number })]); + + const result = type.decode({ foo: '' }); + + expect(isLeft(result)).toBe(true); + }); + + it('merges left to right', () => { + const typeBoolean = merge([ + t.type({ foo: t.string }), + t.type({ foo: jsonRt.pipe(t.boolean) }), + ]); + + const resultBoolean = typeBoolean.decode({ + foo: 'true', + }); + + // @ts-expect-error + expect(resultBoolean.right).toEqual({ + foo: true, + }); + + const typeString = merge([ + t.type({ foo: jsonRt.pipe(t.boolean) }), + t.type({ foo: t.string }), + ]); + + const resultString = typeString.decode({ + foo: 'true', + }); + + // @ts-expect-error + expect(resultString.right).toEqual({ + foo: 'true', + }); + }); + + it('deeply merges values', () => { + const type = merge([ + t.type({ foo: t.type({ baz: t.string }) }), + t.type({ foo: t.type({ bar: t.string }) }), + ]); + + const result = type.decode({ + foo: { + bar: '', + baz: '', + }, + }); + + // @ts-expect-error + expect(result.right).toEqual({ + foo: { + bar: '', + baz: '', + }, + }); + }); +}); diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.ts b/x-pack/plugins/apm/common/runtime_types/merge/index.ts new file mode 100644 index 0000000000000..76a1092436dce --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/merge/index.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { merge as lodashMerge } from 'lodash'; +import { isLeft } from 'fp-ts/lib/Either'; +import { ValuesType } from 'utility-types'; + +export type MergeType< + T extends t.Any[], + U extends ValuesType = ValuesType +> = t.Type & { + _tag: 'MergeType'; + types: T; +}; + +// this is similar to t.intersection, but does a deep merge +// instead of a shallow merge + +export function merge( + types: [A, B] +): MergeType<[A, B]>; + +export function merge(types: t.Any[]) { + const mergeType = new t.Type( + 'merge', + (u): u is unknown => { + return types.every((type) => type.is(u)); + }, + (input, context) => { + const errors: t.Errors = []; + + const successes: unknown[] = []; + + const results = types.map((type, index) => + type.validate( + input, + context.concat({ + key: String(index), + type, + actual: input, + }) + ) + ); + + results.forEach((result) => { + if (isLeft(result)) { + errors.push(...result.left); + } else { + successes.push(result.right); + } + }); + + const mergedValues = lodashMerge({}, ...successes); + + return errors.length > 0 ? t.failures(errors) : t.success(mergedValues); + }, + (a) => types.reduce((val, type) => type.encode(val), a) + ); + + return { + ...mergeType, + _tag: 'MergeType', + types, + }; +} diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts new file mode 100644 index 0000000000000..ac2f7d8e1679a --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { isRight, isLeft } from 'fp-ts/lib/Either'; +import { strictKeysRt } from './'; +import { jsonRt } from '../json_rt'; + +describe('strictKeysRt', () => { + it('correctly and deeply validates object keys', () => { + const checks: Array<{ type: t.Type; passes: any[]; fails: any[] }> = [ + { + type: t.intersection([ + t.type({ foo: t.string }), + t.partial({ bar: t.string }), + ]), + passes: [{ foo: '' }, { foo: '', bar: '' }], + fails: [ + { foo: '', unknownKey: '' }, + { foo: '', bar: '', unknownKey: '' }, + ], + }, + { + type: t.type({ + path: t.union([ + t.type({ serviceName: t.string }), + t.type({ transactionType: t.string }), + ]), + }), + passes: [ + { path: { serviceName: '' } }, + { path: { transactionType: '' } }, + ], + fails: [ + { path: { serviceName: '', unknownKey: '' } }, + { path: { transactionType: '', unknownKey: '' } }, + { path: { serviceName: '', transactionType: '' } }, + { path: { serviceName: '' }, unknownKey: '' }, + ], + }, + { + type: t.intersection([ + t.type({ query: t.type({ bar: t.string }) }), + t.partial({ query: t.partial({ _debug: t.boolean }) }), + ]), + passes: [{ query: { bar: '', _debug: true } }], + fails: [{ query: { _debug: true } }], + }, + ]; + + checks.forEach((check) => { + const { type, passes, fails } = check; + + const strictType = strictKeysRt(type); + + passes.forEach((value) => { + const result = strictType.decode(value); + + if (!isRight(result)) { + throw new Error( + `Expected ${JSON.stringify( + value + )} to be allowed, but validation failed with ${ + result.left[0].message + }` + ); + } + }); + + fails.forEach((value) => { + const result = strictType.decode(value); + + if (!isLeft(result)) { + throw new Error( + `Expected ${JSON.stringify( + value + )} to be disallowed, but validation succeeded` + ); + } + }); + }); + }); + + it('does not support piped types', () => { + const typeA = t.type({ + query: t.type({ filterNames: jsonRt.pipe(t.array(t.string)) }), + } as Record); + + const typeB = t.partial({ + query: t.partial({ _debug: jsonRt.pipe(t.boolean) }), + }); + + const value = { + query: { + _debug: 'true', + filterNames: JSON.stringify(['host', 'agentName']), + }, + }; + + const pipedType = strictKeysRt(typeA.pipe(typeB)); + + expect(isLeft(pipedType.decode(value))).toBe(true); + }); +}); diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts new file mode 100644 index 0000000000000..9ca37b4a0a26a --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { either, isRight } from 'fp-ts/lib/Either'; +import { mapValues, difference, isPlainObject, forEach } from 'lodash'; +import { MergeType, merge } from '../merge'; + +/* + Type that tracks validated keys, and fails when the input value + has keys that have not been validated. +*/ + +type ParsableType = + | t.IntersectionType + | t.UnionType + | t.PartialType + | t.ExactType + | t.InterfaceType + | MergeType; + +function getKeysInObject>( + object: T, + prefix: string = '' +): string[] { + const keys: string[] = []; + forEach(object, (value, key) => { + const ownPrefix = prefix ? `${prefix}.${key}` : key; + keys.push(ownPrefix); + if (isPlainObject(object[key])) { + keys.push( + ...getKeysInObject(object[key] as Record, ownPrefix) + ); + } + }); + return keys; +} + +function addToContextWhenValidated< + T extends t.InterfaceType | t.PartialType +>(type: T, prefix: string): T { + const validate = (input: unknown, context: t.Context) => { + const result = type.validate(input, context); + const keysType = context[0].type as StrictKeysType; + if (!('trackedKeys' in keysType)) { + throw new Error('Expected a top-level StrictKeysType'); + } + if (isRight(result)) { + keysType.trackedKeys.push( + ...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`) + ); + } + return result; + }; + + if (type._tag === 'InterfaceType') { + return new t.InterfaceType( + type.name, + type.is, + validate, + type.encode, + type.props + ) as T; + } + + return new t.PartialType( + type.name, + type.is, + validate, + type.encode, + type.props + ) as T; +} + +function trackKeysOfValidatedTypes( + type: ParsableType | t.Any, + prefix: string = '' +): t.Any { + if (!('_tag' in type)) { + return type; + } + const taggedType = type as ParsableType; + + switch (taggedType._tag) { + case 'IntersectionType': { + const collectionType = type as t.IntersectionType; + return t.intersection( + collectionType.types.map((rt) => + trackKeysOfValidatedTypes(rt, prefix) + ) as [t.Any, t.Any] + ); + } + + case 'UnionType': { + const collectionType = type as t.UnionType; + return t.union( + collectionType.types.map((rt) => + trackKeysOfValidatedTypes(rt, prefix) + ) as [t.Any, t.Any] + ); + } + + case 'MergeType': { + const collectionType = type as MergeType; + return merge( + collectionType.types.map((rt) => + trackKeysOfValidatedTypes(rt, prefix) + ) as [t.Any, t.Any] + ); + } + + case 'PartialType': { + const propsType = type as t.PartialType; + + return addToContextWhenValidated( + t.partial( + mapValues(propsType.props, (val, key) => + trackKeysOfValidatedTypes(val, `${prefix}${key}.`) + ) + ), + prefix + ); + } + + case 'InterfaceType': { + const propsType = type as t.InterfaceType; + + return addToContextWhenValidated( + t.type( + mapValues(propsType.props, (val, key) => + trackKeysOfValidatedTypes(val, `${prefix}${key}.`) + ) + ), + prefix + ); + } + + case 'ExactType': { + const exactType = type as t.ExactType; + + return t.exact( + trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps + ); + } + + default: + return type; + } +} + +class StrictKeysType< + A = any, + O = A, + I = any, + T extends t.Type = t.Type +> extends t.Type { + trackedKeys: string[]; + + constructor(type: T) { + const trackedType = trackKeysOfValidatedTypes(type); + + super( + 'strict_keys', + trackedType.is, + (input, context) => { + this.trackedKeys.length = 0; + return either.chain(trackedType.validate(input, context), (i) => { + const originalKeys = getKeysInObject( + input as Record + ); + const excessKeys = difference(originalKeys, this.trackedKeys); + + if (excessKeys.length) { + return t.failure( + i, + context, + `Excess keys are not allowed: \n${excessKeys.join('\n')}` + ); + } + + return t.success(i); + }); + }, + trackedType.encode + ); + + this.trackedKeys = []; + } +} + +export function strictKeysRt(type: T): T { + return (new StrictKeysType(type) as unknown) as T; +} diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index d75446cb0dd48..e08bd01a1842b 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -24,8 +24,7 @@ import { APIReturnType } from '../../services/rest/createCallApmApi'; import { units } from '../../style/variables'; export type AnomalyDetectionApiResponse = APIReturnType< - '/api/apm/settings/anomaly-detection', - 'GET' + 'GET /api/apm/settings/anomaly-detection' >; const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; @@ -60,7 +59,7 @@ export function AnomalyDetectionSetupLink() { export function MissingJobsAlert({ environment }: { environment?: string }) { const { data = DEFAULT_DATA, status } = useFetcher( (callApmApi) => - callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), + callApmApi({ endpoint: `GET /api/apm/settings/anomaly-detection` }), [], { preservePreviousData: false, showToastOnError: false } ); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index f47674ba5891f..dc97642dec357 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -72,7 +72,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { const { data: errorGroupData } = useFetcher(() => { if (start && end) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/{groupId}', + endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}', params: { path: { serviceName, @@ -91,7 +91,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { const { data: errorDistributionData } = useFetcher(() => { if (start && end) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', + endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index 52fb4b33cbc55..e2a02a2f3e7ae 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -36,7 +36,7 @@ function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { const { data: errorDistributionData } = useFetcher(() => { if (start && end) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', + endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', params: { path: { serviceName, @@ -56,7 +56,7 @@ function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { if (start && end) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors', + endpoint: 'GET /api/apm/services/{serviceName}/errors', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx index f2ae0c2ff99e8..ac1668a54ab95 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx @@ -26,7 +26,7 @@ export function EditAgentConfigurationRouteHandler( const res = useFetcher( (callApmApi) => { return callApmApi({ - pathname: '/api/apm/settings/agent-configuration/view', + endpoint: 'GET /api/apm/settings/agent-configuration/view', params: { query: { name, environment } }, }); }, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index b6924b9552699..237d33a6a89a3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -53,7 +53,7 @@ export function ClientMetrics() { (callApmApi) => { if (uxQuery) { return callApmApi({ - pathname: '/api/apm/rum/client-metrics', + endpoint: 'GET /api/apm/rum/client-metrics', params: { query: { ...uxQuery, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx index 58f00604b8fda..4c4f7110cafb9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ImpactfulMetrics/JSErrors.tsx @@ -39,7 +39,7 @@ export function JSErrors() { (callApmApi) => { if (start && end && serviceName) { return callApmApi({ - pathname: '/api/apm/rum-client/js-errors', + endpoint: 'GET /api/apm/rum-client/js-errors', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index 88d14a0213a96..4b94b98704da7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -37,7 +37,7 @@ export function PageLoadDistribution() { if (start && end && serviceName) { return callApmApi({ - pathname: '/api/apm/rum-client/page-load-distribution', + endpoint: 'GET /api/apm/rum-client/page-load-distribution', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index d6a544333531f..c3f4ab44179fe 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -25,7 +25,7 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { (callApmApi) => { if (start && end && field && value) { return callApmApi({ - pathname: '/api/apm/rum-client/page-load-distribution/breakdown', + endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 621098b6028cb..84668f4b06d77 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -26,7 +26,7 @@ export function PageViewsTrend() { if (start && end && serviceName) { return callApmApi({ - pathname: '/api/apm/rum-client/page-view-trends', + endpoint: 'GET /api/apm/rum-client/page-view-trends', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx index 7c21079885334..6c7e2e22a9893 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx @@ -22,7 +22,7 @@ export function MainFilters() { (callApmApi) => { if (start && end) { return callApmApi({ - pathname: '/api/apm/rum-client/services', + endpoint: 'GET /api/apm/rum-client/services', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx index f9aeb484cbdf9..67692a9a8554b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx @@ -71,7 +71,7 @@ export function URLSearch({ onChange: onFilterChange }: Props) { const { transactionUrl, ...restFilters } = uiFilters; return callApmApi({ - pathname: '/api/apm/rum-client/url-search', + endpoint: 'GET /api/apm/rum-client/url-search', params: { query: { ...uxQuery, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index c7fe8e885020a..2ded35deb58f2 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -55,7 +55,7 @@ export function KeyUXMetrics({ data, loading }: Props) { (callApmApi) => { if (uxQuery) { return callApmApi({ - pathname: '/api/apm/rum-client/long-task-metrics', + endpoint: 'GET /api/apm/rum-client/long-task-metrics', params: { query: { ...uxQuery, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 983e3be1c21a9..95a42ce3018f1 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -33,7 +33,7 @@ export function UXMetrics() { (callApmApi) => { if (uxQuery) { return callApmApi({ - pathname: '/api/apm/rum-client/web-core-vitals', + endpoint: 'GET /api/apm/rum-client/web-core-vitals', params: { query: uxQuery, }, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index 67127f9c2fd81..ce9485690b930 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -22,7 +22,7 @@ export function VisitorBreakdown() { if (start && end && serviceName) { return callApmApi({ - pathname: '/api/apm/rum-client/visitor-breakdown', + endpoint: 'GET /api/apm/rum-client/visitor-breakdown', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts index a9f2486a3c288..4610205cee7ed 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ux_overview_fetchers.ts @@ -19,7 +19,7 @@ export const fetchUxOverviewDate = async ({ serviceName, }: FetchDataParams): Promise => { const data = await callApmApi({ - pathname: '/api/apm/rum-client/web-core-vitals', + endpoint: 'GET /api/apm/rum-client/web-core-vitals', params: { query: { start: new Date(absoluteTime.start).toISOString(), @@ -37,7 +37,7 @@ export const fetchUxOverviewDate = async ({ export async function hasRumData({ absoluteTime }: HasDataParams) { return await callApmApi({ - pathname: '/api/apm/observability_overview/has_rum_data', + endpoint: 'GET /api/apm/observability_overview/has_rum_data', params: { query: { start: new Date(absoluteTime.start).toISOString(), diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index 9e8f1f7a0171e..be8c5cf8cd435 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -42,7 +42,7 @@ export function ServiceStatsFetcher({ (callApmApi) => { if (serviceName && start && end) { return callApmApi({ - pathname: '/api/apm/service-map/service/{serviceName}', + endpoint: 'GET /api/apm/service-map/service/{serviceName}', params: { path: { serviceName }, query: { start, end, uiFilters: JSON.stringify(uiFilters) }, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 15adf8a70d357..1731d3f9430d4 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -83,7 +83,7 @@ export function ServiceMap({ if (start && end) { return callApmApi({ isCachable: false, - pathname: '/api/apm/service-map', + endpoint: 'GET /api/apm/service-map', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx index 7c6b63f75382c..efa6110fea100 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx @@ -58,8 +58,8 @@ export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) { (callApmApi) => { if (start && end) { return callApmApi({ - pathname: - '/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata', + endpoint: + 'GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata', params: { path: { serviceName, serviceNodeName }, query: { diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index b05785db14625..5c9677e3c7af2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -62,7 +62,7 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) { return undefined; } return callApmApi({ - pathname: '/api/apm/services/{serviceName}/serviceNodes', + endpoint: 'GET /api/apm/services/{serviceName}/serviceNodes', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index 869762e360884..7c0869afe0cd1 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -35,7 +35,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( (callApmApi) => { return callApmApi({ - pathname: '/api/apm/settings/agent-configuration/services', + endpoint: 'GET /api/apm/settings/agent-configuration/services', isCachable: true, }); }, @@ -47,7 +47,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { (callApmApi) => { if (newConfig.service.name) { return callApmApi({ - pathname: '/api/apm/settings/agent-configuration/environments', + endpoint: 'GET /api/apm/settings/agent-configuration/environments', params: { query: { serviceName: omitAllOption(newConfig.service.name) }, }, @@ -67,7 +67,7 @@ export function ServicePage({ newConfig, setNewConfig, onClickNext }: Props) { } const { agentName } = await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/agent_name', + endpoint: 'GET /api/apm/settings/agent-configuration/agent_name', params: { query: { serviceName } }, }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts index 4e75b24e6af95..e15a57ff7539e 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts @@ -25,8 +25,7 @@ export async function saveConfig({ }) { try { await callApmApi({ - pathname: '/api/apm/settings/agent-configuration', - method: 'PUT', + endpoint: 'PUT /api/apm/settings/agent-configuration', params: { query: { overwrite: isEditMode }, body: { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index aca04a3e46ad0..3483ad0822801 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -71,8 +71,7 @@ async function deleteConfig( ) { try { await callApmApi({ - pathname: '/api/apm/settings/agent-configuration', - method: 'DELETE', + endpoint: 'DELETE /api/apm/settings/agent-configuration', params: { body: { service: { diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index dfc78028c3596..12c63f8702f25 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -24,7 +24,7 @@ import { AgentConfigurationList } from './List'; export function AgentConfigurations() { const { refetch, data = [], status } = useFetcher( (callApmApi) => - callApmApi({ pathname: '/api/apm/settings/agent-configuration' }), + callApmApi({ endpoint: 'GET /api/apm/settings/agent-configuration' }), [], { preservePreviousData: false, showToastOnError: false } ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index fac947b3ec68e..a1ef9ddd87271 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -72,8 +72,7 @@ async function saveApmIndices({ apmIndices: Record; }) { await callApmApi({ - method: 'POST', - pathname: '/api/apm/settings/apm-indices/save', + endpoint: 'POST /api/apm/settings/apm-indices/save', params: { body: apmIndices, }, @@ -94,7 +93,7 @@ export function ApmIndices() { const { data = INITIAL_STATE, status, refetch } = useFetcher( (_callApmApi) => _callApmApi({ - pathname: `/api/apm/settings/apm-index-settings`, + endpoint: `GET /api/apm/settings/apm-index-settings`, }), [] ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx index 686970c0493ee..5014584c3928a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx @@ -47,8 +47,7 @@ async function deleteConfig( ) { try { await callApmApi({ - pathname: '/api/apm/settings/custom_links/{id}', - method: 'DELETE', + endpoint: 'DELETE /api/apm/settings/custom_links/{id}', params: { path: { id: customLinkId }, }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx index b7250bda30966..25fd8f7ad3caf 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx @@ -31,7 +31,7 @@ interface Props { const fetchTransaction = debounce( async (filters: Filter[], callback: (transaction: Transaction) => void) => { const transaction = await callApmApi({ - pathname: '/api/apm/settings/custom_links/transaction', + endpoint: 'GET /api/apm/settings/custom_links/transaction', params: { query: convertFiltersToQuery(filters) }, }); callback(transaction); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx index a2fd755b234ff..3a2aa01ba3bc4 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/link_preview.test.tsx @@ -18,9 +18,9 @@ export const removeExternalLinkText = (str: string) => str.replace(/\(opens in a new tab or window\)/g, ''); describe('LinkPreview', () => { - let callApmApiSpy: jest.SpyInstance; + let callApmApiSpy: jest.SpyInstance; beforeAll(() => { - callApmApiSpy = jest.spyOn(apmApi, 'callApmApi').mockReturnValue({ + callApmApiSpy = jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({ transaction: { id: 'foo' }, }); }); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts index 8ccd799b7cbc6..cb1eaf6bca3f0 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts @@ -34,8 +34,7 @@ export async function saveCustomLink({ if (id) { await callApmApi({ - pathname: '/api/apm/settings/custom_links/{id}', - method: 'PUT', + endpoint: 'PUT /api/apm/settings/custom_links/{id}', params: { path: { id }, body: customLink, @@ -43,8 +42,7 @@ export async function saveCustomLink({ }); } else { await callApmApi({ - pathname: '/api/apm/settings/custom_links', - method: 'POST', + endpoint: 'POST /api/apm/settings/custom_links', params: { body: customLink, }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index fea22e890dc10..a7feafad11111 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -41,7 +41,7 @@ const data = [ describe('CustomLink', () => { beforeAll(() => { - jest.spyOn(apmApi, 'callApmApi').mockReturnValue({}); + jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({}); }); afterAll(() => { jest.resetAllMocks(); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index a7d7cf40ba849..d872f6d21ed96 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -34,8 +34,9 @@ export function CustomLinkOverview() { CustomLink | undefined >(); - const { data: customLinks, status, refetch } = useFetcher( - (callApmApi) => callApmApi({ pathname: '/api/apm/settings/custom_links' }), + const { data: customLinks = [], status, refetch } = useFetcher( + (callApmApi) => + callApmApi({ endpoint: 'GET /api/apm/settings/custom_links' }), [] ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index a594edb32b083..ccc1778e9fbde 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -42,7 +42,7 @@ export function AddEnvironments({ const { data = [], status } = useFetcher( (callApmApi) => callApmApi({ - pathname: `/api/apm/settings/anomaly-detection/environments`, + endpoint: `GET /api/apm/settings/anomaly-detection/environments`, }), [], { preservePreviousData: false } diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts index 2e2c2ccbad7cf..7106a4c48ef70 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts @@ -27,8 +27,7 @@ export async function createJobs({ }) { try { await callApmApi({ - pathname: '/api/apm/settings/anomaly-detection/jobs', - method: 'POST', + endpoint: 'POST /api/apm/settings/anomaly-detection/jobs', params: { body: { environments }, }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index acc1a1ba1614f..debf3fa85d935 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -18,8 +18,7 @@ import { useLicense } from '../../../../hooks/useLicense'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; export type AnomalyDetectionApiResponse = APIReturnType< - '/api/apm/settings/anomaly-detection', - 'GET' + 'GET /api/apm/settings/anomaly-detection' >; const DEFAULT_VALUE: AnomalyDetectionApiResponse = { @@ -38,7 +37,9 @@ export function AnomalyDetection() { const { refetch, data = DEFAULT_VALUE, status } = useFetcher( (callApmApi) => { if (canGetJobs) { - return callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }); + return callApmApi({ + endpoint: `GET /api/apm/settings/anomaly-detection`, + }); } }, [canGetJobs], diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index ee3b0a33ebbc2..1a41ffe1f606f 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -27,7 +27,7 @@ export function TraceLink({ match }: RouteComponentProps<{ traceId: string }>) { (callApmApi) => { if (traceId) { return callApmApi({ - pathname: '/api/apm/transaction/{traceId}', + endpoint: 'GET /api/apm/transaction/{traceId}', params: { path: { traceId, diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index a87bbdb926a21..cbab2c44132f3 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -15,7 +15,7 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { SearchBar } from '../../shared/search_bar'; import { TraceList } from './TraceList'; -type TracesAPIResponse = APIReturnType<'/api/apm/traces'>; +type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>; const DEFAULT_RESPONSE: TracesAPIResponse = { items: [], isAggregationAccurate: true, @@ -29,7 +29,7 @@ export function TraceOverview() { (callApmApi) => { if (start && end) { return callApmApi({ - pathname: '/api/apm/traces', + endpoint: 'GET /api/apm/traces', params: { query: { start, diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 7a5893314ddf0..83f5f4deb89a3 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -47,7 +47,7 @@ export function ServiceInventory() { (callApmApi) => { if (start && end) { return callApmApi({ - pathname: '/api/apm/services', + endpoint: 'GET /api/apm/services', params: { query: { start, end, uiFilters: JSON.stringify(uiFilters) }, }, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index 34b934c41cca3..82dbd6dd86aab 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -159,7 +159,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { } return callApmApi({ - pathname: '/api/apm/services/{serviceName}/error_groups', + endpoint: 'GET /api/apm/services/{serviceName}/error_groups', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 3f72f07b2a7d2..f5a57544209f5 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -71,7 +71,7 @@ export function TransactionActionMenu({ transaction }: Props) { const { data: customLinks = [], status, refetch } = useFetcher( (callApmApi) => callApmApi({ - pathname: '/api/apm/settings/custom_links', + endpoint: 'GET /api/apm/settings/custom_links', params: { query: convertFiltersToQuery(filters) }, }), [filters] diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index ec0b473c3ade8..9b5f00f76eeb2 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -245,7 +245,7 @@ describe('TransactionActionMenu component', () => { describe('Custom links', () => { beforeAll(() => { // Mocks callApmAPI because it's going to be used to fecth the transaction in the custom links flyout. - jest.spyOn(apmApi, 'callApmApi').mockReturnValue({}); + jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({}); }); afterAll(() => { jest.resetAllMocks(); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 5b977b6991612..dd9a1e2ec2efe 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -44,8 +44,8 @@ export function TransactionErrorRateChart({ const { data, status } = useFetcher(() => { if (serviceName && start && end) { return callApmApi({ - pathname: - '/api/apm/services/{serviceName}/transaction_groups/error_rate', + endpoint: + 'GET /api/apm/services/{serviceName}/transaction_groups/error_rate', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/context/charts_sync_context.tsx b/x-pack/plugins/apm/public/context/charts_sync_context.tsx index 6f69ae097828b..282097fed2460 100644 --- a/x-pack/plugins/apm/public/context/charts_sync_context.tsx +++ b/x-pack/plugins/apm/public/context/charts_sync_context.tsx @@ -34,7 +34,7 @@ export function LegacyChartsSyncContextProvider({ (callApmApi) => { if (start && end && serviceName) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/annotation/search', + endpoint: 'GET /api/apm/services/{serviceName}/annotation/search', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/hooks/useAgentName.ts b/x-pack/plugins/apm/public/hooks/useAgentName.ts index 1f8a3b916ecd0..b226971762fab 100644 --- a/x-pack/plugins/apm/public/hooks/useAgentName.ts +++ b/x-pack/plugins/apm/public/hooks/useAgentName.ts @@ -16,7 +16,7 @@ export function useAgentName() { (callApmApi) => { if (serviceName && start && end) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/agent_name', + endpoint: 'GET /api/apm/services/{serviceName}/agent_name', params: { path: { serviceName }, query: { start, end }, diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts index 56c58bc82967b..5bb36720e7b9b 100644 --- a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts +++ b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts @@ -10,7 +10,7 @@ export function useAnomalyDetectionJobs() { return useFetcher( (callApmApi) => callApmApi({ - pathname: `/api/apm/settings/anomaly-detection`, + endpoint: `GET /api/apm/settings/anomaly-detection`, }), [], { showToastOnError: false } diff --git a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts index 0b4978acdfcb1..d0e12d8537846 100644 --- a/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts @@ -13,7 +13,7 @@ export function useDynamicIndexPattern( const { data, status } = useFetcher( (callApmApi) => { return callApmApi({ - pathname: '/api/apm/index_pattern/dynamic', + endpoint: 'GET /api/apm/index_pattern/dynamic', isCachable: true, params: { query: { diff --git a/x-pack/plugins/apm/public/hooks/useEnvironments.tsx b/x-pack/plugins/apm/public/hooks/useEnvironments.tsx index 9e01dde274ff7..05ac780aefbde 100644 --- a/x-pack/plugins/apm/public/hooks/useEnvironments.tsx +++ b/x-pack/plugins/apm/public/hooks/useEnvironments.tsx @@ -35,7 +35,7 @@ export function useEnvironments({ const { data: environments = [], status = 'loading' } = useFetcher(() => { if (start && end) { return callApmApi({ - pathname: '/api/apm/ui_filters/environments', + endpoint: 'GET /api/apm/ui_filters/environments', params: { query: { start, diff --git a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts index f4a981ff0975b..d264ad6069db3 100644 --- a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts +++ b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts @@ -26,7 +26,7 @@ export function useServiceMetricCharts( (callApmApi) => { if (serviceName && start && end && agentName) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/metrics/charts', + endpoint: 'GET /api/apm/services/{serviceName}/metrics/charts', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx b/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx index 4e110ac2d4380..5f778e3d8834b 100644 --- a/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx +++ b/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx @@ -17,7 +17,7 @@ export function useServiceTransactionTypes(urlParams: IUrlParams) { (callApmApi) => { if (serviceName && start && end) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/transaction_types', + endpoint: 'GET /api/apm/services/{serviceName}/transaction_types', params: { path: { serviceName }, query: { start, end }, diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts index 0705383ecb0ca..1483247686429 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts @@ -19,8 +19,8 @@ export function useTransactionBreakdown() { (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ - pathname: - '/api/apm/services/{serviceName}/transaction_groups/breakdown', + endpoint: + 'GET /api/apm/services/{serviceName}/transaction_groups/breakdown', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts index e66d70a53afa6..78ea30f466cfa 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts @@ -21,7 +21,8 @@ export function useTransactionCharts() { (callApmApi) => { if (serviceName && start && end) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/transaction_groups/charts', + endpoint: + 'GET /api/apm/services/{serviceName}/transaction_groups/charts', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index 8c76225d03486..36b5a7c00d4be 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -38,8 +38,8 @@ export function useTransactionDistribution(urlParams: IUrlParams) { async (callApmApi) => { if (serviceName && start && end && transactionType && transactionName) { const response = await callApmApi({ - pathname: - '/api/apm/services/{serviceName}/transaction_groups/distribution', + endpoint: + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts index b2c2cc30f78ec..e847309fd0265 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -11,7 +11,7 @@ import { APIReturnType } from '../services/rest/createCallApmApi'; import { useFetcher } from './useFetcher'; type TransactionsAPIResponse = APIReturnType< - '/api/apm/services/{serviceName}/transaction_groups' + 'GET /api/apm/services/{serviceName}/transaction_groups' >; const DEFAULT_RESPONSE: Partial = { @@ -28,7 +28,7 @@ export function useTransactionList(urlParams: IUrlParams) { (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/transaction_groups', + endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups', params: { path: { serviceName }, query: { diff --git a/x-pack/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/plugins/apm/public/hooks/useWaterfall.ts index accc92da9ab02..6264ec45088a2 100644 --- a/x-pack/plugins/apm/public/hooks/useWaterfall.ts +++ b/x-pack/plugins/apm/public/hooks/useWaterfall.ts @@ -21,7 +21,7 @@ export function useWaterfall(urlParams: IUrlParams) { (callApmApi) => { if (traceId && start && end) { return callApmApi({ - pathname: '/api/apm/traces/{traceId}', + endpoint: 'GET /api/apm/traces/{traceId}', params: { path: { traceId }, query: { diff --git a/x-pack/plugins/apm/public/hooks/use_annotations.ts b/x-pack/plugins/apm/public/hooks/use_annotations.ts index 2b1c2bec52b3d..e8f6785706a91 100644 --- a/x-pack/plugins/apm/public/hooks/use_annotations.ts +++ b/x-pack/plugins/apm/public/hooks/use_annotations.ts @@ -19,7 +19,7 @@ export function useAnnotations() { const { data = INITIAL_STATE } = useFetcher(() => { if (start && end && serviceName) { return callApmApi({ - pathname: '/api/apm/services/{serviceName}/annotation/search', + endpoint: 'GET /api/apm/services/{serviceName}/annotation/search', params: { path: { serviceName, diff --git a/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts index 3fc673109026b..2307ec9f06bb5 100644 --- a/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts +++ b/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts @@ -23,7 +23,7 @@ describe('callApmApi', () => { it('should format the pathname with the given path params', async () => { await callApmApi({ - pathname: '/api/apm/{param1}/to/{param2}', + endpoint: 'GET /api/apm/{param1}/to/{param2}', params: { path: { param1: 'foo', @@ -42,7 +42,7 @@ describe('callApmApi', () => { it('should add the query parameters to the options object', async () => { await callApmApi({ - pathname: '/api/apm', + endpoint: 'GET /api/apm', params: { query: { foo: 'bar', @@ -65,8 +65,7 @@ describe('callApmApi', () => { it('should stringify the body and add it to the options object', async () => { await callApmApi({ - pathname: '/api/apm', - method: 'POST', + endpoint: 'POST /api/apm', params: { body: { foo: 'bar', diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts index bc1db4eed1d9e..a0ed51be685c7 100644 --- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts +++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.ts @@ -19,7 +19,7 @@ export const fetchObservabilityOverviewPageData = async ({ bucketSize, }: FetchDataParams): Promise => { const data = await callApmApi({ - pathname: '/api/apm/observability_overview', + endpoint: 'GET /api/apm/observability_overview', params: { query: { start: new Date(absoluteTime.start).toISOString(), @@ -58,6 +58,6 @@ export const fetchObservabilityOverviewPageData = async ({ export async function hasData() { return await callApmApi({ - pathname: '/api/apm/observability_overview/has_data', + endpoint: 'GET /api/apm/observability_overview/has_data', }); } diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index 08588bd03008d..2760ed558865a 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -9,10 +9,14 @@ import { callApi } from './callApi'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMAPI } from '../../../server/routes/create_apm_api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Client, HttpMethod } from '../../../server/routes/typings'; +import { Client } from '../../../server/routes/typings'; export type APMClient = Client; -export type APMClientOptions = Omit & { +export type APMClientOptions = Omit< + FetchOptions, + 'query' | 'body' | 'pathname' +> & { + endpoint: string; params?: { body?: any; query?: any; @@ -28,9 +32,10 @@ export let callApmApi: APMClient = () => { export function createCallApmApi(http: HttpSetup) { callApmApi = ((options: APMClientOptions) => { - const { pathname, params = {}, ...opts } = options; + const { endpoint, params = {}, ...opts } = options; const path = (params.path || {}) as Record; + const [method, pathname] = endpoint.split(' '); const formattedPathname = Object.keys(path).reduce((acc, paramName) => { return acc.replace(`{${paramName}}`, path[paramName]); @@ -38,6 +43,7 @@ export function createCallApmApi(http: HttpSetup) { return callApi(http, { ...opts, + method, pathname: formattedPathname, body: params.body, query: params.query, @@ -47,8 +53,7 @@ export function createCallApmApi(http: HttpSetup) { // infer return type from API export type APIReturnType< - TPath extends keyof APMAPI['_S'], - TMethod extends HttpMethod = 'GET' -> = APMAPI['_S'][TPath] extends { [key in TMethod]: { ret: any } } - ? APMAPI['_S'][TPath][TMethod]['ret'] + TPath extends keyof APMAPI['_S'] +> = APMAPI['_S'][TPath] extends { ret: any } + ? APMAPI['_S'][TPath]['ret'] : unknown; diff --git a/x-pack/plugins/apm/public/services/rest/index_pattern.ts b/x-pack/plugins/apm/public/services/rest/index_pattern.ts index 7c96b37738338..6ec542ab6baf3 100644 --- a/x-pack/plugins/apm/public/services/rest/index_pattern.ts +++ b/x-pack/plugins/apm/public/services/rest/index_pattern.ts @@ -8,13 +8,12 @@ import { callApmApi } from './createCallApmApi'; export const createStaticIndexPattern = async () => { return await callApmApi({ - method: 'POST', - pathname: '/api/apm/index_pattern/static', + endpoint: 'POST /api/apm/index_pattern/static', }); }; export const getApmIndexPatternTitle = async () => { return await callApmApi({ - pathname: '/api/apm/index_pattern/title', + endpoint: 'GET /api/apm/index_pattern/title', }); }; diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 65d36c8b36af8..7e128493c8739 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -10,7 +10,6 @@ import { APMConfig } from '../..'; import { KibanaRequest } from '../../../../../../src/core/server'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { ESFilter } from '../../../../../typings/elasticsearch'; -import { ProcessorEvent } from '../../../common/processor_event'; import { isActivePlatinumLicense } from '../../../common/service_map'; import { UIFilters } from '../../../typings/ui_filters'; import { APMRequestHandlerContext } from '../../routes/typings'; @@ -60,7 +59,6 @@ interface SetupRequestParams { */ end?: string; uiFilters?: string; - processorEvent?: ProcessorEvent; }; } diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index 5f8d2afd544f3..19eb639a72bb9 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -12,9 +12,9 @@ import { scoringRt } from '../lib/transaction_groups/correlations/scoring_rt'; import { createRoute } from './create_route'; import { setupRequest } from '../lib/helpers/setup_request'; -export const correlationsForSlowTransactionsRoute = createRoute(() => ({ - path: '/api/apm/correlations/slow_durations', - params: { +export const correlationsForSlowTransactionsRoute = createRoute({ + endpoint: 'GET /api/apm/correlations/slow_durations', + params: t.type({ query: t.intersection([ t.partial({ serviceName: t.string, @@ -29,7 +29,7 @@ export const correlationsForSlowTransactionsRoute = createRoute(() => ({ t.partial({ uiFilters: t.string }), rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { @@ -51,11 +51,11 @@ export const correlationsForSlowTransactionsRoute = createRoute(() => ({ setup, }); }, -})); +}); -export const correlationsForRangesRoute = createRoute(() => ({ - path: '/api/apm/correlations/ranges', - params: { +export const correlationsForRangesRoute = createRoute({ + endpoint: 'GET /api/apm/correlations/ranges', + params: t.type({ query: t.intersection([ t.partial({ serviceName: t.string, @@ -70,7 +70,7 @@ export const correlationsForRangesRoute = createRoute(() => ({ t.partial({ uiFilters: t.string }), rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -98,4 +98,4 @@ export const correlationsForRangesRoute = createRoute(() => ({ setup, }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts index 3d3e26f680e0d..32a5e5c5a5c8a 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.test.ts @@ -6,9 +6,10 @@ import * as t from 'io-ts'; import { createApi } from './index'; import { CoreSetup, Logger } from 'src/core/server'; -import { Params } from '../typings'; +import { RouteParamsRT } from '../typings'; import { BehaviorSubject } from 'rxjs'; import { APMConfig } from '../..'; +import { jsonRt } from '../../../common/runtime_types/json_rt'; const getCoreMock = () => { const get = jest.fn(); @@ -51,30 +52,35 @@ describe('createApi', () => { createApi() .add(() => ({ - path: '/foo', + endpoint: 'GET /foo', handler: async () => null, })) .add(() => ({ - path: '/bar', - method: 'POST', - params: { + endpoint: 'POST /bar', + params: t.type({ body: t.string, - }, + }), handler: async () => null, })) .add(() => ({ - path: '/baz', - method: 'PUT', + endpoint: 'PUT /baz', options: { tags: ['access:apm', 'access:apm_write'], }, handler: async () => null, })) + .add({ + endpoint: 'GET /qux', + options: { + tags: ['access:apm', 'access:apm_write'], + }, + handler: async () => null, + }) .init(mock, context); expect(createRouter).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledTimes(1); + expect(get).toHaveBeenCalledTimes(2); expect(post).toHaveBeenCalledTimes(1); expect(put).toHaveBeenCalledTimes(1); @@ -86,6 +92,14 @@ describe('createApi', () => { validate: expect.anything(), }); + expect(get.mock.calls[1][0]).toEqual({ + options: { + tags: ['access:apm', 'access:apm_write'], + }, + path: '/qux', + validate: expect.anything(), + }); + expect(post.mock.calls[0][0]).toEqual({ options: { tags: ['access:apm'], @@ -104,18 +118,19 @@ describe('createApi', () => { }); describe('when validating', () => { - const initApi = (params: Params) => { + const initApi = (params?: RouteParamsRT) => { const { mock, context, createRouter, get, post } = getCoreMock(); const handlerMock = jest.fn(); createApi() .add(() => ({ - path: '/foo', + endpoint: 'GET /foo', params, handler: handlerMock, })) .init(mock, context); const routeHandler = get.mock.calls[0][1]; + const responseMock = { ok: jest.fn(), internalError: jest.fn(), @@ -142,16 +157,16 @@ describe('createApi', () => { }; it('adds a _debug query parameter by default', async () => { - const { simulate, handlerMock, responseMock } = initApi({}); + const { simulate, handlerMock, responseMock } = initApi(); await simulate({ query: { _debug: 'true' } }); + expect(responseMock.badRequest).not.toHaveBeenCalled(); + expect(handlerMock).toHaveBeenCalledTimes(1); expect(responseMock.ok).toHaveBeenCalled(); - expect(responseMock.badRequest).not.toHaveBeenCalled(); - const params = handlerMock.mock.calls[0][0].context.params; expect(params).toEqual({ @@ -170,7 +185,7 @@ describe('createApi', () => { }); it('throws if any parameters are used but no types are defined', async () => { - const { simulate, responseMock } = initApi({}); + const { simulate, responseMock } = initApi(); await simulate({ query: { @@ -197,11 +212,13 @@ describe('createApi', () => { }); it('validates path parameters', async () => { - const { simulate, handlerMock, responseMock } = initApi({ - path: t.type({ - foo: t.string, - }), - }); + const { simulate, handlerMock, responseMock } = initApi( + t.type({ + path: t.type({ + foo: t.string, + }), + }) + ); await simulate({ params: { @@ -252,17 +269,19 @@ describe('createApi', () => { }); it('validates body parameters', async () => { - const { simulate, handlerMock, responseMock } = initApi({ - body: t.string, - }); + const { simulate, handlerMock, responseMock } = initApi( + t.type({ + body: t.string, + }) + ); await simulate({ body: '', }); + expect(responseMock.badRequest).not.toHaveBeenCalled(); expect(handlerMock).toHaveBeenCalledTimes(1); expect(responseMock.ok).toHaveBeenCalledTimes(1); - expect(responseMock.badRequest).not.toHaveBeenCalled(); const params = handlerMock.mock.calls[0][0].context.params; @@ -281,20 +300,26 @@ describe('createApi', () => { }); it('validates query parameters', async () => { - const { simulate, handlerMock, responseMock } = initApi({ - query: t.type({ bar: t.string }), - }); + const { simulate, handlerMock, responseMock } = initApi( + t.type({ + query: t.type({ + bar: t.string, + filterNames: jsonRt.pipe(t.array(t.string)), + }), + }) + ); await simulate({ query: { bar: '', _debug: 'true', + filterNames: JSON.stringify(['hostName', 'agentName']), }, }); + expect(responseMock.badRequest).not.toHaveBeenCalled(); expect(handlerMock).toHaveBeenCalledTimes(1); expect(responseMock.ok).toHaveBeenCalledTimes(1); - expect(responseMock.badRequest).not.toHaveBeenCalled(); const params = handlerMock.mock.calls[0][0].context.params; @@ -302,6 +327,7 @@ describe('createApi', () => { query: { bar: '', _debug: true, + filterNames: ['hostName', 'agentName'], }, }); diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index cecb4f6ed3367..25a074ea100e5 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -3,31 +3,36 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { pick, difference } from 'lodash'; +import { merge as mergeLodash, pickBy, isEmpty, isPlainObject } from 'lodash'; import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import * as t from 'io-ts'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { isLeft } from 'fp-ts/lib/Either'; import { KibanaResponseFactory, RouteRegistrar } from 'src/core/server'; +import { merge } from '../../../common/runtime_types/merge'; +import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt'; import { APMConfig } from '../..'; -import { - ServerAPI, - RouteFactoryFn, - HttpMethod, - Route, - Params, -} from '../typings'; +import { ServerAPI } from '../typings'; import { jsonRt } from '../../../common/runtime_types/json_rt'; -const debugRt = t.partial({ _debug: jsonRt.pipe(t.boolean) }); +const debugRt = t.exact( + t.partial({ + query: t.exact(t.partial({ _debug: jsonRt.pipe(t.boolean) })), + }) +); + +type RouteOrRouteFactoryFn = Parameters['add']>[0]; + +const isNotEmpty = (val: any) => + val !== undefined && val !== null && !(isPlainObject(val) && isEmpty(val)); export function createApi() { - const factoryFns: Array> = []; + const routes: RouteOrRouteFactoryFn[] = []; const api: ServerAPI<{}> = { _S: {}, - add(fn) { - factoryFns.push(fn); + add(route) { + routes.push((route as unknown) as RouteOrRouteFactoryFn); return this as any; }, init(core, { config$, logger, plugins }) { @@ -39,41 +44,41 @@ export function createApi() { config = val; }); - factoryFns.forEach((fn) => { + routes.forEach((routeOrFactoryFn) => { + const route = + typeof routeOrFactoryFn === 'function' + ? routeOrFactoryFn(core) + : routeOrFactoryFn; + const { - params = {}, - path, + params, + endpoint, options = { tags: ['access:apm'] }, - method, handler, - } = fn(core) as Route; + } = route; - const routerMethod = (method || 'GET').toLowerCase() as + const [method, path] = endpoint.split(' '); + + const typedRouterMethod = method.trim().toLowerCase() as + | 'get' | 'post' | 'put' - | 'get' | 'delete'; + if (!['get', 'post', 'put', 'delete'].includes(typedRouterMethod)) { + throw new Error( + "Couldn't register route, as endpoint was not prefixed with a valid HTTP method" + ); + } + // For all runtime types with props, we create an exact // version that will strip all keys that are unvalidated. - const bodyRt = - params.body && 'props' in params.body - ? t.exact(params.body) - : params.body; - - const rts = { - // Add _debug query parameter to all routes - query: params.query - ? t.exact(t.intersection([params.query, debugRt])) - : t.exact(debugRt), - path: params.path ? t.exact(params.path) : t.strict({}), - body: bodyRt || t.null, - }; + const paramsRt = params ? merge([params, debugRt]) : debugRt; const anyObject = schema.object({}, { unknowns: 'allow' }); - (router[routerMethod] as RouteRegistrar)( + (router[typedRouterMethod] as RouteRegistrar)( { path, options, @@ -89,49 +94,23 @@ export function createApi() { }, async (context, request, response) => { try { - const paramMap = { - path: request.params, - body: request.body, - query: { - _debug: 'false', - ...request.query, + const paramMap = pickBy( + { + path: request.params, + body: request.body, + query: { + _debug: 'false', + ...request.query, + }, }, - }; - - const parsedParams = (Object.keys(rts) as Array< - keyof typeof rts - >).reduce((acc, key) => { - const codec = rts[key]; - const value = paramMap[key]; - - const result = codec.decode(value); - - if (isLeft(result)) { - throw Boom.badRequest(PathReporter.report(result)[0]); - } - - // `io-ts` has stripped unvalidated keys, so we can compare - // the output with the input to see if all object keys are - // known and validated. - const strippedKeys = difference( - Object.keys(value || {}), - Object.keys(result.right || {}) - ); - - if (strippedKeys.length) { - throw Boom.badRequest( - `Unknown keys specified: ${strippedKeys}` - ); - } - - const parsedValue = result.right; - - return { - ...acc, - [key]: parsedValue, - }; - }, {} as Record); + isNotEmpty + ); + const result = strictKeysRt(paramsRt).decode(paramMap); + + if (isLeft(result)) { + throw Boom.badRequest(PathReporter.report(result)[0]); + } const data = await handler({ request, context: { @@ -140,14 +119,16 @@ export function createApi() { // Only return values for parameters that have runtime types, // but always include query as _debug is always set even if // it's not defined in the route. - // @ts-expect-error - params: pick(parsedParams, ...Object.keys(params), 'query'), + params: mergeLodash( + { query: { _debug: false } }, + pickBy(result.right, isNotEmpty) + ), config, logger, }, }); - return response.ok({ body: data }); + return response.ok({ body: data as any }); } catch (error) { if (Boom.isBoom(error)) { return convertBoomToKibanaResponse(error, response); diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 34551c35ee234..a272b448deaf1 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -3,12 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { staticIndexPatternRoute, dynamicIndexPatternRoute, apmIndexPatternTitleRoute, } from './index_pattern'; +import { createApi } from './create_api'; import { errorDistributionRoute, errorGroupsRoute, @@ -65,7 +65,6 @@ import { uiFiltersEnvironmentsRoute, rumOverviewLocalFiltersRoute, } from './ui_filters'; -import { createApi } from './create_api'; import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; import { createCustomLinkRoute, diff --git a/x-pack/plugins/apm/server/routes/create_route.ts b/x-pack/plugins/apm/server/routes/create_route.ts index 892f4ec40de72..0d222f9f30490 100644 --- a/x-pack/plugins/apm/server/routes/create_route.ts +++ b/x-pack/plugins/apm/server/routes/create_route.ts @@ -3,13 +3,26 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { RouteFactoryFn, HttpMethod, Params } from './typings'; + +import { CoreSetup } from 'src/core/server'; +import { Route, RouteParamsRT } from './typings'; + +export function createRoute< + TEndpoint extends string, + TRouteParamsRT extends RouteParamsRT | undefined = undefined, + TReturn = unknown +>( + route: Route +): Route; export function createRoute< - TName extends string, - TReturn, - TMethod extends HttpMethod = 'GET', - TParams extends Params = {} ->(fn: RouteFactoryFn) { - return fn; + TEndpoint extends string, + TRouteParamsRT extends RouteParamsRT | undefined = undefined, + TReturn = unknown +>( + route: (core: CoreSetup) => Route +): (core: CoreSetup) => Route; + +export function createRoute(routeOrFactoryFn: Function | object) { + return routeOrFactoryFn; } diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index 1615550027d3c..189a18698b56f 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -12,9 +12,9 @@ import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; import { uiFiltersRt, rangeRt } from './default_api_types'; -export const errorsRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/errors', - params: { +export const errorsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/errors', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -26,7 +26,7 @@ export const errorsRoute = createRoute(() => ({ uiFiltersRt, rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; @@ -40,27 +40,27 @@ export const errorsRoute = createRoute(() => ({ setup, }); }, -})); +}); -export const errorGroupsRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/errors/{groupId}', - params: { +export const errorGroupsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}', + params: t.type({ path: t.type({ serviceName: t.string, groupId: t.string, }), query: t.intersection([uiFiltersRt, rangeRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName, groupId } = context.params.path; return getErrorGroup({ serviceName, groupId, setup }); }, -})); +}); -export const errorDistributionRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/errors/distribution', - params: { +export const errorDistributionRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -71,7 +71,7 @@ export const errorDistributionRoute = createRoute(() => ({ uiFiltersRt, rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; @@ -79,4 +79,4 @@ export const errorDistributionRoute = createRoute(() => ({ const { groupId } = params.query; return getErrorDistribution({ serviceName, groupId, setup }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index 18bc2986d4061..5b9b211032bf5 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -11,13 +11,14 @@ import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern'; import { getApmIndices } from '../lib/settings/apm_indices/get_apm_indices'; +import { UIProcessorEvent } from '../../common/processor_event'; export const staticIndexPatternRoute = createRoute((core) => ({ - method: 'POST', - path: '/api/apm/index_pattern/static', + endpoint: 'POST /api/apm/index_pattern/static', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const savedObjectsClient = await getInternalSavedObjectsClient(core); + await createStaticIndexPattern(setup, context, savedObjectsClient); // send empty response regardless of outcome @@ -25,9 +26,9 @@ export const staticIndexPatternRoute = createRoute((core) => ({ }, })); -export const dynamicIndexPatternRoute = createRoute(() => ({ - path: '/api/apm/index_pattern/dynamic', - params: { +export const dynamicIndexPatternRoute = createRoute({ + endpoint: 'GET /api/apm/index_pattern/dynamic', + params: t.partial({ query: t.partial({ processorEvent: t.union([ t.literal('transaction'), @@ -35,25 +36,30 @@ export const dynamicIndexPatternRoute = createRoute(() => ({ t.literal('error'), ]), }), - }, + }), handler: async ({ context }) => { const indices = await getApmIndices({ config: context.config, savedObjectsClient: context.core.savedObjects.client, }); + const processorEvent = context.params.query.processorEvent as + | UIProcessorEvent + | undefined; + const dynamicIndexPattern = await getDynamicIndexPattern({ context, indices, + processorEvent, }); return { dynamicIndexPattern }; }, -})); +}); -export const apmIndexPatternTitleRoute = createRoute(() => ({ - path: '/api/apm/index_pattern/title', +export const apmIndexPatternTitleRoute = createRoute({ + endpoint: 'GET /api/apm/index_pattern/title', handler: async ({ context }) => { return getApmIndexPatternTitle(context); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index fabd98c719565..82697a78b424c 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -10,9 +10,9 @@ import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_dat import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; -export const metricsChartsRoute = createRoute(() => ({ - path: `/api/apm/services/{serviceName}/metrics/charts`, - params: { +export const metricsChartsRoute = createRoute({ + endpoint: `GET /api/apm/services/{serviceName}/metrics/charts`, + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -26,7 +26,7 @@ export const metricsChartsRoute = createRoute(() => ({ uiFiltersRt, rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; @@ -39,4 +39,4 @@ export const metricsChartsRoute = createRoute(() => ({ serviceNodeName, }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index 498e8b4792de1..e6d6bc8157a3e 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -12,19 +12,19 @@ import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; -export const observabilityOverviewHasDataRoute = createRoute(() => ({ - path: '/api/apm/observability_overview/has_data', +export const observabilityOverviewHasDataRoute = createRoute({ + endpoint: 'GET /api/apm/observability_overview/has_data', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return await hasData({ setup }); }, -})); +}); -export const observabilityOverviewRoute = createRoute(() => ({ - path: '/api/apm/observability_overview', - params: { +export const observabilityOverviewRoute = createRoute({ + endpoint: 'GET /api/apm/observability_overview', + params: t.type({ query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { bucketSize } = context.params.query; @@ -48,4 +48,4 @@ export const observabilityOverviewRoute = createRoute(() => ({ ]); return { serviceCount, transactionCoordinates }; }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index cfa6eb289688d..ead774c0c7915 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -31,11 +31,11 @@ const uxQueryRt = t.intersection([ t.partial({ urlQuery: t.string, percentile: t.string }), ]); -export const rumClientMetricsRoute = createRoute(() => ({ - path: '/api/apm/rum/client-metrics', - params: { +export const rumClientMetricsRoute = createRoute({ + endpoint: 'GET /api/apm/rum/client-metrics', + params: t.type({ query: uxQueryRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -49,13 +49,13 @@ export const rumClientMetricsRoute = createRoute(() => ({ percentile: percentile ? Number(percentile) : undefined, }); }, -})); +}); -export const rumPageLoadDistributionRoute = createRoute(() => ({ - path: '/api/apm/rum-client/page-load-distribution', - params: { +export const rumPageLoadDistributionRoute = createRoute({ + endpoint: 'GET /api/apm/rum-client/page-load-distribution', + params: t.type({ query: t.intersection([uxQueryRt, percentileRangeRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -70,17 +70,17 @@ export const rumPageLoadDistributionRoute = createRoute(() => ({ urlQuery, }); }, -})); +}); -export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ - path: '/api/apm/rum-client/page-load-distribution/breakdown', - params: { +export const rumPageLoadDistBreakdownRoute = createRoute({ + endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown', + params: t.type({ query: t.intersection([ uxQueryRt, percentileRangeRt, t.type({ breakdown: t.string }), ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -96,13 +96,13 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ urlQuery, }); }, -})); +}); -export const rumPageViewsTrendRoute = createRoute(() => ({ - path: '/api/apm/rum-client/page-view-trends', - params: { +export const rumPageViewsTrendRoute = createRoute({ + endpoint: 'GET /api/apm/rum-client/page-view-trends', + params: t.type({ query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -116,25 +116,25 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ urlQuery, }); }, -})); +}); -export const rumServicesRoute = createRoute(() => ({ - path: '/api/apm/rum-client/services', - params: { +export const rumServicesRoute = createRoute({ + endpoint: 'GET /api/apm/rum-client/services', + params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return getRumServices({ setup }); }, -})); +}); -export const rumVisitorsBreakdownRoute = createRoute(() => ({ - path: '/api/apm/rum-client/visitor-breakdown', - params: { +export const rumVisitorsBreakdownRoute = createRoute({ + endpoint: 'GET /api/apm/rum-client/visitor-breakdown', + params: t.type({ query: uxQueryRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -147,13 +147,13 @@ export const rumVisitorsBreakdownRoute = createRoute(() => ({ urlQuery, }); }, -})); +}); -export const rumWebCoreVitals = createRoute(() => ({ - path: '/api/apm/rum-client/web-core-vitals', - params: { +export const rumWebCoreVitals = createRoute({ + endpoint: 'GET /api/apm/rum-client/web-core-vitals', + params: t.type({ query: uxQueryRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -167,13 +167,13 @@ export const rumWebCoreVitals = createRoute(() => ({ percentile: percentile ? Number(percentile) : undefined, }); }, -})); +}); -export const rumLongTaskMetrics = createRoute(() => ({ - path: '/api/apm/rum-client/long-task-metrics', - params: { +export const rumLongTaskMetrics = createRoute({ + endpoint: 'GET /api/apm/rum-client/long-task-metrics', + params: t.type({ query: uxQueryRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -187,13 +187,13 @@ export const rumLongTaskMetrics = createRoute(() => ({ percentile: percentile ? Number(percentile) : undefined, }); }, -})); +}); -export const rumUrlSearch = createRoute(() => ({ - path: '/api/apm/rum-client/url-search', - params: { +export const rumUrlSearch = createRoute({ + endpoint: 'GET /api/apm/rum-client/url-search', + params: t.type({ query: uxQueryRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -203,18 +203,18 @@ export const rumUrlSearch = createRoute(() => ({ return getUrlSearch({ setup, urlQuery, percentile: Number(percentile) }); }, -})); +}); -export const rumJSErrors = createRoute(() => ({ - path: '/api/apm/rum-client/js-errors', - params: { +export const rumJSErrors = createRoute({ + endpoint: 'GET /api/apm/rum-client/js-errors', + params: t.type({ query: t.intersection([ uiFiltersRt, rangeRt, t.type({ pageSize: t.string, pageIndex: t.string }), t.partial({ urlQuery: t.string }), ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -229,15 +229,15 @@ export const rumJSErrors = createRoute(() => ({ pageIndex: Number(pageIndex), }); }, -})); +}); -export const rumHasDataRoute = createRoute(() => ({ - path: '/api/apm/observability_overview/has_rum_data', - params: { +export const rumHasDataRoute = createRoute({ + endpoint: 'GET /api/apm/observability_overview/has_rum_data', + params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return await hasRumData({ setup }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index ffc8cb84b690a..2ad9d97130d1a 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -18,9 +18,9 @@ import { rangeRt, uiFiltersRt } from './default_api_types'; import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; -export const serviceMapRoute = createRoute(() => ({ - path: '/api/apm/service-map', - params: { +export const serviceMapRoute = createRoute({ + endpoint: 'GET /api/apm/service-map', + params: t.type({ query: t.intersection([ t.partial({ environment: t.string, @@ -28,7 +28,7 @@ export const serviceMapRoute = createRoute(() => ({ }), rangeRt, ]), - }, + }), handler: async ({ context, request }) => { if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); @@ -59,16 +59,16 @@ export const serviceMapRoute = createRoute(() => ({ logger, }); }, -})); +}); -export const serviceMapServiceNodeRoute = createRoute(() => ({ - path: `/api/apm/service-map/service/{serviceName}`, - params: { +export const serviceMapServiceNodeRoute = createRoute({ + endpoint: `GET /api/apm/service-map/service/{serviceName}`, + params: t.type({ path: t.type({ serviceName: t.string, }), query: t.intersection([rangeRt, uiFiltersRt]), - }, + }), handler: async ({ context, request }) => { if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); @@ -92,4 +92,4 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ searchAggregatedTransactions, }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts index 8721407671825..df01a034b06cc 100644 --- a/x-pack/plugins/apm/server/routes/service_nodes.ts +++ b/x-pack/plugins/apm/server/routes/service_nodes.ts @@ -9,14 +9,14 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceNodes } from '../lib/service_nodes'; import { rangeRt, uiFiltersRt } from './default_api_types'; -export const serviceNodesRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/serviceNodes', - params: { +export const serviceNodesRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/serviceNodes', + params: t.type({ path: t.type({ serviceName: t.string, }), query: t.intersection([rangeRt, uiFiltersRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; @@ -27,4 +27,4 @@ export const serviceNodesRoute = createRoute(() => ({ serviceName, }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index ada1674d4555d..10af35df4b0e9 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -20,11 +20,11 @@ import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_trans import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; import { toNumberRt } from '../../common/runtime_types/to_number_rt'; -export const servicesRoute = createRoute(() => ({ - path: '/api/apm/services', - params: { +export const servicesRoute = createRoute({ + endpoint: 'GET /api/apm/services', + params: t.type({ query: t.intersection([uiFiltersRt, rangeRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -40,16 +40,16 @@ export const servicesRoute = createRoute(() => ({ return services; }, -})); +}); -export const serviceAgentNameRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/agent_name', - params: { +export const serviceAgentNameRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/agent_name', + params: t.type({ path: t.type({ serviceName: t.string, }), query: rangeRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -63,16 +63,16 @@ export const serviceAgentNameRoute = createRoute(() => ({ searchAggregatedTransactions, }); }, -})); +}); -export const serviceTransactionTypesRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/transaction_types', - params: { +export const serviceTransactionTypesRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transaction_types', + params: t.type({ path: t.type({ serviceName: t.string, }), query: rangeRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -84,27 +84,28 @@ export const serviceTransactionTypesRoute = createRoute(() => ({ ), }); }, -})); +}); -export const serviceNodeMetadataRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata', - params: { +export const serviceNodeMetadataRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata', + params: t.type({ path: t.type({ serviceName: t.string, serviceNodeName: t.string, }), query: t.intersection([uiFiltersRt, rangeRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName, serviceNodeName } = context.params.path; return getServiceNodeMetadata({ setup, serviceName, serviceNodeName }); }, -})); +}); -export const serviceAnnotationsRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/annotation/search', - params: { +export const serviceAnnotationsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/annotation/search', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -114,7 +115,7 @@ export const serviceAnnotationsRoute = createRoute(() => ({ environment: t.string, }), ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -141,15 +142,14 @@ export const serviceAnnotationsRoute = createRoute(() => ({ logger: context.logger, }); }, -})); +}); -export const serviceAnnotationsCreateRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/annotation', - method: 'POST', +export const serviceAnnotationsCreateRoute = createRoute({ + endpoint: 'POST /api/apm/services/{serviceName}/annotation', options: { tags: ['access:apm', 'access:apm_write'], }, - params: { + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -170,7 +170,7 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({ tags: t.array(t.string), }), ]), - }, + }), handler: async ({ request, context }) => { const annotationsClient = await context.plugins.observability?.getScopedAnnotationsClient( context, @@ -196,11 +196,11 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({ tags: uniq(['apm'].concat(body.tags ?? [])), }); }, -})); +}); -export const serviceErrorGroupsRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/error_groups', - params: { +export const serviceErrorGroupsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/error_groups', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -219,7 +219,7 @@ export const serviceErrorGroupsRoute = createRoute(() => ({ ]), }), ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -238,4 +238,4 @@ export const serviceErrorGroupsRoute = createRoute(() => ({ sortField, }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 7ed5ef442b6fc..942fef5b559ba 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -25,20 +25,20 @@ import { jsonRt } from '../../../common/runtime_types/json_rt'; import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; // get list of configurations -export const agentConfigurationRoute = createRoute(() => ({ - path: '/api/apm/settings/agent-configuration', +export const agentConfigurationRoute = createRoute({ + endpoint: 'GET /api/apm/settings/agent-configuration', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return await listConfigurations({ setup }); }, -})); +}); // get a single configuration -export const getSingleAgentConfigurationRoute = createRoute(() => ({ - path: '/api/apm/settings/agent-configuration/view', - params: { +export const getSingleAgentConfigurationRoute = createRoute({ + endpoint: 'GET /api/apm/settings/agent-configuration/view', + params: t.partial({ query: serviceRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { name, environment } = context.params.query; @@ -56,20 +56,19 @@ export const getSingleAgentConfigurationRoute = createRoute(() => ({ return config._source; }, -})); +}); // delete configuration -export const deleteAgentConfigurationRoute = createRoute(() => ({ - method: 'DELETE', - path: '/api/apm/settings/agent-configuration', +export const deleteAgentConfigurationRoute = createRoute({ + endpoint: 'DELETE /api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'], }, - params: { + params: t.type({ body: t.type({ service: serviceRt, }), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { service } = context.params.body; @@ -92,19 +91,18 @@ export const deleteAgentConfigurationRoute = createRoute(() => ({ setup, }); }, -})); +}); // create/update configuration -export const createOrUpdateAgentConfigurationRoute = createRoute(() => ({ - method: 'PUT', - path: '/api/apm/settings/agent-configuration', +export const createOrUpdateAgentConfigurationRoute = createRoute({ + endpoint: 'PUT /api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'], }, - params: { - query: t.partial({ overwrite: jsonRt.pipe(t.boolean) }), - body: agentConfigurationIntakeRt, - }, + params: t.intersection([ + t.partial({ query: t.partial({ overwrite: jsonRt.pipe(t.boolean) }) }), + t.type({ body: agentConfigurationIntakeRt }), + ]), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { body, query } = context.params; @@ -135,7 +133,7 @@ export const createOrUpdateAgentConfigurationRoute = createRoute(() => ({ setup, }); }, -})); +}); const searchParamsRt = t.intersection([ t.type({ service: serviceRt }), @@ -145,12 +143,11 @@ const searchParamsRt = t.intersection([ export type AgentConfigSearchParams = t.TypeOf; // Lookup single configuration (used by APM Server) -export const agentConfigurationSearchRoute = createRoute(() => ({ - method: 'POST', - path: '/api/apm/settings/agent-configuration/search', - params: { +export const agentConfigurationSearchRoute = createRoute({ + endpoint: 'POST /api/apm/settings/agent-configuration/search', + params: t.type({ body: searchParamsRt, - }, + }), handler: async ({ context, request }) => { const { service, @@ -188,16 +185,15 @@ export const agentConfigurationSearchRoute = createRoute(() => ({ return config; }, -})); +}); /* * Utility endpoints (not documented as part of the public API) */ // get list of services -export const listAgentConfigurationServicesRoute = createRoute(() => ({ - method: 'GET', - path: '/api/apm/settings/agent-configuration/services', +export const listAgentConfigurationServicesRoute = createRoute({ + endpoint: 'GET /api/apm/settings/agent-configuration/services', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const searchAggregatedTransactions = await getSearchAggregatedTransactions( @@ -208,14 +204,14 @@ export const listAgentConfigurationServicesRoute = createRoute(() => ({ searchAggregatedTransactions, }); }, -})); +}); // get environments for service -export const listAgentConfigurationEnvironmentsRoute = createRoute(() => ({ - path: '/api/apm/settings/agent-configuration/environments', - params: { +export const listAgentConfigurationEnvironmentsRoute = createRoute({ + endpoint: 'GET /api/apm/settings/agent-configuration/environments', + params: t.partial({ query: t.partial({ serviceName: t.string }), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.query; @@ -229,18 +225,18 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute(() => ({ searchAggregatedTransactions, }); }, -})); +}); // get agentName for service -export const agentConfigurationAgentNameRoute = createRoute(() => ({ - path: '/api/apm/settings/agent-configuration/agent_name', - params: { +export const agentConfigurationAgentNameRoute = createRoute({ + endpoint: 'GET /api/apm/settings/agent-configuration/agent_name', + params: t.type({ query: t.type({ serviceName: t.string }), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.query; const agentName = await getAgentNameByService({ serviceName, setup }); return { agentName }; }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 3e5a9ee725991..633c284e91a4d 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -18,9 +18,8 @@ import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_tr import { notifyFeatureUsage } from '../../feature'; // get ML anomaly detection jobs for each environment -export const anomalyDetectionJobsRoute = createRoute(() => ({ - method: 'GET', - path: '/api/apm/settings/anomaly-detection', +export const anomalyDetectionJobsRoute = createRoute({ + endpoint: 'GET /api/apm/settings/anomaly-detection', options: { tags: ['access:apm', 'access:ml:canGetJobs'], }, @@ -40,20 +39,19 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ hasLegacyJobs: legacyJobs, }; }, -})); +}); // create new ML anomaly detection jobs for each given environment -export const createAnomalyDetectionJobsRoute = createRoute(() => ({ - method: 'POST', - path: '/api/apm/settings/anomaly-detection/jobs', +export const createAnomalyDetectionJobsRoute = createRoute({ + endpoint: 'POST /api/apm/settings/anomaly-detection/jobs', options: { tags: ['access:apm', 'access:apm_write', 'access:ml:canCreateJob'], }, - params: { + params: t.type({ body: t.type({ environments: t.array(t.string), }), - }, + }), handler: async ({ context, request }) => { const { environments } = context.params.body; const setup = await setupRequest(context, request); @@ -68,12 +66,11 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ featureName: 'ml', }); }, -})); +}); // get all available environments to create anomaly detection jobs for -export const anomalyDetectionEnvironmentsRoute = createRoute(() => ({ - method: 'GET', - path: '/api/apm/settings/anomaly-detection/environments', +export const anomalyDetectionEnvironmentsRoute = createRoute({ + endpoint: 'GET /api/apm/settings/anomaly-detection/environments', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -87,4 +84,4 @@ export const anomalyDetectionEnvironmentsRoute = createRoute(() => ({ includeMissing: true, }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts index 1946bd1111d4b..760ee4225ede2 100644 --- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts @@ -13,34 +13,31 @@ import { import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices'; // get list of apm indices and values -export const apmIndexSettingsRoute = createRoute(() => ({ - method: 'GET', - path: '/api/apm/settings/apm-index-settings', +export const apmIndexSettingsRoute = createRoute({ + endpoint: 'GET /api/apm/settings/apm-index-settings', handler: async ({ context }) => { return await getApmIndexSettings({ context }); }, -})); +}); // get apm indices configuration object -export const apmIndicesRoute = createRoute(() => ({ - method: 'GET', - path: '/api/apm/settings/apm-indices', +export const apmIndicesRoute = createRoute({ + endpoint: 'GET /api/apm/settings/apm-indices', handler: async ({ context }) => { return await getApmIndices({ savedObjectsClient: context.core.savedObjects.client, config: context.config, }); }, -})); +}); // save ui indices -export const saveApmIndicesRoute = createRoute(() => ({ - method: 'POST', - path: '/api/apm/settings/apm-indices/save', +export const saveApmIndicesRoute = createRoute({ + endpoint: 'POST /api/apm/settings/apm-indices/save', options: { tags: ['access:apm', 'access:apm_write'], }, - params: { + params: t.type({ body: t.partial({ /* eslint-disable @typescript-eslint/naming-convention */ 'apm_oss.sourcemapIndices': t.string, @@ -51,10 +48,10 @@ export const saveApmIndicesRoute = createRoute(() => ({ 'apm_oss.metricsIndices': t.string, /* eslint-enable @typescript-eslint/naming-convention */ }), - }, + }), handler: async ({ context }) => { const { body } = context.params; const savedObjectsClient = context.core.savedObjects.client; return await saveApmIndices(savedObjectsClient, body); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index 33769ac1d1c6f..6f06ed4e970df 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -26,11 +26,11 @@ function isActiveGoldLicense(license: ILicense) { return license.isActive && license.hasAtLeast('gold'); } -export const customLinkTransactionRoute = createRoute(() => ({ - path: '/api/apm/settings/custom_links/transaction', - params: { +export const customLinkTransactionRoute = createRoute({ + endpoint: 'GET /api/apm/settings/custom_links/transaction', + params: t.partial({ query: filterOptionsRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { query } = context.params; @@ -38,13 +38,13 @@ export const customLinkTransactionRoute = createRoute(() => ({ const filters = pick(query, FILTER_OPTIONS); return await getTransaction({ setup, filters }); }, -})); +}); -export const listCustomLinksRoute = createRoute(() => ({ - path: '/api/apm/settings/custom_links', - params: { +export const listCustomLinksRoute = createRoute({ + endpoint: 'GET /api/apm/settings/custom_links', + params: t.partial({ query: filterOptionsRt, - }, + }), handler: async ({ context, request }) => { if (!isActiveGoldLicense(context.licensing.license)) { throw Boom.forbidden(INVALID_LICENSE); @@ -55,14 +55,13 @@ export const listCustomLinksRoute = createRoute(() => ({ const filters = pick(query, FILTER_OPTIONS); return await listCustomLinks({ setup, filters }); }, -})); +}); -export const createCustomLinkRoute = createRoute(() => ({ - method: 'POST', - path: '/api/apm/settings/custom_links', - params: { +export const createCustomLinkRoute = createRoute({ + endpoint: 'POST /api/apm/settings/custom_links', + params: t.type({ body: payloadRt, - }, + }), options: { tags: ['access:apm', 'access:apm_write'], }, @@ -80,17 +79,16 @@ export const createCustomLinkRoute = createRoute(() => ({ }); return res; }, -})); +}); -export const updateCustomLinkRoute = createRoute(() => ({ - method: 'PUT', - path: '/api/apm/settings/custom_links/{id}', - params: { +export const updateCustomLinkRoute = createRoute({ + endpoint: 'PUT /api/apm/settings/custom_links/{id}', + params: t.type({ path: t.type({ id: t.string, }), body: payloadRt, - }, + }), options: { tags: ['access:apm', 'access:apm_write'], }, @@ -108,16 +106,15 @@ export const updateCustomLinkRoute = createRoute(() => ({ }); return res; }, -})); +}); -export const deleteCustomLinkRoute = createRoute(() => ({ - method: 'DELETE', - path: '/api/apm/settings/custom_links/{id}', - params: { +export const deleteCustomLinkRoute = createRoute({ + endpoint: 'DELETE /api/apm/settings/custom_links/{id}', + params: t.type({ path: t.type({ id: t.string, }), - }, + }), options: { tags: ['access:apm', 'access:apm_write'], }, @@ -133,4 +130,4 @@ export const deleteCustomLinkRoute = createRoute(() => ({ }); return res; }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 45b334a7f06d2..9bbf6f1cc9061 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -12,11 +12,11 @@ import { createRoute } from './create_route'; import { rangeRt, uiFiltersRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; -export const tracesRoute = createRoute(() => ({ - path: '/api/apm/traces', - params: { +export const tracesRoute = createRoute({ + endpoint: 'GET /api/apm/traces', + params: t.type({ query: t.intersection([rangeRt, uiFiltersRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const searchAggregatedTransactions = await getSearchAggregatedTransactions( @@ -27,18 +27,18 @@ export const tracesRoute = createRoute(() => ({ setup ); }, -})); +}); -export const tracesByIdRoute = createRoute(() => ({ - path: '/api/apm/traces/{traceId}', - params: { +export const tracesByIdRoute = createRoute({ + endpoint: 'GET /api/apm/traces/{traceId}', + params: t.type({ path: t.type({ traceId: t.string, }), query: rangeRt, - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); return getTrace(context.params.path.traceId, setup); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/transaction.ts b/x-pack/plugins/apm/server/routes/transaction.ts index b8cf0f4554d4e..04f6c2e1ce247 100644 --- a/x-pack/plugins/apm/server/routes/transaction.ts +++ b/x-pack/plugins/apm/server/routes/transaction.ts @@ -9,16 +9,16 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; import { createRoute } from './create_route'; -export const transactionByTraceIdRoute = createRoute(() => ({ - path: '/api/apm/transaction/{traceId}', - params: { +export const transactionByTraceIdRoute = createRoute({ + endpoint: 'GET /api/apm/transaction/{traceId}', + params: t.type({ path: t.type({ traceId: t.string, }), - }, + }), handler: async ({ context, request }) => { const { traceId } = context.params.path; const setup = await setupRequest(context, request); return getRootTransactionByTraceId(traceId, setup); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index a3a73222210bb..423506afebe77 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -17,9 +17,9 @@ import { getTransactionSampleForGroup } from '../lib/transaction_groups/get_tran import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; -export const transactionGroupsRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/transaction_groups', - params: { +export const transactionGroupsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -30,7 +30,7 @@ export const transactionGroupsRoute = createRoute(() => ({ uiFiltersRt, rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -50,11 +50,11 @@ export const transactionGroupsRoute = createRoute(() => ({ setup ); }, -})); +}); -export const transactionGroupsChartsRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/transaction_groups/charts', - params: { +export const transactionGroupsChartsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups/charts', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -66,7 +66,7 @@ export const transactionGroupsChartsRoute = createRoute(() => ({ uiFiltersRt, rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const logger = context.logger; @@ -94,11 +94,12 @@ export const transactionGroupsChartsRoute = createRoute(() => ({ return getTransactionCharts(options); }, -})); +}); -export const transactionGroupsDistributionRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/transaction_groups/distribution', - params: { +export const transactionGroupsDistributionRoute = createRoute({ + endpoint: + 'GET /api/apm/services/{serviceName}/transaction_groups/distribution', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -114,7 +115,7 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({ uiFiltersRt, rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -139,11 +140,11 @@ export const transactionGroupsDistributionRoute = createRoute(() => ({ searchAggregatedTransactions, }); }, -})); +}); -export const transactionGroupsBreakdownRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/transaction_groups/breakdown', - params: { +export const transactionGroupsBreakdownRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups/breakdown', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -157,7 +158,7 @@ export const transactionGroupsBreakdownRoute = createRoute(() => ({ uiFiltersRt, rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; @@ -170,17 +171,17 @@ export const transactionGroupsBreakdownRoute = createRoute(() => ({ setup, }); }, -})); +}); -export const transactionSampleForGroupRoute = createRoute(() => ({ - path: `/api/apm/transaction_sample`, - params: { +export const transactionSampleForGroupRoute = createRoute({ + endpoint: `GET /api/apm/transaction_sample`, + params: t.type({ query: t.intersection([ uiFiltersRt, rangeRt, t.type({ serviceName: t.string, transactionName: t.string }), ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); @@ -194,11 +195,11 @@ export const transactionSampleForGroupRoute = createRoute(() => ({ }), }; }, -})); +}); -export const transactionGroupsErrorRateRoute = createRoute(() => ({ - path: '/api/apm/services/{serviceName}/transaction_groups/error_rate', - params: { +export const transactionGroupsErrorRateRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/transaction_groups/error_rate', + params: t.type({ path: t.type({ serviceName: t.string, }), @@ -210,7 +211,7 @@ export const transactionGroupsErrorRateRoute = createRoute(() => ({ transactionName: t.string, }), ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { params } = context; @@ -229,4 +230,4 @@ export const transactionGroupsErrorRateRoute = createRoute(() => ({ searchAggregatedTransactions, }); }, -})); +}); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 78c820fbf4ecd..5f1b344ead5cb 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -4,62 +4,70 @@ * you may not use this file except in compliance with the Elastic License. */ -import t from 'io-ts'; +import t, { Encode, Encoder } from 'io-ts'; import { CoreSetup, KibanaRequest, RequestHandlerContext, Logger, } from 'src/core/server'; -import { PickByValue, Optional } from 'utility-types'; import { Observable } from 'rxjs'; +import { RequiredKeys } from 'utility-types'; import { ObservabilityPluginSetup } from '../../../observability/server'; import { SecurityPluginSetup } from '../../../security/server'; import { MlPluginSetup } from '../../../ml/server'; import { FetchOptions } from '../../common/fetch_options'; import { APMConfig } from '..'; -export interface Params { - query?: t.HasProps; - path?: t.HasProps; - body?: t.Any | t.HasProps; +export interface RouteParams { + path?: Record; + query?: Record; + body?: any; } -type DecodeParams = { - [key in keyof TParams]: TParams[key] extends t.Any - ? t.TypeOf - : never; -}; +type WithoutIncompatibleMethods = Omit< + T, + 'encode' | 'asEncoder' +> & { encode: Encode; asEncoder: () => Encoder }; + +export type RouteParamsRT = WithoutIncompatibleMethods>; + +export type RouteHandler< + TParamsRT extends RouteParamsRT | undefined, + TReturn +> = (kibanaContext: { + context: APMRequestHandlerContext< + (TParamsRT extends RouteParamsRT ? t.TypeOf : {}) & { + query: { _debug: boolean }; + } + >; + request: KibanaRequest; +}) => Promise; -export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; +interface RouteOptions { + tags: Array< + | 'access:apm' + | 'access:apm_write' + | 'access:ml:canGetJobs' + | 'access:ml:canCreateJob' + >; +} export interface Route< - TPath extends string, - TMethod extends HttpMethod | undefined, - TParams extends Params | undefined, + TEndpoint extends string, + TRouteParamsRT extends RouteParamsRT | undefined, TReturn > { - path: TPath; - method?: TMethod; - params?: TParams; - options?: { - tags: Array< - | 'access:apm' - | 'access:apm_write' - | 'access:ml:canGetJobs' - | 'access:ml:canCreateJob' - >; - }; - handler: (kibanaContext: { - context: APMRequestHandlerContext>; - request: KibanaRequest; - }) => Promise; + endpoint: TEndpoint; + options?: RouteOptions; + params?: TRouteParamsRT; + handler: RouteHandler; } export type APMRequestHandlerContext< - TDecodedParams extends { [key in keyof Params]: any } = {} + TRouteParams = {} > = RequestHandlerContext & { - params: { query: { _debug: boolean } } & TDecodedParams; + params: TRouteParams & { query: { _debug: boolean } }; config: APMConfig; logger: Logger; plugins: { @@ -69,39 +77,29 @@ export type APMRequestHandlerContext< }; }; -export type RouteFactoryFn< - TPath extends string, - TMethod extends HttpMethod | undefined, - TParams extends Params, - TReturn -> = (core: CoreSetup) => Route; - export interface RouteState { - [key: string]: { - [key in HttpMethod]: { - params?: Params; - ret: any; - }; + [endpoint: string]: { + params?: RouteParams; + ret: any; }; } export interface ServerAPI { _S: TRouteState; add< - TPath extends string, - TReturn, - // default params allow them to be optional in the route configuration object - TMethod extends HttpMethod = 'GET', - TParams extends Params = {} + TEndpoint extends string, + TRouteParamsRT extends RouteParamsRT | undefined = undefined, + TReturn = unknown >( - factoryFn: RouteFactoryFn + route: + | Route + | ((core: CoreSetup) => Route) ): ServerAPI< TRouteState & { - [Key in TPath]: { - [key in TMethod]: { - ret: TReturn; - } & (TParams extends Params ? { params: TParams } : {}); + [key in TEndpoint]: { + params: TRouteParamsRT; + ret: TReturn; }; } >; @@ -119,49 +117,23 @@ export interface ServerAPI { ) => void; } -// without this, TS does not recognize possible existence of `params` in `options` below -interface NoParams { - params?: TParams; -} - -type GetOptionalParamKeys = keyof PickByValue< - { - [key in keyof TParams]: TParams[key] extends t.PartialType - ? false - : TParams[key] extends t.Any - ? true - : false; - }, - false ->; - -// this type makes the params object optional if no required props are found -type GetParams = Exclude< - keyof TParams, - GetOptionalParamKeys +type MaybeOptional }> = RequiredKeys< + T['params'] > extends never - ? NoParams>> - : { - params: Optional, GetOptionalParamKeys>; - }; + ? { params?: T['params'] } + : { params: T['params'] }; export type Client = < - TPath extends keyof TRouteState & string, - TMethod extends keyof TRouteState[TPath] & string, - TRouteDescription extends TRouteState[TPath][TMethod], - TParams extends TRouteDescription extends { params: Params } - ? TRouteDescription['params'] - : undefined, - TReturn extends TRouteDescription extends { ret: any } - ? TRouteDescription['ret'] - : undefined + TEndpoint extends keyof TRouteState & string >( options: Omit & { forceCache?: boolean; - pathname: TPath; - } & (TMethod extends 'GET' ? { method?: TMethod } : { method: TMethod }) & - // Makes sure params can only be set when types were defined - (TParams extends Params - ? GetParams - : NoParams>) -) => Promise; + endpoint: TEndpoint; + } & (TRouteState[TEndpoint] extends { params: t.Any } + ? MaybeOptional<{ params: t.TypeOf }> + : {}) +) => Promise< + TRouteState[TEndpoint] extends { ret: any } + ? TRouteState[TEndpoint]['ret'] + : unknown +>; diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index 26fe0118c02ed..67e23ebbe2493 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -30,16 +30,16 @@ import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_trans import { APMRequestHandlerContext } from './typings'; import { LocalUIFilterName } from '../../common/ui_filter'; -export const uiFiltersEnvironmentsRoute = createRoute(() => ({ - path: '/api/apm/ui_filters/environments', - params: { +export const uiFiltersEnvironmentsRoute = createRoute({ + endpoint: 'GET /api/apm/ui_filters/environments', + params: t.type({ query: t.intersection([ t.partial({ serviceName: t.string, }), rangeRt, ]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.query; @@ -53,7 +53,7 @@ export const uiFiltersEnvironmentsRoute = createRoute(() => ({ searchAggregatedTransactions, }); }, -})); +}); const filterNamesRt = t.type({ filterNames: jsonRt.pipe( @@ -74,26 +74,26 @@ const localUiBaseQueryRt = t.intersection([ ]); function createLocalFiltersRoute< - TPath extends string, + TEndpoint extends string, TProjection extends Projection, TQueryRT extends t.HasProps >({ - path, + endpoint, getProjection, queryRt, }: { - path: TPath; + endpoint: TEndpoint; getProjection: GetProjection< TProjection, t.IntersectionC<[TQueryRT, BaseQueryType]> >; queryRt: TQueryRT; }) { - return createRoute(() => ({ - path, - params: { + return createRoute({ + endpoint, + params: t.type({ query: t.intersection([localUiBaseQueryRt, queryRt]), - }, + }), handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { uiFilters } = setup; @@ -116,11 +116,11 @@ function createLocalFiltersRoute< localFilterNames: filterNames, }); }, - })); + }); } export const servicesLocalFiltersRoute = createLocalFiltersRoute({ - path: `/api/apm/ui_filters/local_filters/services`, + endpoint: `GET /api/apm/ui_filters/local_filters/services`, getProjection: async ({ context, setup }) => { const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -132,7 +132,7 @@ export const servicesLocalFiltersRoute = createLocalFiltersRoute({ }); export const transactionGroupsLocalFiltersRoute = createLocalFiltersRoute({ - path: '/api/apm/ui_filters/local_filters/transactionGroups', + endpoint: 'GET /api/apm/ui_filters/local_filters/transactionGroups', getProjection: async ({ context, setup, query }) => { const { transactionType, serviceName, transactionName } = query; @@ -163,7 +163,7 @@ export const transactionGroupsLocalFiltersRoute = createLocalFiltersRoute({ }); export const tracesLocalFiltersRoute = createLocalFiltersRoute({ - path: '/api/apm/ui_filters/local_filters/traces', + endpoint: 'GET /api/apm/ui_filters/local_filters/traces', getProjection: async ({ setup, context }) => { const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -178,7 +178,7 @@ export const tracesLocalFiltersRoute = createLocalFiltersRoute({ }); export const transactionsLocalFiltersRoute = createLocalFiltersRoute({ - path: '/api/apm/ui_filters/local_filters/transactions', + endpoint: 'GET /api/apm/ui_filters/local_filters/transactions', getProjection: async ({ context, setup, query }) => { const { transactionType, serviceName, transactionName } = query; @@ -202,7 +202,7 @@ export const transactionsLocalFiltersRoute = createLocalFiltersRoute({ }); export const metricsLocalFiltersRoute = createLocalFiltersRoute({ - path: '/api/apm/ui_filters/local_filters/metrics', + endpoint: 'GET /api/apm/ui_filters/local_filters/metrics', getProjection: ({ setup, query }) => { const { serviceName, serviceNodeName } = query; return getMetricsProjection({ @@ -222,7 +222,7 @@ export const metricsLocalFiltersRoute = createLocalFiltersRoute({ }); export const errorGroupsLocalFiltersRoute = createLocalFiltersRoute({ - path: '/api/apm/ui_filters/local_filters/errorGroups', + endpoint: 'GET /api/apm/ui_filters/local_filters/errorGroups', getProjection: ({ setup, query }) => { const { serviceName } = query; return getErrorGroupsProjection({ @@ -236,7 +236,7 @@ export const errorGroupsLocalFiltersRoute = createLocalFiltersRoute({ }); export const serviceNodesLocalFiltersRoute = createLocalFiltersRoute({ - path: '/api/apm/ui_filters/local_filters/serviceNodes', + endpoint: 'GET /api/apm/ui_filters/local_filters/serviceNodes', getProjection: ({ setup, query }) => { const { serviceName } = query; return getServiceNodesProjection({ @@ -250,7 +250,7 @@ export const serviceNodesLocalFiltersRoute = createLocalFiltersRoute({ }); export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ - path: '/api/apm/ui_filters/local_filters/rumOverview', + endpoint: 'GET /api/apm/ui_filters/local_filters/rumOverview', getProjection: async ({ setup }) => { return getRumPageLoadTransactionsProjection({ setup, From b63830f1055afe31f6301dd4ae0987c853e4b131 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 18 Nov 2020 13:05:14 -0600 Subject: [PATCH 78/99] [Workplace Search] Port Box changes from ent-search (#83675) --- .../components/shared/assets/box.svg | 2 +- .../workplace_search/constants.ts | 3 ++ .../applications/workplace_search/routes.ts | 3 ++ .../views/content_sources/source_data.tsx | 41 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg index 1e7324d9581a7..827f8cf0a55ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 4e093f472d562..1846115d73900 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -55,6 +55,9 @@ export const SOURCE_STATUSES = { }; export const SOURCE_NAMES = { + BOX: i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.box', { + defaultMessage: 'Box', + }), CONFLUENCE: i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.sourceNames.confluence', { defaultMessage: 'Confluence' } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 6099a42e6d7cb..419ae1cbfbc07 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -21,6 +21,7 @@ export const PRIVATE_SOURCES_DOCS_URL = `${DOCUMENT_PERMISSIONS_DOCS_URL}#source export const EXTERNAL_IDENTITIES_DOCS_URL = `${DOCS_PREFIX}/workplace-search-external-identities-api.html`; export const SECURITY_DOCS_URL = `${DOCS_PREFIX}/workplace-search-security.html`; export const SMTP_DOCS_URL = `${DOCS_PREFIX}/workplace-search-smtp-mailer.html`; +export const BOX_DOCS_URL = `${DOCS_PREFIX}/workplace-search-box-connector.html`; export const CONFLUENCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-confluence-cloud-connector.html`; export const CONFLUENCE_SERVER_DOCS_URL = `${DOCS_PREFIX}/workplace-search-confluence-server-connector.html`; export const DROPBOX_DOCS_URL = `${DOCS_PREFIX}/workplace-search-dropbox-connector.html`; @@ -59,6 +60,7 @@ export const ORG_SOURCES_PATH = `${ORG_PATH}${SOURCES_PATH}`; export const SOURCE_ADDED_PATH = `${SOURCES_PATH}/added`; export const ADD_SOURCE_PATH = `${SOURCES_PATH}/add`; +export const ADD_BOX_PATH = `${SOURCES_PATH}/add/box`; export const ADD_CONFLUENCE_PATH = `${SOURCES_PATH}/add/confluence-cloud`; export const ADD_CONFLUENCE_SERVER_PATH = `${SOURCES_PATH}/add/confluence-server`; export const ADD_DROPBOX_PATH = `${SOURCES_PATH}/add/dropbox`; @@ -93,6 +95,7 @@ export const ORG_SETTINGS_PATH = `${ORG_PATH}/settings`; export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`; export const ORG_SETTINGS_CONNECTORS_PATH = `${ORG_SETTINGS_PATH}/connectors`; export const ORG_SETTINGS_OAUTH_APPLICATION_PATH = `${ORG_SETTINGS_PATH}/oauth`; +export const EDIT_BOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/box/edit`; export const EDIT_CONFLUENCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence-cloud/edit`; export const EDIT_CONFLUENCE_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence-server/edit`; export const EDIT_DROPBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/dropbox/edit`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index dff9895dd84f9..882c3861922e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { + ADD_BOX_PATH, ADD_CONFLUENCE_PATH, ADD_CONFLUENCE_SERVER_PATH, ADD_DROPBOX_PATH, @@ -24,6 +25,7 @@ import { ADD_SLACK_PATH, ADD_ZENDESK_PATH, ADD_CUSTOM_PATH, + EDIT_BOX_PATH, EDIT_CONFLUENCE_PATH, EDIT_CONFLUENCE_SERVER_PATH, EDIT_DROPBOX_PATH, @@ -41,6 +43,7 @@ import { EDIT_SLACK_PATH, EDIT_ZENDESK_PATH, EDIT_CUSTOM_PATH, + BOX_DOCS_URL, CONFLUENCE_DOCS_URL, CONFLUENCE_SERVER_DOCS_URL, GITHUB_ENTERPRISE_DOCS_URL, @@ -82,6 +85,44 @@ const connectStepDescription = { }; export const staticSourceData = [ + { + name: SOURCE_NAMES.BOX, + serviceType: 'box', + addPath: ADD_BOX_PATH, + editPath: EDIT_BOX_PATH, + configuration: { + isPublicKey: false, + hasOauthRedirect: true, + needsBaseUrl: false, + documentationUrl: BOX_DOCS_URL, + applicationPortalUrl: 'https://app.box.com/developers/console', + }, + sourceDescription: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.sourceDescriptions.box', + { + defaultMessage: + '{sourceName} is a cloud-based storage service for organizations of all sizes. Create, store, share and automatically synchronize documents across your desktop and web.', + values: { sourceName: SOURCE_NAMES.BOX }, + } + ), + connectStepDescription: connectStepDescription.files, + objTypes: [SOURCE_OBJ_TYPES.ALL_FILES], + features: { + basicOrgContext: [ + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + FeatureIds.GlobalAccessPermissions, + ], + basicOrgContextExcludedFeatures: [FeatureIds.DocumentLevelPermissions], + platinumOrgContext: [FeatureIds.SyncFrequency, FeatureIds.SyncedItems], + platinumPrivateContext: [ + FeatureIds.Private, + FeatureIds.SyncFrequency, + FeatureIds.SyncedItems, + ], + }, + accountContextOnly: false, + }, { name: SOURCE_NAMES.CONFLUENCE, serviceType: 'confluence_cloud', From e7ff3a6f33b51fe7daf2e06330816c4fef6a8b56 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 18 Nov 2020 13:05:36 -0600 Subject: [PATCH 79/99] [Workplace Search] Migrate SourceLogic from ent-search (#83593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial copy/paste of source logic Only changed lodash imports and import order for linting * Add types and route * Update paths and typings Renamed IMeta -> Meta Used object instead of IObject * Remove internal flash messages in favor of globals - All instances of flashAPIErrors(e) are only placeholders until the later commit removing axios. - buttonLoading was set to false when the error flash messages were set. For now I added a `setButtonNotLoading` action to do this manually in a finally block. This will be refactored once axios is removed. - SourcesLogic is no longer needed because we set a queued flash message instead of trying to set it in SourcesLogic, which no longer has local flash messages * Add return types to callback definitions * Update routes According to the API info getSourceReConnectData is supposed to send the source ID and not the service type. In the template, we are actually sending the ID but the logic file parameterizes it as serviceType. This is fixed here. Usage: https://github.com/elastic/ent-search/blob/master/app/javascript/workplace_search/ContentSources/components/AddSource/ReAuthenticate.tsx#L38 * Replace axios with HttpLogic Also removes using history in favor of KibanaLogic’s navigateToUrl * Fix incorrect type This selector is actually an array of strings * Create GenericObject to satisfy TypeScript Previously in `ent-search`, we had a generic `IObject` interface that we could use on keyed objects. It was not migrated over since it uses `any` and Kibana has a generic `object` type we can use in most situations. However, when we are checking for keys in our code, `object` does not work. This commit is an attempt at making a generic interface we can use. * More strict object typing Removes GenericObject from last commit and adds stricter local typing * Add i18n Also added for already-merged SourcesLogic * Move button loading action to finally block * Move route strings to inline --- .../applications/workplace_search/routes.ts | 2 + .../applications/workplace_search/types.ts | 42 ++ .../views/content_sources/source_logic.ts | 633 ++++++++++++++++++ .../views/content_sources/sources_logic.ts | 21 +- 4 files changed, 696 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 419ae1cbfbc07..8f62984db1b5e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -10,6 +10,8 @@ import { CURRENT_MAJOR_VERSION } from '../../../common/version'; export const SETUP_GUIDE_PATH = '/setup_guide'; +export const NOT_FOUND_PATH = '/404'; + export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 801bcda2a319a..1bd3cabb0227d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -8,6 +8,17 @@ export * from '../../../common/types/workplace_search'; export type SpacerSizeTypes = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'; +export interface MetaPage { + current: number; + size: number; + total_pages: number; + total_results: number; +} + +export interface Meta { + page: MetaPage; +} + export interface Group { id: string; name: string; @@ -89,6 +100,30 @@ export interface ContentSourceDetails extends ContentSource { boost: number; } +interface DescriptionList { + title: string; + description: string; +} + +export interface ContentSourceFullData extends ContentSourceDetails { + activities: object[]; + details: DescriptionList[]; + summary: object[]; + groups: object[]; + custom: boolean; + accessToken: string; + key: string; + urlField: string; + titleField: string; + licenseSupportsPermissions: boolean; + serviceTypeSupportsPermissions: boolean; + indexPermissions: boolean; + hasPermissions: boolean; + urlFieldIsLinkable: boolean; + createdAt: string; + serviceName: string; +} + export interface ContentSourceStatus { id: string; name: string; @@ -121,3 +156,10 @@ export enum FeatureIds { GlobalAccessPermissions = 'GlobalAccessPermissions', DocumentLevelPermissions = 'DocumentLevelPermissions', } + +export interface CustomSource { + accessToken: string; + key: string; + name: string; + id: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts new file mode 100644 index 0000000000000..889519b8a9985 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -0,0 +1,633 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { keys, pickBy } from 'lodash'; + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; + +import { + flashAPIErrors, + setSuccessMessage, + setQueuedSuccessMessage, + FlashMessagesLogic, +} from '../../../shared/flash_messages'; + +import { DEFAULT_META } from '../../../shared/constants'; +import { AppLogic } from '../../app_logic'; +import { NOT_FOUND_PATH } from '../../routes'; +import { ContentSourceFullData, CustomSource, Meta } from '../../types'; + +export interface SourceActions { + onInitializeSource(contentSource: ContentSourceFullData): ContentSourceFullData; + onUpdateSourceName(name: string): string; + setSourceConfigData(sourceConfigData: SourceConfigData): SourceConfigData; + setSourceConnectData(sourceConnectData: SourceConnectData): SourceConnectData; + setSearchResults(searchResultsResponse: SearchResultsResponse): SearchResultsResponse; + initializeFederatedSummary(sourceId: string): { sourceId: string }; + onUpdateSummary(summary: object[]): object[]; + setContentFilterValue(contentFilterValue: string): string; + setActivePage(activePage: number): number; + setClientIdValue(clientIdValue: string): string; + setClientSecretValue(clientSecretValue: string): string; + setBaseUrlValue(baseUrlValue: string): string; + setCustomSourceNameValue(customSourceNameValue: string): string; + setSourceLoginValue(loginValue: string): string; + setSourcePasswordValue(passwordValue: string): string; + setSourceSubdomainValue(subdomainValue: string): string; + setSourceIndexPermissionsValue(indexPermissionsValue: boolean): boolean; + setCustomSourceData(data: CustomSource): CustomSource; + setPreContentSourceConfigData(data: PreContentSourceResponse): PreContentSourceResponse; + setSelectedGithubOrganizations(option: string): string; + searchContentSourceDocuments(sourceId: string): { sourceId: string }; + updateContentSource( + sourceId: string, + source: { name: string } + ): { sourceId: string; source: { name: string } }; + resetSourceState(): void; + removeContentSource( + sourceId: string, + successCallback: () => void + ): { sourceId: string; successCallback(): void }; + createContentSource( + serviceType: string, + successCallback: () => void, + errorCallback?: () => void + ): { serviceType: string; successCallback(): void; errorCallback?(): void }; + saveSourceConfig( + isUpdating: boolean, + successCallback?: () => void + ): { isUpdating: boolean; successCallback?(): void }; + initializeSource(sourceId: string, history: object): { sourceId: string; history: object }; + getSourceConfigData(serviceType: string): { serviceType: string }; + getSourceConnectData( + serviceType: string, + successCallback: (oauthUrl: string) => string + ): { serviceType: string; successCallback(oauthUrl: string): void }; + getSourceReConnectData(sourceId: string): { sourceId: string }; + getPreContentSourceConfigData(preContentSourceId: string): { preContentSourceId: string }; + setButtonNotLoading(): void; +} + +interface SourceConfigData { + serviceType: string; + name: string; + configured: boolean; + categories: string[]; + needsPermissions?: boolean; + privateSourcesEnabled: boolean; + configuredFields: { + publicKey: string; + privateKey: string; + consumerKey: string; + baseUrl?: string; + clientId?: string; + clientSecret?: string; + }; + accountContextOnly?: boolean; +} + +interface SourceConnectData { + oauthUrl: string; + serviceType: string; +} + +interface OrganizationsMap { + [key: string]: string | boolean; +} + +interface SourceValues { + contentSource: ContentSourceFullData; + dataLoading: boolean; + sectionLoading: boolean; + buttonLoading: boolean; + contentItems: object[]; + contentMeta: Meta; + contentFilterValue: string; + customSourceNameValue: string; + clientIdValue: string; + clientSecretValue: string; + baseUrlValue: string; + loginValue: string; + passwordValue: string; + subdomainValue: string; + indexPermissionsValue: boolean; + sourceConfigData: SourceConfigData; + sourceConnectData: SourceConnectData; + newCustomSource: CustomSource; + currentServiceType: string; + githubOrganizations: string[]; + selectedGithubOrganizationsMap: OrganizationsMap; + selectedGithubOrganizations: string[]; +} + +interface SearchResultsResponse { + results: object[]; + meta: Meta; +} + +interface PreContentSourceResponse { + id: string; + serviceType: string; + githubOrganizations: string[]; +} + +export const SourceLogic = kea>({ + actions: { + onInitializeSource: (contentSource: ContentSourceFullData) => contentSource, + onUpdateSourceName: (name: string) => name, + setSourceConfigData: (sourceConfigData: SourceConfigData) => sourceConfigData, + setSourceConnectData: (sourceConnectData: SourceConnectData) => sourceConnectData, + onUpdateSummary: (summary: object[]) => summary, + setSearchResults: (searchResultsResponse: SearchResultsResponse) => searchResultsResponse, + setContentFilterValue: (contentFilterValue: string) => contentFilterValue, + setActivePage: (activePage: number) => activePage, + setClientIdValue: (clientIdValue: string) => clientIdValue, + setClientSecretValue: (clientSecretValue: string) => clientSecretValue, + setBaseUrlValue: (baseUrlValue: string) => baseUrlValue, + setCustomSourceNameValue: (customSourceNameValue: string) => customSourceNameValue, + setSourceLoginValue: (loginValue: string) => loginValue, + setSourcePasswordValue: (passwordValue: string) => passwordValue, + setSourceSubdomainValue: (subdomainValue: string) => subdomainValue, + setSourceIndexPermissionsValue: (indexPermissionsValue: boolean) => indexPermissionsValue, + setCustomSourceData: (data: CustomSource) => data, + setPreContentSourceConfigData: (data: PreContentSourceResponse) => data, + setSelectedGithubOrganizations: (option: string) => option, + initializeSource: (sourceId: string, history: object) => ({ sourceId, history }), + initializeFederatedSummary: (sourceId: string) => ({ sourceId }), + searchContentSourceDocuments: (sourceId: string) => ({ sourceId }), + updateContentSource: (sourceId: string, source: { name: string }) => ({ sourceId, source }), + removeContentSource: (sourceId: string, successCallback: () => void) => ({ + sourceId, + successCallback, + }), + getSourceConfigData: (serviceType: string) => ({ serviceType }), + getSourceConnectData: (serviceType: string, successCallback: (oauthUrl: string) => string) => ({ + serviceType, + successCallback, + }), + getSourceReConnectData: (sourceId: string) => ({ sourceId }), + getPreContentSourceConfigData: (preContentSourceId: string) => ({ preContentSourceId }), + saveSourceConfig: (isUpdating: boolean, successCallback?: () => void) => ({ + isUpdating, + successCallback, + }), + createContentSource: ( + serviceType: string, + successCallback: () => void, + errorCallback?: () => void + ) => ({ serviceType, successCallback, errorCallback }), + resetSourceState: () => true, + setButtonNotLoading: () => false, + }, + reducers: { + contentSource: [ + {} as ContentSourceFullData, + { + onInitializeSource: (_, contentSource) => contentSource, + onUpdateSourceName: (contentSource, name) => ({ + ...contentSource, + name, + }), + onUpdateSummary: (contentSource, summary) => ({ + ...contentSource, + summary, + }), + }, + ], + sourceConfigData: [ + {} as SourceConfigData, + { + setSourceConfigData: (_, sourceConfigData) => sourceConfigData, + }, + ], + sourceConnectData: [ + {} as SourceConnectData, + { + setSourceConnectData: (_, sourceConnectData) => sourceConnectData, + }, + ], + dataLoading: [ + true, + { + onInitializeSource: () => false, + setSourceConfigData: () => false, + resetSourceState: () => false, + setPreContentSourceConfigData: () => false, + }, + ], + buttonLoading: [ + false, + { + setButtonNotLoading: () => false, + setSourceConnectData: () => false, + setSourceConfigData: () => false, + resetSourceState: () => false, + removeContentSource: () => true, + saveSourceConfig: () => true, + getSourceConnectData: () => true, + createContentSource: () => true, + }, + ], + sectionLoading: [ + true, + { + searchContentSourceDocuments: () => true, + getPreContentSourceConfigData: () => true, + setSearchResults: () => false, + setPreContentSourceConfigData: () => false, + }, + ], + contentItems: [ + [], + { + setSearchResults: (_, { results }) => results, + }, + ], + contentMeta: [ + DEFAULT_META, + { + setActivePage: (state, activePage) => setPage(state, activePage), + setContentFilterValue: (state) => setPage(state, DEFAULT_META.page.current), + setSearchResults: (_, { meta }) => meta, + }, + ], + contentFilterValue: [ + '', + { + setContentFilterValue: (_, contentFilterValue) => contentFilterValue, + resetSourceState: () => '', + }, + ], + clientIdValue: [ + '', + { + setClientIdValue: (_, clientIdValue) => clientIdValue, + setSourceConfigData: (_, { configuredFields: { clientId } }) => clientId || '', + resetSourceState: () => '', + }, + ], + clientSecretValue: [ + '', + { + setClientSecretValue: (_, clientSecretValue) => clientSecretValue, + setSourceConfigData: (_, { configuredFields: { clientSecret } }) => clientSecret || '', + resetSourceState: () => '', + }, + ], + baseUrlValue: [ + '', + { + setBaseUrlValue: (_, baseUrlValue) => baseUrlValue, + setSourceConfigData: (_, { configuredFields: { baseUrl } }) => baseUrl || '', + resetSourceState: () => '', + }, + ], + loginValue: [ + '', + { + setSourceLoginValue: (_, loginValue) => loginValue, + resetSourceState: () => '', + }, + ], + passwordValue: [ + '', + { + setSourcePasswordValue: (_, passwordValue) => passwordValue, + resetSourceState: () => '', + }, + ], + subdomainValue: [ + '', + { + setSourceSubdomainValue: (_, subdomainValue) => subdomainValue, + resetSourceState: () => '', + }, + ], + indexPermissionsValue: [ + false, + { + setSourceIndexPermissionsValue: (_, indexPermissionsValue) => indexPermissionsValue, + resetSourceState: () => false, + }, + ], + customSourceNameValue: [ + '', + { + setCustomSourceNameValue: (_, customSourceNameValue) => customSourceNameValue, + resetSourceState: () => '', + }, + ], + newCustomSource: [ + {} as CustomSource, + { + setCustomSourceData: (_, newCustomSource) => newCustomSource, + resetSourceState: () => ({} as CustomSource), + }, + ], + currentServiceType: [ + '', + { + setPreContentSourceConfigData: (_, { serviceType }) => serviceType, + resetSourceState: () => '', + }, + ], + githubOrganizations: [ + [], + { + setPreContentSourceConfigData: (_, { githubOrganizations }) => githubOrganizations, + resetSourceState: () => [], + }, + ], + selectedGithubOrganizationsMap: [ + {} as OrganizationsMap, + { + setSelectedGithubOrganizations: (state, option) => ({ + ...state, + ...{ [option]: !state[option] }, + }), + resetSourceState: () => ({}), + }, + ], + }, + selectors: ({ selectors }) => ({ + selectedGithubOrganizations: [ + () => [selectors.selectedGithubOrganizationsMap], + (orgsMap) => keys(pickBy(orgsMap)), + ], + }), + listeners: ({ actions, values }) => ({ + initializeSource: async ({ sourceId }) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}` + : `/api/workplace_search/account/sources/${sourceId}`; + + try { + const response = await HttpLogic.values.http.get(route); + actions.onInitializeSource(response); + if (response.isFederatedSource) { + actions.initializeFederatedSummary(sourceId); + } + } catch (e) { + // TODO: Verify this works once components are there. Not sure if the catch gives a status code. + if (e.response.status === 404) { + KibanaLogic.values.navigateToUrl(NOT_FOUND_PATH); + } else { + flashAPIErrors(e); + } + } + }, + initializeFederatedSummary: async ({ sourceId }) => { + const route = `/api/workplace_search/org/sources/${sourceId}/federated_summary`; + try { + const response = await HttpLogic.values.http.get(route); + actions.onUpdateSummary(response.summary); + } catch (e) { + flashAPIErrors(e); + } + }, + searchContentSourceDocuments: async ({ sourceId }, breakpoint) => { + await breakpoint(300); + + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/documents` + : `/api/workplace_search/account/sources/${sourceId}/documents`; + + const { + contentFilterValue: query, + contentMeta: { page }, + } = values; + + try { + const response = await HttpLogic.values.http.post(route, { + body: JSON.stringify({ query, page }), + }); + actions.setSearchResults(response); + } catch (e) { + flashAPIErrors(e); + } + }, + updateContentSource: async ({ sourceId, source }) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/settings` + : `/api/workplace_search/account/sources/${sourceId}/settings`; + + try { + const response = await HttpLogic.values.http.patch(route, { + body: JSON.stringify({ content_source: source }), + }); + actions.onUpdateSourceName(response.name); + } catch (e) { + flashAPIErrors(e); + } + }, + removeContentSource: async ({ sourceId, successCallback }) => { + FlashMessagesLogic.actions.clearFlashMessages(); + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}` + : `/api/workplace_search/account/sources/${sourceId}`; + + try { + const response = await HttpLogic.values.http.delete(route); + setQueuedSuccessMessage( + i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.contentSourceRemoved', + { + defaultMessage: 'Successfully deleted {sourceName}.', + values: { sourceName: response.name }, + } + ) + ); + successCallback(); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.setButtonNotLoading(); + } + }, + getSourceConfigData: async ({ serviceType }) => { + const route = `/api/workplace_search/org/settings/connectors/${serviceType}`; + + try { + const response = await HttpLogic.values.http.get(route); + actions.setSourceConfigData(response); + } catch (e) { + flashAPIErrors(e); + } + }, + getSourceConnectData: async ({ serviceType, successCallback }) => { + FlashMessagesLogic.actions.clearFlashMessages(); + const { isOrganization } = AppLogic.values; + const { subdomainValue: subdomain, indexPermissionsValue: indexPermissions } = values; + + const route = isOrganization + ? `/api/workplace_search/org/sources/${serviceType}/prepare` + : `/api/workplace_search/account/sources/${serviceType}/prepare`; + + const params = new URLSearchParams(); + if (subdomain) params.append('subdomain', subdomain); + if (indexPermissions) params.append('index_permissions', indexPermissions.toString()); + + try { + const response = await HttpLogic.values.http.get(`${route}?${params}`); + actions.setSourceConnectData(response); + successCallback(response.oauthUrl); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.setButtonNotLoading(); + } + }, + getSourceReConnectData: async ({ sourceId }) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/sources/${sourceId}/reauth_prepare` + : `/api/workplace_search/account/sources/${sourceId}/reauth_prepare`; + + try { + const response = await HttpLogic.values.http.get(route); + actions.setSourceConnectData(response); + } catch (e) { + flashAPIErrors(e); + } + }, + getPreContentSourceConfigData: async ({ preContentSourceId }) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/api/workplace_search/org/pre_sources/${preContentSourceId}` + : `/api/workplace_search/account/pre_sources/${preContentSourceId}`; + + try { + const response = await HttpLogic.values.http.get(route); + actions.setPreContentSourceConfigData(response); + } catch (e) { + flashAPIErrors(e); + } + }, + saveSourceConfig: async ({ isUpdating, successCallback }) => { + FlashMessagesLogic.actions.clearFlashMessages(); + const { + sourceConfigData: { serviceType }, + baseUrlValue, + clientIdValue, + clientSecretValue, + sourceConfigData, + } = values; + + const route = isUpdating + ? `/api/workplace_search/org/settings/connectors/${serviceType}` + : '/api/workplace_search/org/settings/connectors'; + + const http = isUpdating ? HttpLogic.values.http.put : HttpLogic.values.http.post; + + const params = { + base_url: baseUrlValue || undefined, + client_id: clientIdValue || undefined, + client_secret: clientSecretValue || undefined, + service_type: serviceType, + private_key: sourceConfigData.configuredFields?.privateKey, + public_key: sourceConfigData.configuredFields?.publicKey, + consumer_key: sourceConfigData.configuredFields?.consumerKey, + }; + + try { + const response = await http(route, { + body: JSON.stringify({ params }), + }); + if (isUpdating) { + setSuccessMessage( + i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.contentSourceConfigUpdated', + { + defaultMessage: 'Successfully updated configuration.', + } + ) + ); + } + actions.setSourceConfigData(response); + if (successCallback) successCallback(); + } catch (e) { + flashAPIErrors(e); + if (!isUpdating) throw new Error(e); + } finally { + actions.setButtonNotLoading(); + } + }, + createContentSource: async ({ serviceType, successCallback, errorCallback }) => { + FlashMessagesLogic.actions.clearFlashMessages(); + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? '/api/workplace_search/org/create_source' + : '/api/workplace_search/account/create_source'; + + const { + selectedGithubOrganizations: githubOrganizations, + customSourceNameValue, + loginValue, + passwordValue, + indexPermissionsValue, + } = values; + + const params = { + service_type: serviceType, + name: customSourceNameValue || undefined, + login: loginValue || undefined, + password: passwordValue || undefined, + organizations: githubOrganizations.length > 0 ? githubOrganizations : undefined, + indexPermissions: indexPermissionsValue || undefined, + } as { + [key: string]: string | string[] | undefined; + }; + + // Remove undefined values from params + Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]); + + try { + const response = await HttpLogic.values.http.post(route, { + body: JSON.stringify({ params }), + }); + actions.setCustomSourceData(response); + successCallback(); + } catch (e) { + flashAPIErrors(e); + if (errorCallback) errorCallback(); + throw new Error('Auth Error'); + } finally { + actions.setButtonNotLoading(); + } + }, + onUpdateSourceName: (name: string) => { + setSuccessMessage( + i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.contentSourceNameChanged', + { + defaultMessage: 'Successfully changed name to {sourceName}.', + values: { sourceName: name }, + } + ) + ); + }, + resetSourceState: () => { + FlashMessagesLogic.actions.clearFlashMessages(); + }, + }), +}); + +const setPage = (state: Meta, page: number) => ({ + ...state, + page: { + ...state.page, + current: page, + }, +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index eacba312d5da6..5a8da7cd32fa8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -8,6 +8,8 @@ import { cloneDeep, findIndex } from 'lodash'; import { kea, MakeLogicType } from 'kea'; +import { i18n } from '@kbn/i18n'; + import { HttpLogic } from '../../../shared/http'; import { @@ -208,10 +210,25 @@ export const SourcesLogic = kea>( } }, setAddedSource: ({ addedSourceName, additionalConfiguration }) => { + const successfullyConnectedMessage = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.contentSourceConnected', + { + defaultMessage: 'Successfully connected {sourceName}.', + values: { sourceName: addedSourceName }, + } + ); + + const additionalConfigurationMessage = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.flashMessages.additionalConfigurationNeeded', + { + defaultMessage: 'This source requires additional configuration.', + } + ); + setSuccessMessage( [ - `Successfully connected ${addedSourceName}.`, - additionalConfiguration ? 'This source requires additional configuration.' : '', + successfullyConnectedMessage, + additionalConfiguration ? additionalConfigurationMessage : '', ].join(' ') ); }, From 938b7624f711fe6ae5d527038028bab6e0be1ebd Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 18 Nov 2020 22:52:52 +0300 Subject: [PATCH 80/99] disable incremenetal build for legacy tsconfig.json (#82986) --- test/tsconfig.json | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tsconfig.json b/test/tsconfig.json index 2949a764d4b1a..390e0b88c3d5c 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "tsBuildInfoFile": "../build/tsbuildinfo/test", + "incremental": false, "types": ["node", "mocha", "flot"] }, "include": ["**/*", "../typings/elastic__node_crypto.d.ts", "typings/**/*"], diff --git a/tsconfig.json b/tsconfig.json index 00b33bd0b4451..88ae3e1e826b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "tsBuildInfoFile": "./build/tsbuildinfo/kibana" + "incremental": false }, "include": ["kibana.d.ts", "src/**/*", "typings/**/*", "test_utils/**/*"], "exclude": [ From 4b603da9c6cd5f4638a87f06340b171e136c3dfb Mon Sep 17 00:00:00 2001 From: ymao1 Date: Wed, 18 Nov 2020 15:59:26 -0500 Subject: [PATCH 81/99] Not resetting server log level if level is defined (#83651) --- .../server_log/server_log_params.test.tsx | 4 +++- .../builtin_action_types/server_log/server_log_params.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx index e8429a54b618c..3243c37a04ee7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx @@ -12,6 +12,7 @@ import { coreMock } from 'src/core/public/mocks'; describe('ServerLogParamsFields renders', () => { const mocks = coreMock.createSetup(); + const editAction = jest.fn(); test('all params fields is rendered', () => { const actionParams = { @@ -22,7 +23,7 @@ describe('ServerLogParamsFields renders', () => { {}} + editAction={editAction} index={0} defaultMessage={'test default message'} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} @@ -30,6 +31,7 @@ describe('ServerLogParamsFields renders', () => { http={mocks.http} /> ); + expect(editAction).not.toHaveBeenCalled(); expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); expect( wrapper.find('[data-test-subj="loggingLevelSelect"]').first().prop('value') diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx index a3619f96a45b2..c4f434f138747 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx @@ -25,7 +25,9 @@ export const ServerLogParamsFields: React.FunctionComponent { - editAction('level', 'info', index); + if (!actionParams?.level) { + editAction('level', 'info', index); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From acc3e2f443e3c60dfc923aa1b3b179f34cf69804 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Wed, 18 Nov 2020 16:02:31 -0500 Subject: [PATCH 82/99] [Alerting] Add `alert.updatedAt` field to represent date of last user edit (#83578) * Adding alert.updatedAt field that only updates on user edit * Updating unit tests * Functional tests * Updating alert attributes excluded from AAD * Fixing test * PR comments --- .../server/alerts_client/alerts_client.ts | 39 ++++++++--------- .../server/alerts_client/tests/create.test.ts | 7 +++ .../alerts_client/tests/disable.test.ts | 6 ++- .../server/alerts_client/tests/enable.test.ts | 6 ++- .../server/alerts_client/tests/find.test.ts | 1 + .../server/alerts_client/tests/get.test.ts | 1 + .../tests/get_alert_instance_summary.test.ts | 1 + .../alerts_client/tests/mute_all.test.ts | 5 ++- .../alerts_client/tests/mute_instance.test.ts | 5 ++- .../alerts_client/tests/unmute_all.test.ts | 5 ++- .../tests/unmute_instance.test.ts | 5 ++- .../server/alerts_client/tests/update.test.ts | 7 ++- .../tests/update_api_key.test.ts | 6 ++- .../alerts/server/saved_objects/index.ts | 2 + .../alerts/server/saved_objects/mappings.json | 3 ++ .../server/saved_objects/migrations.test.ts | 43 ++++++++++++++++++- .../alerts/server/saved_objects/migrations.ts | 20 +++++++++ .../partially_update_alert.test.ts | 1 + x-pack/plugins/alerts/server/types.ts | 1 + .../spaces_only/tests/alerting/create.ts | 1 + .../tests/alerting/execution_status.ts | 22 ++++++++++ .../spaces_only/tests/alerting/migrations.ts | 9 ++++ 22 files changed, 166 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index e97b37f16faf0..c08ff9449d151 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -228,14 +228,17 @@ export class AlertsClient { this.validateActions(alertType, data.actions); + const createTime = Date.now(); const { references, actions } = await this.denormalizeActions(data.actions); + const rawAlert: RawAlert = { ...data, ...this.apiKeyAsAlertAttributes(createdAPIKey, username), actions, createdBy: username, updatedBy: username, - createdAt: new Date().toISOString(), + createdAt: new Date(createTime).toISOString(), + updatedAt: new Date(createTime).toISOString(), params: validatedAlertTypeParams as RawAlert['params'], muteAll: false, mutedInstanceIds: [], @@ -289,12 +292,7 @@ export class AlertsClient { }); createdAlert.attributes.scheduledTaskId = scheduledTask.id; } - return this.getAlertFromRaw( - createdAlert.id, - createdAlert.attributes, - createdAlert.updated_at, - references - ); + return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references); } public async get({ id }: { id: string }): Promise { @@ -304,7 +302,7 @@ export class AlertsClient { result.attributes.consumer, ReadOperations.Get ); - return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); + return this.getAlertFromRaw(result.id, result.attributes, result.references); } public async getAlertState({ id }: { id: string }): Promise { @@ -393,13 +391,11 @@ export class AlertsClient { type: 'alert', }); - // eslint-disable-next-line @typescript-eslint/naming-convention - const authorizedData = data.map(({ id, attributes, updated_at, references }) => { + const authorizedData = data.map(({ id, attributes, references }) => { ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); return this.getAlertFromRaw( id, fields ? (pick(attributes, fields) as RawAlert) : attributes, - updated_at, references ); }); @@ -585,6 +581,7 @@ export class AlertsClient { params: validatedAlertTypeParams as RawAlert['params'], actions, updatedBy: username, + updatedAt: new Date().toISOString(), }); try { updatedObject = await this.unsecuredSavedObjectsClient.create( @@ -607,12 +604,7 @@ export class AlertsClient { throw e; } - return this.getPartialAlertFromRaw( - id, - updatedObject.attributes, - updatedObject.updated_at, - updatedObject.references - ); + return this.getPartialAlertFromRaw(id, updatedObject.attributes, updatedObject.references); } private apiKeyAsAlertAttributes( @@ -677,6 +669,7 @@ export class AlertsClient { await this.createAPIKey(this.generateAPIKeyName(attributes.alertTypeId, attributes.name)), username ), + updatedAt: new Date().toISOString(), updatedBy: username, }); try { @@ -751,6 +744,7 @@ export class AlertsClient { username ), updatedBy: username, + updatedAt: new Date().toISOString(), }); try { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); @@ -829,6 +823,7 @@ export class AlertsClient { apiKey: null, apiKeyOwner: null, updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), }), { version } ); @@ -875,6 +870,7 @@ export class AlertsClient { muteAll: true, mutedInstanceIds: [], updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), }); const updateOptions = { version }; @@ -913,6 +909,7 @@ export class AlertsClient { muteAll: false, mutedInstanceIds: [], updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), }); const updateOptions = { version }; @@ -957,6 +954,7 @@ export class AlertsClient { this.updateMeta({ mutedInstanceIds, updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), }), { version } ); @@ -999,6 +997,7 @@ export class AlertsClient { alertId, this.updateMeta({ updatedBy: await this.getUserName(), + updatedAt: new Date().toISOString(), mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), }), { version } @@ -1050,19 +1049,17 @@ export class AlertsClient { private getAlertFromRaw( id: string, rawAlert: RawAlert, - updatedAt: SavedObject['updated_at'], references: SavedObjectReference[] | undefined ): Alert { // In order to support the partial update API of Saved Objects we have to support // partial updates of an Alert, but when we receive an actual RawAlert, it is safe // to cast the result to an Alert - return this.getPartialAlertFromRaw(id, rawAlert, updatedAt, references) as Alert; + return this.getPartialAlertFromRaw(id, rawAlert, references) as Alert; } private getPartialAlertFromRaw( id: string, - { createdAt, meta, scheduledTaskId, ...rawAlert }: Partial, - updatedAt: SavedObject['updated_at'] = createdAt, + { createdAt, updatedAt, meta, scheduledTaskId, ...rawAlert }: Partial, references: SavedObjectReference[] | undefined ): PartialAlert { // Not the prettiest code here, but if we want to use most of the diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index ee407b1a6d50c..6d259029ac480 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -196,6 +196,7 @@ describe('create()', () => { createdAt: '2019-02-12T21:01:22.479Z', createdBy: 'elastic', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', muteAll: false, mutedInstanceIds: [], actions: [ @@ -330,6 +331,7 @@ describe('create()', () => { "foo", ], "throttle": null, + "updatedAt": "2019-02-12T21:01:22.479Z", "updatedBy": "elastic", } `); @@ -418,6 +420,7 @@ describe('create()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -555,6 +558,7 @@ describe('create()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -631,6 +635,7 @@ describe('create()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -971,6 +976,7 @@ describe('create()', () => { createdBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', enabled: true, meta: { versionApiKeyLastmodified: 'v7.10.0', @@ -1092,6 +1098,7 @@ describe('create()', () => { createdBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', enabled: false, meta: { versionApiKeyLastmodified: 'v7.10.0', diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts index 11ce0027f82d8..8c9ab9494a50a 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); @@ -45,6 +45,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('disable()', () => { let alertsClient: AlertsClient; const existingAlert = { @@ -136,6 +138,7 @@ describe('disable()', () => { scheduledTaskId: null, apiKey: null, apiKeyOwner: null, + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ { @@ -190,6 +193,7 @@ describe('disable()', () => { scheduledTaskId: null, apiKey: null, apiKeyOwner: null, + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts index 16e83c42d8930..feec1d1b9334a 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts @@ -13,7 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); @@ -46,6 +46,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('enable()', () => { let alertsClient: AlertsClient; const existingAlert = { @@ -186,6 +188,7 @@ describe('enable()', () => { meta: { versionApiKeyLastmodified: kibanaVersion, }, + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', apiKey: null, apiKeyOwner: null, @@ -292,6 +295,7 @@ describe('enable()', () => { apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', actions: [ { group: 'default', diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts index 1b3a776bd23e0..3d7473a746986 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts @@ -79,6 +79,7 @@ describe('find()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts index 5c0d80f159b31..3f0c783f424d1 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts @@ -59,6 +59,7 @@ describe('get()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 269b2eb2ab7a7..9bd61c0fe66d2 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -76,6 +76,7 @@ const BaseAlertInstanceSummarySavedObject: SavedObject = { createdBy: null, updatedBy: null, createdAt: mockedDateString, + updatedAt: mockedDateString, apiKey: null, apiKeyOwner: null, throttle: null, diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts index 868fa3d8c6aa2..14ebca2135587 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -43,6 +43,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('muteAll()', () => { test('mutes an alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -74,6 +76,7 @@ describe('muteAll()', () => { { muteAll: true, mutedInstanceIds: [], + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts index 05ca741f480ca..c2188f128cb4d 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -44,6 +44,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('muteInstance()', () => { test('mutes an alert instance', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -68,6 +70,7 @@ describe('muteInstance()', () => { '1', { mutedInstanceIds: ['2'], + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts index 5ef1af9b6f0ee..d92304ab873be 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -44,6 +44,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('unmuteAll()', () => { test('unmutes an alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -75,6 +77,7 @@ describe('unmuteAll()', () => { { muteAll: false, mutedInstanceIds: [], + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', }, { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts index 88692239ac2fe..3486df98f2f05 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -44,6 +44,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('unmuteInstance()', () => { test('unmutes an alert instance', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -69,6 +71,7 @@ describe('unmuteInstance()', () => { { mutedInstanceIds: [], updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', }, { version: '123' } ); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index ad58e36ade722..d0bb2607f7a47 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -140,8 +140,8 @@ describe('update()', () => { ], scheduledTaskId: 'task-123', createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), }, - updated_at: new Date().toISOString(), references: [ { name: 'action_0', @@ -300,6 +300,7 @@ describe('update()', () => { "foo", ], "throttle": null, + "updatedAt": "2019-02-12T21:01:22.479Z", "updatedBy": "elastic", } `); @@ -362,6 +363,7 @@ describe('update()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -484,6 +486,7 @@ describe('update()', () => { "foo", ], "throttle": "5m", + "updatedAt": "2019-02-12T21:01:22.479Z", "updatedBy": "elastic", } `); @@ -534,6 +537,7 @@ describe('update()', () => { bar: true, }, createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), actions: [ { group: 'default', @@ -648,6 +652,7 @@ describe('update()', () => { "foo", ], "throttle": "5m", + "updatedAt": "2019-02-12T21:01:22.479Z", "updatedBy": "elastic", } `); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts index af178a1fac5f5..ca5f44078f513 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts @@ -12,7 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; -import { getBeforeSetup } from './lib'; +import { getBeforeSetup, setGlobalDate } from './lib'; import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); @@ -44,6 +44,8 @@ beforeEach(() => { getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); }); +setGlobalDate(); + describe('updateApiKey()', () => { let alertsClient: AlertsClient; const existingAlert = { @@ -113,6 +115,7 @@ describe('updateApiKey()', () => { apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', actions: [ { group: 'default', @@ -162,6 +165,7 @@ describe('updateApiKey()', () => { enabled: true, apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ { diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index da30273e93c6b..dfe122f56bc48 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -16,6 +16,7 @@ export const AlertAttributesExcludedFromAAD = [ 'muteAll', 'mutedInstanceIds', 'updatedBy', + 'updatedAt', 'executionStatus', ]; @@ -28,6 +29,7 @@ export type AlertAttributesExcludedFromAADType = | 'muteAll' | 'mutedInstanceIds' | 'updatedBy' + | 'updatedAt' | 'executionStatus'; export function setupSavedObjects( diff --git a/x-pack/plugins/alerts/server/saved_objects/mappings.json b/x-pack/plugins/alerts/server/saved_objects/mappings.json index a6c92080f18be..f40a7d9075eed 100644 --- a/x-pack/plugins/alerts/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerts/server/saved_objects/mappings.json @@ -62,6 +62,9 @@ "createdAt": { "type": "date" }, + "updatedAt": { + "type": "date" + }, "apiKey": { "type": "binary" }, diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 8c9d10769b18a..a4cbc18e13b47 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -261,8 +261,48 @@ describe('7.10.0 migrates with failure', () => { }); }); +describe('7.11.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('add updatedAt field to alert - set to SavedObject updated_at attribute', () => { + const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0']; + const alert = getMockData({}, true); + expect(migration711(alert, { log })).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + updatedAt: alert.updated_at, + }, + }); + }); + + test('add updatedAt field to alert - set to createdAt when SavedObject updated_at is not defined', () => { + const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0']; + const alert = getMockData({}); + expect(migration711(alert, { log })).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + updatedAt: alert.attributes.createdAt, + }, + }); + }); +}); + +function getUpdatedAt(): string { + const updatedAt = new Date(); + updatedAt.setHours(updatedAt.getHours() + 2); + return updatedAt.toISOString(); +} + function getMockData( - overwrites: Record = {} + overwrites: Record = {}, + withSavedObjectUpdatedAt: boolean = false ): SavedObjectUnsanitizedDoc> { return { attributes: { @@ -295,6 +335,7 @@ function getMockData( ], ...overwrites, }, + updated_at: withSavedObjectUpdatedAt ? getUpdatedAt() : undefined, id: uuid.v4(), type: 'alert', }; diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 0b2c86b84f67b..d8ebced03c5a6 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -37,8 +37,15 @@ export function getMigrations( ) ); + const migrationAlertUpdatedAtDate = encryptedSavedObjects.createMigration( + // migrate all documents in 7.11 in order to add the "updatedAt" field + (doc): doc is SavedObjectUnsanitizedDoc => true, + pipeMigrations(setAlertUpdatedAtDate) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), + '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtDate, '7.11.0'), }; } @@ -59,6 +66,19 @@ function executeMigrationWithErrorHandling( }; } +const setAlertUpdatedAtDate = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc => { + const updatedAt = doc.updated_at || doc.attributes.createdAt; + return { + ...doc, + attributes: { + ...doc.attributes, + updatedAt, + }, + }; +}; + const consumersToChange: Map = new Map( Object.entries({ alerting: 'alerts', diff --git a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts index 50815c797e399..8041ec551bb0d 100644 --- a/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/partially_update_alert.test.ts @@ -95,6 +95,7 @@ const DefaultAttributes = { muteAll: true, mutedInstanceIds: ['muted-instance-id-1', 'muted-instance-id-2'], updatedBy: 'someone', + updatedAt: '2019-02-12T21:01:22.479Z', }; const InvalidAttributes = { ...DefaultAttributes, foo: 'bar' }; diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index dde1628156658..4ccf251540a15 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -148,6 +148,7 @@ export interface RawAlert extends SavedObjectAttributes { createdBy: string | null; updatedBy: string | null; createdAt: string; + updatedAt: string; apiKey: string | null; apiKeyOwner: string | null; throttle: string | null; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 41f6b66c30aaf..cf7fc9edd9529 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -91,6 +91,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { }); expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.eql(Date.parse(response.body.createdAt)); expect(typeof response.body.scheduledTaskId).to.be('string'); const { _source: taskRecord } = await getScheduledTask(response.body.scheduledTaskId); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts index 5ebce8edf6fb7..642173a7c2c6c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/execution_status.ts @@ -63,6 +63,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; dates.push(response.body.executionStatus.lastExecutionDate); objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); @@ -70,6 +71,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon dates.push(executionStatus.lastExecutionDate); dates.push(Date.now()); ensureDatetimesAreOrdered(dates); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); // Ensure AAD isn't broken await checkAAD({ @@ -97,6 +99,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; dates.push(response.body.executionStatus.lastExecutionDate); objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); @@ -104,6 +107,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon dates.push(executionStatus.lastExecutionDate); dates.push(Date.now()); ensureDatetimesAreOrdered(dates); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); // Ensure AAD isn't broken await checkAAD({ @@ -128,6 +132,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; dates.push(response.body.executionStatus.lastExecutionDate); objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); @@ -135,6 +140,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon dates.push(executionStatus.lastExecutionDate); dates.push(Date.now()); ensureDatetimesAreOrdered(dates); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); // Ensure AAD isn't broken await checkAAD({ @@ -162,12 +168,14 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); const executionStatus = await waitForStatus(alertId, new Set(['error'])); expect(executionStatus.error).to.be.ok(); expect(executionStatus.error.reason).to.be('execute'); expect(executionStatus.error.message).to.be('this alert is intended to fail'); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); }); it('should eventually have error reason "unknown" when appropriate', async () => { @@ -183,6 +191,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon ); expect(response.status).to.eql(200); const alertId = response.body.id; + const alertUpdatedAt = response.body.updatedAt; objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts'); let executionStatus = await waitForStatus(alertId, new Set(['ok'])); @@ -201,6 +210,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon executionStatus = await waitForStatus(alertId, new Set(['error'])); expect(executionStatus.error).to.be.ok(); expect(executionStatus.error.reason).to.be('unknown'); + ensureAlertUpdatedAtHasNotChanged(alertId, alertUpdatedAt); const message = 'params invalid: [param1]: expected value of type [string] but got [number]'; expect(executionStatus.error.message).to.be(message); @@ -306,6 +316,18 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon await delay(WaitForStatusIncrement); return await waitForStatus(id, statuses, waitMillis - WaitForStatusIncrement); } + + async function ensureAlertUpdatedAtHasNotChanged(alertId: string, originalUpdatedAt: string) { + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert/${alertId}` + ); + const { updatedAt, executionStatus } = response.body; + expect(Date.parse(updatedAt)).to.be.greaterThan(0); + expect(Date.parse(updatedAt)).to.eql(Date.parse(originalUpdatedAt)); + expect(Date.parse(executionStatus.lastExecutionDate)).to.be.greaterThan( + Date.parse(originalUpdatedAt) + ); + } } function expectErrorExecutionStatus(executionStatus: Record, startDate: number) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index 17070a14069ce..bd6afacf206d9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -82,5 +82,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { }, ]); }); + + it('7.11.0 migrates alerts to contain `updatedAt` field', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ); + + expect(response.status).to.eql(200); + expect(response.body.updatedAt).to.eql('2020-06-17T15:35:39.839Z'); + }); }); } From 3651748b77472754dedea50a83acc2c6e53a5ffe Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 18 Nov 2020 13:13:50 -0800 Subject: [PATCH 83/99] Fixed console error, which appears when saving changes in Edit Alert flyout (#83610) --- .../alert_details/components/alert_details.tsx | 12 ++++++++++-- .../sections/alert_form/alert_edit.tsx | 17 ++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index abd8127962561..603058e6fcb52 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, Fragment, useEffect } from 'react'; +import React, { useState, Fragment, useEffect, useReducer } from 'react'; import { keyBy } from 'lodash'; import { useHistory } from 'react-router-dom'; import { @@ -41,6 +41,7 @@ import { AlertEdit } from '../../alert_form'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { routeToAlertDetails } from '../../../constants'; import { alertsErrorReasonTranslationsMapping } from '../../alerts_list/translations'; +import { alertReducer } from '../../alert_form/alert_reducer'; type AlertDetailsProps = { alert: Alert; @@ -73,6 +74,10 @@ export const AlertDetails: React.FunctionComponent = ({ setBreadcrumbs, chrome, } = useAppDependencies(); + const [{}, dispatch] = useReducer(alertReducer, { alert }); + const setInitialAlert = (key: string, value: any) => { + dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); + }; // Set breadcrumb and page title useEffect(() => { @@ -166,7 +171,10 @@ export const AlertDetails: React.FunctionComponent = ({ > setEditFlyoutVisibility(false)} + onClose={() => { + setInitialAlert('alert', alert); + setEditFlyoutVisibility(false); + }} /> )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 5eadc742a9dc8..d5ae701546c64 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useCallback, useReducer, useState } from 'react'; +import React, { Fragment, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -40,9 +40,6 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState( false ); - const setAlert = (key: string, value: any) => { - dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); - }; const { reloadAlerts, @@ -53,12 +50,6 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { docLinks, } = useAlertsContext(); - const closeFlyout = useCallback(() => { - onClose(); - setAlert('alert', initialAlert); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [onClose]); - const alertType = alertTypeRegistry.get(alert.alertTypeId); const errors = { @@ -105,7 +96,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { return ( onClose()} aria-labelledby="flyoutAlertEditTitle" size="m" maxWidth={620} @@ -155,7 +146,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { onClose()} > {i18n.translate( 'xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', @@ -179,7 +170,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { const savedAlert = await onSaveAlert(); setIsSaving(false); if (savedAlert) { - closeFlyout(); + onClose(); if (reloadAlerts) { reloadAlerts(); } From a2d288d134cf86458fc821b9ae07f6004ee2de22 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 18 Nov 2020 21:46:42 +0000 Subject: [PATCH 84/99] fix(NA): search examples kibana version declaration (#83182) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- examples/search_examples/kibana.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/search_examples/kibana.json b/examples/search_examples/kibana.json index 7e392b8417360..9577ec353a4c9 100644 --- a/examples/search_examples/kibana.json +++ b/examples/search_examples/kibana.json @@ -1,6 +1,7 @@ { "id": "searchExamples", - "version": "8.0.0", + "version": "0.0.1", + "kibanaVersion": "kibana", "server": true, "ui": true, "requiredPlugins": ["navigation", "data", "developerExamples"], From 0546f98070943e8750398e64dba1ff8a07e894c3 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Wed, 18 Nov 2020 14:47:46 -0700 Subject: [PATCH 85/99] [Maps] Add query bar inputs to geo threshold alerts tracked points & boundaries (#80871) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/stack_alerts/kibana.json | 2 +- ...eshold_alert_type_expression.test.tsx.snap | 210 ++++++++++++++++++ ...o_threshold_alert_type_expression.test.tsx | 94 ++++++++ .../geo_threshold/query_builder/index.tsx | 67 ++++++ .../public/alert_types/geo_threshold/types.ts | 4 + .../alert_types/geo_threshold/alert_type.ts | 5 + .../geo_threshold/es_query_builder.ts | 80 +++++-- .../geo_threshold/geo_threshold.ts | 3 +- .../tests/es_query_builder.test.ts | 67 ++++++ .../plugins/triggers_actions_ui/kibana.json | 4 +- .../public/application/app.tsx | 8 +- .../public/application/app_context.tsx | 7 +- .../public/application/boot.tsx | 9 +- .../actions_connectors_list.test.tsx | 10 +- .../components/alert_details.test.tsx | 2 +- .../components/alert_details.tsx | 8 +- .../sections/alert_form/alert_add.test.tsx | 2 +- .../components/alerts_list.test.tsx | 8 +- .../alerts_list/components/alerts_list.tsx | 8 +- .../public/application/test_utils/index.ts | 12 +- .../triggers_actions_ui/public/index.ts | 1 + .../triggers_actions_ui/public/plugin.ts | 6 +- 22 files changed, 559 insertions(+), 58 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts diff --git a/x-pack/plugins/stack_alerts/kibana.json b/x-pack/plugins/stack_alerts/kibana.json index b7405c38d1611..884d33ef669e5 100644 --- a/x-pack/plugins/stack_alerts/kibana.json +++ b/x-pack/plugins/stack_alerts/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerts", "features", "triggersActionsUi", "kibanaReact"], + "requiredPlugins": ["alerts", "features", "triggersActionsUi", "kibanaReact", "savedObjects", "data"], "configPath": ["xpack", "stack_alerts"], "ui": true } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap new file mode 100644 index 0000000000000..dae168417b0bc --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/__snapshots__/geo_threshold_alert_type_expression.test.tsx.snap @@ -0,0 +1,210 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render BoundaryIndexExpression 1`] = ` + + + + + + + + + + + + } +/> +`; + +exports[`should render EntityIndexExpression 1`] = ` + + + + + + } + labelType="label" + > + + + + + + + } +/> +`; + +exports[`should render EntityIndexExpression w/ invalid flag if invalid 1`] = ` + + + + + + } + labelType="label" + > + + + + + + + } +/> +`; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx new file mode 100644 index 0000000000000..d115dbeb76e37 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/geo_threshold_alert_type_expression.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EntityIndexExpression } from './expressions/entity_index_expression'; +import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; +import { ApplicationStart, DocLinksStart, HttpSetup, ToastsStart } from 'kibana/public'; +import { + ActionTypeRegistryContract, + AlertTypeRegistryContract, + IErrorObject, +} from '../../../../../triggers_actions_ui/public'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +const alertsContext = { + http: (null as unknown) as HttpSetup, + alertTypeRegistry: (null as unknown) as AlertTypeRegistryContract, + actionTypeRegistry: (null as unknown) as ActionTypeRegistryContract, + toastNotifications: (null as unknown) as ToastsStart, + docLinks: (null as unknown) as DocLinksStart, + capabilities: (null as unknown) as ApplicationStart['capabilities'], +}; + +const alertParams = { + index: '', + indexId: '', + geoField: '', + entity: '', + dateField: '', + trackingEvent: '', + boundaryType: '', + boundaryIndexTitle: '', + boundaryIndexId: '', + boundaryGeoField: '', +}; + +test('should render EntityIndexExpression', async () => { + const component = shallow( + {}} + setAlertParamsGeoField={() => {}} + setAlertProperty={() => {}} + setIndexPattern={() => {}} + indexPattern={('' as unknown) as IIndexPattern} + isInvalid={false} + /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render EntityIndexExpression w/ invalid flag if invalid', async () => { + const component = shallow( + {}} + setAlertParamsGeoField={() => {}} + setAlertProperty={() => {}} + setIndexPattern={() => {}} + indexPattern={('' as unknown) as IIndexPattern} + isInvalid={true} + /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render BoundaryIndexExpression', async () => { + const component = shallow( + {}} + setBoundaryGeoField={() => {}} + setBoundaryNameField={() => {}} + boundaryNameField={'testNameField'} + /> + ); + + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx index f138c08c0f993..623223d66ea00 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx @@ -30,6 +30,12 @@ import { EntityIndexExpression } from './expressions/entity_index_expression'; import { EntityByExpression } from './expressions/entity_by_expression'; import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; +import { + esQuery, + esKuery, + Query, + QueryStringInput, +} from '../../../../../../../src/plugins/data/public'; const DEFAULT_VALUES = { TRACKING_EVENT: '', @@ -67,6 +73,18 @@ const labelForDelayOffset = ( ); +function validateQuery(query: Query) { + try { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + query.language === 'kuery' + ? esKuery.fromKueryExpression(query.query) + : esQuery.luceneStringToDsl(query.query); + } catch (err) { + return false; + } + return true; +} + export const GeoThresholdAlertTypeExpression: React.FunctionComponent( + indexQuery || { + query: '', + language: 'kuery', + } + ); const [boundaryIndexPattern, _setBoundaryIndexPattern] = useState({ id: '', fields: [], @@ -118,6 +144,12 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent( + boundaryIndexQuery || { + query: '', + language: 'kuery', + } + ); const [delayOffset, _setDelayOffset] = useState(0); function setDelayOffset(_delayOffset: number) { setAlertParams('delayOffsetWithUnits', `${_delayOffset}${delayOffsetUnit}`); @@ -248,6 +280,23 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent + + + { + if (query.language) { + if (validateQuery(query)) { + setAlertParams('indexQuery', query); + } + setIndexQueryInput(query); + } + }} + /> + @@ -313,6 +362,24 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent + + + { + if (query.language) { + if (validateQuery(query)) { + setAlertParams('boundaryIndexQuery', query); + } + setBoundaryIndexQueryInput(query); + } + }} + /> + + ); }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts index 0358fcd66a467..86faa4ed2fb4a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Query } from '../../../../../../src/plugins/data/common'; + export enum TrackingEvent { entered = 'entered', exited = 'exited', @@ -22,6 +24,8 @@ export interface GeoThresholdAlertParams { boundaryGeoField: string; boundaryNameField?: string; delayOffsetWithUnits?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; } // Will eventually include 'geo_shape' diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts index 9fc46fe2f2586..0c40f5b5f3866 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts @@ -15,6 +15,7 @@ import { ActionVariable, AlertTypeState, } from '../../../../alerts/server'; +import { Query } from '../../../../../../src/plugins/data/common/query'; export const GEO_THRESHOLD_ID = '.geo-threshold'; export type TrackingEvent = 'entered' | 'exited'; @@ -155,6 +156,8 @@ export const ParamsSchema = schema.object({ boundaryGeoField: schema.string({ minLength: 1 }), boundaryNameField: schema.maybe(schema.string({ minLength: 1 })), delayOffsetWithUnits: schema.maybe(schema.string({ minLength: 1 })), + indexQuery: schema.maybe(schema.any({})), + boundaryIndexQuery: schema.maybe(schema.any({})), }); export interface GeoThresholdParams { @@ -170,6 +173,8 @@ export interface GeoThresholdParams { boundaryGeoField: string; boundaryNameField?: string; delayOffsetWithUnits?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; } export function getAlertType( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts index 97be51b2a6256..02ac19e7b6f1e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts @@ -7,6 +7,13 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { Logger } from 'src/core/server'; +import { + Query, + IIndexPattern, + fromKueryExpression, + toElasticsearchQuery, + luceneStringToDsl, +} from '../../../../../../src/plugins/data/common'; export const OTHER_CATEGORY = 'other'; // Consider dynamically obtaining from config? @@ -14,6 +21,19 @@ const MAX_TOP_LEVEL_QUERY_SIZE = 0; const MAX_SHAPES_QUERY_SIZE = 10000; const MAX_BUCKETS_LIMIT = 65535; +export const getEsFormattedQuery = (query: Query, indexPattern?: IIndexPattern) => { + let esFormattedQuery; + + const queryLanguage = query.language; + if (queryLanguage === 'kuery') { + const ast = fromKueryExpression(query.query); + esFormattedQuery = toElasticsearchQuery(ast, indexPattern); + } else { + esFormattedQuery = luceneStringToDsl(query.query); + } + return esFormattedQuery; +}; + export async function getShapesFilters( boundaryIndexTitle: string, boundaryGeoField: string, @@ -21,7 +41,8 @@ export async function getShapesFilters( callCluster: ILegacyScopedClusterClient['callAsCurrentUser'], log: Logger, alertId: string, - boundaryNameField?: string + boundaryNameField?: string, + boundaryIndexQuery?: Query ) { const filters: Record = {}; const shapesIdsNamesMap: Record = {}; @@ -30,8 +51,10 @@ export async function getShapesFilters( index: boundaryIndexTitle, body: { size: MAX_SHAPES_QUERY_SIZE, + ...(boundaryIndexQuery ? { query: getEsFormattedQuery(boundaryIndexQuery) } : {}), }, }); + boundaryData.hits.hits.forEach(({ _index, _id }) => { filters[_id] = { geo_shape: { @@ -66,6 +89,7 @@ export async function executeEsQueryFactory( boundaryGeoField, geoField, boundaryIndexTitle, + indexQuery, }: { entity: string; index: string; @@ -74,6 +98,7 @@ export async function executeEsQueryFactory( geoField: string; boundaryIndexTitle: string; boundaryNameField?: string; + indexQuery?: Query; }, { callCluster }: { callCluster: ILegacyScopedClusterClient['callAsCurrentUser'] }, log: Logger, @@ -83,6 +108,19 @@ export async function executeEsQueryFactory( gteDateTime: Date | null, ltDateTime: Date | null ): Promise | undefined> => { + let esFormattedQuery; + if (indexQuery) { + const gteEpochDateTime = gteDateTime ? new Date(gteDateTime).getTime() : null; + const ltEpochDateTime = ltDateTime ? new Date(ltDateTime).getTime() : null; + const dateRangeUpdatedQuery = + indexQuery.language === 'kuery' + ? `(${dateField} >= "${gteEpochDateTime}" and ${dateField} < "${ltEpochDateTime}") and (${indexQuery.query})` + : `(${dateField}:[${gteDateTime} TO ${ltDateTime}]) AND (${indexQuery.query})`; + esFormattedQuery = getEsFormattedQuery({ + query: dateRangeUpdatedQuery, + language: indexQuery.language, + }); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any const esQuery: Record = { index, @@ -120,27 +158,29 @@ export async function executeEsQueryFactory( }, }, }, - query: { - bool: { - must: [], - filter: [ - { - match_all: {}, - }, - { - range: { - [dateField]: { - ...(gteDateTime ? { gte: gteDateTime } : {}), - lt: ltDateTime, // 'less than' to prevent overlap between intervals - format: 'strict_date_optional_time', + query: esFormattedQuery + ? esFormattedQuery + : { + bool: { + must: [], + filter: [ + { + match_all: {}, }, - }, + { + range: { + [dateField]: { + ...(gteDateTime ? { gte: gteDateTime } : {}), + lt: ltDateTime, // 'less than' to prevent overlap between intervals + format: 'strict_date_optional_time', + }, + }, + }, + ], + should: [], + must_not: [], }, - ], - should: [], - must_not: [], - }, - }, + }, stored_fields: ['*'], docvalue_fields: [ { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts index e223cdb7ea545..8247cc787d365 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts @@ -194,7 +194,8 @@ export const getGeoThresholdExecutor = (log: Logger) => services.callCluster, log, alertId, - params.boundaryNameField + params.boundaryNameField, + params.boundaryIndexQuery ); const executeEsQuery = await executeEsQueryFactory(params, services, log, shapesFilters); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts new file mode 100644 index 0000000000000..d577a88e8e2f8 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/es_query_builder.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getEsFormattedQuery } from '../es_query_builder'; + +describe('esFormattedQuery', () => { + it('lucene queries are converted correctly', async () => { + const testLuceneQuery1 = { + query: `"airport": "Denver"`, + language: 'lucene', + }; + const esFormattedQuery1 = getEsFormattedQuery(testLuceneQuery1); + expect(esFormattedQuery1).toStrictEqual({ query_string: { query: '"airport": "Denver"' } }); + const testLuceneQuery2 = { + query: `title:"Fun with turnips" AND text:Cabbage, cabbage and more cabbage!`, + language: 'lucene', + }; + const esFormattedQuery2 = getEsFormattedQuery(testLuceneQuery2); + expect(esFormattedQuery2).toStrictEqual({ + query_string: { + query: `title:"Fun with turnips" AND text:Cabbage, cabbage and more cabbage!`, + }, + }); + }); + + it('kuery queries are converted correctly', async () => { + const testKueryQuery1 = { + query: `"airport": "Denver"`, + language: 'kuery', + }; + const esFormattedQuery1 = getEsFormattedQuery(testKueryQuery1); + expect(esFormattedQuery1).toStrictEqual({ + bool: { minimum_should_match: 1, should: [{ match_phrase: { airport: 'Denver' } }] }, + }); + const testKueryQuery2 = { + query: `"airport": "Denver" and ("animal": "goat" or "animal": "narwhal")`, + language: 'kuery', + }; + const esFormattedQuery2 = getEsFormattedQuery(testKueryQuery2); + expect(esFormattedQuery2).toStrictEqual({ + bool: { + filter: [ + { bool: { should: [{ match_phrase: { airport: 'Denver' } }], minimum_should_match: 1 } }, + { + bool: { + should: [ + { + bool: { should: [{ match_phrase: { animal: 'goat' } }], minimum_should_match: 1 }, + }, + { + bool: { + should: [{ match_phrase: { animal: 'narwhal' } }], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 9d79ab9232bf3..ab2d6c6a3c400 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -4,8 +4,8 @@ "server": true, "ui": true, "optionalPlugins": ["alerts", "features", "home"], - "requiredPlugins": ["management", "charts", "data"], + "requiredPlugins": ["management", "charts", "data", "kibanaReact", "savedObjects"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], - "requiredBundles": ["home", "alerts", "esUiShared"] + "requiredBundles": ["home", "alerts", "esUiShared", "kibanaReact", "kibanaUtils"] } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 5c1e0aa0100e8..fa38c4501379f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -15,6 +15,7 @@ import { ChromeBreadcrumb, CoreStart, ScopedHistory, + SavedObjectsClientContract, } from 'kibana/public'; import { KibanaFeature } from '../../../features/common'; import { Section, routeToAlertDetails } from './constants'; @@ -24,6 +25,7 @@ import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerts/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; const TriggersActionsUIHome = lazy(async () => import('./home')); const AlertDetailsRoute = lazy( @@ -31,13 +33,14 @@ const AlertDetailsRoute = lazy( ); export interface AppDeps { - dataPlugin: DataPublicPluginStart; + data: DataPublicPluginStart; charts: ChartsPluginStart; chrome: ChromeStart; alerts?: AlertingStart; navigateToApp: CoreStart['application']['navigateToApp']; docLinks: DocLinksStart; toastNotifications: ToastsSetup; + storage?: Storage; http: HttpSetup; uiSettings: IUiSettingsClient; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; @@ -45,6 +48,9 @@ export interface AppDeps { actionTypeRegistry: ActionTypeRegistryContract; alertTypeRegistry: AlertTypeRegistryContract; history: ScopedHistory; + savedObjects?: { + client: SavedObjectsClientContract; + }; kibanaFeatures: KibanaFeature[]; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app_context.tsx index bf2e0c7274e7b..a4568d069c21c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app_context.tsx @@ -5,6 +5,7 @@ */ import React, { createContext, useContext } from 'react'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { AppDeps } from './app'; const AppContext = createContext(null); @@ -16,7 +17,11 @@ export const AppContextProvider = ({ appDeps: AppDeps | null; children: React.ReactNode; }) => { - return appDeps ? {children} : null; + return appDeps ? ( + + {children} + + ) : null; }; export const useAppDependencies = (): AppDeps => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx index bb46fd02a98a9..e18bf4ce84871 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx @@ -6,21 +6,20 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { SavedObjectsClientContract } from 'src/core/public'; - import { App, AppDeps } from './app'; import { setSavedObjectsClient } from '../common/lib/data_apis'; interface BootDeps extends AppDeps { element: HTMLElement; - savedObjects: SavedObjectsClientContract; I18nContext: any; } export const boot = (bootDeps: BootDeps) => { - const { I18nContext, element, savedObjects, ...appDeps } = bootDeps; + const { I18nContext, element, ...appDeps } = bootDeps; - setSavedObjectsClient(savedObjects); + if (appDeps.savedObjects) { + setSavedObjectsClient(appDeps.savedObjects.client); + } render( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 65d5389078880..71e1c60a92aed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -55,7 +55,7 @@ describe('actions_connectors_list component empty', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -165,7 +165,7 @@ describe('actions_connectors_list component with items', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -256,7 +256,7 @@ describe('actions_connectors_list component empty with show only capability', () const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -348,7 +348,7 @@ describe('actions_connectors_list with show only capability', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -452,7 +452,7 @@ describe('actions_connectors_list component with disabled items', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, injectedMetadata: mockes.injectedMetadata, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 70b6fb0b750dd..c2a7635b4cf96 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -42,7 +42,7 @@ jest.mock('../../../app_context', () => ({ toastNotifications: mockes.notifications.toasts, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, uiSettings: mockes.uiSettings, - dataPlugin: jest.fn(), + data: jest.fn(), charts: jest.fn(), })), })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 603058e6fcb52..b38f0e749a28d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -70,7 +70,7 @@ export const AlertDetails: React.FunctionComponent = ({ uiSettings, docLinks, charts, - dataPlugin, + data, setBreadcrumbs, chrome, } = useAppDependencies(); @@ -162,11 +162,11 @@ export const AlertDetails: React.FunctionComponent = ({ uiSettings, docLinks, charts, - dataFieldsFormats: dataPlugin.fieldFormats, + dataFieldsFormats: data.fieldFormats, reloadAlerts: setAlert, capabilities, - dataUi: dataPlugin.ui, - dataIndexPatterns: dataPlugin.indexPatterns, + dataUi: data.ui, + dataIndexPatterns: data.indexPatterns, }} > { toastNotifications: mocks.notifications.toasts, http: mocks.http, uiSettings: mocks.uiSettings, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), actionTypeRegistry, alertTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 611846cf4a521..a29c112b536fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -108,7 +108,7 @@ describe('alerts_list component empty', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -279,7 +279,7 @@ describe('alerts_list component with items', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -362,7 +362,7 @@ describe('alerts_list component empty with show only capability', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, @@ -483,7 +483,7 @@ describe('alerts_list with show only capability', () => { const deps = { chrome, docLinks, - dataPlugin: dataPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), alerting: alertingPluginMock.createStartContract(), toastNotifications: mockes.notifications.toasts, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 75f359888a858..11d6f3470fec2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -83,7 +83,7 @@ export const AlertsList: React.FunctionComponent = () => { uiSettings, docLinks, charts, - dataPlugin, + data, kibanaFeatures, } = useAppDependencies(); const canExecuteActions = hasExecuteActionsCapability(capabilities); @@ -668,10 +668,10 @@ export const AlertsList: React.FunctionComponent = () => { uiSettings, docLinks, charts, - dataFieldsFormats: dataPlugin.fieldFormats, + dataFieldsFormats: data.fieldFormats, capabilities, - dataUi: dataPlugin.ui, - dataIndexPatterns: dataPlugin.indexPatterns, + dataUi: data.ui, + dataIndexPatterns: data.indexPatterns, kibanaFeatures, }} > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts index b5ab53d868cf1..061f3faaa6c0f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/test_utils/index.ts @@ -26,20 +26,20 @@ export async function getMockedAppDependencies() { const kibanaFeatures = await featuresPluginMock.createStart().getFeatures(); return { + data: dataPluginMock.createStartContract(), + charts: chartPluginMock.createStartContract(), chrome, + navigateToApp, docLinks, - dataPlugin: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), - alerting: alertingPluginMock.createStartContract(), toastNotifications: coreSetupMock.notifications.toasts, http: coreSetupMock.http, uiSettings: coreSetupMock.uiSettings, - navigateToApp, - capabilities, - history: scopedHistoryMock.create(), setBreadcrumbs: jest.fn(), + capabilities, actionTypeRegistry, alertTypeRegistry, + history: scopedHistoryMock.create(), + alerting: alertingPluginMock.createStartContract(), kibanaFeatures, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 3794112e1d502..3187451d2600e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -17,6 +17,7 @@ export { AlertTypeModel, ActionType, ActionTypeRegistryContract, + AlertTypeRegistryContract, AlertTypeParamsExpressionProps, ValidationResult, ActionVariable, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 2d93d368ad8e5..a30747afe6914 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -22,6 +22,7 @@ import { import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { PluginStartContract as AlertingStart } from '../../alerts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; @@ -102,16 +103,17 @@ export class Plugin const { boot } = await import('./application/boot'); const kibanaFeatures = await pluginsStart.features.getFeatures(); return boot({ - dataPlugin: pluginsStart.data, + data: pluginsStart.data, charts: pluginsStart.charts, alerts: pluginsStart.alerts, element: params.element, toastNotifications: coreStart.notifications.toasts, + storage: new Storage(window.localStorage), http: coreStart.http, uiSettings: coreStart.uiSettings, docLinks: coreStart.docLinks, chrome: coreStart.chrome, - savedObjects: coreStart.savedObjects.client, + savedObjects: coreStart.savedObjects, I18nContext: coreStart.i18n.Context, capabilities: coreStart.application.capabilities, navigateToApp: coreStart.application.navigateToApp, From 47d6612baed59b9fd21762b0c33f78452c0ad893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Wed, 18 Nov 2020 23:16:18 +0100 Subject: [PATCH 86/99] Add Managed label to data streams and a view switch for the table (#83049) * Add Managed label to data streams and a view switch for the table * Fix i18n errors * Updated some wording and made filter function easier (managed data streams) * Update x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts Co-authored-by: Alison Goryachev * Renamed view to include (managed data streams) * Update x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> * Update x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> * Update x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alison Goryachev Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> --- .../helpers/test_subjects.ts | 1 + .../home/data_streams_tab.helpers.ts | 12 ++- .../home/data_streams_tab.test.ts | 78 ++++++++++++++++--- .../home/indices_tab.test.ts | 4 +- .../common/lib/data_stream_serialization.ts | 2 + .../common/types/data_streams.ts | 10 +++ .../public/application/lib/data_streams.tsx | 15 ++++ .../data_stream_list/data_stream_list.tsx | 37 ++++++++- .../data_stream_table/data_stream_table.tsx | 50 +++++++++--- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 11 files changed, 182 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/lib/data_streams.tsx diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index 313ebefb85301..04843cae6a57e 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -21,6 +21,7 @@ export type TestSubjects = | 'filterList.filterItem' | 'ilmPolicyLink' | 'includeStatsSwitch' + | 'includeManagedSwitch' | 'indexTable' | 'indexTableIncludeHiddenIndicesToggle' | 'indexTableIndexNameLink' diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index 148b20e5de533..4e0486e55720d 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -19,6 +19,7 @@ export interface DataStreamsTabTestBed extends TestBed { goToDataStreamsList: () => void; clickEmptyPromptIndexTemplateLink: () => void; clickIncludeStatsSwitch: () => void; + clickIncludeManagedSwitch: () => void; clickReloadButton: () => void; clickNameAt: (index: number) => void; clickIndicesAt: (index: number) => void; @@ -80,6 +81,11 @@ export const setup = async (overridingDependencies: any = {}): Promise { + const { find } = testBed; + find('includeManagedSwitch').simulate('click'); + }; + const clickReloadButton = () => { const { find } = testBed; find('reloadButton').simulate('click'); @@ -183,6 +189,7 @@ export const setup = async (overridingDependencies: any = {}): Promise ({ - name, +export const createDataStreamPayload = (dataStream: Partial): DataStream => ({ + name: 'my-data-stream', timeStampField: { name: '@timestamp' }, indices: [ { @@ -216,6 +223,7 @@ export const createDataStreamPayload = (name: string): DataStream => ({ indexTemplateName: 'indexTemplate', storageSize: '1b', maxTimeStamp: 420, + ...dataStream, }); export const createDataStreamBackingIndex = (indexName: string, dataStreamName: string) => ({ diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index 633184c9afecc..a76d5dc99cbaf 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -99,10 +99,10 @@ describe('Data Streams tab', () => { createNonDataStreamIndex('non-data-stream-index'), ]); - const dataStreamForDetailPanel = createDataStreamPayload('dataStream1'); + const dataStreamForDetailPanel = createDataStreamPayload({ name: 'dataStream1' }); setLoadDataStreamsResponse([ dataStreamForDetailPanel, - createDataStreamPayload('dataStream2'), + createDataStreamPayload({ name: 'dataStream2' }), ]); setLoadDataStreamResponse(dataStreamForDetailPanel); @@ -287,9 +287,9 @@ describe('Data Streams tab', () => { createDataStreamBackingIndex('data-stream-index2', 'dataStream2'), ]); - const dataStreamDollarSign = createDataStreamPayload('%dataStream'); - setLoadDataStreamsResponse([dataStreamDollarSign]); - setLoadDataStreamResponse(dataStreamDollarSign); + const dataStreamPercentSign = createDataStreamPayload({ name: '%dataStream' }); + setLoadDataStreamsResponse([dataStreamPercentSign]); + setLoadDataStreamResponse(dataStreamPercentSign); testBed = await setup({ history: createMemoryHistory(), @@ -327,10 +327,10 @@ describe('Data Streams tab', () => { test('with an ILM url generator and an ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; - const dataStreamForDetailPanel = { - ...createDataStreamPayload('dataStream1'), + const dataStreamForDetailPanel = createDataStreamPayload({ + name: 'dataStream1', ilmPolicyName: 'my_ilm_policy', - }; + }); setLoadDataStreamsResponse([dataStreamForDetailPanel]); setLoadDataStreamResponse(dataStreamForDetailPanel); @@ -351,7 +351,7 @@ describe('Data Streams tab', () => { test('with an ILM url generator and no ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; - const dataStreamForDetailPanel = createDataStreamPayload('dataStream1'); + const dataStreamForDetailPanel = createDataStreamPayload({ name: 'dataStream1' }); setLoadDataStreamsResponse([dataStreamForDetailPanel]); setLoadDataStreamResponse(dataStreamForDetailPanel); @@ -373,10 +373,10 @@ describe('Data Streams tab', () => { test('without an ILM url generator and with an ILM policy', async () => { const { setLoadDataStreamsResponse, setLoadDataStreamResponse } = httpRequestsMockHelpers; - const dataStreamForDetailPanel = { - ...createDataStreamPayload('dataStream1'), + const dataStreamForDetailPanel = createDataStreamPayload({ + name: 'dataStream1', ilmPolicyName: 'my_ilm_policy', - }; + }); setLoadDataStreamsResponse([dataStreamForDetailPanel]); setLoadDataStreamResponse(dataStreamForDetailPanel); @@ -395,4 +395,58 @@ describe('Data Streams tab', () => { expect(findDetailPanelIlmPolicyName().contains('my_ilm_policy')).toBeTruthy(); }); }); + + describe('managed data streams', () => { + const nonBreakingSpace = ' '; + beforeEach(async () => { + const managedDataStream = createDataStreamPayload({ + name: 'managed-data-stream', + _meta: { + package: 'test', + managed: true, + managed_by: 'ingest-manager', + }, + }); + const nonManagedDataStream = createDataStreamPayload({ name: 'non-managed-data-stream' }); + httpRequestsMockHelpers.setLoadDataStreamsResponse([managedDataStream, nonManagedDataStream]); + + testBed = await setup({ + history: createMemoryHistory(), + }); + await act(async () => { + testBed.actions.goToDataStreamsList(); + }); + testBed.component.update(); + }); + + test('listed in the table with Managed label', () => { + const { table } = testBed; + const { tableCellsValues } = table.getMetaData('dataStreamTable'); + + expect(tableCellsValues).toEqual([ + ['', `managed-data-stream${nonBreakingSpace}Managed`, 'green', '1', 'Delete'], + ['', 'non-managed-data-stream', 'green', '1', 'Delete'], + ]); + }); + + test('turning off "Include managed" switch hides managed data streams', async () => { + const { exists, actions, component, table } = testBed; + let { tableCellsValues } = table.getMetaData('dataStreamTable'); + + expect(tableCellsValues).toEqual([ + ['', `managed-data-stream${nonBreakingSpace}Managed`, 'green', '1', 'Delete'], + ['', 'non-managed-data-stream', 'green', '1', 'Delete'], + ]); + + expect(exists('includeManagedSwitch')).toBe(true); + + await act(async () => { + actions.clickIncludeManagedSwitch(); + }); + component.update(); + + ({ tableCellsValues } = table.getMetaData('dataStreamTable')); + expect(tableCellsValues).toEqual([['', 'non-managed-data-stream', 'green', '1', 'Delete']]); + }); + }); }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index db4624d4389ff..9a5dca01e1b98 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -74,7 +74,9 @@ describe('', () => { // The detail panel should still appear even if there are no data streams. httpRequestsMockHelpers.setLoadDataStreamsResponse([]); - httpRequestsMockHelpers.setLoadDataStreamResponse(createDataStreamPayload('dataStream1')); + httpRequestsMockHelpers.setLoadDataStreamResponse( + createDataStreamPayload({ name: 'dataStream1' }) + ); testBed = await setup({ history: createMemoryHistory(), diff --git a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts index 69004eaa020eb..2d8e038d2a60f 100644 --- a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts @@ -17,6 +17,7 @@ export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataS ilm_policy: ilmPolicyName, store_size: storageSize, maximum_timestamp: maxTimeStamp, + _meta, } = dataStreamFromEs; return { @@ -35,6 +36,7 @@ export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataS ilmPolicyName, storageSize, maxTimeStamp, + _meta, }; } diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts index 7c348f9a8085d..adb7104043fbb 100644 --- a/x-pack/plugins/index_management/common/types/data_streams.ts +++ b/x-pack/plugins/index_management/common/types/data_streams.ts @@ -10,6 +10,14 @@ interface TimestampFieldFromEs { type TimestampField = TimestampFieldFromEs; +interface MetaFieldFromEs { + managed_by: string; + package: any; + managed: boolean; +} + +type MetaField = MetaFieldFromEs; + export type HealthFromEs = 'GREEN' | 'YELLOW' | 'RED'; export interface DataStreamFromEs { @@ -17,6 +25,7 @@ export interface DataStreamFromEs { timestamp_field: TimestampFieldFromEs; indices: DataStreamIndexFromEs[]; generation: number; + _meta?: MetaFieldFromEs; status: HealthFromEs; template: string; ilm_policy?: string; @@ -41,6 +50,7 @@ export interface DataStream { ilmPolicyName?: string; storageSize?: string; maxTimeStamp?: number; + _meta?: MetaField; } export interface DataStreamIndex { diff --git a/x-pack/plugins/index_management/public/application/lib/data_streams.tsx b/x-pack/plugins/index_management/public/application/lib/data_streams.tsx new file mode 100644 index 0000000000000..ca5297e399339 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/lib/data_streams.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DataStream } from '../../../common'; + +export const isManagedByIngestManager = (dataStream: DataStream): boolean => { + return Boolean(dataStream._meta?.managed && dataStream._meta?.managed_by === 'ingest-manager'); +}; + +export const filterDataStreams = (dataStreams: DataStream[]): DataStream[] => { + return dataStreams.filter((dataStream: DataStream) => !isManagedByIngestManager(dataStream)); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index 20b93d9d71d04..0df5697a4281a 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -32,6 +32,7 @@ import { documentationService } from '../../../services/documentation'; import { Section } from '../home'; import { DataStreamTable } from './data_stream_table'; import { DataStreamDetailPanel } from './data_stream_detail_panel'; +import { filterDataStreams } from '../../../lib/data_streams'; interface MatchParams { dataStreamName?: string; @@ -52,6 +53,7 @@ export const DataStreamList: React.FunctionComponent ); } else if (Array.isArray(dataStreams) && dataStreams.length > 0) { + const filteredDataStreams = isIncludeManagedChecked + ? dataStreams + : filterDataStreams(dataStreams); content = ( <> - {/* TODO: Add a switch for toggling on data streams created by Ingest Manager */} + + + + setIsIncludeManagedChecked(e.target.checked)} + data-test-subj="includeManagedSwitch" + /> + + + + + + +
@@ -212,7 +245,7 @@ export const DataStreamList: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - render: (name: DataStream['name']) => { + render: (name: DataStream['name'], dataStream: DataStream) => { return ( - - {name} - + + + {name} + + {isManagedByIngestManager(dataStream) ? ( + +   + + + + + + + ) : null} + ); }, }); @@ -121,7 +151,7 @@ export const DataStreamTable: React.FunctionComponent = ({ name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteText', { defaultMessage: 'Delete', }), - description: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteDecription', { + description: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteDescription', { defaultMessage: 'Delete this data stream', }), icon: 'trash', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cd45a4f01fc64..7115f8c6eeb6f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8143,7 +8143,6 @@ "xpack.idxMgmt.dataStreamList.loadingDataStreamsErrorMessage": "データストリームの読み込み中にエラーが発生", "xpack.idxMgmt.dataStreamList.reloadDataStreamsButtonLabel": "再読み込み", "xpack.idxMgmt.dataStreamList.table.actionColumnTitle": "アクション", - "xpack.idxMgmt.dataStreamList.table.actionDeleteDecription": "このデータストリームを削除", "xpack.idxMgmt.dataStreamList.table.actionDeleteText": "削除", "xpack.idxMgmt.dataStreamList.table.deleteDataStreamsButtonLabel": "{count, plural, one {個のデータストリーム} other {個のデータストリーム}}を削除", "xpack.idxMgmt.dataStreamList.table.healthColumnTitle": "ヘルス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 97396b09ca6c6..b945c443741b6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8151,7 +8151,6 @@ "xpack.idxMgmt.dataStreamList.loadingDataStreamsErrorMessage": "加载数据流时出错", "xpack.idxMgmt.dataStreamList.reloadDataStreamsButtonLabel": "重新加载", "xpack.idxMgmt.dataStreamList.table.actionColumnTitle": "操作", - "xpack.idxMgmt.dataStreamList.table.actionDeleteDecription": "删除此数据流", "xpack.idxMgmt.dataStreamList.table.actionDeleteText": "删除", "xpack.idxMgmt.dataStreamList.table.deleteDataStreamsButtonLabel": "删除{count, plural, one {数据流} other {数据流} }", "xpack.idxMgmt.dataStreamList.table.healthColumnTitle": "运行状况", From 8ede715869de8acab83ea58d1019fe03af40e0a1 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Wed, 18 Nov 2020 14:26:25 -0800 Subject: [PATCH 87/99] Updating code-owners to use new core/app-services team names (#83731) * Updating code-owners to use new core/app-services team names * And the comment as well --- .github/CODEOWNERS | 126 ++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d92725b233e3e..5b43f9883a2c1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -30,40 +30,40 @@ /src/plugins/visualizations/ @elastic/kibana-app # Application Services -/examples/bfetch_explorer/ @elastic/kibana-app-arch -/examples/dashboard_embeddable_examples/ @elastic/kibana-app-arch -/examples/demo_search/ @elastic/kibana-app-arch -/examples/developer_examples/ @elastic/kibana-app-arch -/examples/embeddable_examples/ @elastic/kibana-app-arch -/examples/embeddable_explorer/ @elastic/kibana-app-arch -/examples/state_containers_examples/ @elastic/kibana-app-arch -/examples/ui_action_examples/ @elastic/kibana-app-arch -/examples/ui_actions_explorer/ @elastic/kibana-app-arch -/examples/url_generators_examples/ @elastic/kibana-app-arch -/examples/url_generators_explorer/ @elastic/kibana-app-arch -/packages/elastic-datemath/ @elastic/kibana-app-arch -/packages/kbn-interpreter/ @elastic/kibana-app-arch -/src/plugins/bfetch/ @elastic/kibana-app-arch -/src/plugins/data/ @elastic/kibana-app-arch -/src/plugins/embeddable/ @elastic/kibana-app-arch -/src/plugins/expressions/ @elastic/kibana-app-arch -/src/plugins/inspector/ @elastic/kibana-app-arch -/src/plugins/kibana_react/ @elastic/kibana-app-arch +/examples/bfetch_explorer/ @elastic/kibana-app-services +/examples/dashboard_embeddable_examples/ @elastic/kibana-app-services +/examples/demo_search/ @elastic/kibana-app-services +/examples/developer_examples/ @elastic/kibana-app-services +/examples/embeddable_examples/ @elastic/kibana-app-services +/examples/embeddable_explorer/ @elastic/kibana-app-services +/examples/state_containers_examples/ @elastic/kibana-app-services +/examples/ui_action_examples/ @elastic/kibana-app-services +/examples/ui_actions_explorer/ @elastic/kibana-app-services +/examples/url_generators_examples/ @elastic/kibana-app-services +/examples/url_generators_explorer/ @elastic/kibana-app-services +/packages/elastic-datemath/ @elastic/kibana-app-services +/packages/kbn-interpreter/ @elastic/kibana-app-services +/src/plugins/bfetch/ @elastic/kibana-app-services +/src/plugins/data/ @elastic/kibana-app-services +/src/plugins/embeddable/ @elastic/kibana-app-services +/src/plugins/expressions/ @elastic/kibana-app-services +/src/plugins/inspector/ @elastic/kibana-app-services +/src/plugins/kibana_react/ @elastic/kibana-app-services /src/plugins/kibana_react/public/code_editor @elastic/kibana-presentation -/src/plugins/kibana_utils/ @elastic/kibana-app-arch -/src/plugins/navigation/ @elastic/kibana-app-arch -/src/plugins/share/ @elastic/kibana-app-arch -/src/plugins/ui_actions/ @elastic/kibana-app-arch -/x-pack/examples/ui_actions_enhanced_examples/ @elastic/kibana-app-arch -/x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch -/x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-arch -/x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-arch -#CC# /src/plugins/bfetch/ @elastic/kibana-app-arch -#CC# /src/plugins/index_pattern_management/ @elastic/kibana-app-arch -#CC# /src/plugins/inspector/ @elastic/kibana-app-arch -#CC# /src/plugins/share/ @elastic/kibana-app-arch -#CC# /x-pack/plugins/drilldowns/ @elastic/kibana-app-arch -#CC# /packages/kbn-interpreter/ @elastic/kibana-app-arch +/src/plugins/kibana_utils/ @elastic/kibana-app-services +/src/plugins/navigation/ @elastic/kibana-app-services +/src/plugins/share/ @elastic/kibana-app-services +/src/plugins/ui_actions/ @elastic/kibana-app-services +/x-pack/examples/ui_actions_enhanced_examples/ @elastic/kibana-app-services +/x-pack/plugins/data_enhanced/ @elastic/kibana-app-services +/x-pack/plugins/embeddable_enhanced/ @elastic/kibana-app-services +/x-pack/plugins/ui_actions_enhanced/ @elastic/kibana-app-services +#CC# /src/plugins/bfetch/ @elastic/kibana-app-services +#CC# /src/plugins/index_pattern_management/ @elastic/kibana-app-services +#CC# /src/plugins/inspector/ @elastic/kibana-app-services +#CC# /src/plugins/share/ @elastic/kibana-app-services +#CC# /x-pack/plugins/drilldowns/ @elastic/kibana-app-services +#CC# /packages/kbn-interpreter/ @elastic/kibana-app-services # APM /x-pack/plugins/apm/ @elastic/apm-ui @@ -172,38 +172,38 @@ /test/functional/services/lib @elastic/kibana-qa /test/functional/services/remote @elastic/kibana-qa -# Platform -/src/core/ @elastic/kibana-platform -/src/plugins/saved_objects_tagging_oss @elastic/kibana-platform -/config/kibana.yml @elastic/kibana-platform -/x-pack/plugins/features/ @elastic/kibana-platform -/x-pack/plugins/licensing/ @elastic/kibana-platform -/x-pack/plugins/global_search/ @elastic/kibana-platform -/x-pack/plugins/cloud/ @elastic/kibana-platform -/x-pack/plugins/saved_objects_tagging/ @elastic/kibana-platform -/x-pack/test/saved_objects_field_count/ @elastic/kibana-platform -/x-pack/test/saved_object_tagging/ @elastic/kibana-platform -/packages/kbn-config-schema/ @elastic/kibana-platform -/packages/kbn-std/ @elastic/kibana-platform -/src/legacy/server/config/ @elastic/kibana-platform -/src/legacy/server/http/ @elastic/kibana-platform -/src/legacy/server/logging/ @elastic/kibana-platform -/src/plugins/status_page/ @elastic/kibana-platform -/src/plugins/saved_objects_management/ @elastic/kibana-platform -/src/dev/run_check_published_api_changes.ts @elastic/kibana-platform -#CC# /src/core/server/csp/ @elastic/kibana-platform -#CC# /src/legacy/server/config/ @elastic/kibana-platform -#CC# /src/legacy/server/http/ @elastic/kibana-platform -#CC# /src/legacy/ui/public/documentation_links @elastic/kibana-platform -#CC# /src/plugins/legacy_export/ @elastic/kibana-platform -#CC# /src/plugins/saved_objects/ @elastic/kibana-platform -#CC# /src/plugins/status_page/ @elastic/kibana-platform -#CC# /x-pack/plugins/cloud/ @elastic/kibana-platform -#CC# /x-pack/plugins/features/ @elastic/kibana-platform -#CC# /x-pack/plugins/global_search/ @elastic/kibana-platform +# Core +/src/core/ @elastic/kibana-core +/src/plugins/saved_objects_tagging_oss @elastic/kibana-core +/config/kibana.yml @elastic/kibana-core +/x-pack/plugins/features/ @elastic/kibana-core +/x-pack/plugins/licensing/ @elastic/kibana-core +/x-pack/plugins/global_search/ @elastic/kibana-core +/x-pack/plugins/cloud/ @elastic/kibana-core +/x-pack/plugins/saved_objects_tagging/ @elastic/kibana-core +/x-pack/test/saved_objects_field_count/ @elastic/kibana-core +/x-pack/test/saved_object_tagging/ @elastic/kibana-core +/packages/kbn-config-schema/ @elastic/kibana-core +/packages/kbn-std/ @elastic/kibana-core +/src/legacy/server/config/ @elastic/kibana-core +/src/legacy/server/http/ @elastic/kibana-core +/src/legacy/server/logging/ @elastic/kibana-core +/src/plugins/status_page/ @elastic/kibana-core +/src/plugins/saved_objects_management/ @elastic/kibana-core +/src/dev/run_check_published_api_changes.ts @elastic/kibana-core +#CC# /src/core/server/csp/ @elastic/kibana-core +#CC# /src/legacy/server/config/ @elastic/kibana-core +#CC# /src/legacy/server/http/ @elastic/kibana-core +#CC# /src/legacy/ui/public/documentation_links @elastic/kibana-core +#CC# /src/plugins/legacy_export/ @elastic/kibana-core +#CC# /src/plugins/saved_objects/ @elastic/kibana-core +#CC# /src/plugins/status_page/ @elastic/kibana-core +#CC# /x-pack/plugins/cloud/ @elastic/kibana-core +#CC# /x-pack/plugins/features/ @elastic/kibana-core +#CC# /x-pack/plugins/global_search/ @elastic/kibana-core # Security -/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform +/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core /src/plugins/security_oss/ @elastic/kibana-security /test/security_functional/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security From a7670518cc15e36f9e81a977c53e94764e3d2791 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Wed, 18 Nov 2020 15:43:26 -0700 Subject: [PATCH 88/99] [Maps] Add 'crossed' & 'exited' events to tracking alert (#82463) --- .../geo_threshold/query_builder/index.tsx | 2 +- .../public/alert_types/geo_threshold/types.ts | 1 + .../geo_threshold/geo_threshold.ts | 56 +++++++++++-------- .../geo_threshold/tests/geo_threshold.test.ts | 53 ++++++++++++++++-- 4 files changed, 85 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx index 623223d66ea00..c573d3b738373 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx @@ -326,7 +326,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent setAlertParams('trackingEvent', e.target.value)} - options={[conditionOptions[0]]} // TODO: Make all options avab. before merge + options={conditionOptions} />
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts index 86faa4ed2fb4a..5ac9c7fd29317 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts @@ -9,6 +9,7 @@ import { Query } from '../../../../../../src/plugins/data/common'; export enum TrackingEvent { entered = 'entered', exited = 'exited', + crossed = 'crossed', } export interface GeoThresholdAlertParams { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts index 8247cc787d365..5cb4156e84623 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts @@ -144,11 +144,14 @@ export function getMovedEntities( [] ) // Do not track entries to or exits from 'other' - .filter((entityMovementDescriptor: EntityMovementDescriptor) => - trackingEvent === 'entered' - ? entityMovementDescriptor.currLocation.shapeId !== OTHER_CATEGORY - : entityMovementDescriptor.prevLocation.shapeId !== OTHER_CATEGORY - ) + .filter((entityMovementDescriptor: EntityMovementDescriptor) => { + if (trackingEvent !== 'crossed') { + return trackingEvent === 'entered' + ? entityMovementDescriptor.currLocation.shapeId !== OTHER_CATEGORY + : entityMovementDescriptor.prevLocation.shapeId !== OTHER_CATEGORY; + } + return true; + }) ); } @@ -254,27 +257,36 @@ export const getGeoThresholdExecutor = (log: Logger) => movedEntities.forEach(({ entityName, currLocation, prevLocation }) => { const toBoundaryName = shapesIdsNamesMap[currLocation.shapeId] || currLocation.shapeId; const fromBoundaryName = shapesIdsNamesMap[prevLocation.shapeId] || prevLocation.shapeId; - services - .alertInstanceFactory(`${entityName}-${toBoundaryName || currLocation.shapeId}`) - .scheduleActions(ActionGroupId, { - entityId: entityName, - timeOfDetection: new Date(currIntervalEndTime).getTime(), - crossingLine: `LINESTRING (${prevLocation.location[0]} ${prevLocation.location[1]}, ${currLocation.location[0]} ${currLocation.location[1]})`, + let alertInstance; + if (params.trackingEvent === 'entered') { + alertInstance = `${entityName}-${toBoundaryName || currLocation.shapeId}`; + } else if (params.trackingEvent === 'exited') { + alertInstance = `${entityName}-${fromBoundaryName || prevLocation.shapeId}`; + } else { + // == 'crossed' + alertInstance = `${entityName}-${fromBoundaryName || prevLocation.shapeId}-${ + toBoundaryName || currLocation.shapeId + }`; + } + services.alertInstanceFactory(alertInstance).scheduleActions(ActionGroupId, { + entityId: entityName, + timeOfDetection: new Date(currIntervalEndTime).getTime(), + crossingLine: `LINESTRING (${prevLocation.location[0]} ${prevLocation.location[1]}, ${currLocation.location[0]} ${currLocation.location[1]})`, - toEntityLocation: `POINT (${currLocation.location[0]} ${currLocation.location[1]})`, - toEntityDateTime: currLocation.date, - toEntityDocumentId: currLocation.docId, + toEntityLocation: `POINT (${currLocation.location[0]} ${currLocation.location[1]})`, + toEntityDateTime: currLocation.date, + toEntityDocumentId: currLocation.docId, - toBoundaryId: currLocation.shapeId, - toBoundaryName, + toBoundaryId: currLocation.shapeId, + toBoundaryName, - fromEntityLocation: `POINT (${prevLocation.location[0]} ${prevLocation.location[1]})`, - fromEntityDateTime: prevLocation.date, - fromEntityDocumentId: prevLocation.docId, + fromEntityLocation: `POINT (${prevLocation.location[0]} ${prevLocation.location[1]})`, + fromEntityDateTime: prevLocation.date, + fromEntityDocumentId: prevLocation.docId, - fromBoundaryId: prevLocation.shapeId, - fromBoundaryName, - }); + fromBoundaryId: prevLocation.shapeId, + fromBoundaryName, + }); }); // Combine previous results w/ current results for state of next run diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts index e4cee9c677713..5b5197ac62a39 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/geo_threshold.test.ts @@ -99,7 +99,6 @@ describe('geo_threshold', () => { }); describe('getMovedEntities', () => { - const trackingEvent = 'entered'; it('should return empty array if only movements were within same shapes', async () => { const currLocationArr = [ { @@ -133,7 +132,7 @@ describe('geo_threshold', () => { shapeLocationId: 'sameShape2', }, ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, trackingEvent); + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); expect(movedEntities).toEqual([]); }); @@ -170,7 +169,7 @@ describe('geo_threshold', () => { shapeLocationId: 'thisOneDidntMove', }, ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, trackingEvent); + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); expect(movedEntities.length).toEqual(1); }); @@ -193,7 +192,7 @@ describe('geo_threshold', () => { shapeLocationId: 'oldShapeLocation', }, ]; - const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, trackingEvent); + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'entered'); expect(movedEntities).toEqual([]); }); @@ -219,5 +218,51 @@ describe('geo_threshold', () => { const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'exited'); expect(movedEntities).toEqual([]); }); + + it('should not ignore "crossed" results from "other"', async () => { + const currLocationArr = [ + { + dateInShape: '2020-09-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 41.62806099653244], + shapeLocationId: 'newShapeLocation', + }, + ]; + const prevLocationArr = [ + { + dateInShape: '2020-08-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 40.62806099653244], + shapeLocationId: OTHER_CATEGORY, + }, + ]; + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'crossed'); + expect(movedEntities.length).toEqual(1); + }); + + it('should not ignore "crossed" results to "other"', async () => { + const currLocationArr = [ + { + dateInShape: '2020-08-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 40.62806099653244], + shapeLocationId: OTHER_CATEGORY, + }, + ]; + const prevLocationArr = [ + { + dateInShape: '2020-09-28T18:01:41.190Z', + docId: 'N-ng1XQB6yyY-xQxnGSM', + entityName: '936', + location: [-82.8814151789993, 41.62806099653244], + shapeLocationId: 'newShapeLocation', + }, + ]; + const movedEntities = getMovedEntities(currLocationArr, prevLocationArr, 'crossed'); + expect(movedEntities.length).toEqual(1); + }); }); }); From 640a7b9b7f65c284dd82ca4572caa189f483f450 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 18 Nov 2020 14:49:14 -0800 Subject: [PATCH 89/99] [Enterprise Search] Rename React Router helpers (#83718) * Rename EUI React Router components - Instead of bogarting the EUI component names, use EuiLinkTo instead of EuiLink Other misc renaming - eui_link.tsx to eui_components.tsx for clearer file name - EuiReactRouterHelper to ReactRouterHelper, to make the distinction between EUI and React Router clearer (in theory you could use this helper for non-EUI components) - other misc type renaming * Update simple instances of previous EUI RR components to Eui*To * Clean up complex/renamed instances of Eui*To (hopefully much more straightforward now) - unfortunately side_nav requires an eslint disable --- .../components/engines/engines_table.test.tsx | 4 +-- .../components/engines/engines_table.tsx | 10 +++---- .../product_card/product_card.test.tsx | 8 +++--- .../components/product_card/product_card.tsx | 6 ++-- .../setup_guide/setup_guide_cta.tsx | 6 ++-- .../shared/error_state/error_state_prompt.tsx | 6 ++-- .../shared/layout/side_nav.test.tsx | 8 +++--- .../applications/shared/layout/side_nav.tsx | 19 +++++-------- .../shared/not_found/not_found.tsx | 12 ++++---- ..._link.test.tsx => eui_components.test.tsx} | 20 ++++++------- .../{eui_link.tsx => eui_components.tsx} | 28 +++++++++---------- .../shared/react_router_helpers/index.ts | 6 +--- .../shared/source_row/source_row.tsx | 10 +++---- .../groups/components/group_manager_modal.tsx | 6 ++-- .../views/groups/components/group_row.tsx | 10 +++---- .../views/groups/groups.test.tsx | 4 +-- .../workplace_search/views/groups/groups.tsx | 10 +++---- 17 files changed, 82 insertions(+), 91 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/{eui_link.test.tsx => eui_components.test.tsx} (78%) rename x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/{eui_link.tsx => eui_components.tsx} (73%) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx index e9ac51b6a901c..c8872fe43a184 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx @@ -10,7 +10,7 @@ import { mockTelemetryActions, mountWithIntl } from '../../../__mocks__/'; import React from 'react'; import { EuiBasicTable, EuiPagination, EuiButtonEmpty } from '@elastic/eui'; -import { EuiLink } from '../../../shared/react_router_helpers'; +import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { EnginesTable } from './engines_table'; @@ -50,7 +50,7 @@ describe('EnginesTable', () => { }); it('contains engine links which send telemetry', () => { - const engineLinks = wrapper.find(EuiLink); + const engineLinks = wrapper.find(EuiLinkTo); engineLinks.forEach((link) => { expect(link.prop('to')).toEqual('/engines/test-engine'); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index 9591bbda1f7c2..7d69cd2b4d4da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -11,7 +11,7 @@ import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/reac import { i18n } from '@kbn/i18n'; import { TelemetryLogic } from '../../../shared/telemetry'; -import { EuiLink } from '../../../shared/react_router_helpers'; +import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { getEngineRoute } from '../../routes'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; @@ -59,9 +59,9 @@ export const EnginesTable: React.FC = ({ defaultMessage: 'Name', }), render: (name: string) => ( - + {name} - + ), width: '30%', truncateText: true, @@ -122,12 +122,12 @@ export const EnginesTable: React.FC = ({ ), dataType: 'string', render: (name: string) => ( - + - + ), align: 'right', width: '100px', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index a257ccde9f474..8ba2da11604c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -12,7 +12,7 @@ import { useValues } from 'kea'; import { shallow } from 'enzyme'; import { EuiCard } from '@elastic/eui'; -import { EuiButton } from '../../../shared/react_router_helpers'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { ProductCard } from './'; @@ -29,7 +29,7 @@ describe('ProductCard', () => { expect(card.find('h2').text()).toEqual('Elastic App Search'); expect(card.find('.productCard__image').prop('src')).toEqual('as.jpg'); - const button = card.find(EuiButton); + const button = card.find(EuiButtonTo); expect(button.prop('to')).toEqual('/app/enterprise_search/app_search'); expect(button.prop('children')).toEqual('Launch App Search'); @@ -47,7 +47,7 @@ describe('ProductCard', () => { expect(card.find('h2').text()).toEqual('Elastic Workplace Search'); expect(card.find('.productCard__image').prop('src')).toEqual('ws.jpg'); - const button = card.find(EuiButton); + const button = card.find(EuiButtonTo); expect(button.prop('to')).toEqual('/app/enterprise_search/workplace_search'); expect(button.prop('children')).toEqual('Launch Workplace Search'); @@ -63,7 +63,7 @@ describe('ProductCard', () => { const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); - const button = card.find(EuiButton); + const button = card.find(EuiButtonTo); expect(button.prop('children')).toEqual('Setup Workplace Search'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index de553acf11f7b..954743f2d0439 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -10,7 +10,7 @@ import { snakeCase } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiCard, EuiTextColor } from '@elastic/eui'; -import { EuiButton } from '../../../shared/react_router_helpers'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry'; import { KibanaLogic } from '../../../shared/kibana'; @@ -63,7 +63,7 @@ export const ProductCard: React.FC = ({ product, image }) => { paddingSize="l" description={{product.CARD_DESCRIPTION}} footer={ - = ({ product, image }) => { } > {config.host ? LAUNCH_BUTTON_TEXT : SETUP_BUTTON_TEXT} - + } /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx index 2a0e2ffc34f3f..bb5d7ab570a08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; -import { EuiPanel } from '../../../shared/react_router_helpers'; +import { EuiPanelTo } from '../../../shared/react_router_helpers'; import CtaImage from './assets/getting_started.png'; import './setup_guide_cta.scss'; export const SetupGuideCta: React.FC = () => ( - + @@ -34,5 +34,5 @@ export const SetupGuideCta: React.FC = () => ( - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index b92a5bbf1c64e..a04357816886c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -9,7 +9,7 @@ import { useValues } from 'kea'; import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton } from '../react_router_helpers'; +import { EuiButtonTo } from '../react_router_helpers'; import { KibanaLogic } from '../../shared/kibana'; import './error_state_prompt.scss'; @@ -90,12 +90,12 @@ export const ErrorStatePrompt: React.FC = () => { } actions={ - + - + } /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx index 9eaa2ba4c4d6f..a7cc21fa63f42 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { EuiLink as EuiLinkExternal } from '@elastic/eui'; -import { EuiLink } from '../react_router_helpers'; +import { EuiLink } from '@elastic/eui'; +import { EuiLinkTo } from '../react_router_helpers'; import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN } from '../../../../common/constants'; import { SideNav, SideNavLink, SideNavItem } from './'; @@ -42,7 +42,7 @@ describe('SideNavLink', () => { const wrapper = shallow(Link); expect(wrapper.type()).toEqual('li'); - expect(wrapper.find(EuiLink)).toHaveLength(1); + expect(wrapper.find(EuiLinkTo)).toHaveLength(1); expect(wrapper.find('.enterpriseSearchNavLinks__item')).toHaveLength(1); }); @@ -52,7 +52,7 @@ describe('SideNavLink', () => { Link ); - const externalLink = wrapper.find(EuiLinkExternal); + const externalLink = wrapper.find(EuiLink); expect(externalLink).toHaveLength(1); expect(externalLink.prop('href')).toEqual('http://website.com'); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx index c75a48d5af41d..8da8f45757961 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -9,8 +9,8 @@ import { useLocation } from 'react-router-dom'; import classNames from 'classnames'; import { i18n } from '@kbn/i18n'; -import { EuiIcon, EuiTitle, EuiText, EuiLink as EuiLinkExternal } from '@elastic/eui'; // TODO: Remove EuiLinkExternal after full Kibana transition -import { EuiLink } from '../react_router_helpers'; +import { EuiIcon, EuiTitle, EuiText, EuiLink } from '@elastic/eui'; // TODO: Remove EuiLink after full Kibana transition +import { EuiLinkTo } from '../react_router_helpers'; import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../common/constants'; import { stripTrailingSlash } from '../../../../common/strip_slashes'; @@ -96,19 +96,14 @@ export const SideNavLink: React.FC = ({ return (
  • {isExternal ? ( - + // eslint-disable-next-line @elastic/eui/href-or-on-click + {children} - + ) : ( - + {children} - + )} {subNav &&
      {subNav}
    }
  • diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx index 05374cb5f0274..d0140b8730229 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx @@ -13,7 +13,7 @@ import { EuiTitle, EuiFlexGroup, EuiFlexItem, - EuiButton as EuiButtonExternal, + EuiButton, } from '@elastic/eui'; import { @@ -22,7 +22,7 @@ import { LICENSED_SUPPORT_URL, } from '../../../../common/constants'; -import { EuiButton } from '../react_router_helpers'; +import { EuiButtonTo } from '../react_router_helpers'; import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; import { LicensingLogic } from '../licensing'; @@ -89,18 +89,18 @@ export const NotFound: React.FC = ({ product = {} }) => { actions={ - + {i18n.translate('xpack.enterpriseSearch.notFound.action1', { defaultMessage: 'Back to your dashboard', })} - + - + {i18n.translate('xpack.enterpriseSearch.notFound.action2', { defaultMessage: 'Contact support', })} - + } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx similarity index 78% rename from x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx index 3a4585b6d9a71..37784fbf4ffb5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx @@ -12,7 +12,7 @@ import { EuiLink, EuiButton, EuiPanel } from '@elastic/eui'; import { mockKibanaValues, mockHistory } from '../../__mocks__'; -import { EuiReactRouterLink, EuiReactRouterButton, EuiReactRouterPanel } from './eui_link'; +import { EuiLinkTo, EuiButtonTo, EuiPanelTo } from './eui_components'; describe('EUI & React Router Component Helpers', () => { beforeEach(() => { @@ -20,26 +20,26 @@ describe('EUI & React Router Component Helpers', () => { }); it('renders an EuiLink', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiLink)).toHaveLength(1); }); it('renders an EuiButton', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiButton)).toHaveLength(1); }); it('renders an EuiPanel', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(EuiPanel)).toHaveLength(1); expect(wrapper.find(EuiPanel).prop('paddingSize')).toEqual('l'); }); it('passes down all ...rest props', () => { - const wrapper = shallow(); + const wrapper = shallow(); const link = wrapper.find(EuiLink); expect(link.prop('external')).toEqual(true); @@ -47,7 +47,7 @@ describe('EUI & React Router Component Helpers', () => { }); it('renders with the correct href and onClick props', () => { - const wrapper = mount(); + const wrapper = mount(); const link = wrapper.find(EuiLink); expect(link.prop('onClick')).toBeInstanceOf(Function); @@ -56,7 +56,7 @@ describe('EUI & React Router Component Helpers', () => { }); it('renders with the correct non-basenamed href when shouldNotCreateHref is passed', () => { - const wrapper = mount(); + const wrapper = mount(); const link = wrapper.find(EuiLink); expect(link.prop('href')).toEqual('/foo/bar'); @@ -65,7 +65,7 @@ describe('EUI & React Router Component Helpers', () => { describe('onClick', () => { it('prevents default navigation and uses React Router history', () => { - const wrapper = mount(); + const wrapper = mount(); const simulatedEvent = { button: 0, @@ -79,7 +79,7 @@ describe('EUI & React Router Component Helpers', () => { }); it('does not prevent default browser behavior on new tab/window clicks', () => { - const wrapper = mount(); + const wrapper = mount(); const simulatedEvent = { shiftKey: true, @@ -92,7 +92,7 @@ describe('EUI & React Router Component Helpers', () => { it('calls inherited onClick actions in addition to default navigation', () => { const customOnClick = jest.fn(); // Can be anything from telemetry to a state reset - const wrapper = mount(); + const wrapper = mount(); wrapper.find(EuiLink).simulate('click', { shiftKey: true }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx similarity index 73% rename from x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx rename to x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx index 61f6b31d3e2e9..56beed8780707 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx @@ -20,7 +20,7 @@ import { letBrowserHandleEvent, createHref } from './'; * https://github.com/elastic/eui/blob/master/wiki/react-router.md#react-router-51 */ -interface EuiReactRouterProps { +interface ReactRouterProps { to: string; onClick?(): void; // Used to navigate outside of the React Router plugin basename but still within Kibana, @@ -28,7 +28,7 @@ interface EuiReactRouterProps { shouldNotCreateHref?: boolean; } -export const EuiReactRouterHelper: React.FC = ({ +export const ReactRouterHelper: React.FC = ({ to, onClick, shouldNotCreateHref, @@ -59,38 +59,38 @@ export const EuiReactRouterHelper: React.FC = ({ * Component helpers */ -type EuiReactRouterLinkProps = EuiLinkAnchorProps & EuiReactRouterProps; -export const EuiReactRouterLink: React.FC = ({ +type ReactRouterEuiLinkProps = ReactRouterProps & EuiLinkAnchorProps; +export const EuiLinkTo: React.FC = ({ to, onClick, shouldNotCreateHref, ...rest }) => ( - + - + ); -type EuiReactRouterButtonProps = EuiButtonProps & EuiReactRouterProps; -export const EuiReactRouterButton: React.FC = ({ +type ReactRouterEuiButtonProps = ReactRouterProps & EuiButtonProps; +export const EuiButtonTo: React.FC = ({ to, onClick, shouldNotCreateHref, ...rest }) => ( - + - + ); -type EuiReactRouterPanelProps = EuiPanelProps & EuiReactRouterProps; -export const EuiReactRouterPanel: React.FC = ({ +type ReactRouterEuiPanelProps = ReactRouterProps & EuiPanelProps; +export const EuiPanelTo: React.FC = ({ to, onClick, shouldNotCreateHref, ...rest }) => ( - + - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts index 326bfb32e41f4..05a9450e4a348 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts @@ -6,8 +6,4 @@ export { letBrowserHandleEvent } from './link_events'; export { createHref, CreateHrefOptions } from './create_href'; -export { - EuiReactRouterLink as EuiLink, - EuiReactRouterButton as EuiButton, - EuiReactRouterPanel as EuiPanel, -} from './eui_link'; +export { EuiLinkTo, EuiButtonTo, EuiPanelTo } from './eui_components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx index 896b8f8f5b4c7..818d06c55dd12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -23,7 +23,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { EuiLink } from '../../../../shared/react_router_helpers'; +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; import { SOURCE_STATUSES as statuses } from '../../../constants'; import { ContentSourceDetails } from '../../../types'; import { ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, getContentSourcePath } from '../../../routes'; @@ -77,9 +77,9 @@ export const SourceRow: React.FC = ({ const imageClass = classNames('source-row__icon', { 'source-row__icon--loading': isIndexing }); const fixLink = ( - + Fix - + ); const remoteTooltip = ( @@ -159,13 +159,13 @@ export const SourceRow: React.FC = ({ {showFix && {fixLink}} {showDetails && ( - Details - + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx index 11c0430a8b429..c0f8bf57989ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx @@ -26,7 +26,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { EuiButton as EuiLinkButton } from '../../../../shared/react_router_helpers'; +import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { Group } from '../../../types'; import { ORG_SOURCES_PATH } from '../../../routes'; @@ -96,9 +96,9 @@ export const GroupManagerModal: React.FC = ({ const handleSelectAll = () => selectAll(allSelected ? [] : allItems); const sourcesButton = ( - + {ADD_SOURCE_BUTTON_TEXT} - + ); const emptyState = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 5cebb96d00eb9..9d33f810edae6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { EuiTableRow, EuiTableRowCell, EuiIcon } from '@elastic/eui'; import { TruncatedContent } from '../../../../shared/truncate'; -import { EuiLink } from '../../../../shared/react_router_helpers'; +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; import { Group } from '../../../types'; @@ -64,9 +64,9 @@ export const GroupRow: React.FC = ({ - + - +
    {GROUP_UPDATED_TEXT} @@ -93,9 +93,9 @@ export const GroupRow: React.FC = ({ )} - + - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx index cc50c4d0af5c4..85175d156f886 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -27,7 +27,7 @@ import { TableFilters } from './components/table_filters'; import { DEFAULT_META } from '../../../shared/constants'; import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui'; -import { EuiButton as EuiLinkButton } from '../../../shared/react_router_helpers'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; const getSearchResults = jest.fn(); const openNewGroupModal = jest.fn(); @@ -138,7 +138,7 @@ describe('GroupOverview', () => { const action = shallow(); expect(action.find('[data-test-subj="InviteUsersButton"]')).toHaveLength(1); - expect(action.find(EuiLinkButton)).toHaveLength(1); + expect(action.find(EuiButtonTo)).toHaveLength(1); }); it('does not render inviteUsersButton when federated auth', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 4064391c09893..97647f149bc9b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -10,7 +10,7 @@ import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; -import { EuiButton as EuiLinkButton } from '../../../shared/react_router_helpers'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { AppLogic } from '../../app_logic'; @@ -61,7 +61,7 @@ export const Groups: React.FC = () => { if (newGroup && hasMessages) { messages[0].description = ( - { {i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action', { defaultMessage: 'Manage Group', })} - + ); } const clearFilters = hasFiltersSet && ; const inviteUsersButton = !isFederatedAuth ? ( - + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.groups.inviteUsers.action', { defaultMessage: 'Invite users', })} - + ) : null; const headerAction = ( From b819287ce3489283d49def9a74dd2f82ae0b68be Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 18 Nov 2020 16:50:14 -0600 Subject: [PATCH 90/99] [Workplace Search] Update SourceIcon to match latest changes in ent-search (#83714) * Move source icons into subfolder * Copy over new icons * Update SourceIcon to account for full bleed images * Remove unused file * Fix broken icon path --- .../shared/assets/{ => source_icons}/box.svg | 0 .../assets/{ => source_icons}/confluence.svg | 0 .../connection_illustration.svg | 0 .../assets/{ => source_icons}/crawler.svg | 0 .../assets/{ => source_icons}/custom.svg | 0 .../assets/{ => source_icons}/drive.svg | 0 .../assets/{ => source_icons}/dropbox.svg | 0 .../assets/{ => source_icons}/github.svg | 0 .../assets/{ => source_icons}/gmail.svg | 0 .../assets/{ => source_icons}/google.svg | 0 .../{ => source_icons}/google_drive.svg | 0 .../shared/assets/{ => source_icons}/index.ts | 0 .../shared/assets/{ => source_icons}/jira.svg | 0 .../assets/{ => source_icons}/jira_server.svg | 0 .../{ => source_icons}/loading_small.svg | 0 .../assets/{ => source_icons}/office365.svg | 0 .../assets/{ => source_icons}/one_drive.svg | 0 .../assets/{ => source_icons}/outlook.svg | 0 .../assets/{ => source_icons}/people.svg | 0 .../assets/{ => source_icons}/salesforce.svg | 0 .../assets/{ => source_icons}/service_now.svg | 0 .../{ => source_icons}/share_circle.svg | 0 .../assets/{ => source_icons}/share_point.svg | 0 .../assets/{ => source_icons}/slack.svg | 0 .../assets/{ => source_icons}/zendesk.svg | 0 .../assets/sources_full_bleed/confluence.svg | 1 + .../assets/sources_full_bleed/custom.svg | 1 + .../assets/sources_full_bleed/dropbox.svg | 1 + .../assets/sources_full_bleed/github.svg | 1 + .../assets/sources_full_bleed/gmail.svg | 1 + .../sources_full_bleed/google_drive.svg | 1 + .../shared/assets/sources_full_bleed/index.ts | 42 +++++++++++++++++++ .../shared/assets/sources_full_bleed/jira.svg | 1 + .../assets/sources_full_bleed/jira_server.svg | 1 + .../assets/sources_full_bleed/onedrive.svg | 1 + .../assets/sources_full_bleed/salesforce.svg | 1 + .../assets/sources_full_bleed/servicenow.svg | 1 + .../assets/sources_full_bleed/sharepoint.svg | 1 + .../assets/sources_full_bleed/slack.svg | 1 + .../assets/sources_full_bleed/zendesk.svg | 1 + .../shared/source_icon/source_icon.tsx | 17 +++++--- .../views/overview/onboarding_steps.tsx | 2 +- 42 files changed, 69 insertions(+), 6 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/box.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/confluence.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/connection_illustration.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/crawler.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/custom.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/drive.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/dropbox.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/github.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/gmail.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/google.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/google_drive.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/index.ts (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/jira.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/jira_server.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/loading_small.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/office365.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/one_drive.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/outlook.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/people.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/salesforce.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/service_now.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/share_circle.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/share_point.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/slack.svg (100%) rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{ => source_icons}/zendesk.svg (100%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/onedrive.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/servicenow.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/sharepoint.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/box.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/box.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/box.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/confluence.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/connection_illustration.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/connection_illustration.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/connection_illustration.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/crawler.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/custom.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/drive.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/dropbox.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/github.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/gmail.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/google_drive.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/jira_server.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/loading_small.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/loading_small.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/loading_small.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/office365.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/one_drive.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/outlook.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/people.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/salesforce.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/service_now.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_circle.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_circle.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/share_point.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/slack.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/zendesk.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg new file mode 100644 index 0000000000000..7aac36a6fe3c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg new file mode 100644 index 0000000000000..cc07fbbc50877 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg new file mode 100644 index 0000000000000..01e5a7735de12 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg new file mode 100644 index 0000000000000..aa9c3e5b45146 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg new file mode 100644 index 0000000000000..98d418244c22f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg new file mode 100644 index 0000000000000..6541b3f9e753f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts new file mode 100644 index 0000000000000..e749fb9482758 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import confluence from './confluence.svg'; +import custom from './custom.svg'; +import dropbox from './dropbox.svg'; +import github from './github.svg'; +import gmail from './gmail.svg'; +import googleDrive from './google_drive.svg'; +import jira from './jira.svg'; +import jiraServer from './jira_server.svg'; +import oneDrive from './onedrive.svg'; +import salesforce from './salesforce.svg'; +import serviceNow from './servicenow.svg'; +import sharePoint from './sharepoint.svg'; +import slack from './slack.svg'; +import zendesk from './zendesk.svg'; + +export const imagesFull = { + confluence, + confluenceCloud: confluence, + confluenceServer: confluence, + custom, + dropbox, + github, + githubEnterpriseServer: github, + gmail, + googleDrive, + jira, + jiraServer, + jiraCloud: jira, + oneDrive, + salesforce, + salesforceSandbox: salesforce, + serviceNow, + sharePoint, + slack, + zendesk, +} as { [key: string]: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg new file mode 100644 index 0000000000000..c12e55798d889 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg new file mode 100644 index 0000000000000..4dfd0fd910079 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/onedrive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/onedrive.svg new file mode 100644 index 0000000000000..c390dff1e418f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/onedrive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg new file mode 100644 index 0000000000000..ef6d552949424 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/servicenow.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/servicenow.svg new file mode 100644 index 0000000000000..6388ec44d21d7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/servicenow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/sharepoint.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/sharepoint.svg new file mode 100644 index 0000000000000..aebfd7a8e49c0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/sharepoint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg new file mode 100644 index 0000000000000..8f6fc0c987eaa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg new file mode 100644 index 0000000000000..8afd143dd9a7c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx index 857b6f3aaf997..dec9e25fe2440 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx @@ -6,17 +6,17 @@ import React from 'react'; -// Prefer importing entire lodash library, e.g. import { get } from "lodash" -// eslint-disable-next-line no-restricted-imports -import _camelCase from 'lodash/camelCase'; +import { camelCase } from 'lodash'; -import { images } from '../assets'; +import { images } from '../assets/source_icons'; +import { imagesFull } from '../assets/sources_full_bleed'; interface SourceIconProps { serviceType: string; name: string; className?: string; wrapped?: boolean; + fullBleed?: boolean; } export const SourceIcon: React.FC = ({ @@ -24,8 +24,15 @@ export const SourceIcon: React.FC = ({ serviceType, className, wrapped, + fullBleed = false, }) => { - const icon = {name}; + const icon = ( + {name} + ); return wrapped ? (
    {icon} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index 7251461b848a4..ed5136a6f7a4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -21,7 +21,7 @@ import { EuiButtonEmptyProps, EuiLinkProps, } from '@elastic/eui'; -import sharedSourcesIcon from '../../components/shared/assets/share_circle.svg'; +import sharedSourcesIcon from '../../components/shared/assets/source_icons/share_circle.svg'; import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; From 71f972dc837d6186ad2b8157af8fb178d86e6a96 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 18 Nov 2020 15:53:45 -0800 Subject: [PATCH 91/99] [App Search] Engine overview layout stub (#83504) * Set up Overview file * Finish Overview page logic, stub out empty/metric views * Stub in basic empty engine overview - Minus document creation button & API code example * Stub out EngineOverviewMetrics and unavailable empty prompt * Stub out EngineOverMetrics components (stats, charts, logs) * [Refactor] Pull out some document creation i18n strings to constants - They're repeated/reused by the DocumentCreationPopover component * PR feedback: Drop the regex * PR feedback: RecentLogs -> RecentApiLogs * PR feedback: Copy * PR feedback: Copy, sentence-casing --- .../components/analytics/constants.ts | 27 +++++ .../components/api_logs/constants.ts | 12 +++ .../document_creation/constants.tsx | 54 ++++++++++ .../app_search/components/engine/constants.ts | 4 - .../components/engine/engine_nav.tsx | 2 +- .../components/engine/engine_router.test.tsx | 3 +- .../components/engine/engine_router.tsx | 5 +- .../engine_overview/components/index.ts | 10 ++ .../components/recent_api_logs.test.tsx | 32 ++++++ .../components/recent_api_logs.tsx | 50 ++++++++++ .../components/total_charts.test.tsx | 46 +++++++++ .../components/total_charts.tsx | 99 +++++++++++++++++++ .../components/total_stats.test.tsx | 51 ++++++++++ .../components/total_stats.tsx | 37 +++++++ .../components/unavailable_prompt.test.tsx | 18 ++++ .../components/unavailable_prompt.tsx | 30 ++++++ .../components/engine_overview/constants.ts | 27 +++++ .../engine_overview/engine_overview.test.tsx | 80 +++++++++++++++ .../engine_overview/engine_overview.tsx | 44 +++++++++ .../engine_overview_empty.test.tsx | 40 ++++++++ .../engine_overview/engine_overview_empty.tsx | 98 ++++++++++++++++++ .../engine_overview_metrics.test.tsx | 34 +++++++ .../engine_overview_metrics.tsx | 44 +++++++++ .../components/engine_overview/index.ts | 2 + 24 files changed, 841 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts new file mode 100644 index 0000000000000..51ae11ad2ab82 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const TOTAL_DOCUMENTS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.totalDocuments', + { defaultMessage: 'Total documents' } +); + +export const TOTAL_API_OPERATIONS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.totalApiOperations', + { defaultMessage: 'Total API operations' } +); + +export const TOTAL_QUERIES = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.totalQueries', + { defaultMessage: 'Total queries' } +); + +export const TOTAL_CLICKS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.totalClicks', + { defaultMessage: 'Total clicks' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts new file mode 100644 index 0000000000000..6fd60b7a34ebc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const RECENT_API_EVENTS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.apiLogs.recent', + { defaultMessage: 'Recent API events' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx new file mode 100644 index 0000000000000..736ef09fa6cf3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode, EuiLink } from '@elastic/eui'; + +import { DOCS_PREFIX } from '../../routes'; + +export const DOCUMENT_CREATION_DESCRIPTION = ( + .json, + postCode: POST, + documentsApiLink: ( + + documents API + + ), + apiStrong: Indexing by API, + }} + /> +); + +export const DOCUMENT_API_INDEXING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.documentCreation.api.title', + { defaultMessage: 'Indexing by API' } +); + +export const DOCUMENT_API_INDEXING_DESCRIPTION = ( + + documents API + + ), + clientLibrariesLink: ( + + client libraries + + ), + }} + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts index 3c963e415f33b..9ce524038075b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts @@ -9,10 +9,6 @@ import { i18n } from '@kbn/i18n'; // TODO: It's very likely that we'll move these i18n constants to their respective component // folders once those are migrated over. This is a temporary way of DRYing them out for now. -export const OVERVIEW_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.overview.title', - { defaultMessage: 'Overview' } -); export const ANALYTICS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.title', { defaultMessage: 'Analytics' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 77aca8a71994d..a7ac6f203b1f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -28,8 +28,8 @@ import { } from '../../routes'; import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { ENGINES_TITLE } from '../engines'; +import { OVERVIEW_TITLE } from '../engine_overview'; import { - OVERVIEW_TITLE, ANALYTICS_TITLE, DOCUMENTS_TITLE, SCHEMA_TITLE, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index 8f067754c48a0..e8609c169855b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -18,6 +18,7 @@ jest.mock('../../../shared/flash_messages', () => ({ import { setQueuedErrorMessage } from '../../../shared/flash_messages'; import { Loading } from '../../../shared/loading'; +import { EngineOverview } from '../engine_overview'; import { EngineRouter } from './'; @@ -71,7 +72,7 @@ describe('EngineRouter', () => { const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="EngineOverviewTODO"]')).toHaveLength(1); + expect(wrapper.find(EngineOverview)).toHaveLength(1); }); it('renders an analytics view', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 9833305c438c1..f586106924f2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -31,8 +31,8 @@ import { // ENGINE_API_LOGS_PATH, } from '../../routes'; import { ENGINES_TITLE } from '../engines'; +import { OVERVIEW_TITLE } from '../engine_overview'; import { - OVERVIEW_TITLE, ANALYTICS_TITLE, // DOCUMENTS_TITLE, // SCHEMA_TITLE, @@ -46,6 +46,7 @@ import { } from './constants'; import { Loading } from '../../../shared/loading'; +import { EngineOverview } from '../engine_overview'; import { EngineLogic } from './'; @@ -100,7 +101,7 @@ export const EngineRouter: React.FC = () => { )} -
    Overview
    +
    ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts new file mode 100644 index 0000000000000..11e7b2a5fba97 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { UnavailablePrompt } from './unavailable_prompt'; +export { TotalStats } from './total_stats'; +export { TotalCharts } from './total_charts'; +export { RecentApiLogs } from './recent_api_logs'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx new file mode 100644 index 0000000000000..725c89894b80b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButton } from '../../../../shared/react_router_helpers'; + +import { RecentApiLogs } from './recent_api_logs'; + +describe('RecentApiLogs', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + jest.clearAllMocks(); + setMockValues({ + engineName: 'some-engine', + }); + wrapper = shallow(); + }); + + it('renders the recent API logs table', () => { + expect(wrapper.find('h2').text()).toEqual('Recent API events'); + expect(wrapper.find(EuiButton).prop('to')).toEqual('/engines/some-engine/api-logs'); + // TODO: expect(wrapper.find(ApiLogsTable)).toHaveLength(1) + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx new file mode 100644 index 0000000000000..05eb731116884 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; + +import { + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageContentBody, + EuiTitle, +} from '@elastic/eui'; + +import { EuiButton } from '../../../../shared/react_router_helpers'; + +import { ENGINE_API_LOGS_PATH, getEngineRoute } from '../../../routes'; +import { RECENT_API_EVENTS } from '../../api_logs/constants'; +import { VIEW_API_LOGS } from '../constants'; + +import { EngineLogic } from '../../engine'; + +export const RecentApiLogs: React.FC = () => { + const { engineName } = useValues(EngineLogic); + const engineRoute = getEngineRoute(engineName); + + return ( + + + + +

    {RECENT_API_EVENTS}

    +
    +
    + + + {VIEW_API_LOGS} + + +
    + + TODO: API Logs Table + {/* */} + +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx new file mode 100644 index 0000000000000..a56b41dfb0503 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButton } from '../../../../shared/react_router_helpers'; + +import { TotalCharts } from './total_charts'; + +describe('TotalCharts', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + jest.clearAllMocks(); + setMockValues({ + engineName: 'some-engine', + startDate: '1970-01-01', + endDate: '1970-01-08', + queriesPerDay: [0, 1, 2, 3, 5, 10, 50], + operationsPerDay: [0, 0, 0, 0, 0, 0, 0], + }); + wrapper = shallow(); + }); + + it('renders the total queries chart', () => { + const chart = wrapper.find('[data-test-subj="TotalQueriesChart"]'); + + expect(chart.find('h2').text()).toEqual('Total queries'); + expect(chart.find(EuiButton).prop('to')).toEqual('/engines/some-engine/analytics'); + // TODO: find chart component + }); + + it('renders the total API operations chart', () => { + const chart = wrapper.find('[data-test-subj="TotalApiOperationsChart"]'); + + expect(chart.find('h2').text()).toEqual('Total API operations'); + expect(chart.find(EuiButton).prop('to')).toEqual('/engines/some-engine/api-logs'); + // TODO: find chart component + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx new file mode 100644 index 0000000000000..e27fe3cdfc799 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageContentBody, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { EuiButton } from '../../../../shared/react_router_helpers'; + +import { ENGINE_ANALYTICS_PATH, ENGINE_API_LOGS_PATH, getEngineRoute } from '../../../routes'; +import { TOTAL_QUERIES, TOTAL_API_OPERATIONS } from '../../analytics/constants'; +import { VIEW_ANALYTICS, VIEW_API_LOGS, LAST_7_DAYS } from '../constants'; + +import { EngineLogic } from '../../engine'; +import { EngineOverviewLogic } from '../'; + +export const TotalCharts: React.FC = () => { + const { engineName } = useValues(EngineLogic); + const engineRoute = getEngineRoute(engineName); + + const { + // startDate, + // endDate, + // queriesPerDay, + // operationsPerDay, + } = useValues(EngineOverviewLogic); + + return ( + + + + + + +

    {TOTAL_QUERIES}

    +
    + + {LAST_7_DAYS} + +
    + + + {VIEW_ANALYTICS} + + +
    + + TODO: Analytics chart + {/* */} + +
    +
    + + + + + +

    {TOTAL_API_OPERATIONS}

    +
    + + {LAST_7_DAYS} + +
    + + + {VIEW_API_LOGS} + + +
    + + TODO: API Logs chart + {/* */} + +
    +
    +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx new file mode 100644 index 0000000000000..6cb47e8b419f3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { EuiStat } from '@elastic/eui'; + +import { TotalStats } from './total_stats'; + +describe('TotalStats', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + jest.clearAllMocks(); + setMockValues({ + totalQueries: 11, + documentCount: 22, + totalClicks: 33, + }); + wrapper = shallow(); + }); + + it('renders the total queries stat', () => { + expect(wrapper.find('[data-test-subj="TotalQueriesCard"]')).toHaveLength(1); + + const card = wrapper.find(EuiStat).at(0); + expect(card.prop('title')).toEqual(11); + expect(card.prop('description')).toEqual('Total queries'); + }); + + it('renders the total documents stat', () => { + expect(wrapper.find('[data-test-subj="TotalDocumentsCard"]')).toHaveLength(1); + + const card = wrapper.find(EuiStat).at(1); + expect(card.prop('title')).toEqual(22); + expect(card.prop('description')).toEqual('Total documents'); + }); + + it('renders the total clicks stat', () => { + expect(wrapper.find('[data-test-subj="TotalClicksCard"]')).toHaveLength(1); + + const card = wrapper.find(EuiStat).at(2); + expect(card.prop('title')).toEqual(33); + expect(card.prop('description')).toEqual('Total clicks'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx new file mode 100644 index 0000000000000..a27142938f558 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui'; + +import { TOTAL_QUERIES, TOTAL_DOCUMENTS, TOTAL_CLICKS } from '../../analytics/constants'; + +import { EngineOverviewLogic } from '../'; + +export const TotalStats: React.FC = () => { + const { totalQueries, documentCount, totalClicks } = useValues(EngineOverviewLogic); + + return ( + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx new file mode 100644 index 0000000000000..3ddfd14b0eb0c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { UnavailablePrompt } from './unavailable_prompt'; + +describe('UnavailablePrompt', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx new file mode 100644 index 0000000000000..e9cc6e2f05bf3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +export const UnavailablePrompt: React.FC = () => ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.unavailableTitle', { + defaultMessage: 'Dashboard metrics are currently unavailable', + })} + + } + body={ +

    + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.unavailableBody', { + defaultMessage: 'Please try again in a few minutes.', + })} +

    + } + /> +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/constants.ts new file mode 100644 index 0000000000000..797811ec6cde8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/constants.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const OVERVIEW_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.overview.title', + { defaultMessage: 'Overview' } +); + +export const VIEW_ANALYTICS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.overview.analyticsLink', + { defaultMessage: 'View analytics' } +); + +export const VIEW_API_LOGS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.overview.apiLogsLink', + { defaultMessage: 'View API logs' } +); + +export const LAST_7_DAYS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.overview.chartDuration', + { defaultMessage: 'Last 7 days' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx new file mode 100644 index 0000000000000..196fb2ca2bf13 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Loading } from '../../../shared/loading'; +import { EmptyEngineOverview } from './engine_overview_empty'; +import { EngineOverviewMetrics } from './engine_overview_metrics'; +import { EngineOverview } from './'; + +describe('EngineOverview', () => { + const values = { + dataLoading: false, + documentCount: 0, + myRole: {}, + isMetaEngine: false, + }; + const actions = { + pollForOverviewMetrics: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="EngineOverview"]')).toHaveLength(1); + }); + + it('initializes data on mount', () => { + shallow(); + expect(actions.pollForOverviewMetrics).toHaveBeenCalledTimes(1); + }); + + it('renders a loading component if async data is still loading', () => { + setMockValues({ ...values, dataLoading: true }); + const wrapper = shallow(); + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + describe('EmptyEngineOverview', () => { + it('renders when the engine has no documents & the user can add documents', () => { + const myRole = { canManageEngineDocuments: true, canViewEngineCredentials: true }; + setMockValues({ ...values, myRole, documentCount: 0 }); + const wrapper = shallow(); + expect(wrapper.find(EmptyEngineOverview)).toHaveLength(1); + }); + }); + + describe('EngineOverviewMetrics', () => { + it('renders when the engine has documents', () => { + setMockValues({ ...values, documentCount: 1 }); + const wrapper = shallow(); + expect(wrapper.find(EngineOverviewMetrics)).toHaveLength(1); + }); + + it('renders when the user does not have the ability to add documents', () => { + const myRole = { canManageEngineDocuments: false, canViewEngineCredentials: false }; + setMockValues({ ...values, myRole }); + const wrapper = shallow(); + expect(wrapper.find(EngineOverviewMetrics)).toHaveLength(1); + }); + + it('always renders for meta engines', () => { + setMockValues({ ...values, isMetaEngine: true }); + const wrapper = shallow(); + expect(wrapper.find(EngineOverviewMetrics)).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx new file mode 100644 index 0000000000000..dd43bc67b3e88 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useActions, useValues } from 'kea'; + +import { AppLogic } from '../../app_logic'; +import { EngineLogic } from '../engine'; +import { Loading } from '../../../shared/loading'; + +import { EngineOverviewLogic } from './'; +import { EmptyEngineOverview } from './engine_overview_empty'; +import { EngineOverviewMetrics } from './engine_overview_metrics'; + +export const EngineOverview: React.FC = () => { + const { + myRole: { canManageEngineDocuments, canViewEngineCredentials }, + } = useValues(AppLogic); + const { isMetaEngine } = useValues(EngineLogic); + + const { pollForOverviewMetrics } = useActions(EngineOverviewLogic); + const { dataLoading, documentCount } = useValues(EngineOverviewLogic); + + useEffect(() => { + pollForOverviewMetrics(); + }, []); + + if (dataLoading) { + return ; + } + + const engineHasDocuments = documentCount > 0; + const canAddDocuments = canManageEngineDocuments && canViewEngineCredentials; + const showEngineOverview = engineHasDocuments || !canAddDocuments || isMetaEngine; + + return ( +
    + {showEngineOverview ? : } +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx new file mode 100644 index 0000000000000..8ebe09820a67e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/enterprise_search_url.mock'; +import { setMockValues } from '../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { EuiButton } from '@elastic/eui'; + +import { CURRENT_MAJOR_VERSION } from '../../../../../common/version'; + +import { EmptyEngineOverview } from './engine_overview_empty'; + +describe('EmptyEngineOverview', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + jest.clearAllMocks(); + setMockValues({ + engineName: 'empty-engine', + }); + wrapper = shallow(); + }); + + it('renders', () => { + expect(wrapper.find('h1').text()).toEqual('Engine setup'); + expect(wrapper.find('h2').text()).toEqual('Setting up the “empty-engine” engine'); + expect(wrapper.find('h3').text()).toEqual('Indexing by API'); + }); + + it('renders correctly versioned documentation URLs', () => { + expect(wrapper.find(EuiButton).prop('href')).toEqual( + `https://www.elastic.co/guide/en/app-search/${CURRENT_MAJOR_VERSION}/index.html` + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx new file mode 100644 index 0000000000000..f2bf5a54f810c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentBody, + EuiTitle, + EuiText, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; + +import { EngineLogic } from '../engine'; + +import { DOCS_PREFIX } from '../../routes'; +import { + DOCUMENT_CREATION_DESCRIPTION, + DOCUMENT_API_INDEXING_TITLE, + DOCUMENT_API_INDEXING_DESCRIPTION, +} from '../document_creation/constants'; +// TODO +// import { DocumentCreationButtons, CodeExample } from '../document_creation' + +export const EmptyEngineOverview: React.FC = () => { + const { engineName } = useValues(EngineLogic); + + return ( + <> + + + +

    + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.empty.heading', { + defaultMessage: 'Engine setup', + })} +

    +
    +
    + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.overview.empty.headingAction', + { defaultMessage: 'View documentation' } + )} + + +
    + + + +

    + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.empty.subheading', { + defaultMessage: 'Setting up the “{engineName}” engine', + values: { engineName }, + })} +

    +
    +
    + + +

    {DOCUMENT_CREATION_DESCRIPTION}

    +
    + + {/* TODO: */} +
    + + + +

    {DOCUMENT_API_INDEXING_TITLE}

    +
    +
    + + +

    {DOCUMENT_API_INDEXING_DESCRIPTION}

    +

    + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.empty.apiExample', { + defaultMessage: + 'To see the API in action, you can experiment with the example request below using a command line or a client library.', + })} +

    +
    + + {/* */} +
    +
    + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx new file mode 100644 index 0000000000000..8250446e231b3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues } from '../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; +import { EngineOverviewMetrics } from './engine_overview_metrics'; + +describe('EngineOverviewMetrics', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find('h1').text()).toEqual('Engine overview'); + }); + + it('renders an unavailable prompt if engine data is still indexing', () => { + setMockValues({ apiLogsUnavailable: true }); + const wrapper = shallow(); + expect(wrapper.find(UnavailablePrompt)).toHaveLength(1); + }); + + it('renders total stats, charts, and recent logs when metrics are available', () => { + setMockValues({ apiLogsUnavailable: false }); + const wrapper = shallow(); + expect(wrapper.find(TotalStats)).toHaveLength(1); + expect(wrapper.find(TotalCharts)).toHaveLength(1); + expect(wrapper.find(RecentApiLogs)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx new file mode 100644 index 0000000000000..9630f6fa2f81d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { EuiPageHeader, EuiTitle, EuiSpacer } from '@elastic/eui'; + +import { EngineOverviewLogic } from './'; + +import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; + +export const EngineOverviewMetrics: React.FC = () => { + const { apiLogsUnavailable } = useValues(EngineOverviewLogic); + + return ( + <> + + +

    + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.heading', { + defaultMessage: 'Engine overview', + })} +

    +
    +
    + {apiLogsUnavailable ? ( + + ) : ( + <> + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts index fcd92ba6a338c..82c5d7dc8e60a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts @@ -5,3 +5,5 @@ */ export { EngineOverviewLogic } from './engine_overview_logic'; +export { EngineOverview } from './engine_overview'; +export { OVERVIEW_TITLE } from './constants'; From f2d97a9fe2064eb47c4e871a41cd07e7cbae9258 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 19 Nov 2020 00:08:50 +0000 Subject: [PATCH 92/99] chore(NA): update lmdb store to v0.8.15 (#83726) * chore(NA): upgrade lmdb-store to v0.8.15 * chore(NA): remove unused ts-error statements --- package.json | 2 +- packages/kbn-optimizer/src/node/cache.ts | 4 ---- yarn.lock | 25 ++++++++++-------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 87e51abe49be3..23f7a0b430654 100644 --- a/package.json +++ b/package.json @@ -723,7 +723,7 @@ "less": "npm:@elastic/less@2.7.3-kibana", "license-checker": "^16.0.0", "listr": "^0.14.1", - "lmdb-store": "^0.8.11", + "lmdb-store": "^0.8.15", "load-grunt-config": "^3.0.1", "loader-utils": "^1.2.3", "log-symbols": "^2.2.0", diff --git a/packages/kbn-optimizer/src/node/cache.ts b/packages/kbn-optimizer/src/node/cache.ts index dc96bf47fafcf..a73dba5b16469 100644 --- a/packages/kbn-optimizer/src/node/cache.ts +++ b/packages/kbn-optimizer/src/node/cache.ts @@ -49,23 +49,19 @@ export class Cache { this.codes = LmdbStore.open({ name: 'codes', path: CACHE_DIR, - // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 maxReaders: 500, }); - // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 this.atimes = this.codes.openDB({ name: 'atimes', encoding: 'string', }); - // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 this.mtimes = this.codes.openDB({ name: 'mtimes', encoding: 'string', }); - // @ts-expect-error See https://github.com/DoctorEvidence/lmdb-store/pull/18 this.sourceMaps = this.codes.openDB({ name: 'sourceMaps', encoding: 'msgpack', diff --git a/yarn.lock b/yarn.lock index 2a82e7024a895..9be39ea18e3d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18798,24 +18798,24 @@ livereload-js@^2.3.0: resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c" integrity sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw== -lmdb-store-0.9@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/lmdb-store-0.9/-/lmdb-store-0.9-0.7.2.tgz#45b907a46d0a676fee955629bd2f70f06efb26bb" - integrity sha512-/MO8G6p4l7ku1ltCCdE/2ZOtSQBSM0B02vIemMHjoKgjE/fooanJYXIFwtZYM5r/hBDxmO+L3q5ASAXbfHQ6pQ== +lmdb-store-0.9@0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/lmdb-store-0.9/-/lmdb-store-0.9-0.7.3.tgz#c2cb27dfa916ab966cceed692c67e4236813104a" + integrity sha512-t8iCnN6T3NZPFelPmjYIjCg+nhGbOdc0xcHCG40v01AWRTN49OINSt2k/u+16/2/HrI+b6Ssb8WByXUhbyHz6w== dependencies: fs-extra "^9.0.1" msgpackr "^0.5.3" nan "^2.14.1" node-gyp-build "^4.2.3" - weak-lru-cache "^0.2.0" + weak-lru-cache "^0.3.9" -lmdb-store@^0.8.11: - version "0.8.11" - resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.8.11.tgz#7f7c756a115ceab32c51c0948444bfd5d1716ab3" - integrity sha512-CFgxh2/TL1NXyJ8FQPXY50O/gADxih7Gx0RjKRScGlyxihqSLd/fGzMkbvDdeAOAS8bsnYpLojAMTSeNiwN8dQ== +lmdb-store@^0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/lmdb-store/-/lmdb-store-0.8.15.tgz#4efb0341c2df505dd6f3a7f26f834f0a142a80a2" + integrity sha512-4Q0WZh2FmcJC6esZRUWMfkCmNiz0WU9cOgrxt97ZMTnVfHyOdZhtrt0oOF5EQPfetxxJf/BorKY28aX92R6G6g== dependencies: fs-extra "^9.0.1" - lmdb-store-0.9 "0.7.2" + lmdb-store-0.9 "0.7.3" msgpackr "^0.5.4" nan "^2.14.1" node-gyp-build "^4.2.3" @@ -29141,11 +29141,6 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -weak-lru-cache@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-0.2.0.tgz#447379ccff6dfda1b7a9566c9ef168260be859d1" - integrity sha512-M1l5CzKvM7maa7tCbtL0NW6sOnp8gqup853+9Aq7GL0XNWKNnFOkeE3v3Z5X2IeMzedPwQyPbi4RlFvD6rxs7A== - weak-lru-cache@^0.3.9: version "0.3.9" resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-0.3.9.tgz#9e56920d4115e8542625d8ef8cc278cbd97f7624" From a04cb37f2b510de522e9284ff71cfeab00f7c06e Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 18 Nov 2020 18:14:22 -0700 Subject: [PATCH 93/99] [Metrics UI] Optimizations for Snapshot and Inventory Metadata (#83596) * [Metrics UI] Add time range to inventory metadata request * Adding optimizations for snapshot request * Adding sorting to dataset request * Only query inventory metadata for AWS * moving check inside getCloudMetadata * removing unused deps --- .../common/http_api/inventory_meta_api.ts | 1 + .../inventory_view/components/layout.tsx | 2 +- .../components/toolbars/toolbar.tsx | 5 ++-- .../hooks/use_inventory_meta.ts | 7 +++++- .../inventory_view/hooks/use_snaphot.ts | 2 +- .../server/routes/inventory_metadata/index.ts | 6 +++-- .../lib/get_cloud_metadata.ts | 24 +++++++++++++++++-- .../lib/find_interval_for_metrics.ts | 3 ++- .../lib/get_dataset_for_field.ts | 21 ++++++++++++++-- .../lib/create_timerange_with_interval.ts | 5 +++- .../server/routes/snapshot/lib/get_nodes.ts | 13 +++++----- 11 files changed, 69 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/infra/common/http_api/inventory_meta_api.ts b/x-pack/plugins/infra/common/http_api/inventory_meta_api.ts index 77de515c9cc46..43f3b2037e381 100644 --- a/x-pack/plugins/infra/common/http_api/inventory_meta_api.ts +++ b/x-pack/plugins/infra/common/http_api/inventory_meta_api.ts @@ -21,6 +21,7 @@ export const InventoryMetaResponseRT = rt.type({ export const InventoryMetaRequestRT = rt.type({ sourceId: rt.string, nodeType: ItemTypeRT, + currentTime: rt.number, }); export type InventoryMetaRequest = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 92aa015113b2a..2e5ddab77d374 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -124,7 +124,7 @@ export const Layout = () => { <> - + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx index e9ffc56d8c47f..7bcb1270c30a5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx @@ -54,11 +54,12 @@ const wrapToolbarItems = ( interface Props { nodeType: InventoryItemType; + currentTime: number; } -export const Toolbar = ({ nodeType }: Props) => { +export const Toolbar = ({ nodeType, currentTime }: Props) => { const { sourceId } = useSourceContext(); - const { accounts, regions } = useInventoryMeta(sourceId, nodeType); + const { accounts, regions } = useInventoryMeta(sourceId, nodeType, currentTime); const ToolbarItems = findToolbar(nodeType); return wrapToolbarItems(ToolbarItems, accounts, regions); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts index b038491690a13..01811eb21a110 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts @@ -15,7 +15,11 @@ import { } from '../../../../../common/http_api/inventory_meta_api'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; -export function useInventoryMeta(sourceId: string, nodeType: InventoryItemType) { +export function useInventoryMeta( + sourceId: string, + nodeType: InventoryItemType, + currentTime: number +) { const decodeResponse = (response: any) => { return pipe( InventoryMetaResponseRT.decode(response), @@ -29,6 +33,7 @@ export function useInventoryMeta(sourceId: string, nodeType: InventoryItemType) JSON.stringify({ sourceId, nodeType, + currentTime, }), decodeResponse ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts index 702213516c123..eec46b0486287 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts @@ -44,7 +44,7 @@ export function useSnapshot( interval: '1m', to: currentTime, from: currentTime - 1200 * 1000, - lookbackSize: 20, + lookbackSize: 5, }; const { error, loading, response, makeRequest } = useHTTPRequest( diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts index 8b5271cb960c1..c784aa0f7d20b 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts @@ -33,7 +33,7 @@ export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { }, async (requestContext, request, response) => { try { - const { sourceId, nodeType } = pipe( + const { sourceId, nodeType, currentTime } = pipe( InventoryMetaRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); @@ -42,11 +42,13 @@ export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { requestContext.core.savedObjects.client, sourceId ); + const awsMetadata = await getCloudMetadata( framework, requestContext, configuration, - nodeType + nodeType, + currentTime ); return response.ok({ diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts index b4288dae0c221..af9e9c5f57c5b 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts @@ -25,9 +25,18 @@ export const getCloudMetadata = async ( framework: KibanaFramework, req: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, - nodeType: InventoryItemType + nodeType: InventoryItemType, + currentTime: number ): Promise => { const model = findInventoryModel(nodeType); + // Only run this for AWS modules, eventually we might have more. + if (model.requiredModule !== 'aws') { + return { + accounts: [], + projects: [], + regions: [], + }; + } const metricQuery = { allowNoIndices: true, @@ -36,7 +45,18 @@ export const getCloudMetadata = async ( body: { query: { bool: { - must: [{ match: { 'event.module': model.requiredModule } }], + must: [ + { + range: { + [sourceConfiguration.fields.timestamp]: { + gte: currentTime - 86400000, // 24 hours ago + lte: currentTime, + format: 'epoch_millis', + }, + }, + }, + { match: { 'event.module': model.requiredModule } }, + ], }, }, size: 0, diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts index 8ab0f4a44c85d..b3d960e30404f 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts @@ -34,7 +34,8 @@ export const findIntervalForMetrics = async ( const modules = await Promise.all( fields.map( - async (field) => await getDatasetForField(client, field as string, options.indexPattern) + async (field) => + await getDatasetForField(client, field as string, options.indexPattern, options.timerange) ) ); diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts index 85bb5b106c87c..15e6f7ba86d01 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts @@ -17,7 +17,8 @@ interface EventDatasetHit { export const getDatasetForField = async ( client: ESSearchClient, field: string, - indexPattern: string + indexPattern: string, + timerange: { field: string; to: number; from: number } ) => { const params = { allowNoIndices: true, @@ -25,9 +26,25 @@ export const getDatasetForField = async ( terminateAfter: 1, index: indexPattern, body: { - query: { exists: { field } }, + query: { + bool: { + filter: [ + { exists: { field } }, + { + range: { + [timerange.field]: { + gte: timerange.from, + lte: timerange.to, + format: 'epoch_millis', + }, + }, + }, + ], + }, + }, size: 1, _source: ['event.dataset'], + sort: [{ [timerange.field]: { order: 'desc' } }], }, }; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts index 827e0901c1c01..833b5349f4b56 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts @@ -75,7 +75,10 @@ const aggregationsToModules = async ( const fields = await Promise.all( uniqueFields.map( async (field) => - await getDatasetForField(client, field as string, options.sourceConfiguration.metricAlias) + await getDatasetForField(client, field as string, options.sourceConfiguration.metricAlias, { + ...options.timerange, + field: options.sourceConfiguration.fields.timestamp, + }) ) ); return fields.filter((f) => f) as string[]; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index 9332d5aee1f52..7a2985188dccf 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -23,12 +23,11 @@ export const getNodes = async ( snapshotRequest ); const metricsApiResponse = await queryAllData(client, metricsApiRequest); - return copyMissingMetrics( - transformMetricsApiResponseToSnapshotResponse( - metricsApiRequest, - snapshotRequest, - source, - metricsApiResponse - ) + const snapshotResponse = transformMetricsApiResponseToSnapshotResponse( + metricsApiRequest, + snapshotRequest, + source, + metricsApiResponse ); + return copyMissingMetrics(snapshotResponse); }; From 5375ea41356a1e47f7f8267b6b78b48af908b67f Mon Sep 17 00:00:00 2001 From: Bill McConaghy Date: Wed, 18 Nov 2020 20:19:13 -0500 Subject: [PATCH 94/99] Adding documentation for global action configuration options (#83557) * Adding documentation for global action configuration options * Update docs/user/alerting/defining-alerts.asciidoc Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * incorporating PR feedback Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/user/alerting/defining-alerts.asciidoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 89a487ca8fb32..05d022d039b23 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -74,6 +74,21 @@ image::images/alert-flyout-add-action.png[You can add multiple actions on an ale Actions are not required on alerts. In some cases you may want to run an alert without actions first to understand its behavior, and configure actions later. ============================================== +[float] +=== Global actions configuration +Some actions configuration options apply to all actions. +If you are using an *on-prem* Elastic Stack deployment, you can set these in the kibana.yml file. +If you are using a cloud deployment, you can set these via the console. + +Here's a list of the available global configuration options and an explanation of what each one does: + +* `xpack.actions.allowedHosts`: Specifies an array of host names which actions such as email, Slack, PagerDuty, and webhook can connect to. An element of * indicates any host can be connected to. An empty array indicates no hosts can be connected to. Default: [ {asterisk} ] +* `xpack.actions.enabledActionTypes`: Specifies to an array of action types that are enabled. An {asterisk} indicates all action types registered are enabled. The action types that {kib} provides are: .server-log, .slack, .email, .index, .pagerduty, .webhook. Default: [ {asterisk} ] +* `xpack.actions.proxyUrl`: Specifies the proxy URL to use, if using a proxy for actions. +* `xpack.actions.proxyHeader`: Specifies HTTP headers for proxy, if using a proxy for actions. +* `xpack.actions.proxyRejectUnauthorizedCertificates`: Set to `false` to bypass certificate validation for proxy, if using a proxy for actions. +* `xpack.actions.rejectUnauthorized`: Set to `false` to bypass certificate validation for actions. + [float] === Managing alerts From 92acf4586ee2a0ccffab4f6c8b9fa8a5e8f693b4 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 18 Nov 2020 18:35:36 -0700 Subject: [PATCH 95/99] Revert "[App Search] Engine overview layout stub (#83504)" This reverts commit 71f972dc837d6186ad2b8157af8fb178d86e6a96. --- .../components/analytics/constants.ts | 27 ----- .../components/api_logs/constants.ts | 12 --- .../document_creation/constants.tsx | 54 ---------- .../app_search/components/engine/constants.ts | 4 + .../components/engine/engine_nav.tsx | 2 +- .../components/engine/engine_router.test.tsx | 3 +- .../components/engine/engine_router.tsx | 5 +- .../engine_overview/components/index.ts | 10 -- .../components/recent_api_logs.test.tsx | 32 ------ .../components/recent_api_logs.tsx | 50 ---------- .../components/total_charts.test.tsx | 46 --------- .../components/total_charts.tsx | 99 ------------------- .../components/total_stats.test.tsx | 51 ---------- .../components/total_stats.tsx | 37 ------- .../components/unavailable_prompt.test.tsx | 18 ---- .../components/unavailable_prompt.tsx | 30 ------ .../components/engine_overview/constants.ts | 27 ----- .../engine_overview/engine_overview.test.tsx | 80 --------------- .../engine_overview/engine_overview.tsx | 44 --------- .../engine_overview_empty.test.tsx | 40 -------- .../engine_overview/engine_overview_empty.tsx | 98 ------------------ .../engine_overview_metrics.test.tsx | 34 ------- .../engine_overview_metrics.tsx | 44 --------- .../components/engine_overview/index.ts | 2 - 24 files changed, 8 insertions(+), 841 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/constants.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts deleted file mode 100644 index 51ae11ad2ab82..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts +++ /dev/null @@ -1,27 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const TOTAL_DOCUMENTS = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.totalDocuments', - { defaultMessage: 'Total documents' } -); - -export const TOTAL_API_OPERATIONS = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.totalApiOperations', - { defaultMessage: 'Total API operations' } -); - -export const TOTAL_QUERIES = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.totalQueries', - { defaultMessage: 'Total queries' } -); - -export const TOTAL_CLICKS = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.analytics.totalClicks', - { defaultMessage: 'Total clicks' } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts deleted file mode 100644 index 6fd60b7a34ebc..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/constants.ts +++ /dev/null @@ -1,12 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const RECENT_API_EVENTS = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.apiLogs.recent', - { defaultMessage: 'Recent API events' } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx deleted file mode 100644 index 736ef09fa6cf3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx +++ /dev/null @@ -1,54 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCode, EuiLink } from '@elastic/eui'; - -import { DOCS_PREFIX } from '../../routes'; - -export const DOCUMENT_CREATION_DESCRIPTION = ( - .json, - postCode: POST, - documentsApiLink: ( - - documents API - - ), - apiStrong: Indexing by API, - }} - /> -); - -export const DOCUMENT_API_INDEXING_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.documentCreation.api.title', - { defaultMessage: 'Indexing by API' } -); - -export const DOCUMENT_API_INDEXING_DESCRIPTION = ( - - documents API - - ), - clientLibrariesLink: ( - - client libraries - - ), - }} - /> -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts index 9ce524038075b..3c963e415f33b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/constants.ts @@ -9,6 +9,10 @@ import { i18n } from '@kbn/i18n'; // TODO: It's very likely that we'll move these i18n constants to their respective component // folders once those are migrated over. This is a temporary way of DRYing them out for now. +export const OVERVIEW_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.overview.title', + { defaultMessage: 'Overview' } +); export const ANALYTICS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.title', { defaultMessage: 'Analytics' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index a7ac6f203b1f7..77aca8a71994d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -28,8 +28,8 @@ import { } from '../../routes'; import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { ENGINES_TITLE } from '../engines'; -import { OVERVIEW_TITLE } from '../engine_overview'; import { + OVERVIEW_TITLE, ANALYTICS_TITLE, DOCUMENTS_TITLE, SCHEMA_TITLE, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index e8609c169855b..8f067754c48a0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -18,7 +18,6 @@ jest.mock('../../../shared/flash_messages', () => ({ import { setQueuedErrorMessage } from '../../../shared/flash_messages'; import { Loading } from '../../../shared/loading'; -import { EngineOverview } from '../engine_overview'; import { EngineRouter } from './'; @@ -72,7 +71,7 @@ describe('EngineRouter', () => { const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); - expect(wrapper.find(EngineOverview)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="EngineOverviewTODO"]')).toHaveLength(1); }); it('renders an analytics view', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index f586106924f2c..9833305c438c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -31,8 +31,8 @@ import { // ENGINE_API_LOGS_PATH, } from '../../routes'; import { ENGINES_TITLE } from '../engines'; -import { OVERVIEW_TITLE } from '../engine_overview'; import { + OVERVIEW_TITLE, ANALYTICS_TITLE, // DOCUMENTS_TITLE, // SCHEMA_TITLE, @@ -46,7 +46,6 @@ import { } from './constants'; import { Loading } from '../../../shared/loading'; -import { EngineOverview } from '../engine_overview'; import { EngineLogic } from './'; @@ -101,7 +100,7 @@ export const EngineRouter: React.FC = () => { )} - +
    Overview
    ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts deleted file mode 100644 index 11e7b2a5fba97..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/index.ts +++ /dev/null @@ -1,10 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { UnavailablePrompt } from './unavailable_prompt'; -export { TotalStats } from './total_stats'; -export { TotalCharts } from './total_charts'; -export { RecentApiLogs } from './recent_api_logs'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx deleted file mode 100644 index 725c89894b80b..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx +++ /dev/null @@ -1,32 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { setMockValues } from '../../../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; - -import { EuiButton } from '../../../../shared/react_router_helpers'; - -import { RecentApiLogs } from './recent_api_logs'; - -describe('RecentApiLogs', () => { - let wrapper: ShallowWrapper; - - beforeAll(() => { - jest.clearAllMocks(); - setMockValues({ - engineName: 'some-engine', - }); - wrapper = shallow(); - }); - - it('renders the recent API logs table', () => { - expect(wrapper.find('h2').text()).toEqual('Recent API events'); - expect(wrapper.find(EuiButton).prop('to')).toEqual('/engines/some-engine/api-logs'); - // TODO: expect(wrapper.find(ApiLogsTable)).toHaveLength(1) - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx deleted file mode 100644 index 05eb731116884..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx +++ /dev/null @@ -1,50 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { useValues } from 'kea'; - -import { - EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiPageContentBody, - EuiTitle, -} from '@elastic/eui'; - -import { EuiButton } from '../../../../shared/react_router_helpers'; - -import { ENGINE_API_LOGS_PATH, getEngineRoute } from '../../../routes'; -import { RECENT_API_EVENTS } from '../../api_logs/constants'; -import { VIEW_API_LOGS } from '../constants'; - -import { EngineLogic } from '../../engine'; - -export const RecentApiLogs: React.FC = () => { - const { engineName } = useValues(EngineLogic); - const engineRoute = getEngineRoute(engineName); - - return ( - - - - -

    {RECENT_API_EVENTS}

    -
    -
    - - - {VIEW_API_LOGS} - - -
    - - TODO: API Logs Table - {/* */} - -
    - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx deleted file mode 100644 index a56b41dfb0503..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx +++ /dev/null @@ -1,46 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { setMockValues } from '../../../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; - -import { EuiButton } from '../../../../shared/react_router_helpers'; - -import { TotalCharts } from './total_charts'; - -describe('TotalCharts', () => { - let wrapper: ShallowWrapper; - - beforeAll(() => { - jest.clearAllMocks(); - setMockValues({ - engineName: 'some-engine', - startDate: '1970-01-01', - endDate: '1970-01-08', - queriesPerDay: [0, 1, 2, 3, 5, 10, 50], - operationsPerDay: [0, 0, 0, 0, 0, 0, 0], - }); - wrapper = shallow(); - }); - - it('renders the total queries chart', () => { - const chart = wrapper.find('[data-test-subj="TotalQueriesChart"]'); - - expect(chart.find('h2').text()).toEqual('Total queries'); - expect(chart.find(EuiButton).prop('to')).toEqual('/engines/some-engine/analytics'); - // TODO: find chart component - }); - - it('renders the total API operations chart', () => { - const chart = wrapper.find('[data-test-subj="TotalApiOperationsChart"]'); - - expect(chart.find('h2').text()).toEqual('Total API operations'); - expect(chart.find(EuiButton).prop('to')).toEqual('/engines/some-engine/api-logs'); - // TODO: find chart component - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx deleted file mode 100644 index e27fe3cdfc799..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx +++ /dev/null @@ -1,99 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { useValues } from 'kea'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentHeaderSection, - EuiPageContentBody, - EuiTitle, - EuiText, -} from '@elastic/eui'; - -import { EuiButton } from '../../../../shared/react_router_helpers'; - -import { ENGINE_ANALYTICS_PATH, ENGINE_API_LOGS_PATH, getEngineRoute } from '../../../routes'; -import { TOTAL_QUERIES, TOTAL_API_OPERATIONS } from '../../analytics/constants'; -import { VIEW_ANALYTICS, VIEW_API_LOGS, LAST_7_DAYS } from '../constants'; - -import { EngineLogic } from '../../engine'; -import { EngineOverviewLogic } from '../'; - -export const TotalCharts: React.FC = () => { - const { engineName } = useValues(EngineLogic); - const engineRoute = getEngineRoute(engineName); - - const { - // startDate, - // endDate, - // queriesPerDay, - // operationsPerDay, - } = useValues(EngineOverviewLogic); - - return ( - - - - - - -

    {TOTAL_QUERIES}

    -
    - - {LAST_7_DAYS} - -
    - - - {VIEW_ANALYTICS} - - -
    - - TODO: Analytics chart - {/* */} - -
    -
    - - - - - -

    {TOTAL_API_OPERATIONS}

    -
    - - {LAST_7_DAYS} - -
    - - - {VIEW_API_LOGS} - - -
    - - TODO: API Logs chart - {/* */} - -
    -
    -
    - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx deleted file mode 100644 index 6cb47e8b419f3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx +++ /dev/null @@ -1,51 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { setMockValues } from '../../../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiStat } from '@elastic/eui'; - -import { TotalStats } from './total_stats'; - -describe('TotalStats', () => { - let wrapper: ShallowWrapper; - - beforeAll(() => { - jest.clearAllMocks(); - setMockValues({ - totalQueries: 11, - documentCount: 22, - totalClicks: 33, - }); - wrapper = shallow(); - }); - - it('renders the total queries stat', () => { - expect(wrapper.find('[data-test-subj="TotalQueriesCard"]')).toHaveLength(1); - - const card = wrapper.find(EuiStat).at(0); - expect(card.prop('title')).toEqual(11); - expect(card.prop('description')).toEqual('Total queries'); - }); - - it('renders the total documents stat', () => { - expect(wrapper.find('[data-test-subj="TotalDocumentsCard"]')).toHaveLength(1); - - const card = wrapper.find(EuiStat).at(1); - expect(card.prop('title')).toEqual(22); - expect(card.prop('description')).toEqual('Total documents'); - }); - - it('renders the total clicks stat', () => { - expect(wrapper.find('[data-test-subj="TotalClicksCard"]')).toHaveLength(1); - - const card = wrapper.find(EuiStat).at(2); - expect(card.prop('title')).toEqual(33); - expect(card.prop('description')).toEqual('Total clicks'); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx deleted file mode 100644 index a27142938f558..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx +++ /dev/null @@ -1,37 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { useValues } from 'kea'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui'; - -import { TOTAL_QUERIES, TOTAL_DOCUMENTS, TOTAL_CLICKS } from '../../analytics/constants'; - -import { EngineOverviewLogic } from '../'; - -export const TotalStats: React.FC = () => { - const { totalQueries, documentCount, totalClicks } = useValues(EngineOverviewLogic); - - return ( - - - - - - - - - - - - - - - - - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx deleted file mode 100644 index 3ddfd14b0eb0c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx +++ /dev/null @@ -1,18 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { EuiEmptyPrompt } from '@elastic/eui'; - -import { UnavailablePrompt } from './unavailable_prompt'; - -describe('UnavailablePrompt', () => { - it('renders', () => { - const wrapper = shallow(); - expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx deleted file mode 100644 index e9cc6e2f05bf3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx +++ /dev/null @@ -1,30 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { i18n } from '@kbn/i18n'; -import { EuiEmptyPrompt } from '@elastic/eui'; - -export const UnavailablePrompt: React.FC = () => ( - - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.unavailableTitle', { - defaultMessage: 'Dashboard metrics are currently unavailable', - })} - - } - body={ -

    - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.unavailableBody', { - defaultMessage: 'Please try again in a few minutes.', - })} -

    - } - /> -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/constants.ts deleted file mode 100644 index 797811ec6cde8..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/constants.ts +++ /dev/null @@ -1,27 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const OVERVIEW_TITLE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.overview.title', - { defaultMessage: 'Overview' } -); - -export const VIEW_ANALYTICS = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.overview.analyticsLink', - { defaultMessage: 'View analytics' } -); - -export const VIEW_API_LOGS = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.overview.apiLogsLink', - { defaultMessage: 'View API logs' } -); - -export const LAST_7_DAYS = i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.overview.chartDuration', - { defaultMessage: 'Last 7 days' } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx deleted file mode 100644 index 196fb2ca2bf13..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ /dev/null @@ -1,80 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import '../../../__mocks__/shallow_useeffect.mock'; -import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { Loading } from '../../../shared/loading'; -import { EmptyEngineOverview } from './engine_overview_empty'; -import { EngineOverviewMetrics } from './engine_overview_metrics'; -import { EngineOverview } from './'; - -describe('EngineOverview', () => { - const values = { - dataLoading: false, - documentCount: 0, - myRole: {}, - isMetaEngine: false, - }; - const actions = { - pollForOverviewMetrics: jest.fn(), - }; - - beforeEach(() => { - jest.clearAllMocks(); - setMockValues(values); - setMockActions(actions); - }); - - it('renders', () => { - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="EngineOverview"]')).toHaveLength(1); - }); - - it('initializes data on mount', () => { - shallow(); - expect(actions.pollForOverviewMetrics).toHaveBeenCalledTimes(1); - }); - - it('renders a loading component if async data is still loading', () => { - setMockValues({ ...values, dataLoading: true }); - const wrapper = shallow(); - expect(wrapper.find(Loading)).toHaveLength(1); - }); - - describe('EmptyEngineOverview', () => { - it('renders when the engine has no documents & the user can add documents', () => { - const myRole = { canManageEngineDocuments: true, canViewEngineCredentials: true }; - setMockValues({ ...values, myRole, documentCount: 0 }); - const wrapper = shallow(); - expect(wrapper.find(EmptyEngineOverview)).toHaveLength(1); - }); - }); - - describe('EngineOverviewMetrics', () => { - it('renders when the engine has documents', () => { - setMockValues({ ...values, documentCount: 1 }); - const wrapper = shallow(); - expect(wrapper.find(EngineOverviewMetrics)).toHaveLength(1); - }); - - it('renders when the user does not have the ability to add documents', () => { - const myRole = { canManageEngineDocuments: false, canViewEngineCredentials: false }; - setMockValues({ ...values, myRole }); - const wrapper = shallow(); - expect(wrapper.find(EngineOverviewMetrics)).toHaveLength(1); - }); - - it('always renders for meta engines', () => { - setMockValues({ ...values, isMetaEngine: true }); - const wrapper = shallow(); - expect(wrapper.find(EngineOverviewMetrics)).toHaveLength(1); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx deleted file mode 100644 index dd43bc67b3e88..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ /dev/null @@ -1,44 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect } from 'react'; -import { useActions, useValues } from 'kea'; - -import { AppLogic } from '../../app_logic'; -import { EngineLogic } from '../engine'; -import { Loading } from '../../../shared/loading'; - -import { EngineOverviewLogic } from './'; -import { EmptyEngineOverview } from './engine_overview_empty'; -import { EngineOverviewMetrics } from './engine_overview_metrics'; - -export const EngineOverview: React.FC = () => { - const { - myRole: { canManageEngineDocuments, canViewEngineCredentials }, - } = useValues(AppLogic); - const { isMetaEngine } = useValues(EngineLogic); - - const { pollForOverviewMetrics } = useActions(EngineOverviewLogic); - const { dataLoading, documentCount } = useValues(EngineOverviewLogic); - - useEffect(() => { - pollForOverviewMetrics(); - }, []); - - if (dataLoading) { - return ; - } - - const engineHasDocuments = documentCount > 0; - const canAddDocuments = canManageEngineDocuments && canViewEngineCredentials; - const showEngineOverview = engineHasDocuments || !canAddDocuments || isMetaEngine; - - return ( -
    - {showEngineOverview ? : } -
    - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx deleted file mode 100644 index 8ebe09820a67e..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx +++ /dev/null @@ -1,40 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import '../../../__mocks__/enterprise_search_url.mock'; -import { setMockValues } from '../../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { EuiButton } from '@elastic/eui'; - -import { CURRENT_MAJOR_VERSION } from '../../../../../common/version'; - -import { EmptyEngineOverview } from './engine_overview_empty'; - -describe('EmptyEngineOverview', () => { - let wrapper: ShallowWrapper; - - beforeAll(() => { - jest.clearAllMocks(); - setMockValues({ - engineName: 'empty-engine', - }); - wrapper = shallow(); - }); - - it('renders', () => { - expect(wrapper.find('h1').text()).toEqual('Engine setup'); - expect(wrapper.find('h2').text()).toEqual('Setting up the “empty-engine” engine'); - expect(wrapper.find('h3').text()).toEqual('Indexing by API'); - }); - - it('renders correctly versioned documentation URLs', () => { - expect(wrapper.find(EuiButton).prop('href')).toEqual( - `https://www.elastic.co/guide/en/app-search/${CURRENT_MAJOR_VERSION}/index.html` - ); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx deleted file mode 100644 index f2bf5a54f810c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx +++ /dev/null @@ -1,98 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { useValues } from 'kea'; - -import { i18n } from '@kbn/i18n'; -import { - EuiPageHeader, - EuiPageHeaderSection, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentBody, - EuiTitle, - EuiText, - EuiButton, - EuiSpacer, -} from '@elastic/eui'; - -import { EngineLogic } from '../engine'; - -import { DOCS_PREFIX } from '../../routes'; -import { - DOCUMENT_CREATION_DESCRIPTION, - DOCUMENT_API_INDEXING_TITLE, - DOCUMENT_API_INDEXING_DESCRIPTION, -} from '../document_creation/constants'; -// TODO -// import { DocumentCreationButtons, CodeExample } from '../document_creation' - -export const EmptyEngineOverview: React.FC = () => { - const { engineName } = useValues(EngineLogic); - - return ( - <> - - - -

    - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.empty.heading', { - defaultMessage: 'Engine setup', - })} -

    -
    -
    - - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.engine.overview.empty.headingAction', - { defaultMessage: 'View documentation' } - )} - - -
    - - - -

    - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.empty.subheading', { - defaultMessage: 'Setting up the “{engineName}” engine', - values: { engineName }, - })} -

    -
    -
    - - -

    {DOCUMENT_CREATION_DESCRIPTION}

    -
    - - {/* TODO: */} -
    - - - -

    {DOCUMENT_API_INDEXING_TITLE}

    -
    -
    - - -

    {DOCUMENT_API_INDEXING_DESCRIPTION}

    -

    - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.empty.apiExample', { - defaultMessage: - 'To see the API in action, you can experiment with the example request below using a command line or a client library.', - })} -

    -
    - - {/* */} -
    -
    - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx deleted file mode 100644 index 8250446e231b3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx +++ /dev/null @@ -1,34 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { setMockValues } from '../../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; -import { EngineOverviewMetrics } from './engine_overview_metrics'; - -describe('EngineOverviewMetrics', () => { - it('renders', () => { - const wrapper = shallow(); - expect(wrapper.find('h1').text()).toEqual('Engine overview'); - }); - - it('renders an unavailable prompt if engine data is still indexing', () => { - setMockValues({ apiLogsUnavailable: true }); - const wrapper = shallow(); - expect(wrapper.find(UnavailablePrompt)).toHaveLength(1); - }); - - it('renders total stats, charts, and recent logs when metrics are available', () => { - setMockValues({ apiLogsUnavailable: false }); - const wrapper = shallow(); - expect(wrapper.find(TotalStats)).toHaveLength(1); - expect(wrapper.find(TotalCharts)).toHaveLength(1); - expect(wrapper.find(RecentApiLogs)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx deleted file mode 100644 index 9630f6fa2f81d..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx +++ /dev/null @@ -1,44 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { useValues } from 'kea'; - -import { i18n } from '@kbn/i18n'; -import { EuiPageHeader, EuiTitle, EuiSpacer } from '@elastic/eui'; - -import { EngineOverviewLogic } from './'; - -import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; - -export const EngineOverviewMetrics: React.FC = () => { - const { apiLogsUnavailable } = useValues(EngineOverviewLogic); - - return ( - <> - - -

    - {i18n.translate('xpack.enterpriseSearch.appSearch.engine.overview.heading', { - defaultMessage: 'Engine overview', - })} -

    -
    -
    - {apiLogsUnavailable ? ( - - ) : ( - <> - - - - - - - )} - - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts index 82c5d7dc8e60a..fcd92ba6a338c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts @@ -5,5 +5,3 @@ */ export { EngineOverviewLogic } from './engine_overview_logic'; -export { EngineOverview } from './engine_overview'; -export { OVERVIEW_TITLE } from './constants'; From d97ddcd4dae437bf2b81251926b89a024d69f6d5 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 18 Nov 2020 18:42:37 -0700 Subject: [PATCH 96/99] [maps] convert VectorStyleEditor to TS (#83582) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../style_property_descriptor_types.ts | 24 +- .../blended_vector_layer.ts | 11 +- .../create_choropleth_layer_descriptor.ts | 2 +- .../create_region_map_layer_descriptor.ts | 4 +- .../create_tile_map_layer_descriptor.ts | 10 +- .../maps/public/classes/layers/layer.tsx | 18 +- .../observability/create_layer_descriptor.ts | 6 +- .../security/create_layer_descriptors.ts | 8 +- .../clusters_layer_wizard.tsx | 4 +- .../classes/styles/heatmap/heatmap_style.tsx | 6 +- .../maps/public/classes/styles/style.ts | 11 +- .../public/classes/styles/tile/tile_style.ts | 2 +- .../color/vector_style_color_editor.tsx | 6 +- ...ditor.js => vector_style_label_editor.tsx} | 9 +- ...editor.js => vector_style_size_editor.tsx} | 9 +- .../vector/components/style_prop_editor.tsx | 10 +- ...editor.js => vector_style_icon_editor.tsx} | 9 +- ...tyle_editor.js => vector_style_editor.tsx} | 252 ++++++++++++------ .../styles/vector/style_fields_helper.ts | 2 +- .../classes/styles/vector/vector_style.tsx | 43 ++- .../vector/vector_style_defaults.test.ts | 8 +- .../styles/vector/vector_style_defaults.ts | 36 +-- .../style_settings/style_settings.js | 4 +- 23 files changed, 297 insertions(+), 197 deletions(-) rename x-pack/plugins/maps/public/classes/styles/vector/components/label/{vector_style_label_editor.js => vector_style_label_editor.tsx} (62%) rename x-pack/plugins/maps/public/classes/styles/vector/components/size/{vector_style_size_editor.js => vector_style_size_editor.tsx} (62%) rename x-pack/plugins/maps/public/classes/styles/vector/components/symbol/{vector_style_icon_editor.js => vector_style_icon_editor.tsx} (62%) rename x-pack/plugins/maps/public/classes/styles/vector/components/{vector_style_editor.js => vector_style_editor.tsx} (64%) diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts index 5aba9b06a6ccf..d52afebcaa254 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts @@ -174,18 +174,18 @@ export type SizeStylePropertyDescriptor = }; export type VectorStylePropertiesDescriptor = { - [VECTOR_STYLES.SYMBOLIZE_AS]?: SymbolizeAsStylePropertyDescriptor; - [VECTOR_STYLES.FILL_COLOR]?: ColorStylePropertyDescriptor; - [VECTOR_STYLES.LINE_COLOR]?: ColorStylePropertyDescriptor; - [VECTOR_STYLES.LINE_WIDTH]?: SizeStylePropertyDescriptor; - [VECTOR_STYLES.ICON]?: IconStylePropertyDescriptor; - [VECTOR_STYLES.ICON_SIZE]?: SizeStylePropertyDescriptor; - [VECTOR_STYLES.ICON_ORIENTATION]?: OrientationStylePropertyDescriptor; - [VECTOR_STYLES.LABEL_TEXT]?: LabelStylePropertyDescriptor; - [VECTOR_STYLES.LABEL_COLOR]?: ColorStylePropertyDescriptor; - [VECTOR_STYLES.LABEL_SIZE]?: SizeStylePropertyDescriptor; - [VECTOR_STYLES.LABEL_BORDER_COLOR]?: ColorStylePropertyDescriptor; - [VECTOR_STYLES.LABEL_BORDER_SIZE]?: LabelBorderSizeStylePropertyDescriptor; + [VECTOR_STYLES.SYMBOLIZE_AS]: SymbolizeAsStylePropertyDescriptor; + [VECTOR_STYLES.FILL_COLOR]: ColorStylePropertyDescriptor; + [VECTOR_STYLES.LINE_COLOR]: ColorStylePropertyDescriptor; + [VECTOR_STYLES.LINE_WIDTH]: SizeStylePropertyDescriptor; + [VECTOR_STYLES.ICON]: IconStylePropertyDescriptor; + [VECTOR_STYLES.ICON_SIZE]: SizeStylePropertyDescriptor; + [VECTOR_STYLES.ICON_ORIENTATION]: OrientationStylePropertyDescriptor; + [VECTOR_STYLES.LABEL_TEXT]: LabelStylePropertyDescriptor; + [VECTOR_STYLES.LABEL_COLOR]: ColorStylePropertyDescriptor; + [VECTOR_STYLES.LABEL_SIZE]: SizeStylePropertyDescriptor; + [VECTOR_STYLES.LABEL_BORDER_COLOR]: ColorStylePropertyDescriptor; + [VECTOR_STYLES.LABEL_BORDER_SIZE]: LabelBorderSizeStylePropertyDescriptor; }; export type StyleDescriptor = { diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 2ab8a70f2e4df..85391ea82cbf2 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -36,6 +36,7 @@ import { LayerDescriptor, VectorLayerDescriptor, VectorSourceRequestMeta, + VectorStylePropertiesDescriptor, } from '../../../../common/descriptor_types'; import { IVectorSource } from '../../sources/vector_source'; import { LICENSED_FEATURES } from '../../../licensed_features'; @@ -79,13 +80,15 @@ function getClusterStyleDescriptor( clusterSource: ESGeoGridSource ): VectorStyleDescriptor { const defaultDynamicProperties = getDefaultDynamicProperties(); - const clusterStyleDescriptor: VectorStyleDescriptor = { + const clusterStyleDescriptor: Omit & { + properties: Partial; + } = { type: LAYER_STYLE_TYPE.VECTOR, properties: { [VECTOR_STYLES.LABEL_TEXT]: { type: STYLE_TYPE.DYNAMIC, options: { - ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT]!.options, + ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, field: { name: COUNT_PROP_NAME, origin: FIELD_ORIGIN.SOURCE, @@ -95,7 +98,7 @@ function getClusterStyleDescriptor( [VECTOR_STYLES.ICON_SIZE]: { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions), field: { name: COUNT_PROP_NAME, origin: FIELD_ORIGIN.SOURCE, @@ -157,7 +160,7 @@ function getClusterStyleDescriptor( } }); - return clusterStyleDescriptor; + return clusterStyleDescriptor as VectorStyleDescriptor; } export interface BlendedVectorLayerArguments { diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts index cdfe60946f5f9..fa82b9dc3b542 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/create_choropleth_layer_descriptor.ts @@ -71,7 +71,7 @@ function createChoroplethLayerDescriptor({ [VECTOR_STYLES.FILL_COLOR]: { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options as ColorDynamicOptions), field: { name: joinKey, origin: FIELD_ORIGIN.JOIN, diff --git a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts index 6f9bb686459b5..5fa2524b1b790 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_region_map_layer_descriptor.ts @@ -100,7 +100,7 @@ export function createRegionMapLayerDescriptor({ [VECTOR_STYLES.FILL_COLOR]: { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options as ColorDynamicOptions), field: { name: joinKey, origin: FIELD_ORIGIN.JOIN, @@ -108,7 +108,7 @@ export function createRegionMapLayerDescriptor({ color: colorPallette ? colorPallette.value : 'Yellow to Red', type: COLOR_MAP_TYPE.ORDINAL, fieldMetaOptions: { - ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions) + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options as ColorDynamicOptions) .fieldMetaOptions, isEnabled: false, }, diff --git a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts index 5b89373f2db48..05616f6916f62 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_tile_map_layer_descriptor.ts @@ -113,16 +113,16 @@ export function createTileMapLayerDescriptor({ const colorPallette = NUMERICAL_COLOR_PALETTES.find((pallette) => { return pallette.value.toLowerCase() === colorSchema.toLowerCase(); }); - const styleProperties: VectorStylePropertiesDescriptor = { + const styleProperties: Partial = { [VECTOR_STYLES.FILL_COLOR]: { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options as ColorDynamicOptions), field: metricStyleField, color: colorPallette ? colorPallette.value : 'Yellow to Red', type: COLOR_MAP_TYPE.ORDINAL, fieldMetaOptions: { - ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions) + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options as ColorDynamicOptions) .fieldMetaOptions, isEnabled: false, }, @@ -139,11 +139,11 @@ export function createTileMapLayerDescriptor({ styleProperties[VECTOR_STYLES.ICON_SIZE] = { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions), maxSize: 18, field: metricStyleField, fieldMetaOptions: { - ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions) + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions) .fieldMetaOptions, isEnabled: false, }, diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index b982e6452e8cb..060ff4d46fa2a 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -76,11 +76,9 @@ export interface ILayer { getType(): string | undefined; isVisible(): boolean; cloneDescriptor(): Promise; - renderStyleEditor({ - onStyleDescriptorChange, - }: { - onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; - }): ReactElement | null; + renderStyleEditor( + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void + ): ReactElement | null; getInFlightRequestTokens(): symbol[]; getPrevRequestToken(dataId: string): symbol | undefined; destroy: () => void; @@ -437,16 +435,14 @@ export class AbstractLayer implements ILayer { return null; } - renderStyleEditor({ - onStyleDescriptorChange, - }: { - onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; - }): ReactElement | null { + renderStyleEditor( + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void + ): ReactElement | null { const style = this.getStyleForEditing(); if (!style) { return null; } - return style.renderEditor({ layer: this, onStyleDescriptorChange }); + return style.renderEditor(onStyleDescriptorChange); } getIndexPatternIds(): string[] { diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts index dea551866f4a9..7e8a216685bbd 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts @@ -50,7 +50,7 @@ function createDynamicFillColorDescriptor( return { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options as ColorDynamicOptions), field, color: layer === OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE ? 'Green to Red' : 'Yellow to Red', @@ -226,12 +226,12 @@ export function createLayerDescriptor({ origin: FIELD_ORIGIN.SOURCE, }; - const styleProperties: VectorStylePropertiesDescriptor = { + const styleProperties: Partial = { [VECTOR_STYLES.FILL_COLOR]: createDynamicFillColorDescriptor(layer, metricStyleField), [VECTOR_STYLES.ICON_SIZE]: { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions), field: metricStyleField, }, }, diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts index 909cd93b3df7a..b52ce02acb5f0 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/create_layer_descriptors.ts @@ -68,7 +68,7 @@ function createSourceLayerDescriptor(indexPatternId: string, indexPatternTitle: ], }); - const styleProperties: VectorStylePropertiesDescriptor = { + const styleProperties: Partial = { [VECTOR_STYLES.FILL_COLOR]: { type: STYLE_TYPE.STATIC, options: { color: euiVisColorPalette[1] }, @@ -121,7 +121,7 @@ function createDestinationLayerDescriptor(indexPatternId: string, indexPatternTi ], }); - const styleProperties: VectorStylePropertiesDescriptor = { + const styleProperties: Partial = { [VECTOR_STYLES.FILL_COLOR]: { type: STYLE_TYPE.STATIC, options: { color: euiVisColorPalette[2] }, @@ -168,7 +168,7 @@ function createLineLayerDescriptor(indexPatternId: string, indexPatternTitle: st ], }); - const styleProperties: VectorStylePropertiesDescriptor = { + const styleProperties: Partial = { [VECTOR_STYLES.LINE_COLOR]: { type: STYLE_TYPE.STATIC, options: { color: euiVisColorPalette[1] }, @@ -176,7 +176,7 @@ function createLineLayerDescriptor(indexPatternId: string, indexPatternTitle: st [VECTOR_STYLES.LINE_WIDTH]: { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH]!.options as SizeDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options as SizeDynamicOptions), field: { name: COUNT_PROP_NAME, origin: FIELD_ORIGIN.SOURCE, diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index 0f596c47fc9b6..1fd6a9c9ecc8e 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -77,7 +77,7 @@ export const clustersLayerWizardConfig: LayerWizard = { [VECTOR_STYLES.ICON_SIZE]: { type: STYLE_TYPE.DYNAMIC, options: { - ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions), + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options as SizeDynamicOptions), field: { name: COUNT_PROP_NAME, origin: FIELD_ORIGIN.SOURCE, @@ -87,7 +87,7 @@ export const clustersLayerWizardConfig: LayerWizard = { [VECTOR_STYLES.LABEL_TEXT]: { type: STYLE_TYPE.DYNAMIC, options: { - ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT]!.options, + ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, field: { name: COUNT_PROP_NAME, origin: FIELD_ORIGIN.SOURCE, diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx index c75698805225f..599f3b2dfbb02 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx @@ -41,11 +41,7 @@ export class HeatmapStyle implements IStyle { return LAYER_STYLE_TYPE.HEATMAP; } - renderEditor({ - onStyleDescriptorChange, - }: { - onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; - }) { + renderEditor(onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void) { const onHeatmapColorChange = ({ colorRampName }: { colorRampName: string }) => { const styleDescriptor = HeatmapStyle.createDescriptor(colorRampName); onStyleDescriptorChange(styleDescriptor); diff --git a/x-pack/plugins/maps/public/classes/styles/style.ts b/x-pack/plugins/maps/public/classes/styles/style.ts index abaa6184b0ca4..de14ab990fa23 100644 --- a/x-pack/plugins/maps/public/classes/styles/style.ts +++ b/x-pack/plugins/maps/public/classes/styles/style.ts @@ -6,15 +6,10 @@ import { ReactElement } from 'react'; import { StyleDescriptor } from '../../../common/descriptor_types'; -import { ILayer } from '../layers/layer'; export interface IStyle { getType(): string; - renderEditor({ - layer, - onStyleDescriptorChange, - }: { - layer: ILayer; - onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; - }): ReactElement | null; + renderEditor( + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void + ): ReactElement | null; } diff --git a/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts b/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts index cac3913d3149d..dad26d4172e0a 100644 --- a/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts +++ b/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts @@ -21,7 +21,7 @@ export class TileStyle implements IStyle { return LAYER_STYLE_TYPE.TILE; } - renderEditor(/* { layer, onStyleDescriptorChange } */) { + renderEditor(/* onStyleDescriptorChange */) { return null; } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/color/vector_style_color_editor.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/color/vector_style_color_editor.tsx index 4527f56c04d2e..d45c33bbc3f57 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/color/vector_style_color_editor.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/color/vector_style_color_editor.tsx @@ -14,7 +14,11 @@ import { DynamicColorForm } from './dynamic_color_form'; import { StaticColorForm } from './static_color_form'; import { ColorDynamicOptions, ColorStaticOptions } from '../../../../../../common/descriptor_types'; -export function VectorStyleColorEditor(props: Props) { +type ColorEditorProps = Omit, 'children'> & { + swatches: string[]; +}; + +export function VectorStyleColorEditor(props: ColorEditorProps) { const colorForm = props.styleProperty.isDynamic() ? ( ) : ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_editor.tsx similarity index 62% rename from x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_editor.tsx index aaa21ea315f36..586d4fc0576ad 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_editor.tsx @@ -6,11 +6,16 @@ import React from 'react'; -import { StylePropEditor } from '../style_prop_editor'; +import { Props, StylePropEditor } from '../style_prop_editor'; +// @ts-expect-error import { DynamicLabelForm } from './dynamic_label_form'; +// @ts-expect-error import { StaticLabelForm } from './static_label_form'; +import { LabelDynamicOptions, LabelStaticOptions } from '../../../../../../common/descriptor_types'; -export function VectorStyleLabelEditor(props) { +type LabelEditorProps = Omit, 'children'>; + +export function VectorStyleLabelEditor(props: LabelEditorProps) { const labelForm = props.styleProperty.isDynamic() ? ( ) : ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/size/vector_style_size_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/size/vector_style_size_editor.tsx similarity index 62% rename from x-pack/plugins/maps/public/classes/styles/vector/components/size/vector_style_size_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/size/vector_style_size_editor.tsx index e344f72bd429a..c492f24661e71 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/size/vector_style_size_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/size/vector_style_size_editor.tsx @@ -6,11 +6,16 @@ import React from 'react'; -import { StylePropEditor } from '../style_prop_editor'; +import { Props, StylePropEditor } from '../style_prop_editor'; +// @ts-expect-error import { DynamicSizeForm } from './dynamic_size_form'; +// @ts-expect-error import { StaticSizeForm } from './static_size_form'; +import { SizeDynamicOptions, SizeStaticOptions } from '../../../../../../common/descriptor_types'; -export function VectorStyleSizeEditor(props) { +type SizeEditorProps = Omit, 'children'>; + +export function VectorStyleSizeEditor(props: SizeEditorProps) { const sizeForm = props.styleProperty.isDynamic() ? ( ) : ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx index 43b088074a30e..f3363a9443cfd 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.tsx @@ -25,12 +25,12 @@ export interface Props { customStaticOptionLabel?: string; defaultStaticStyleOptions: StaticOptions; defaultDynamicStyleOptions: DynamicOptions; - disabled: boolean; + disabled?: boolean; disabledBy?: VECTOR_STYLES; fields: StyleField[]; onDynamicStyleChange: (propertyName: VECTOR_STYLES, options: DynamicOptions) => void; onStaticStyleChange: (propertyName: VECTOR_STYLES, options: StaticOptions) => void; - styleProperty: IStyleProperty; + styleProperty: IStyleProperty; } export class StylePropEditor extends Component< @@ -42,7 +42,7 @@ export class StylePropEditor extends Component< _onTypeToggle = () => { if (this.props.styleProperty.isDynamic()) { // preserve current dynmaic style - this._prevDynamicStyleOptions = this.props.styleProperty.getOptions(); + this._prevDynamicStyleOptions = this.props.styleProperty.getOptions() as DynamicOptions; // toggle to static style this.props.onStaticStyleChange( this.props.styleProperty.getStyleName(), @@ -50,7 +50,7 @@ export class StylePropEditor extends Component< ); } else { // preserve current static style - this._prevStaticStyleOptions = this.props.styleProperty.getOptions(); + this._prevStaticStyleOptions = this.props.styleProperty.getOptions() as StaticOptions; // toggle to dynamic style this.props.onDynamicStyleChange( this.props.styleProperty.getStyleName(), @@ -61,7 +61,7 @@ export class StylePropEditor extends Component< _onFieldMetaOptionsChange = (fieldMetaOptions: FieldMetaOptions) => { const options = { - ...this.props.styleProperty.getOptions(), + ...(this.props.styleProperty.getOptions() as DynamicOptions), fieldMetaOptions, }; this.props.onDynamicStyleChange(this.props.styleProperty.getStyleName(), options); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.tsx similarity index 62% rename from x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.tsx index 2a983a32f0d82..bd6cda0b57f8d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.tsx @@ -6,11 +6,16 @@ import React from 'react'; -import { StylePropEditor } from '../style_prop_editor'; +import { Props, StylePropEditor } from '../style_prop_editor'; +// @ts-expect-error import { DynamicIconForm } from './dynamic_icon_form'; +// @ts-expect-error import { StaticIconForm } from './static_icon_form'; +import { IconDynamicOptions, IconStaticOptions } from '../../../../../../common/descriptor_types'; -export function VectorStyleIconEditor(props) { +type IconEditorProps = Omit, 'children'>; + +export function VectorStyleIconEditor(props: IconEditorProps) { const iconForm = props.styleProperty.isDynamic() ? ( ) : ( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx similarity index 64% rename from x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx index d577912efb830..95e32f0e9969b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.tsx @@ -7,34 +7,95 @@ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { VectorStyleColorEditor } from './color/vector_style_color_editor'; import { VectorStyleSizeEditor } from './size/vector_style_size_editor'; +// @ts-expect-error import { VectorStyleSymbolizeAsEditor } from './symbol/vector_style_symbolize_as_editor'; import { VectorStyleIconEditor } from './symbol/vector_style_icon_editor'; import { VectorStyleLabelEditor } from './label/vector_style_label_editor'; +// @ts-expect-error import { VectorStyleLabelBorderSizeEditor } from './label/vector_style_label_border_size_editor'; +// @ts-expect-error import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../vector_style_defaults'; import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_palettes'; -import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui'; import { LABEL_BORDER_SIZES, VECTOR_STYLES, STYLE_TYPE, VECTOR_SHAPE_TYPE, } from '../../../../../common/constants'; -import { createStyleFieldsHelper } from '../style_fields_helper'; - -export class VectorStyleEditor extends Component { - state = { - styleFields: [], - defaultDynamicProperties: getDefaultDynamicProperties(), - defaultStaticProperties: getDefaultStaticProperties(), - supportedFeatures: undefined, - selectedFeature: null, - }; +import { createStyleFieldsHelper, StyleField, StyleFieldsHelper } from '../style_fields_helper'; +import { + ColorDynamicOptions, + ColorStaticOptions, + DynamicStylePropertyOptions, + IconDynamicOptions, + IconStaticOptions, + LabelDynamicOptions, + LabelStaticOptions, + SizeDynamicOptions, + SizeStaticOptions, + StaticStylePropertyOptions, + StylePropertyOptions, + VectorStylePropertiesDescriptor, +} from '../../../../../common/descriptor_types'; +import { IStyleProperty } from '../properties/style_property'; +import { SymbolizeAsProperty } from '../properties/symbolize_as_property'; +import { LabelBorderSizeProperty } from '../properties/label_border_size_property'; +import { StaticTextProperty } from '../properties/static_text_property'; +import { StaticSizeProperty } from '../properties/static_size_property'; +import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; + +export interface StyleProperties { + [key: string]: IStyleProperty; +} + +interface Props { + layer: IVectorLayer; + isPointsOnly: boolean; + isLinesOnly: boolean; + onIsTimeAwareChange: (isTimeAware: boolean) => void; + handlePropertyChange: (propertyName: VECTOR_STYLES, stylePropertyDescriptor: unknown) => void; + hasBorder: boolean; + styleProperties: StyleProperties; + isTimeAware: boolean; + showIsTimeAware: boolean; +} + +interface State { + styleFields: StyleField[]; + defaultDynamicProperties: VectorStylePropertiesDescriptor; + defaultStaticProperties: VectorStylePropertiesDescriptor; + supportedFeatures: VECTOR_SHAPE_TYPE[]; + selectedFeature: VECTOR_SHAPE_TYPE; + styleFieldsHelper?: StyleFieldsHelper; +} + +export class VectorStyleEditor extends Component { + private _isMounted: boolean = false; + + constructor(props: Props) { + super(props); + + let selectedFeature = VECTOR_SHAPE_TYPE.POLYGON; + if (props.isPointsOnly) { + selectedFeature = VECTOR_SHAPE_TYPE.POINT; + } else if (props.isLinesOnly) { + selectedFeature = VECTOR_SHAPE_TYPE.LINE; + } + + this.state = { + styleFields: [], + defaultDynamicProperties: getDefaultDynamicProperties(), + defaultStaticProperties: getDefaultStaticProperties(), + supportedFeatures: [], + selectedFeature, + }; + } componentWillUnmount() { this._isMounted = false; @@ -68,36 +129,20 @@ export class VectorStyleEditor extends Component { async _loadSupportedFeatures() { const supportedFeatures = await this.props.layer.getSource().getSupportedShapeTypes(); - if (!this._isMounted) { - return; - } - - if (!_.isEqual(supportedFeatures, this.state.supportedFeatures)) { + if (this._isMounted && !_.isEqual(supportedFeatures, this.state.supportedFeatures)) { this.setState({ supportedFeatures }); } - - if (this.state.selectedFeature === null) { - let selectedFeature = VECTOR_SHAPE_TYPE.POLYGON; - if (this.props.isPointsOnly) { - selectedFeature = VECTOR_SHAPE_TYPE.POINT; - } else if (this.props.isLinesOnly) { - selectedFeature = VECTOR_SHAPE_TYPE.LINE; - } - this.setState({ - selectedFeature: selectedFeature, - }); - } } - _handleSelectedFeatureChange = (selectedFeature) => { - this.setState({ selectedFeature }); + _handleSelectedFeatureChange = (selectedFeature: string) => { + this.setState({ selectedFeature: selectedFeature as VECTOR_SHAPE_TYPE }); }; - _onIsTimeAwareChange = (event) => { + _onIsTimeAwareChange = (event: EuiSwitchEvent) => { this.props.onIsTimeAwareChange(event.target.checked); }; - _onStaticStyleChange = (propertyName, options) => { + _onStaticStyleChange = (propertyName: VECTOR_STYLES, options: StaticStylePropertyOptions) => { const styleDescriptor = { type: STYLE_TYPE.STATIC, options, @@ -105,7 +150,7 @@ export class VectorStyleEditor extends Component { this.props.handlePropertyChange(propertyName, styleDescriptor); }; - _onDynamicStyleChange = (propertyName, options) => { + _onDynamicStyleChange = (propertyName: VECTOR_STYLES, options: DynamicStylePropertyOptions) => { const styleDescriptor = { type: STYLE_TYPE.DYNAMIC, options, @@ -115,18 +160,21 @@ export class VectorStyleEditor extends Component { _hasMarkerOrIcon() { const iconSize = this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]; - return iconSize.isDynamic() || iconSize.getOptions().size > 0; + return iconSize.isDynamic() || (iconSize as StaticSizeProperty).getOptions().size > 0; } _hasLabel() { const label = this.props.styleProperties[VECTOR_STYLES.LABEL_TEXT]; return label.isDynamic() ? label.isComplete() - : label.getOptions().value != null && label.getOptions().value.length; + : (label as StaticTextProperty).getOptions().value != null && + (label as StaticTextProperty).getOptions().value.length; } _hasLabelBorder() { - const labelBorderSize = this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_SIZE]; + const labelBorderSize = this.props.styleProperties[ + VECTOR_STYLES.LABEL_BORDER_SIZE + ] as LabelBorderSizeProperty; return labelBorderSize.getOptions().size !== LABEL_BORDER_SIZES.NONE; } @@ -138,13 +186,18 @@ export class VectorStyleEditor extends Component { swatches={DEFAULT_FILL_COLORS} onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} - styleProperty={this.props.styleProperties[VECTOR_STYLES.FILL_COLOR]} - fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.FILL_COLOR)} + styleProperty={ + this.props.styleProperties[VECTOR_STYLES.FILL_COLOR] as IStyleProperty< + ColorDynamicOptions | ColorStaticOptions + > + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.FILL_COLOR)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options + this.state.defaultStaticProperties[VECTOR_STYLES.FILL_COLOR].options as ColorStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options + this.state.defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR] + .options as ColorDynamicOptions } /> ); @@ -159,13 +212,18 @@ export class VectorStyleEditor extends Component { swatches={DEFAULT_LINE_COLORS} onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} - styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_COLOR]} - fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LINE_COLOR)} + styleProperty={ + this.props.styleProperties[VECTOR_STYLES.LINE_COLOR] as IStyleProperty< + ColorDynamicOptions | ColorStaticOptions + > + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.LINE_COLOR)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options + this.state.defaultStaticProperties[VECTOR_STYLES.LINE_COLOR].options as ColorStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR].options + this.state.defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR] + .options as ColorDynamicOptions } /> ); @@ -178,13 +236,18 @@ export class VectorStyleEditor extends Component { disabledBy={VECTOR_STYLES.ICON_SIZE} onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} - styleProperty={this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH]} - fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LINE_WIDTH)} + styleProperty={ + this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH] as IStyleProperty< + SizeDynamicOptions | SizeStaticOptions + > + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.LINE_WIDTH)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.LINE_WIDTH].options + this.state.defaultStaticProperties[VECTOR_STYLES.LINE_WIDTH].options as SizeStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options + this.state.defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH] + .options as SizeDynamicOptions } /> ); @@ -198,13 +261,19 @@ export class VectorStyleEditor extends Component { + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.LABEL_TEXT)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_TEXT].options + this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_TEXT] + .options as LabelStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options + this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT] + .options as LabelDynamicOptions } /> @@ -215,13 +284,19 @@ export class VectorStyleEditor extends Component { swatches={DEFAULT_LINE_COLORS} onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} - styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_COLOR]} - fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LABEL_COLOR)} + styleProperty={ + this.props.styleProperties[VECTOR_STYLES.LABEL_COLOR] as IStyleProperty< + ColorDynamicOptions | ColorStaticOptions + > + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.LABEL_COLOR)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR].options + this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_COLOR] + .options as ColorStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_COLOR].options + this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_COLOR] + .options as ColorDynamicOptions } /> @@ -231,13 +306,19 @@ export class VectorStyleEditor extends Component { disabledBy={VECTOR_STYLES.LABEL_TEXT} onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} - styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_SIZE]} - fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LABEL_SIZE)} + styleProperty={ + this.props.styleProperties[VECTOR_STYLES.LABEL_SIZE] as IStyleProperty< + SizeDynamicOptions | SizeStaticOptions + > + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.LABEL_SIZE)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_SIZE].options + this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_SIZE] + .options as SizeStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_SIZE].options + this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_SIZE] + .options as SizeDynamicOptions } /> @@ -248,13 +329,19 @@ export class VectorStyleEditor extends Component { swatches={DEFAULT_LINE_COLORS} onStaticStyleChange={this._onStaticStyleChange} onDynamicStyleChange={this._onDynamicStyleChange} - styleProperty={this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_COLOR]} - fields={this.state.styleFieldsHelper.getFieldsForStyle(VECTOR_STYLES.LABEL_BORDER_COLOR)} + styleProperty={ + this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_COLOR] as IStyleProperty< + ColorDynamicOptions | ColorStaticOptions + > + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.LABEL_BORDER_COLOR)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_BORDER_COLOR].options + this.state.defaultStaticProperties[VECTOR_STYLES.LABEL_BORDER_COLOR] + .options as ColorStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_BORDER_COLOR].options + this.state.defaultDynamicProperties[VECTOR_STYLES.LABEL_BORDER_COLOR] + .options as ColorDynamicOptions } /> @@ -274,7 +361,11 @@ export class VectorStyleEditor extends Component { const hasMarkerOrIcon = this._hasMarkerOrIcon(); let iconOrientationEditor; let iconEditor; - if (this.props.styleProperties[VECTOR_STYLES.SYMBOLIZE_AS].isSymbolizedAsIcon()) { + if ( + (this.props.styleProperties[ + VECTOR_STYLES.SYMBOLIZE_AS + ] as SymbolizeAsProperty).isSymbolizedAsIcon() + ) { iconOrientationEditor = ( + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.ICON)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.ICON].options + this.state.defaultStaticProperties[VECTOR_STYLES.ICON].options as IconStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.ICON].options + this.state.defaultDynamicProperties[VECTOR_STYLES.ICON].options as IconDynamicOptions } /> @@ -341,13 +436,18 @@ export class VectorStyleEditor extends Component { + } + fields={this.state.styleFieldsHelper!.getFieldsForStyle(VECTOR_STYLES.ICON_SIZE)} defaultStaticStyleOptions={ - this.state.defaultStaticProperties[VECTOR_STYLES.ICON_SIZE].options + this.state.defaultStaticProperties[VECTOR_STYLES.ICON_SIZE].options as SizeStaticOptions } defaultDynamicStyleOptions={ - this.state.defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options + this.state.defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE] + .options as SizeDynamicOptions } /> @@ -385,7 +485,7 @@ export class VectorStyleEditor extends Component { _renderProperties() { const { supportedFeatures, selectedFeature, styleFieldsHelper } = this.state; - if (!supportedFeatures || !styleFieldsHelper) { + if (supportedFeatures.length === 0 || !styleFieldsHelper) { return null; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_fields_helper.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_fields_helper.ts index 8613f9e1e946f..fbe643a401484 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_fields_helper.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_fields_helper.ts @@ -34,7 +34,7 @@ export async function createStyleFieldsHelper(fields: IField[]): Promise = {}, + isTimeAware = true + ) { return { type: LAYER_STYLE_TYPE.VECTOR, - properties: { ...getDefaultProperties(), ...properties }, + properties: { ...getDefaultStaticProperties(), ...properties }, isTimeAware, }; } static createDefaultStyleProperties(mapColors: string[]) { - return getDefaultProperties(mapColors); + return getDefaultStaticProperties(mapColors); } constructor( @@ -192,7 +188,7 @@ export class VectorStyle implements IVectorStyle { this._styleMeta = new StyleMeta(this._descriptor.__styleMeta); this._symbolizeAsStyleProperty = new SymbolizeAsProperty( - this._descriptor.properties[VECTOR_STYLES.SYMBOLIZE_AS]!.options, + this._descriptor.properties[VECTOR_STYLES.SYMBOLIZE_AS].options, VECTOR_STYLES.SYMBOLIZE_AS ); this._lineColorStyleProperty = this._makeColorProperty( @@ -237,7 +233,7 @@ export class VectorStyle implements IVectorStyle { VECTOR_STYLES.LABEL_BORDER_COLOR ); this._labelBorderSizeStyleProperty = new LabelBorderSizeProperty( - this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_SIZE]!.options, + this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_SIZE].options, VECTOR_STYLES.LABEL_BORDER_SIZE, this._labelSizeStyleProperty ); @@ -270,16 +266,10 @@ export class VectorStyle implements IVectorStyle { : (this._lineWidthStyleProperty as StaticSizeProperty).getOptions().size !== 0; } - renderEditor({ - layer, - onStyleDescriptorChange, - }: { - layer: ILayer; - onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; - }) { + renderEditor(onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void) { const rawProperties = this.getRawProperties(); - const handlePropertyChange = (propertyName: VECTOR_STYLES, settings: any) => { - rawProperties[propertyName] = settings; // override single property, but preserve the rest + const handlePropertyChange = (propertyName: VECTOR_STYLES, stylePropertyDescriptor: any) => { + rawProperties[propertyName] = stylePropertyDescriptor; // override single property, but preserve the rest const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, this.isTimeAware()); onStyleDescriptorChange(vectorStyleDescriptor); }; @@ -293,9 +283,8 @@ export class VectorStyle implements IVectorStyle { return dynamicStyleProp.isFieldMetaEnabled(); }); - const styleProperties: VectorStylePropertiesDescriptor = {}; + const styleProperties: StyleProperties = {}; this.getAllStyleProperties().forEach((styleProperty) => { - // @ts-expect-error styleProperties[styleProperty.getStyleName()] = styleProperty; }); @@ -303,7 +292,7 @@ export class VectorStyle implements IVectorStyle { { test('Should use first color in DEFAULT_*_COLORS when no colors are used on the map', () => { const styleProperties = getDefaultStaticProperties([]); - expect(styleProperties[VECTOR_STYLES.FILL_COLOR]!.options.color).toBe('#54B399'); - expect(styleProperties[VECTOR_STYLES.LINE_COLOR]!.options.color).toBe('#41937c'); + expect(styleProperties[VECTOR_STYLES.FILL_COLOR].options.color).toBe('#54B399'); + expect(styleProperties[VECTOR_STYLES.LINE_COLOR].options.color).toBe('#41937c'); }); test('Should next color in DEFAULT_*_COLORS when colors are used on the map', () => { const styleProperties = getDefaultStaticProperties(['#54B399']); - expect(styleProperties[VECTOR_STYLES.FILL_COLOR]!.options.color).toBe('#6092C0'); - expect(styleProperties[VECTOR_STYLES.LINE_COLOR]!.options.color).toBe('#4379aa'); + expect(styleProperties[VECTOR_STYLES.FILL_COLOR].options.color).toBe('#6092C0'); + expect(styleProperties[VECTOR_STYLES.LINE_COLOR].options.color).toBe('#4379aa'); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts index 50321510c2ba8..fc152b9e5a974 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts @@ -37,22 +37,6 @@ export const POLYGON_STYLES = [ VECTOR_STYLES.LINE_WIDTH, ]; -export function getDefaultProperties(mapColors: string[] = []): VectorStylePropertiesDescriptor { - return { - ...getDefaultStaticProperties(mapColors), - [VECTOR_STYLES.SYMBOLIZE_AS]: { - options: { - value: SYMBOLIZE_AS_TYPES.CIRCLE, - }, - }, - [VECTOR_STYLES.LABEL_BORDER_SIZE]: { - options: { - size: LABEL_BORDER_SIZES.SMALL, - }, - }, - }; -} - export function getDefaultStaticProperties( mapColors: string[] = [] ): VectorStylePropertiesDescriptor { @@ -129,6 +113,16 @@ export function getDefaultStaticProperties( color: isDarkMode ? '#000000' : '#FFFFFF', }, }, + [VECTOR_STYLES.SYMBOLIZE_AS]: { + options: { + value: SYMBOLIZE_AS_TYPES.CIRCLE, + }, + }, + [VECTOR_STYLES.LABEL_BORDER_SIZE]: { + options: { + size: LABEL_BORDER_SIZES.SMALL, + }, + }, }; } @@ -244,5 +238,15 @@ export function getDefaultDynamicProperties(): VectorStylePropertiesDescriptor { }, }, }, + [VECTOR_STYLES.SYMBOLIZE_AS]: { + options: { + value: SYMBOLIZE_AS_TYPES.CIRCLE, + }, + }, + [VECTOR_STYLES.LABEL_BORDER_SIZE]: { + options: { + size: LABEL_BORDER_SIZES.SMALL, + }, + }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js b/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js index 69cf51fb29c0d..e460d7728a319 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js @@ -11,9 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiPanel, EuiSpacer } from '@elast import { FormattedMessage } from '@kbn/i18n/react'; export function StyleSettings({ layer, updateStyleDescriptor }) { - const settingsEditor = layer.renderStyleEditor({ - onStyleDescriptorChange: updateStyleDescriptor, - }); + const settingsEditor = layer.renderStyleEditor(updateStyleDescriptor); if (!settingsEditor) { return null; From 9b30de41b6cf682b97e5a0518a18bb142fdb96bd Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 18 Nov 2020 22:04:26 -0700 Subject: [PATCH 97/99] [data.search] Server-side background session service (#81099) * [Search] Add request context and asScoped pattern * Update docs * Unify interface for getting search client * [WIP] [data.search] Server-side background session service * Update examples/search_examples/server/my_strategy.ts Co-authored-by: Anton Dosov * Review feedback * Fix checks * Add tapFirst and additional props for session * Fix CI * Fix security search * Fix test * Fix test for reals * Add restore method * Add code to search examples * Add restore and search using restored ID * Fix handling of preference and order of params * Trim & cleanup * Fix types * Review feedback * Add tests and remove handling of username * Update docs * Move utils to server * Review feedback * More review feedback * Regenerate docs * Review feedback * Doc changes Co-authored-by: Anton Dosov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...ns-data-public.isearchoptions.isrestore.md | 13 + ...ins-data-public.isearchoptions.isstored.md | 13 + ...ugin-plugins-data-public.isearchoptions.md | 2 + ...gins-data-public.isessionservice.delete.md | 13 + ...lugins-data-public.isessionservice.find.md | 13 + ...plugins-data-public.isessionservice.get.md | 13 + ...s-data-public.isessionservice.isrestore.md | 13 + ...ns-data-public.isessionservice.isstored.md | 13 + ...gin-plugins-data-public.isessionservice.md | 9 +- ...ins-data-public.isessionservice.restore.md | 2 +- ...lugins-data-public.isessionservice.save.md | 13 + ...gins-data-public.isessionservice.update.md | 13 + ...ns-data-server.isearchoptions.isrestore.md | 13 + ...ins-data-server.isearchoptions.isstored.md | 13 + ...ugin-plugins-data-server.isearchoptions.md | 2 + examples/search_examples/kibana.json | 2 +- .../data/common/search/session/index.ts | 1 + .../data/common/search/session/mocks.ts | 7 + .../data/common/search/session/status.ts | 26 ++ .../data/common/search/session/types.ts | 62 ++++- src/plugins/data/common/search/types.ts | 11 + src/plugins/data/common/utils/index.ts | 1 + .../data/common/utils/tap_first.test.ts | 30 +++ src/plugins/data/common/utils/tap_first.ts | 31 +++ src/plugins/data/public/public.api.md | 25 +- .../data/public/search/search_interceptor.ts | 19 +- .../data/public/search/session_service.ts | 69 +++++- .../saved_objects/background_session.ts | 56 +++++ .../data/server/saved_objects/index.ts | 1 + src/plugins/data/server/search/mocks.ts | 21 ++ .../data/server/search/routes/search.ts | 14 +- .../data/server/search/routes/session.test.ts | 119 +++++++++ .../data/server/search/routes/session.ts | 201 +++++++++++++++ .../data/server/search/search_service.ts | 44 +++- .../data/server/search/session/index.ts | 20 ++ .../search/session/session_service.test.ts | 233 ++++++++++++++++++ .../server/search/session/session_service.ts | 204 +++++++++++++++ .../data/server/search/session/utils.test.ts | 37 +++ .../data/server/search/session/utils.ts | 30 +++ src/plugins/data/server/server.api.md | 2 + src/plugins/embeddable/public/public.api.md | 4 +- .../public/search/search_interceptor.ts | 8 +- .../server/search/es_search_strategy.ts | 1 + 43 files changed, 1407 insertions(+), 30 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isrestore.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isstored.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isrestore.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isstored.md create mode 100644 src/plugins/data/common/search/session/status.ts create mode 100644 src/plugins/data/common/utils/tap_first.test.ts create mode 100644 src/plugins/data/common/utils/tap_first.ts create mode 100644 src/plugins/data/server/saved_objects/background_session.ts create mode 100644 src/plugins/data/server/search/routes/session.test.ts create mode 100644 src/plugins/data/server/search/routes/session.ts create mode 100644 src/plugins/data/server/search/session/index.ts create mode 100644 src/plugins/data/server/search/session/session_service.test.ts create mode 100644 src/plugins/data/server/search/session/session_service.ts create mode 100644 src/plugins/data/server/search/session/utils.test.ts create mode 100644 src/plugins/data/server/search/session/utils.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isrestore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isrestore.md new file mode 100644 index 0000000000000..672d77719962f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isrestore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) + +## ISearchOptions.isRestore property + +Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) + +Signature: + +```typescript +isRestore?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isstored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isstored.md new file mode 100644 index 0000000000000..0d2c173f351c8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.isstored.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) + +## ISearchOptions.isStored property + +Whether the session is already saved (i.e. sent to background) + +Signature: + +```typescript +isStored?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index 76d0914173447..5acd837495dac 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -15,6 +15,8 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [isRestore](./kibana-plugin-plugins-data-public.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | +| [isStored](./kibana-plugin-plugins-data-public.isearchoptions.isstored.md) | boolean | Whether the session is already saved (i.e. sent to background) | | [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md new file mode 100644 index 0000000000000..eabb966160c4d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.delete.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) + +## ISessionService.delete property + +Deletes a session + +Signature: + +```typescript +delete: (sessionId: string) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md new file mode 100644 index 0000000000000..58e2fea0e6fe9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.find.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) + +## ISessionService.find property + +Gets a list of saved sessions + +Signature: + +```typescript +find: (options: SearchSessionFindOptions) => Promise>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md new file mode 100644 index 0000000000000..a2dff2f18253b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) + +## ISessionService.get property + +Gets a saved session + +Signature: + +```typescript +get: (sessionId: string) => Promise>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md new file mode 100644 index 0000000000000..8d8cd1f31bb95 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isrestore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) + +## ISessionService.isRestore property + +Whether the active session is restored (i.e. reusing previous search IDs) + +Signature: + +```typescript +isRestore: () => boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md new file mode 100644 index 0000000000000..db737880bb84e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.isstored.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) + +## ISessionService.isStored property + +Whether the active session is already saved (i.e. sent to background) + +Signature: + +```typescript +isStored: () => boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md index 174f9dbe66bf4..02c0a821e552d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.md @@ -15,8 +15,15 @@ export interface ISessionService | Property | Type | Description | | --- | --- | --- | | [clear](./kibana-plugin-plugins-data-public.isessionservice.clear.md) | () => void | Clears the active session. | +| [delete](./kibana-plugin-plugins-data-public.isessionservice.delete.md) | (sessionId: string) => Promise<void> | Deletes a session | +| [find](./kibana-plugin-plugins-data-public.isessionservice.find.md) | (options: SearchSessionFindOptions) => Promise<SavedObjectsFindResponse<BackgroundSessionSavedObjectAttributes>> | Gets a list of saved sessions | +| [get](./kibana-plugin-plugins-data-public.isessionservice.get.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Gets a saved session | | [getSession$](./kibana-plugin-plugins-data-public.isessionservice.getsession_.md) | () => Observable<string | undefined> | Returns the observable that emits an update every time the session ID changes | | [getSessionId](./kibana-plugin-plugins-data-public.isessionservice.getsessionid.md) | () => string | undefined | Returns the active session ID | -| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => void | Restores existing session | +| [isRestore](./kibana-plugin-plugins-data-public.isessionservice.isrestore.md) | () => boolean | Whether the active session is restored (i.e. reusing previous search IDs) | +| [isStored](./kibana-plugin-plugins-data-public.isessionservice.isstored.md) | () => boolean | Whether the active session is already saved (i.e. sent to background) | +| [restore](./kibana-plugin-plugins-data-public.isessionservice.restore.md) | (sessionId: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Restores existing session | +| [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) | (name: string, url: string) => Promise<SavedObject<BackgroundSessionSavedObjectAttributes>> | Saves a session | | [start](./kibana-plugin-plugins-data-public.isessionservice.start.md) | () => string | Starts a new session | +| [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) | (sessionId: string, attributes: Partial<BackgroundSessionSavedObjectAttributes>) => Promise<any> | Updates a session | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md index 857e85bbd30eb..96106a6ef7e2d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.restore.md @@ -9,5 +9,5 @@ Restores existing session Signature: ```typescript -restore: (sessionId: string) => void; +restore: (sessionId: string) => Promise>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md new file mode 100644 index 0000000000000..4ac4a96614467 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.save.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [save](./kibana-plugin-plugins-data-public.isessionservice.save.md) + +## ISessionService.save property + +Saves a session + +Signature: + +```typescript +save: (name: string, url: string) => Promise>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md new file mode 100644 index 0000000000000..5e2ff53d58ab7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isessionservice.update.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) > [update](./kibana-plugin-plugins-data-public.isessionservice.update.md) + +## ISessionService.update property + +Updates a session + +Signature: + +```typescript +update: (sessionId: string, attributes: Partial) => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isrestore.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isrestore.md new file mode 100644 index 0000000000000..ae518e5a052fc --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isrestore.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) + +## ISearchOptions.isRestore property + +Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) + +Signature: + +```typescript +isRestore?: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isstored.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isstored.md new file mode 100644 index 0000000000000..aceee7fd6df68 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.isstored.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) + +## ISearchOptions.isStored property + +Whether the session is already saved (i.e. sent to background) + +Signature: + +```typescript +isStored?: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index af96e1413ba0c..85847e1c61d25 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -15,6 +15,8 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [isRestore](./kibana-plugin-plugins-data-server.isearchoptions.isrestore.md) | boolean | Whether the session is restored (i.e. search requests should re-use the stored search IDs, rather than starting from scratch) | +| [isStored](./kibana-plugin-plugins-data-server.isearchoptions.isstored.md) | boolean | Whether the session is already saved (i.e. sent to background) | | [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/examples/search_examples/kibana.json b/examples/search_examples/kibana.json index 9577ec353a4c9..07bb6a0b750e3 100644 --- a/examples/search_examples/kibana.json +++ b/examples/search_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["navigation", "data", "developerExamples"], + "requiredPlugins": ["navigation", "data", "developerExamples", "kibanaUtils"], "optionalPlugins": [], "requiredBundles": [] } diff --git a/src/plugins/data/common/search/session/index.ts b/src/plugins/data/common/search/session/index.ts index d8f7b5091eb8f..0feb43f8f1d4b 100644 --- a/src/plugins/data/common/search/session/index.ts +++ b/src/plugins/data/common/search/session/index.ts @@ -17,4 +17,5 @@ * under the License. */ +export * from './status'; export * from './types'; diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts index 370faaa640c56..4604e15e4e93b 100644 --- a/src/plugins/data/common/search/session/mocks.ts +++ b/src/plugins/data/common/search/session/mocks.ts @@ -27,5 +27,12 @@ export function getSessionServiceMock(): jest.Mocked { restore: jest.fn(), getSessionId: jest.fn(), getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()), + isStored: jest.fn(), + isRestore: jest.fn(), + save: jest.fn(), + get: jest.fn(), + find: jest.fn(), + update: jest.fn(), + delete: jest.fn(), }; } diff --git a/src/plugins/data/common/search/session/status.ts b/src/plugins/data/common/search/session/status.ts new file mode 100644 index 0000000000000..1f6b6eb3084bb --- /dev/null +++ b/src/plugins/data/common/search/session/status.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum BackgroundSessionStatus { + IN_PROGRESS = 'in_progress', + ERROR = 'error', + COMPLETE = 'complete', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts index 6660b8395547f..d1ab22057695a 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/common/search/session/types.ts @@ -18,6 +18,7 @@ */ import { Observable } from 'rxjs'; +import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; export interface ISessionService { /** @@ -30,6 +31,17 @@ export interface ISessionService { * @returns `Observable` */ getSession$: () => Observable; + + /** + * Whether the active session is already saved (i.e. sent to background) + */ + isStored: () => boolean; + + /** + * Whether the active session is restored (i.e. reusing previous search IDs) + */ + isRestore: () => boolean; + /** * Starts a new session */ @@ -38,10 +50,58 @@ export interface ISessionService { /** * Restores existing session */ - restore: (sessionId: string) => void; + restore: (sessionId: string) => Promise>; /** * Clears the active session. */ clear: () => void; + + /** + * Saves a session + */ + save: (name: string, url: string) => Promise>; + + /** + * Gets a saved session + */ + get: (sessionId: string) => Promise>; + + /** + * Gets a list of saved sessions + */ + find: ( + options: SearchSessionFindOptions + ) => Promise>; + + /** + * Updates a session + */ + update: ( + sessionId: string, + attributes: Partial + ) => Promise; + + /** + * Deletes a session + */ + delete: (sessionId: string) => Promise; +} + +export interface BackgroundSessionSavedObjectAttributes { + name: string; + created: string; + expires: string; + status: string; + initialState: Record; + restoreState: Record; + idMapping: Record; +} + +export interface SearchSessionFindOptions { + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + filter?: string; } diff --git a/src/plugins/data/common/search/types.ts b/src/plugins/data/common/search/types.ts index 7451edf5e2fa3..695ee34d3b468 100644 --- a/src/plugins/data/common/search/types.ts +++ b/src/plugins/data/common/search/types.ts @@ -92,4 +92,15 @@ export interface ISearchOptions { * A session ID, grouping multiple search requests into a single session. */ sessionId?: string; + + /** + * Whether the session is already saved (i.e. sent to background) + */ + isStored?: boolean; + + /** + * Whether the session is restored (i.e. search requests should re-use the stored search IDs, + * rather than starting from scratch) + */ + isRestore?: boolean; } diff --git a/src/plugins/data/common/utils/index.ts b/src/plugins/data/common/utils/index.ts index 8b8686c51b9c1..4b602cb963a8f 100644 --- a/src/plugins/data/common/utils/index.ts +++ b/src/plugins/data/common/utils/index.ts @@ -19,3 +19,4 @@ /** @internal */ export { shortenDottedString } from './shorten_dotted_string'; +export { tapFirst } from './tap_first'; diff --git a/src/plugins/data/common/utils/tap_first.test.ts b/src/plugins/data/common/utils/tap_first.test.ts new file mode 100644 index 0000000000000..033ae59f8c715 --- /dev/null +++ b/src/plugins/data/common/utils/tap_first.test.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { of } from 'rxjs'; +import { tapFirst } from './tap_first'; + +describe('tapFirst', () => { + it('should tap the first and only the first', () => { + const fn = jest.fn(); + of(1, 2, 3).pipe(tapFirst(fn)).subscribe(); + expect(fn).toBeCalledTimes(1); + expect(fn).lastCalledWith(1); + }); +}); diff --git a/src/plugins/data/common/utils/tap_first.ts b/src/plugins/data/common/utils/tap_first.ts new file mode 100644 index 0000000000000..2c783a3ef87f0 --- /dev/null +++ b/src/plugins/data/common/utils/tap_first.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { pipe } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +export function tapFirst(next: (x: T) => void) { + let isFirst = true; + return pipe( + tap((x: T) => { + if (isFirst) next(x); + isFirst = false; + }) + ); +} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 165e11517311c..6c4609e5506c2 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -70,10 +70,12 @@ import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; -import { SavedObject } from 'src/core/server'; -import { SavedObject as SavedObject_2 } from 'src/core/public'; +import { SavedObject } from 'kibana/server'; +import { SavedObject as SavedObject_2 } from 'src/core/server'; +import { SavedObject as SavedObject_3 } from 'src/core/public'; import { SavedObjectReference } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObjectsFindResponse } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; @@ -1389,7 +1391,7 @@ export class IndexPatternsService { // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts // // (undocumented) - getCache: () => Promise[] | null | undefined>; + getCache: () => Promise[] | null | undefined>; getDefault: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts @@ -1401,7 +1403,7 @@ export class IndexPatternsService { }>>; getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; - savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; + savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; setDefault: (id: string, force?: boolean) => Promise; updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; } @@ -1446,6 +1448,8 @@ export type ISearchGeneric = | undefined // @public (undocumented) export interface ISessionService { clear: () => void; + delete: (sessionId: string) => Promise; + // Warning: (ae-forgotten-export) The symbol "SearchSessionFindOptions" needs to be exported by the entry point index.d.ts + find: (options: SearchSessionFindOptions) => Promise>; + get: (sessionId: string) => Promise>; getSession$: () => Observable; getSessionId: () => string | undefined; - restore: (sessionId: string) => void; + isRestore: () => boolean; + isStored: () => boolean; + // Warning: (ae-forgotten-export) The symbol "BackgroundSessionSavedObjectAttributes" needs to be exported by the entry point index.d.ts + restore: (sessionId: string) => Promise>; + save: (name: string, url: string) => Promise>; start: () => string; + update: (sessionId: string, attributes: Partial) => Promise; } // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2069,7 +2082,7 @@ export class SearchInterceptor { // @internal protected pendingCount$: BehaviorSubject; // @internal (undocumented) - protected runSearch(request: IKibanaSearchRequest, signal: AbortSignal, strategy?: string): Promise; + protected runSearch(request: IKibanaSearchRequest, options?: ISearchOptions): Promise; search(request: IKibanaSearchRequest, options?: ISearchOptions): Observable; // @internal (undocumented) protected setupAbortSignal({ abortSignal, timeout, }: { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 78e65802bcf99..3fadb723b27cd 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -126,18 +126,25 @@ export class SearchInterceptor { */ protected runSearch( request: IKibanaSearchRequest, - signal: AbortSignal, - strategy?: string + options?: ISearchOptions ): Promise { const { id, ...searchRequest } = request; - const path = trimEnd(`/internal/search/${strategy || ES_SEARCH_STRATEGY}/${id || ''}`, '/'); - const body = JSON.stringify(searchRequest); + const path = trimEnd( + `/internal/search/${options?.strategy ?? ES_SEARCH_STRATEGY}/${id ?? ''}`, + '/' + ); + const body = JSON.stringify({ + sessionId: options?.sessionId, + isStored: options?.isStored, + isRestore: options?.isRestore, + ...searchRequest, + }); return this.deps.http.fetch({ method: 'POST', path, body, - signal, + signal: options?.abortSignal, }); } @@ -235,7 +242,7 @@ export class SearchInterceptor { abortSignal: options?.abortSignal, }); this.pendingCount$.next(this.pendingCount$.getValue() + 1); - return from(this.runSearch(request, combinedSignal, options?.strategy)).pipe( + return from(this.runSearch(request, { ...options, abortSignal: combinedSignal })).pipe( catchError((e: Error) => { return throwError(this.handleSearchError(e, request, timeoutSignal, options)); }), diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts index a172738812937..0141cff258a9f 100644 --- a/src/plugins/data/public/search/session_service.ts +++ b/src/plugins/data/public/search/session_service.ts @@ -19,9 +19,13 @@ import uuid from 'uuid'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; +import { HttpStart, PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; import { ConfigSchema } from '../../config'; -import { ISessionService } from '../../common/search'; +import { + ISessionService, + BackgroundSessionSavedObjectAttributes, + SearchSessionFindOptions, +} from '../../common'; export class SessionService implements ISessionService { private session$ = new BehaviorSubject(undefined); @@ -30,6 +34,18 @@ export class SessionService implements ISessionService { } private appChangeSubscription$?: Subscription; private curApp?: string; + private http!: HttpStart; + + /** + * Has the session already been stored (i.e. "sent to background")? + */ + private _isStored: boolean = false; + + /** + * Is this session a restored session (have these requests already been made, and we're just + * looking to re-use the previous search IDs)? + */ + private _isRestore: boolean = false; constructor( initializerContext: PluginInitializerContext, @@ -39,6 +55,8 @@ export class SessionService implements ISessionService { Make sure that apps don't leave sessions open. */ getStartServices().then(([coreStart]) => { + this.http = coreStart.http; + this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { if (this.sessionId) { const message = `Application '${this.curApp}' had an open session while navigating`; @@ -69,16 +87,63 @@ export class SessionService implements ISessionService { return this.session$.asObservable(); } + public isStored() { + return this._isStored; + } + + public isRestore() { + return this._isRestore; + } + public start() { + this._isStored = false; + this._isRestore = false; this.session$.next(uuid.v4()); return this.sessionId!; } public restore(sessionId: string) { + this._isStored = true; + this._isRestore = true; this.session$.next(sessionId); + return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); } public clear() { + this._isStored = false; + this._isRestore = false; this.session$.next(undefined); } + + public async save(name: string, url: string) { + const response = await this.http.post(`/internal/session`, { + body: JSON.stringify({ + name, + url, + sessionId: this.sessionId, + }), + }); + this._isStored = true; + return response; + } + + public get(sessionId: string) { + return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); + } + + public find(options: SearchSessionFindOptions) { + return this.http.post(`/internal/session`, { + body: JSON.stringify(options), + }); + } + + public update(sessionId: string, attributes: Partial) { + return this.http.put(`/internal/session/${encodeURIComponent(sessionId)}`, { + body: JSON.stringify(attributes), + }); + } + + public delete(sessionId: string) { + return this.http.delete(`/internal/session/${encodeURIComponent(sessionId)}`); + } } diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/src/plugins/data/server/saved_objects/background_session.ts new file mode 100644 index 0000000000000..74b03c4d867e4 --- /dev/null +++ b/src/plugins/data/server/saved_objects/background_session.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsType } from 'kibana/server'; + +export const BACKGROUND_SESSION_TYPE = 'background-session'; + +export const backgroundSessionMapping: SavedObjectsType = { + name: BACKGROUND_SESSION_TYPE, + namespaceType: 'single', + hidden: true, + mappings: { + properties: { + name: { + type: 'keyword', + }, + created: { + type: 'date', + }, + expires: { + type: 'date', + }, + status: { + type: 'keyword', + }, + initialState: { + type: 'object', + enabled: false, + }, + restoreState: { + type: 'object', + enabled: false, + }, + idMapping: { + type: 'object', + enabled: false, + }, + }, + }, +}; diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts index 077f9380823d0..7cd4d319e6417 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -20,3 +20,4 @@ export { querySavedObjectType } from './query'; export { indexPatternSavedObjectType } from './index_patterns'; export { kqlTelemetry } from './kql_telemetry'; export { searchTelemetry } from './search_telemetry'; +export { BACKGROUND_SESSION_TYPE, backgroundSessionMapping } from './background_session'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 4914726c85ef8..290e94ee7cf99 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -17,6 +17,8 @@ * under the License. */ +import type { RequestHandlerContext } from 'src/core/server'; +import { coreMock } from '../../../../core/server/mocks'; import { ISearchSetup, ISearchStart } from './types'; import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; @@ -40,3 +42,22 @@ export function createSearchStartMock(): jest.Mocked { searchSource: searchSourceMock.createStartContract(), }; } + +export function createSearchRequestHandlerContext(): jest.Mocked { + return { + core: coreMock.createRequestHandlerContext(), + search: { + search: jest.fn(), + cancel: jest.fn(), + session: { + save: jest.fn(), + get: jest.fn(), + find: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + trackId: jest.fn(), + getId: jest.fn(), + }, + }, + }; +} diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index a4161fe47b388..ed519164c8e43 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -35,11 +35,18 @@ export function registerSearchRoute(router: IRouter): void { query: schema.object({}, { unknowns: 'allow' }), - body: schema.object({}, { unknowns: 'allow' }), + body: schema.object( + { + sessionId: schema.maybe(schema.string()), + isStored: schema.maybe(schema.boolean()), + isRestore: schema.maybe(schema.boolean()), + }, + { unknowns: 'allow' } + ), }, }, async (context, request, res) => { - const searchRequest = request.body; + const { sessionId, isStored, isRestore, ...searchRequest } = request.body; const { strategy, id } = request.params; const abortSignal = getRequestAbortedSignal(request.events.aborted$); @@ -50,6 +57,9 @@ export function registerSearchRoute(router: IRouter): void { { abortSignal, strategy, + sessionId, + isStored, + isRestore, } ) .pipe(first()) diff --git a/src/plugins/data/server/search/routes/session.test.ts b/src/plugins/data/server/search/routes/session.test.ts new file mode 100644 index 0000000000000..f697f6d5a5c2b --- /dev/null +++ b/src/plugins/data/server/search/routes/session.test.ts @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { MockedKeys } from '@kbn/utility-types/jest'; +import type { CoreSetup, RequestHandlerContext } from 'kibana/server'; +import type { DataPluginStart } from '../../plugin'; +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { createSearchRequestHandlerContext } from '../mocks'; +import { registerSessionRoutes } from './session'; + +describe('registerSessionRoutes', () => { + let mockCoreSetup: MockedKeys>; + let mockContext: jest.Mocked; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + mockContext = createSearchRequestHandlerContext(); + registerSessionRoutes(mockCoreSetup.http.createRouter()); + }); + + it('save calls session.save with sessionId and attributes', async () => { + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const name = 'my saved background search session'; + const body = { sessionId, name }; + + const mockRequest = httpServerMock.createKibanaRequest({ body }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [[, saveHandler]] = mockRouter.post.mock.calls; + + saveHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.save).toHaveBeenCalledWith(sessionId, { name }); + }); + + it('get calls session.get with sessionId', async () => { + const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const params = { id }; + + const mockRequest = httpServerMock.createKibanaRequest({ params }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [[, getHandler]] = mockRouter.get.mock.calls; + + getHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.get).toHaveBeenCalledWith(id); + }); + + it('find calls session.find with options', async () => { + const page = 1; + const perPage = 5; + const sortField = 'my_field'; + const sortOrder = 'desc'; + const filter = 'foo: bar'; + const body = { page, perPage, sortField, sortOrder, filter }; + + const mockRequest = httpServerMock.createKibanaRequest({ body }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [, [, findHandler]] = mockRouter.post.mock.calls; + + findHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.find).toHaveBeenCalledWith(body); + }); + + it('update calls session.update with id and attributes', async () => { + const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const name = 'my saved background search session'; + const expires = new Date().toISOString(); + const params = { id }; + const body = { name, expires }; + + const mockRequest = httpServerMock.createKibanaRequest({ params, body }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [[, updateHandler]] = mockRouter.put.mock.calls; + + updateHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.update).toHaveBeenCalledWith(id, body); + }); + + it('delete calls session.delete with id', async () => { + const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const params = { id }; + + const mockRequest = httpServerMock.createKibanaRequest({ params }); + const mockResponse = httpServerMock.createResponseFactory(); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const [[, deleteHandler]] = mockRouter.delete.mock.calls; + + deleteHandler(mockContext, mockRequest, mockResponse); + + expect(mockContext.search!.session.delete).toHaveBeenCalledWith(id); + }); +}); diff --git a/src/plugins/data/server/search/routes/session.ts b/src/plugins/data/server/search/routes/session.ts new file mode 100644 index 0000000000000..93f07ecfb92ff --- /dev/null +++ b/src/plugins/data/server/search/routes/session.ts @@ -0,0 +1,201 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerSessionRoutes(router: IRouter): void { + router.post( + { + path: '/internal/session', + validate: { + body: schema.object({ + sessionId: schema.string(), + name: schema.string(), + expires: schema.maybe(schema.string()), + initialState: schema.maybe(schema.object({}, { unknowns: 'allow' })), + restoreState: schema.maybe(schema.object({}, { unknowns: 'allow' })), + }), + }, + }, + async (context, request, res) => { + const { sessionId, name, expires, initialState, restoreState } = request.body; + + try { + const response = await context.search!.session.save(sessionId, { + name, + expires, + initialState, + restoreState, + }); + + return res.ok({ + body: response, + }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); + + router.get( + { + path: '/internal/session/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, res) => { + const { id } = request.params; + try { + const response = await context.search!.session.get(id); + + return res.ok({ + body: response, + }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); + + router.post( + { + path: '/internal/session/_find', + validate: { + body: schema.object({ + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.string()), + filter: schema.maybe(schema.string()), + }), + }, + }, + async (context, request, res) => { + const { page, perPage, sortField, sortOrder, filter } = request.body; + try { + const response = await context.search!.session.find({ + page, + perPage, + sortField, + sortOrder, + filter, + }); + + return res.ok({ + body: response, + }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); + + router.delete( + { + path: '/internal/session/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, res) => { + const { id } = request.params; + try { + await context.search!.session.delete(id); + + return res.ok(); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); + + router.put( + { + path: '/internal/session/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + name: schema.maybe(schema.string()), + expires: schema.maybe(schema.string()), + }), + }, + }, + async (context, request, res) => { + const { id } = request.params; + const { name, expires } = request.body; + try { + const response = await context.search!.session.update(id, { name, expires }); + + return res.ok({ + body: response, + }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); +} diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index d8aa588719e3e..b44980164d097 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, from, Observable } from 'rxjs'; import { pick } from 'lodash'; import { CoreSetup, @@ -29,7 +29,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; -import { first } from 'rxjs/operators'; +import { first, switchMap } from 'rxjs/operators'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { ISearchSetup, @@ -49,7 +49,7 @@ import { DataPluginStart } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; -import { searchTelemetry } from '../saved_objects'; +import { BACKGROUND_SESSION_TYPE, searchTelemetry } from '../saved_objects'; import { IEsSearchRequest, IEsSearchResponse, @@ -70,10 +70,14 @@ import { } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; +import { BackgroundSessionService, ISearchSessionClient } from './session'; +import { registerSessionRoutes } from './routes/session'; +import { backgroundSessionMapping } from '../saved_objects'; +import { tapFirst } from '../../common/utils'; declare module 'src/core/server' { interface RequestHandlerContext { - search?: ISearchClient; + search?: ISearchClient & { session: ISearchSessionClient }; } } @@ -102,6 +106,7 @@ export class SearchService implements Plugin { private readonly searchSourceService = new SearchSourceService(); private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; + private sessionService: BackgroundSessionService = new BackgroundSessionService(); constructor( private initializerContext: PluginInitializerContext, @@ -121,12 +126,17 @@ export class SearchService implements Plugin { }; registerSearchRoute(router); registerMsearchRoute(router, routeDependencies); + registerSessionRoutes(router); core.http.registerRouteHandlerContext('search', async (context, request) => { const [coreStart] = await core.getStartServices(); - return this.asScopedProvider(coreStart)(request); + const search = this.asScopedProvider(coreStart)(request); + const session = this.sessionService.asScopedProvider(coreStart)(request); + return { ...search, session }; }); + core.savedObjects.registerType(backgroundSessionMapping); + this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider( @@ -223,6 +233,7 @@ export class SearchService implements Plugin { public stop() { this.aggsService.stop(); + this.sessionService.stop(); } private registerSearchStrategy = < @@ -248,7 +259,24 @@ export class SearchService implements Plugin { options.strategy ); - return strategy.search(searchRequest, options, deps); + // If this is a restored background search session, look up the ID using the provided sessionId + const getSearchRequest = async () => + !options.isRestore || searchRequest.id + ? searchRequest + : { + ...searchRequest, + id: await this.sessionService.getId(searchRequest, options, deps), + }; + + return from(getSearchRequest()).pipe( + switchMap((request) => strategy.search(request, options, deps)), + tapFirst((response) => { + if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; + this.sessionService.trackId(searchRequest, response.id, options, { + savedObjectsClient: deps.savedObjectsClient, + }); + }) + ); }; private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { @@ -273,7 +301,9 @@ export class SearchService implements Plugin { private asScopedProvider = ({ elasticsearch, savedObjects, uiSettings }: CoreStart) => { return (request: KibanaRequest): ISearchClient => { - const savedObjectsClient = savedObjects.getScopedClient(request); + const savedObjectsClient = savedObjects.getScopedClient(request, { + includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + }); const deps = { savedObjectsClient, esClient: elasticsearch.client.asScoped(request), diff --git a/src/plugins/data/server/search/session/index.ts b/src/plugins/data/server/search/session/index.ts new file mode 100644 index 0000000000000..11b5b16a02b56 --- /dev/null +++ b/src/plugins/data/server/search/session/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { BackgroundSessionService, ISearchSessionClient } from './session_service'; diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts new file mode 100644 index 0000000000000..1ceebae967d4c --- /dev/null +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -0,0 +1,233 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { BackgroundSessionStatus } from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { BackgroundSessionService } from './session_service'; +import { createRequestHash } from './utils'; + +describe('BackgroundSessionService', () => { + let savedObjectsClient: jest.Mocked; + let service: BackgroundSessionService; + + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + idMapping: {}, + }, + references: [], + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + service = new BackgroundSessionService(); + }); + + it('save throws if `name` is not provided', () => { + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + + it('get calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const response = await service.get(sessionId, { savedObjectsClient }); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + it('find calls saved objects client', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find(options, { savedObjectsClient }); + + expect(response).toBe(mockResponse); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }); + + it('update calls saved objects client', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const attributes = { name: 'new_name' }; + const response = await service.update(sessionId, attributes, { savedObjectsClient }); + + expect(response).toBe(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }); + + it('delete calls saved objects client', async () => { + savedObjectsClient.delete.mockResolvedValue({}); + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const response = await service.delete(sessionId, { savedObjectsClient }); + + expect(response).toEqual({}); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + describe('trackId', () => { + it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const isStored = false; + const name = 'my saved background search session'; + const created = new Date().toISOString(); + const expires = new Date().toISOString(); + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + + await service.save(sessionId, { name, created, expires }, { savedObjectsClient }); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + { + name, + created, + expires, + initialState: {}, + restoreState: {}, + status: BackgroundSessionStatus.IN_PROGRESS, + idMapping: { [requestHash]: searchId }, + }, + { id: sessionId } + ); + }); + + it('updates saved object when `isStored` is `true`', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const isStored = true; + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { + idMapping: { [requestHash]: searchId }, + }); + }); + }); + + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId(searchRequest, {}, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); + + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + + expect(() => + service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); + + it('throws if not restoring a saved session', () => { + const searchRequest = { params: {} }; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + + expect(() => + service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: false }, + { savedObjectsClient } + ) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); + + it('returns the search ID from the saved object ID mapping', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const mockSession = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + idMapping: { [requestHash]: searchId }, + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + + const id = await service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: true }, + { savedObjectsClient } + ); + + expect(id).toBe(searchId); + }); + }); +}); diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts new file mode 100644 index 0000000000000..eca5f428b8555 --- /dev/null +++ b/src/plugins/data/server/search/session/session_service.ts @@ -0,0 +1,204 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { + BackgroundSessionSavedObjectAttributes, + IKibanaSearchRequest, + ISearchOptions, + SearchSessionFindOptions, + BackgroundSessionStatus, +} from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { createRequestHash } from './utils'; + +const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; + +export interface BackgroundSessionDependencies { + savedObjectsClient: SavedObjectsClientContract; +} + +export type ISearchSessionClient = ReturnType< + ReturnType +>; + +export class BackgroundSessionService { + /** + * Map of sessionId to { [requestHash]: searchId } + * @private + */ + private sessionSearchMap = new Map>(); + + constructor() {} + + public setup = () => {}; + + public start = (core: CoreStart) => { + return { + asScoped: this.asScopedProvider(core), + }; + }; + + public stop = () => { + this.sessionSearchMap.clear(); + }; + + // TODO: Generate the `userId` from the realm type/realm name/username + public save = async ( + sessionId: string, + { + name, + created = new Date().toISOString(), + expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), + status = BackgroundSessionStatus.IN_PROGRESS, + initialState = {}, + restoreState = {}, + }: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + if (!name) throw new Error('Name is required'); + + // Get the mapping of request hash/search ID for this session + const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); + const idMapping = Object.fromEntries(searchMap.entries()); + const attributes = { name, created, expires, status, initialState, restoreState, idMapping }; + const session = await savedObjectsClient.create( + BACKGROUND_SESSION_TYPE, + attributes, + { id: sessionId } + ); + + // Clear out the entries for this session ID so they don't get saved next time + this.sessionSearchMap.delete(sessionId); + + return session; + }; + + // TODO: Throw an error if this session doesn't belong to this user + public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.get( + BACKGROUND_SESSION_TYPE, + sessionId + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public find = ( + options: SearchSessionFindOptions, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.find({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public update = ( + sessionId: string, + attributes: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.update( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); + }; + + /** + * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just + * store it in memory until a saved session exists. + * @internal + */ + public trackId = async ( + searchRequest: IKibanaSearchRequest, + searchId: string, + { sessionId, isStored }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId || !searchId) return; + const requestHash = createRequestHash(searchRequest.params); + + // If there is already a saved object for this session, update it to include this request/ID. + // Otherwise, just update the in-memory mapping for this session for when the session is saved. + if (isStored) { + const attributes = { idMapping: { [requestHash]: searchId } }; + await this.update(sessionId, attributes, deps); + } else { + const map = this.sessionSearchMap.get(sessionId) ?? new Map(); + map.set(requestHash, searchId); + this.sessionSearchMap.set(sessionId, map); + } + }; + + /** + * Look up an existing search ID that matches the given request in the given session so that the + * request can continue rather than restart. + * @internal + */ + public getId = async ( + searchRequest: IKibanaSearchRequest, + { sessionId, isStored, isRestore }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId) { + throw new Error('Session ID is required'); + } else if (!isStored) { + throw new Error('Cannot get search ID from a session that is not stored'); + } else if (!isRestore) { + throw new Error('Get search ID is only supported when restoring a session'); + } + + const session = await this.get(sessionId, deps); + const requestHash = createRequestHash(searchRequest.params); + if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { + throw new Error('No search ID in this session matching the given search request'); + } + + return session.attributes.idMapping[requestHash]; + }; + + public asScopedProvider = ({ savedObjects }: CoreStart) => { + return (request: KibanaRequest) => { + const savedObjectsClient = savedObjects.getScopedClient(request, { + includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + }); + const deps = { savedObjectsClient }; + return { + save: (sessionId: string, attributes: Partial) => + this.save(sessionId, attributes, deps), + get: (sessionId: string) => this.get(sessionId, deps), + find: (options: SearchSessionFindOptions) => this.find(options, deps), + update: (sessionId: string, attributes: Partial) => + this.update(sessionId, attributes, deps), + delete: (sessionId: string) => this.delete(sessionId, deps), + trackId: (searchRequest: IKibanaSearchRequest, searchId: string, options: ISearchOptions) => + this.trackId(searchRequest, searchId, options, deps), + getId: (searchRequest: IKibanaSearchRequest, options: ISearchOptions) => + this.getId(searchRequest, options, deps), + }; + }; + }; +} diff --git a/src/plugins/data/server/search/session/utils.test.ts b/src/plugins/data/server/search/session/utils.test.ts new file mode 100644 index 0000000000000..d190f892a7f84 --- /dev/null +++ b/src/plugins/data/server/search/session/utils.test.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createRequestHash } from './utils'; + +describe('data/search/session utils', () => { + describe('createRequestHash', () => { + it('ignores `preference`', () => { + const request = { + foo: 'bar', + }; + + const withPreference = { + ...request, + preference: 1234, + }; + + expect(createRequestHash(request)).toEqual(createRequestHash(withPreference)); + }); + }); +}); diff --git a/src/plugins/data/server/search/session/utils.ts b/src/plugins/data/server/search/session/utils.ts new file mode 100644 index 0000000000000..c3332f80b6e3f --- /dev/null +++ b/src/plugins/data/server/search/session/utils.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createHash } from 'crypto'; + +/** + * Generate the hash for this request so that, in the future, this hash can be used to look up + * existing search IDs for this request. Ignores the `preference` parameter since it generally won't + * match from one request to another identical request. + */ +export function createRequestHash(keys: Record) { + const { preference, ...params } = keys; + return createHash(`sha256`).update(JSON.stringify(params)).digest('hex'); +} diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index ce66610edf880..8d1699c4ad5ed 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -753,6 +753,8 @@ export class IndexPatternsService implements Plugin_3( - () => this.runSearch(request, combinedSignal, strategy), - (requestId) => this.runSearch({ ...request, id: requestId }, combinedSignal, strategy), + () => this.runSearch(request, { ...options, strategy, abortSignal: combinedSignal }), + (requestId) => + this.runSearch( + { ...request, id: requestId }, + { ...options, strategy, abortSignal: combinedSignal } + ), (r) => !r.isRunning, (response) => response.id, id, diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 53bcac02cb01d..2070610ceb20e 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -57,6 +57,7 @@ export const enhancedEsSearchStrategyProvider = ( utils.toSnakeCase({ ...(await getDefaultSearchParams(uiSettingsClient)), batchedReduceSize: 64, + keepOnCompletion: !!options.sessionId, // Always return an ID, even if the request completes quickly ...asyncOptions, ...request.params, }) From f2ad337fefc434cdd420e098c4d94a3a944f38b3 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 19 Nov 2020 07:05:10 +0100 Subject: [PATCH 98/99] Increase bulk request timeout during esArchiver load (#83657) This PR fixes some timeouts during esArchive load by increasing the request timeout. --- .../kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts index 28790176af73d..46c46ad5d1b68 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts @@ -44,7 +44,7 @@ export function createIndexDocRecordsStream( ); }); - const resp = await client.bulk({ body }); + const resp = await client.bulk({ requestTimeout: 2 * 60 * 1000, body }); if (resp.errors) { throw new Error(`Failed to index all documents: ${JSON.stringify(resp, null, 2)}`); } From 8d9e383980ddb9ff19338d6d748ed9dc38562e06 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 18 Nov 2020 22:29:11 -0800 Subject: [PATCH 99/99] Skip failing cypress test Signed-off-by: Tyler Smalley --- .../cypress/integration/alerts_detection_rules_custom.spec.ts | 3 ++- .../cypress/integration/alerts_detection_rules_export.spec.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index fb1f2920aaceb..d14e09d9384a2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -114,7 +114,8 @@ const expectedEditedtags = editedRule.tags.join(''); const expectedEditedIndexPatterns = editedRule.index && editedRule.index.length ? editedRule.index : indexPatterns; -describe('Custom detection rules creation', () => { +// SKIP: https://github.com/elastic/kibana/issues/83769 +describe.skip('Custom detection rules creation', () => { before(() => { esArchiverLoad('timeline'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index eb8448233c624..6f995045dfc6a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -17,7 +17,8 @@ import { DETECTIONS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -describe('Export rules', () => { +// SKIP: https://github.com/elastic/kibana/issues/83769 +describe.skip('Export rules', () => { before(() => { esArchiverLoad('export_rule'); cy.server();