From 7e5408b8fa487613dcff5d47c4e8a2c3127b91f4 Mon Sep 17 00:00:00 2001 From: Ioana Tagirta Date: Mon, 2 May 2022 15:26:46 +0200 Subject: [PATCH 01/50] Remove unused user filtering (#131316) --- .../applications/workplace_search/types.ts | 2 - .../groups/__mocks__/groups_logic.mock.ts | 7 +- .../groups/components/group_overview.test.tsx | 1 - .../views/groups/components/group_row.tsx | 6 -- .../workplace_search/views/groups/groups.tsx | 3 +- .../views/groups/groups_logic.test.ts | 101 +----------------- .../views/groups/groups_logic.ts | 78 +------------- .../routes/workplace_search/groups.test.ts | 1 - .../server/routes/workplace_search/groups.ts | 1 - .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 12 files changed, 6 insertions(+), 197 deletions(-) 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 19263f057e40f..984e6664681b4 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 @@ -30,8 +30,6 @@ export interface Group { createdAt: string; updatedAt: string; contentSources: ContentSource[]; - users: User[]; - usersCount: number; color?: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts index 0e072210d2489..02e80d9f8c5b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts @@ -6,12 +6,11 @@ */ import { DEFAULT_META } from '../../../../shared/constants'; -import { ContentSource, User, Group } from '../../../types'; +import { ContentSource, Group } from '../../../types'; export const mockGroupsValues = { groups: [] as Group[], contentSources: [] as ContentSource[], - users: [] as User[], groupsDataLoading: true, groupListLoading: true, newGroupModalOpen: false, @@ -21,10 +20,6 @@ export const mockGroupsValues = { newGroupNameErrors: [], filterSourcesDropdownOpen: false, filteredSources: [], - filterUsersDropdownOpen: false, - filteredUsers: [], - allGroupUsersLoading: false, - allGroupUsers: [], filterValue: '', groupsMeta: DEFAULT_META, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx index e1ecc47f02669..97b2879ceef53 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx @@ -91,7 +91,6 @@ describe('GroupOverview', () => { ...mockValues, group: { ...groups[0], - users: [], contentSources: [], }, }); 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 effacfa3aa4f8..d118037a2d80c 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 @@ -28,12 +28,6 @@ export const NO_SOURCES_MESSAGE = i18n.translate( defaultMessage: 'No organizational content sources', } ); -export const NO_USERS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage', - { - defaultMessage: 'No users', - } -); const dateDisplay = (date: string) => moment(date).isAfter(moment().subtract(DAYS_CUTOFF, 'days')) 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 7af93490b2eb2..5b8b01a4bb1ef 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 @@ -38,7 +38,6 @@ export const Groups: React.FC = () => { page: { total_results: numGroups }, }, filteredSources, - filteredUsers, filterValue, } = useValues(GroupsLogic); @@ -47,7 +46,7 @@ export const Groups: React.FC = () => { useEffect(() => { getSearchResults(true); return resetGroups; - }, [filteredSources, filteredUsers, filterValue]); + }, [filteredSources, filterValue]); if (newGroup && hasMessages) { messages[0].description = ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts index 97163f1529938..bc82c95871676 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -12,7 +12,6 @@ import { } from '../../../__mocks__/kea_logic'; import { contentSources } from '../../__mocks__/content_sources.mock'; import { groups } from '../../__mocks__/groups.mock'; -import { users } from '../../__mocks__/users.mock'; import { mockGroupsValues } from './__mocks__/groups_logic.mock'; import { nextTick } from '@kbn/test-jest-helpers'; @@ -49,13 +48,12 @@ describe('GroupsLogic', () => { describe('actions', () => { describe('onInitializeGroups', () => { it('sets reducers', () => { - GroupsLogic.actions.onInitializeGroups({ contentSources, users }); + GroupsLogic.actions.onInitializeGroups({ contentSources }); expect(GroupsLogic.values).toEqual({ ...mockGroupsValues, groupsDataLoading: false, contentSources, - users, }); }); }); @@ -103,59 +101,6 @@ describe('GroupsLogic', () => { }); }); - describe('addFilteredUser', () => { - it('sets reducers', () => { - GroupsLogic.actions.addFilteredUser('foo'); - GroupsLogic.actions.addFilteredUser('bar'); - GroupsLogic.actions.addFilteredUser('baz'); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - hasFiltersSet: true, - filteredUsers: ['bar', 'baz', 'foo'], - }); - }); - }); - - describe('removeFilteredUser', () => { - it('sets reducers', () => { - GroupsLogic.actions.addFilteredUser('foo'); - GroupsLogic.actions.addFilteredUser('bar'); - GroupsLogic.actions.addFilteredUser('baz'); - GroupsLogic.actions.removeFilteredUser('foo'); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - hasFiltersSet: true, - filteredUsers: ['bar', 'baz'], - }); - }); - }); - - describe('setGroupUsers', () => { - it('sets reducers', () => { - GroupsLogic.actions.setGroupUsers(users); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - allGroupUsersLoading: false, - allGroupUsers: users, - }); - }); - }); - - describe('setAllGroupLoading', () => { - it('sets reducer', () => { - GroupsLogic.actions.setAllGroupLoading(true); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - allGroupUsersLoading: true, - allGroupUsers: [], - }); - }); - }); - describe('setFilterValue', () => { it('sets reducer', () => { GroupsLogic.actions.setFilterValue('foo'); @@ -190,7 +135,6 @@ describe('GroupsLogic', () => { newGroup: groups[0], newGroupNameErrors: [], filteredSources: [], - filteredUsers: [], groupsMeta: DEFAULT_META, }); }); @@ -234,19 +178,6 @@ describe('GroupsLogic', () => { }); }); - describe('closeFilterUsersDropdown', () => { - it('sets reducer', () => { - // Open dropdown first - GroupsLogic.actions.toggleFilterUsersDropdown(); - GroupsLogic.actions.closeFilterUsersDropdown(); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - filterUsersDropdownOpen: false, - }); - }); - }); - describe('setGroupsLoading', () => { it('sets reducer', () => { // Set to false first @@ -294,7 +225,6 @@ describe('GroupsLogic', () => { const search = { query: '', content_source_ids: [], - user_ids: [], }; const payload = { @@ -352,22 +282,6 @@ describe('GroupsLogic', () => { }); }); - describe('fetchGroupUsers', () => { - it('calls API and sets values', async () => { - const setGroupUsersSpy = jest.spyOn(GroupsLogic.actions, 'setGroupUsers'); - http.get.mockReturnValue(Promise.resolve(users)); - - GroupsLogic.actions.fetchGroupUsers('123'); - expect(http.get).toHaveBeenCalledWith('/internal/workplace_search/groups/123/group_users'); - await nextTick(); - expect(setGroupUsersSpy).toHaveBeenCalledWith(users); - }); - - itShowsServerErrorAsFlashMessage(http.get, () => { - GroupsLogic.actions.fetchGroupUsers('123'); - }); - }); - describe('saveNewGroup', () => { it('calls API and sets values', async () => { const GROUP_NAME = 'new group'; @@ -430,7 +344,6 @@ describe('GroupsLogic', () => { expect(GroupsLogic.values).toEqual({ ...mockGroupsValues, filteredSources: [], - filteredUsers: [], filterValue: '', groupsMeta: DEFAULT_META, }); @@ -449,17 +362,5 @@ describe('GroupsLogic', () => { expect(clearFlashMessages).toHaveBeenCalled(); }); }); - - describe('toggleFilterUsersDropdown', () => { - it('sets reducer and clears flash messages', () => { - GroupsLogic.actions.toggleFilterUsersDropdown(); - - expect(GroupsLogic.values).toEqual({ - ...mockGroupsValues, - filterUsersDropdownOpen: true, - }); - expect(clearFlashMessages).toHaveBeenCalled(); - }); - }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts index c14538346ad31..3e137ea8a6713 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts @@ -18,13 +18,12 @@ import { flashSuccessToast, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; -import { ContentSource, Group, User } from '../../types'; +import { ContentSource, Group } from '../../types'; export const MAX_NAME_LENGTH = 40; interface GroupsServerData { contentSources: ContentSource[]; - users: User[]; } interface GroupsSearchResponse { @@ -37,10 +36,6 @@ interface GroupsActions { setSearchResults(data: GroupsSearchResponse): GroupsSearchResponse; addFilteredSource(sourceId: string): string; removeFilteredSource(sourceId: string): string; - addFilteredUser(userId: string): string; - removeFilteredUser(userId: string): string; - setGroupUsers(allGroupUsers: User[]): User[]; - setAllGroupLoading(allGroupUsersLoading: boolean): boolean; setFilterValue(filterValue: string): string; setActivePage(activePage: number): number; setNewGroupName(newGroupName: string): string; @@ -49,22 +44,18 @@ interface GroupsActions { openNewGroupModal(): void; closeNewGroupModal(): void; closeFilterSourcesDropdown(): void; - closeFilterUsersDropdown(): void; toggleFilterSourcesDropdown(): void; - toggleFilterUsersDropdown(): void; setGroupsLoading(): void; resetGroupsFilters(): void; resetGroups(): void; initializeGroups(): void; getSearchResults(resetPagination?: boolean): { resetPagination: boolean | undefined }; - fetchGroupUsers(groupId: string): { groupId: string }; saveNewGroup(): void; } interface GroupsValues { groups: Group[]; contentSources: ContentSource[]; - users: User[]; groupsDataLoading: boolean; groupListLoading: boolean; newGroupModalOpen: boolean; @@ -73,10 +64,6 @@ interface GroupsValues { newGroupNameErrors: string[]; filterSourcesDropdownOpen: boolean; filteredSources: string[]; - filterUsersDropdownOpen: boolean; - filteredUsers: string[]; - allGroupUsersLoading: boolean; - allGroupUsers: User[]; filterValue: string; groupsMeta: Meta; hasFiltersSet: boolean; @@ -89,10 +76,6 @@ export const GroupsLogic = kea>({ setSearchResults: (data) => data, addFilteredSource: (sourceId) => sourceId, removeFilteredSource: (sourceId) => sourceId, - addFilteredUser: (userId) => userId, - removeFilteredUser: (userId) => userId, - setGroupUsers: (allGroupUsers) => allGroupUsers, - setAllGroupLoading: (allGroupUsersLoading: boolean) => allGroupUsersLoading, setFilterValue: (filterValue) => filterValue, setActivePage: (activePage) => activePage, setNewGroupName: (newGroupName) => newGroupName, @@ -101,15 +84,12 @@ export const GroupsLogic = kea>({ openNewGroupModal: () => true, closeNewGroupModal: () => true, closeFilterSourcesDropdown: () => true, - closeFilterUsersDropdown: () => true, toggleFilterSourcesDropdown: () => true, - toggleFilterUsersDropdown: () => true, setGroupsLoading: () => true, resetGroupsFilters: () => true, resetGroups: () => true, initializeGroups: () => true, getSearchResults: (resetPagination) => ({ resetPagination }), - fetchGroupUsers: (groupId) => ({ groupId }), saveNewGroup: () => true, }, reducers: { @@ -125,12 +105,6 @@ export const GroupsLogic = kea>({ onInitializeGroups: (_, { contentSources }) => contentSources, }, ], - users: [ - [], - { - onInitializeGroups: (_, { users }) => users, - }, - ], groupsDataLoading: [ true, { @@ -193,36 +167,6 @@ export const GroupsLogic = kea>({ removeFilteredSource: (state, sourceId) => state.filter((id) => id !== sourceId), }, ], - filterUsersDropdownOpen: [ - false, - { - toggleFilterUsersDropdown: (state) => !state, - closeFilterUsersDropdown: () => false, - }, - ], - filteredUsers: [ - [], - { - resetGroupsFilters: () => [], - setNewGroup: () => [], - addFilteredUser: (state, userId) => [...state, userId].sort(), - removeFilteredUser: (state, userId) => state.filter((id) => id !== userId), - }, - ], - allGroupUsersLoading: [ - false, - { - setAllGroupLoading: (_, allGroupUsersLoading) => allGroupUsersLoading, - setGroupUsers: () => false, - }, - ], - allGroupUsers: [ - [], - { - setGroupUsers: (_, allGroupUsers) => allGroupUsers, - setAllGroupLoading: () => [], - }, - ], filterValue: [ '', { @@ -248,8 +192,8 @@ export const GroupsLogic = kea>({ }, selectors: ({ selectors }) => ({ hasFiltersSet: [ - () => [selectors.filteredUsers, selectors.filteredSources], - (filteredUsers, filteredSources) => filteredUsers.length > 0 || filteredSources.length > 0, + () => [selectors.filteredSources], + (filteredSources) => filteredSources.length > 0, ], }), listeners: ({ actions, values }) => ({ @@ -275,7 +219,6 @@ export const GroupsLogic = kea>({ }, filterValue, filteredSources, - filteredUsers, } = values; // Is the user changes the query while on a different page, we want to start back over at 1. @@ -286,7 +229,6 @@ export const GroupsLogic = kea>({ const search = { query: filterValue, content_source_ids: filteredSources, - user_ids: filteredUsers, }; try { @@ -306,17 +248,6 @@ export const GroupsLogic = kea>({ flashAPIErrors(e); } }, - fetchGroupUsers: async ({ groupId }) => { - actions.setAllGroupLoading(true); - try { - const response = await HttpLogic.values.http.get( - `/internal/workplace_search/groups/${groupId}/group_users` - ); - actions.setGroupUsers(response); - } catch (e) { - flashAPIErrors(e); - } - }, saveNewGroup: async () => { try { const response = await HttpLogic.values.http.post( @@ -354,8 +285,5 @@ export const GroupsLogic = kea>({ toggleFilterSourcesDropdown: () => { clearFlashMessages(); }, - toggleFilterUsersDropdown: () => { - clearFlashMessages(); - }, }), }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts index 40ee46c7a9ffd..dc1308a4140d3 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts @@ -107,7 +107,6 @@ describe('groups routes', () => { search: { query: 'foo', content_source_ids: ['123', '234'], - user_ids: ['345', '456'], }, }, }; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts index c5c161cf7b2f8..8dc153e7a2923 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.ts @@ -51,7 +51,6 @@ export function registerSearchGroupsRoute({ search: schema.object({ query: schema.string(), content_source_ids: schema.arrayOf(schema.string()), - user_ids: schema.arrayOf(schema.string()), }), }), }, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a5df7f232217d..63a348198a160 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11923,7 +11923,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action": "Gérer le groupe", "xpack.enterpriseSearch.workplaceSearch.groups.newGroupSavedSuccess": "{groupName} créé avec succès", "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "Aucune source de contenu organisationnelle", - "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "Aucun utilisateur", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "Supprimer {name}", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "Votre groupe sera supprimé de Workplace Search. Voulez-vous vraiment supprimer {name} ?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "Confirmer", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ce64e7d5ffda3..118408cc7a6d2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11924,7 +11924,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action": "グループを管理", "xpack.enterpriseSearch.workplaceSearch.groups.newGroupSavedSuccess": "{groupName}が正常に作成されました", "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "組織コンテンツソースがありません", - "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "ユーザーがありません", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "{name}を削除", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "グループはWorkplace Searchから削除されます。{name}を削除してよろしいですか?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "確認", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 224ddc34cc13c..0c7f624c259d0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11946,7 +11946,6 @@ "xpack.enterpriseSearch.workplaceSearch.groups.newGroup.action": "管理组", "xpack.enterpriseSearch.workplaceSearch.groups.newGroupSavedSuccess": "已成功创建 {groupName}", "xpack.enterpriseSearch.workplaceSearch.groups.noSourcesMessage": "无组织内容源", - "xpack.enterpriseSearch.workplaceSearch.groups.noUsersMessage": "无用户", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveButtonText": "删除 {name}", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmRemoveDescription": "您的组将从 Workplace Search 中删除。确定要移除 {name}?", "xpack.enterpriseSearch.workplaceSearch.groups.overview.confirmTitleText": "确认", From 15ac5206fe84b924f9cf833318d0db4bb7cfce4a Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 2 May 2022 15:39:37 +0200 Subject: [PATCH 02/50] [DataViewEditor] Skip empty prompt screen (#130862) * [DataViewEditor] Skip empty prompt screen * Rename skipEmptyPrompt > skipNoDataViewsPrompt * Rename skipNoDataViewsPrompt > showEmptyPrompt Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/data_view_editor.tsx | 2 ++ .../components/data_view_editor_flyout_content.tsx | 9 ++++++++- .../data_view_flyout_content_container.tsx | 2 ++ .../components/empty_prompts/empty_prompts.tsx | 13 +++++++++++-- src/plugins/data_view_editor/public/open_editor.tsx | 2 ++ src/plugins/data_view_editor/public/types.ts | 5 +++++ 6 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/plugins/data_view_editor/public/components/data_view_editor.tsx b/src/plugins/data_view_editor/public/components/data_view_editor.tsx index 18af3c4ebd6d5..e09acfaca4d52 100644 --- a/src/plugins/data_view_editor/public/components/data_view_editor.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_editor.tsx @@ -23,6 +23,7 @@ export const DataViewEditor = ({ services, defaultTypeIsRollup = false, requireTimestampField = false, + showEmptyPrompt = true, }: DataViewEditorPropsWithServices) => { const { Provider: KibanaReactContextProvider } = createKibanaReactContext(services); @@ -35,6 +36,7 @@ export const DataViewEditor = ({ onCancel={onCancel} defaultTypeIsRollup={defaultTypeIsRollup} requireTimestampField={requireTimestampField} + showEmptyPrompt={showEmptyPrompt} /> diff --git a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx index 9d21af4b2df09..9cdfad745bea3 100644 --- a/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -58,6 +58,7 @@ export interface Props { onCancel: () => void; defaultTypeIsRollup?: boolean; requireTimestampField?: boolean; + showEmptyPrompt?: boolean; } const editorTitle = i18n.translate('indexPatternEditor.title', { @@ -69,6 +70,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ onCancel, defaultTypeIsRollup, requireTimestampField = false, + showEmptyPrompt = true, }: Props) => { const { services: { http, dataViews, uiSettings, searchClient }, @@ -316,7 +318,12 @@ const IndexPatternEditorFlyoutContentComponent = ({ ); return ( - + diff --git a/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx b/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx index 2fe95d753bb09..dd6d474068c2a 100644 --- a/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx +++ b/src/plugins/data_view_editor/public/components/data_view_flyout_content_container.tsx @@ -18,6 +18,7 @@ const IndexPatternFlyoutContentContainer = ({ onCancel = () => {}, defaultTypeIsRollup, requireTimestampField = false, + showEmptyPrompt = true, }: DataViewEditorProps) => { const { services: { dataViews, notifications }, @@ -48,6 +49,7 @@ const IndexPatternFlyoutContentContainer = ({ onCancel={onCancel} defaultTypeIsRollup={defaultTypeIsRollup} requireTimestampField={requireTimestampField} + showEmptyPrompt={showEmptyPrompt} /> ); }; diff --git a/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx b/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx index 686decce9c655..ecfdd9e5c1c92 100644 --- a/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx +++ b/src/plugins/data_view_editor/public/components/empty_prompts/empty_prompts.tsx @@ -27,6 +27,7 @@ interface Props { onCancel: () => void; allSources: MatchedItem[]; loadSources: () => void; + showEmptyPrompt?: boolean; } export function isUserDataIndex(source: MatchedItem) { @@ -45,7 +46,13 @@ export function isUserDataIndex(source: MatchedItem) { return true; } -export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSources }) => { +export const EmptyPrompts: FC = ({ + allSources, + onCancel, + children, + loadSources, + showEmptyPrompt, +}) => { const { services: { docLinks, application, http, searchClient, dataViews }, } = useKibana(); @@ -93,7 +100,7 @@ export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSo ); - } else { + } else if (showEmptyPrompt) { // first time return ( <> @@ -108,6 +115,8 @@ export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSo ); + } else { + setGoToForm(true); } } diff --git a/src/plugins/data_view_editor/public/open_editor.tsx b/src/plugins/data_view_editor/public/open_editor.tsx index 7fd5163edacc4..ed57870c49783 100644 --- a/src/plugins/data_view_editor/public/open_editor.tsx +++ b/src/plugins/data_view_editor/public/open_editor.tsx @@ -49,6 +49,7 @@ export const getEditorOpener = onCancel = () => {}, defaultTypeIsRollup = false, requireTimestampField = false, + showEmptyPrompt = true, }: DataViewEditorProps): CloseEditor => { const closeEditor = () => { if (overlayRef) { @@ -77,6 +78,7 @@ export const getEditorOpener = }} defaultTypeIsRollup={defaultTypeIsRollup} requireTimestampField={requireTimestampField} + showEmptyPrompt={showEmptyPrompt} /> , diff --git a/src/plugins/data_view_editor/public/types.ts b/src/plugins/data_view_editor/public/types.ts index fe6928ee73734..a2d359ba8420b 100644 --- a/src/plugins/data_view_editor/public/types.ts +++ b/src/plugins/data_view_editor/public/types.ts @@ -49,6 +49,11 @@ export interface DataViewEditorProps { * Sets whether a timestamp field is required to create an index pattern. Defaults to false. */ requireTimestampField?: boolean; + /** + * If set to false, the screen for prompting a user to create a data view will be skipped, and the user will be taken directly + * to data view creation. + */ + showEmptyPrompt?: boolean; } // eslint-disable-next-line @typescript-eslint/no-empty-interface From 074519644526ab6e2466a87dca858f63dbfa4086 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Mon, 2 May 2022 10:04:25 -0400 Subject: [PATCH 03/50] Disable time slider control (#130978) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/controls/public/plugin.ts | 7 ++++++- src/plugins/controls/server/plugin.ts | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/plugins/controls/public/plugin.ts b/src/plugins/controls/public/plugin.ts index 51705640b06f2..9b0d754b3f150 100644 --- a/src/plugins/controls/public/plugin.ts +++ b/src/plugins/controls/public/plugin.ts @@ -30,12 +30,14 @@ import { CONTROL_GROUP_TYPE, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, - TIME_SLIDER_CONTROL, + // TIME_SLIDER_CONTROL, } from '.'; +/* import { TimesliderEmbeddableFactory, TimeSliderControlEmbeddableInput, } from './control_types/time_slider'; +*/ import { controlsService } from './services/kibana/controls'; export class ControlsPlugin @@ -104,6 +106,7 @@ export class ControlsPlugin registerControlType(rangeSliderFactory); // Time Slider Control Factory Setup + /* Temporary disabling Time Slider const timeSliderFactoryDef = new TimesliderEmbeddableFactory(); const timeSliderFactory = embeddable.registerEmbeddableFactory( TIME_SLIDER_CONTROL, @@ -113,8 +116,10 @@ export class ControlsPlugin timeSliderFactoryDef, timeSliderFactory ); + registerControlType(timeSliderFactory); + */ }); return { diff --git a/src/plugins/controls/server/plugin.ts b/src/plugins/controls/server/plugin.ts index cbe9d39234361..fb39acfaf913c 100644 --- a/src/plugins/controls/server/plugin.ts +++ b/src/plugins/controls/server/plugin.ts @@ -14,7 +14,7 @@ import { PluginSetup as UnifiedSearchSetup } from '@kbn/unified-search-plugin/se import { setupOptionsListSuggestionsRoute } from './control_types/options_list/options_list_suggestions_route'; import { controlGroupContainerPersistableStateServiceFactory } from './control_group/control_group_container_factory'; import { optionsListPersistableStateServiceFactory } from './control_types/options_list/options_list_embeddable_factory'; -import { timeSliderPersistableStateServiceFactory } from './control_types/time_slider/time_slider_embeddable_factory'; +// import { timeSliderPersistableStateServiceFactory } from './control_types/time_slider/time_slider_embeddable_factory'; interface SetupDeps { embeddable: EmbeddableSetup; @@ -25,7 +25,8 @@ interface SetupDeps { export class ControlsPlugin implements Plugin { public setup(core: CoreSetup, { embeddable, unifiedSearch }: SetupDeps) { embeddable.registerEmbeddableFactory(optionsListPersistableStateServiceFactory()); - embeddable.registerEmbeddableFactory(timeSliderPersistableStateServiceFactory()); + // Temporary disabling Time Slider + // embeddable.registerEmbeddableFactory(timeSliderPersistableStateServiceFactory()); embeddable.registerEmbeddableFactory( controlGroupContainerPersistableStateServiceFactory(embeddable) From 6629d413596ac0f5ca0925cdb253584654692435 Mon Sep 17 00:00:00 2001 From: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> Date: Mon, 2 May 2022 17:15:41 +0200 Subject: [PATCH 04/50] [Lens] Range event annotations (#129848) * [Lens] add Range static annotations * range annotations tests * feedback applied * outsideDimension fix * colors manipulations * outsideDimension calculation * outside dimension calculation * reduce bundle * fix tests * custom swatches and comment * better swatches * types * types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../annotation_layer_config.ts | 2 +- .../__snapshots__/xy_chart.test.tsx.snap | 123 +++-- .../public/components/annotations.scss | 4 + .../public/components/annotations.tsx | 92 +++- .../public/components/xy_chart.test.tsx | 161 +++---- .../public/components/xy_chart.tsx | 33 +- .../common/event_annotation_group/index.ts | 2 +- src/plugins/event_annotation/common/index.ts | 13 +- .../common/manual_event_annotation/index.ts | 83 +++- .../common/manual_event_annotation/types.ts | 24 +- src/plugins/event_annotation/common/types.ts | 31 +- .../event_annotation_service/helpers.ts | 16 + .../event_annotation_service/service.tsx | 85 ++-- .../public/event_annotation_service/types.ts | 4 +- src/plugins/event_annotation/public/index.ts | 6 +- src/plugins/event_annotation/public/plugin.ts | 9 +- src/plugins/event_annotation/server/plugin.ts | 9 +- .../dimension_panel/dimension_panel.test.tsx | 4 +- x-pack/plugins/lens/public/mocks/index.ts | 4 +- .../public/shared_components/name_input.tsx | 3 +- x-pack/plugins/lens/public/types.ts | 2 +- .../xy_visualization/annotations/helpers.tsx | 17 +- .../public/xy_visualization/to_expression.ts | 24 +- .../annotations_config_panel/index.scss | 12 + .../annotations_config_panel/index.test.tsx | 222 +++++++++ .../annotations_config_panel/index.tsx | 436 +++++++++++++++--- .../xy_config_panel/color_picker.tsx | 97 ++-- .../xy_config_panel/reference_line_panel.tsx | 2 + .../xy_config_panel/shared/icon_select.tsx | 13 +- .../shared/marker_decoration_settings.tsx | 3 + .../test/functional/page_objects/lens_page.ts | 2 +- 31 files changed, 1223 insertions(+), 315 deletions(-) create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.scss create mode 100644 x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.ts index 9ec3a43e1d710..1f46f12626f0b 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/annotation_layer_config.ts @@ -33,7 +33,7 @@ export function annotationLayerConfigFunction(): ExpressionFunctionDefinition< help: 'Show details', }, annotations: { - types: ['manual_event_annotation'], + types: ['manual_point_event_annotation', 'manual_range_event_annotation'], help: '', multi: true, }, diff --git a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index f534db5a6c4e9..23354c7bdc786 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`XYChart component annotations should render basic annotation 1`] = ` +exports[`XYChart component annotations should render basic line annotation 1`] = ` `; -exports[`XYChart component annotations should render grouped annotations preserving the shared styles 1`] = ` +exports[`XYChart component annotations should render basic range annotation 1`] = ` +Array [ + , + , +] +`; + +exports[`XYChart component annotations should render grouped line annotations preserving the shared styles 1`] = ` `; -exports[`XYChart component annotations should render grouped annotations with default styles 1`] = ` +exports[`XYChart component annotations should render grouped line annotations with default styles 1`] = ` `; -exports[`XYChart component annotations should render simplified annotation when hide is true 1`] = ` +exports[`XYChart component annotations should render simplified annotations when hide is true 1`] = ` - } - markerBody={ - - } markerPosition="top" style={ Object { @@ -213,6 +236,50 @@ exports[`XYChart component annotations should render simplified annotation when /> `; +exports[`XYChart component annotations should render simplified annotations when hide is true 2`] = ` +Array [ + , + , +] +`; + exports[`XYChart component it renders area 1`] = ` >; hide?: boolean; minInterval?: number; isBarChart?: boolean; + outsideDimension: number; } -interface CollectiveConfig extends EventAnnotationArgs { +interface CollectiveConfig extends ManualPointEventAnnotationArgs { roundedTimestamp: number; axisMode: 'bottom'; customTooltipDetails?: AnnotationTooltipFormatter | undefined; @@ -55,9 +64,11 @@ const groupVisibleConfigsByInterval = ( firstTimestamp?: number ) => { return layers - .flatMap(({ annotations }) => annotations.filter((a) => !a.isHidden)) + .flatMap(({ annotations }) => + annotations.filter((a) => !a.isHidden && a.type === 'manual_point_event_annotation') + ) .sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf()) - .reduce>((acc, current) => { + .reduce>((acc, current) => { const roundedTimestamp = getRoundedTimestamp( moment(current.time).valueOf(), firstTimestamp, @@ -72,7 +83,7 @@ const groupVisibleConfigsByInterval = ( const createCustomTooltipDetails = ( - config: EventAnnotationArgs[], + config: ManualPointEventAnnotationArgs[], formatter?: FieldFormat ): AnnotationTooltipFormatter | undefined => () => { @@ -95,8 +106,8 @@ const createCustomTooltipDetails = ); }; -function getCommonProperty( - configArr: EventAnnotationArgs[], +function getCommonProperty( + configArr: ManualPointEventAnnotationArgs[], propertyName: K, fallbackValue: T ) { @@ -107,9 +118,9 @@ function getCommonProperty( return fallbackValue; } -const getCommonStyles = (configArr: EventAnnotationArgs[]) => { +const getCommonStyles = (configArr: ManualPointEventAnnotationArgs[]) => { return { - color: getCommonProperty( + color: getCommonProperty( configArr, 'color', defaultAnnotationColor @@ -120,6 +131,20 @@ const getCommonStyles = (configArr: EventAnnotationArgs[]) => { }; }; +export const getRangeAnnotations = (layers: AnnotationLayerConfigResult[]) => { + return layers + .flatMap(({ annotations }) => + annotations.filter( + (a): a is ManualRangeEventAnnotationOutput => + a.type === 'manual_range_event_annotation' && !a.isHidden + ) + ) + .sort((a, b) => moment(a.time).valueOf() - moment(b.time).valueOf()); +}; + +export const OUTSIDE_RECT_ANNOTATION_WIDTH = 8; +export const OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION = 2; + export const getAnnotationsGroupedByInterval = ( layers: AnnotationLayerConfigResult[], minInterval?: number, @@ -147,18 +172,23 @@ export const getAnnotationsGroupedByInterval = ( }); }; +// todo: remove when closed https://github.com/elastic/elastic-charts/issues/1647 +RectAnnotation.displayName = 'RectAnnotation'; + export const Annotations = ({ - groupedAnnotations, + groupedLineAnnotations, + rangeAnnotations, formatter, isHorizontal, paddingMap, hide, minInterval, isBarChart, + outsideDimension, }: AnnotationsProps) => { return ( <> - {groupedAnnotations.map((annotation) => { + {groupedLineAnnotations.map((annotation) => { const markerPositionVertical = Position.Top; const markerPosition = isHorizontal ? mapVerticalToHorizontalPlacement(markerPositionVertical) @@ -229,6 +259,40 @@ export const Annotations = ({ /> ); })} + {rangeAnnotations.map(({ label, time, color, endTime, outside }) => { + const id = snakeCase(label); + + return ( + ( +
+ +

+ {formatter + ? `${formatter.convert(time)} — ${formatter?.convert(endTime)}` + : `${moment(time).toISOString()} — ${moment(endTime).toISOString()}`} +

+
+
{label}
+
+ )} + dataValues={[ + { + coordinates: { + x0: moment(time).valueOf(), + x1: moment(endTime).valueOf(), + }, + details: label, + }, + ]} + style={{ fill: color || defaultAnnotationRangeColor, opacity: 1 }} + outside={Boolean(outside)} + outsideDimension={outsideDimension} + /> + ); + })} ); }; diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx index 6ac39b1c0f941..fc93f48a594ad 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.test.tsx @@ -27,6 +27,7 @@ import { LineAnnotation, LineSeries, Position, + RectAnnotation, ScaleType, SeriesNameFn, Settings, @@ -53,6 +54,7 @@ import { import { XYChart, XYChartRenderProps } from './xy_chart'; import { eventAnnotationServiceMock } from '@kbn/event-annotation-plugin/public/mocks'; import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common'; +import { Annotations } from './annotations'; const onClickValue = jest.fn(); const onSelectRange = jest.fn(); @@ -2533,30 +2535,35 @@ describe('XYChart component', () => { }); describe('annotations', () => { - const sampleStyledAnnotation: EventAnnotationOutput = { + const customLineStaticAnnotation: EventAnnotationOutput = { time: '2022-03-18T08:25:00.000Z', label: 'Event 1', icon: 'triangle', - type: 'manual_event_annotation', + type: 'manual_point_event_annotation' as const, color: 'red', lineStyle: 'dashed', lineWidth: 3, }; - const sampleAnnotationLayers: AnnotationLayerConfigResult[] = [ - { - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [ - { - time: '2022-03-18T08:25:17.140Z', - label: 'Annotation', - type: 'manual_event_annotation', - }, - ], - }, - ]; - function sampleArgsWithAnnotation(annotationLayers = sampleAnnotationLayers) { + const defaultLineStaticAnnotation = { + time: '2022-03-18T08:25:17.140Z', + label: 'Annotation', + type: 'manual_point_event_annotation' as const, + }; + const defaultRangeStaticAnnotation = { + time: '2022-03-18T08:25:17.140Z', + endTime: '2022-03-31T08:25:17.140Z', + label: 'Event range', + type: 'manual_range_event_annotation' as const, + }; + const createLayerWithAnnotations = ( + annotations: EventAnnotationOutput[] = [defaultLineStaticAnnotation] + ): AnnotationLayerConfigResult => ({ + type: 'annotationLayer', + layerType: LayerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations, + }); + function sampleArgsWithAnnotations(annotationLayers = [createLayerWithAnnotations()]) { const { args } = sampleArgs(); return { data: dateHistogramData, @@ -2566,34 +2573,39 @@ describe('XYChart component', () => { } as XYArgs, }; } - test('should render basic annotation', () => { - const { data, args } = sampleArgsWithAnnotation(); + test('should render basic line annotation', () => { + const { data, args } = sampleArgsWithAnnotations(); const component = mount(); expect(component.find('LineAnnotation')).toMatchSnapshot(); }); - test('should render simplified annotation when hide is true', () => { - const { data, args } = sampleArgsWithAnnotation(); - (args.layers[0] as AnnotationLayerConfigResult).hide = true; + test('should render basic range annotation', () => { + const { data, args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([defaultLineStaticAnnotation, defaultRangeStaticAnnotation]), + ]); + const component = mount(); + expect(component.find(RectAnnotation)).toMatchSnapshot(); + }); + test('should render simplified annotations when hide is true', () => { + const { data, args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([defaultLineStaticAnnotation, defaultRangeStaticAnnotation]), + ]); + (args.layers[1] as AnnotationLayerConfigResult).hide = true; const component = mount(); expect(component.find('LineAnnotation')).toMatchSnapshot(); + expect(component.find('RectAnnotation')).toMatchSnapshot(); }); - test('should render grouped annotations preserving the shared styles', () => { - const { data, args } = sampleArgsWithAnnotation([ - { - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [ - sampleStyledAnnotation, - { ...sampleStyledAnnotation, time: '2022-03-18T08:25:00.020Z', label: 'Event 2' }, - { - ...sampleStyledAnnotation, - time: '2022-03-18T08:25:00.001Z', - label: 'Event 3', - }, - ], - }, + test('should render grouped line annotations preserving the shared styles', () => { + const { data, args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([ + customLineStaticAnnotation, + { ...customLineStaticAnnotation, time: '2022-03-18T08:25:00.020Z', label: 'Event 2' }, + { + ...customLineStaticAnnotation, + time: '2022-03-18T08:25:00.001Z', + label: 'Event 3', + }, + ]), ]); const component = mount(); const groupedAnnotation = component.find(LineAnnotation); @@ -2613,30 +2625,20 @@ describe('XYChart component', () => { ' Event 1 2022-03-18T08:25:00.000Z Event 3 2022-03-18T08:25:00.001Z Event 2 2022-03-18T08:25:00.020Z' ); }); - test('should render grouped annotations with default styles', () => { - const { data, args } = sampleArgsWithAnnotation([ - { - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [sampleStyledAnnotation], - }, - { - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [ - { - ...sampleStyledAnnotation, - icon: 'square', - color: 'blue', - lineStyle: 'dotted', - lineWidth: 10, - time: '2022-03-18T08:25:00.001Z', - label: 'Event 2', - }, - ], - }, + test('should render grouped line annotations with default styles', () => { + const { data, args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([customLineStaticAnnotation]), + createLayerWithAnnotations([ + { + ...customLineStaticAnnotation, + icon: 'square', + color: 'blue', + lineStyle: 'dotted', + lineWidth: 10, + time: '2022-03-18T08:25:00.001Z', + label: 'Event 2', + }, + ]), ]); const component = mount(); const groupedAnnotation = component.find(LineAnnotation); @@ -2646,27 +2648,26 @@ describe('XYChart component', () => { expect(groupedAnnotation).toMatchSnapshot(); }); test('should not render hidden annotations', () => { - const { data, args } = sampleArgsWithAnnotation([ - { - type: 'annotationLayer', - layerType: LayerTypes.ANNOTATIONS, - layerId: 'annotation', - annotations: [ - sampleStyledAnnotation, - { ...sampleStyledAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' }, - { - ...sampleStyledAnnotation, - time: '2022-03-18T08:35:00.001Z', - label: 'Event 3', - isHidden: true, - }, - ], - }, + const { data, args } = sampleArgsWithAnnotations([ + createLayerWithAnnotations([ + customLineStaticAnnotation, + { ...customLineStaticAnnotation, time: '2022-03-18T08:30:00.020Z', label: 'Event 2' }, + { + ...customLineStaticAnnotation, + time: '2022-03-18T08:35:00.001Z', + label: 'Event 3', + isHidden: true, + }, + defaultRangeStaticAnnotation, + { ...defaultRangeStaticAnnotation, label: 'range', isHidden: true }, + ]), ]); const component = mount(); - const annotations = component.find(LineAnnotation); + const lineAnnotations = component.find(LineAnnotation); + const rectAnnotations = component.find(Annotations).find(RectAnnotation); - expect(annotations.length).toEqual(2); + expect(lineAnnotations.length).toEqual(2); + expect(rectAnnotations.length).toEqual(1); }); }); }); diff --git a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx index 4f543ae0f09ae..72fc7d05eb13d 100644 --- a/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/plugins/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -65,7 +65,13 @@ import { getLegendAction } from './legend_action'; import { ReferenceLineAnnotations, computeChartMargins } from './reference_lines'; import { visualizationDefinitions } from '../definitions'; import { XYLayerConfigResult } from '../../common/types'; -import { Annotations, getAnnotationsGroupedByInterval } from './annotations'; +import { + Annotations, + getAnnotationsGroupedByInterval, + getRangeAnnotations, + OUTSIDE_RECT_ANNOTATION_WIDTH, + OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION, +} from './annotations'; import './xy_chart.scss'; @@ -265,18 +271,21 @@ export function XYChart({ const xColumnId = firstTable.columns.find((col) => col.id === filteredLayers[0].xAccessor)?.id; - const groupedAnnotations = getAnnotationsGroupedByInterval( + const groupedLineAnnotations = getAnnotationsGroupedByInterval( annotationsLayers, minInterval, xColumnId ? firstTable.rows[0]?.[xColumnId] : undefined, xAxisFormatter ); + const rangeAnnotations = getRangeAnnotations(annotationsLayers); + const visualConfigs = [ ...referenceLineLayers.flatMap(({ yConfig }) => yConfig), - ...groupedAnnotations, + ...groupedLineAnnotations, ].filter(Boolean); - const linesPaddings = getLinesCausedPaddings(visualConfigs, yAxesMap); + const shouldHideDetails = annotationsLayers.length > 0 ? annotationsLayers[0].hide : false; + const linesPaddings = !shouldHideDetails ? getLinesCausedPaddings(visualConfigs, yAxesMap) : {}; const getYAxesStyle = (groupId: 'left' | 'right') => { const tickVisible = @@ -507,7 +516,6 @@ export function XYChart({ : undefined, }, }; - return ( ) : null} - {groupedAnnotations.length ? ( + {rangeAnnotations.length || groupedLineAnnotations.length ? ( 0} minInterval={minInterval} + hide={annotationsLayers?.[0].hide} + outsideDimension={ + rangeAnnotations.length && shouldHideDetails + ? OUTSIDE_RECT_ANNOTATION_WIDTH_SUGGESTION + : shouldUseNewTimeAxis + ? Number(MULTILAYER_TIME_AXIS_STYLE.tickLine?.padding || 0) + + Number(chartTheme.axes?.tickLabel?.fontSize || 0) + : Number(chartTheme.axes?.tickLine?.size) || OUTSIDE_RECT_ANNOTATION_WIDTH + } /> ) : null} diff --git a/src/plugins/event_annotation/common/event_annotation_group/index.ts b/src/plugins/event_annotation/common/event_annotation_group/index.ts index f6a1f38459c13..a3a36505b1c2d 100644 --- a/src/plugins/event_annotation/common/event_annotation_group/index.ts +++ b/src/plugins/event_annotation/common/event_annotation_group/index.ts @@ -35,7 +35,7 @@ export function eventAnnotationGroup(): ExpressionFunctionDefinition< }), args: { annotations: { - types: ['manual_event_annotation'], + types: ['manual_point_event_annotation', 'manual_range_event_annotation'], help: i18n.translate('eventAnnotation.group.args.annotationConfigs', { defaultMessage: 'Annotation configs', }), diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index 332fa19150aad..3ed43b19a705c 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -6,8 +6,15 @@ * Side Public License, v 1. */ -export type { EventAnnotationArgs, EventAnnotationOutput } from './manual_event_annotation/types'; -export { manualEventAnnotation } from './manual_event_annotation'; +export type { + EventAnnotationArgs, + EventAnnotationOutput, + ManualPointEventAnnotationArgs, + ManualPointEventAnnotationOutput, + ManualRangeEventAnnotationArgs, + ManualRangeEventAnnotationOutput, +} from './manual_event_annotation/types'; +export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation'; export { eventAnnotationGroup } from './event_annotation_group'; export type { EventAnnotationGroupArgs } from './event_annotation_group'; -export type { EventAnnotationConfig } from './types'; +export type { EventAnnotationConfig, RangeEventAnnotationConfig } from './types'; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/index.ts b/src/plugins/event_annotation/common/manual_event_annotation/index.ts index 167adcb3ed739..2401af53df76c 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/index.ts @@ -8,16 +8,22 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; -import type { EventAnnotationArgs, EventAnnotationOutput } from './types'; -export const manualEventAnnotation: ExpressionFunctionDefinition< - 'manual_event_annotation', +import type { + ManualRangeEventAnnotationArgs, + ManualRangeEventAnnotationOutput, + ManualPointEventAnnotationArgs, + ManualPointEventAnnotationOutput, +} from './types'; + +export const manualPointEventAnnotation: ExpressionFunctionDefinition< + 'manual_point_event_annotation', null, - EventAnnotationArgs, - EventAnnotationOutput + ManualPointEventAnnotationArgs, + ManualPointEventAnnotationOutput > = { - name: 'manual_event_annotation', + name: 'manual_point_event_annotation', aliases: [], - type: 'manual_event_annotation', + type: 'manual_point_event_annotation', help: i18n.translate('eventAnnotation.manualAnnotation.description', { defaultMessage: `Configure manual annotation`, }), @@ -73,9 +79,68 @@ export const manualEventAnnotation: ExpressionFunctionDefinition< }), }, }, - fn: function fn(input: unknown, args: EventAnnotationArgs) { + fn: function fn(input: unknown, args: ManualPointEventAnnotationArgs) { + return { + type: 'manual_point_event_annotation', + ...args, + }; + }, +}; + +export const manualRangeEventAnnotation: ExpressionFunctionDefinition< + 'manual_range_event_annotation', + null, + ManualRangeEventAnnotationArgs, + ManualRangeEventAnnotationOutput +> = { + name: 'manual_range_event_annotation', + aliases: [], + type: 'manual_range_event_annotation', + help: i18n.translate('eventAnnotation.manualAnnotation.description', { + defaultMessage: `Configure manual annotation`, + }), + inputTypes: ['null'], + args: { + time: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.time', { + defaultMessage: `Timestamp for annotation`, + }), + }, + endTime: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.endTime', { + defaultMessage: `Timestamp for range annotation`, + }), + required: false, + }, + outside: { + types: ['boolean'], + help: '', + required: false, + }, + label: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.label', { + defaultMessage: `The name of the annotation`, + }), + }, + color: { + types: ['string'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.color', { + defaultMessage: 'The color of the line', + }), + }, + isHidden: { + types: ['boolean'], + help: i18n.translate('eventAnnotation.manualAnnotation.args.isHidden', { + defaultMessage: `Switch to hide annotation`, + }), + }, + }, + fn: function fn(input: unknown, args: ManualRangeEventAnnotationArgs) { return { - type: 'manual_event_annotation', + type: 'manual_range_event_annotation', ...args, }; }, diff --git a/src/plugins/event_annotation/common/manual_event_annotation/types.ts b/src/plugins/event_annotation/common/manual_event_annotation/types.ts index e1bed4a592d23..208383734924c 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/types.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/types.ts @@ -6,10 +6,26 @@ * Side Public License, v 1. */ -import { StyleProps } from '../types'; +import { PointStyleProps, RangeStyleProps } from '../types'; -export type EventAnnotationArgs = { +export type ManualPointEventAnnotationArgs = { time: string; -} & StyleProps; +} & PointStyleProps; -export type EventAnnotationOutput = EventAnnotationArgs & { type: 'manual_event_annotation' }; +export type ManualPointEventAnnotationOutput = ManualPointEventAnnotationArgs & { + type: 'manual_point_event_annotation'; +}; + +export type ManualRangeEventAnnotationArgs = { + time: string; + endTime: string; +} & RangeStyleProps; + +export type ManualRangeEventAnnotationOutput = ManualRangeEventAnnotationArgs & { + type: 'manual_range_event_annotation'; +}; + +export type EventAnnotationArgs = ManualPointEventAnnotationArgs | ManualRangeEventAnnotationArgs; +export type EventAnnotationOutput = + | ManualPointEventAnnotationOutput + | ManualRangeEventAnnotationOutput; diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index 95275804d1d1f..664138c9eb9e6 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -7,10 +7,11 @@ */ export type LineStyle = 'solid' | 'dashed' | 'dotted'; +export type Fill = 'inside' | 'outside' | 'none'; export type AnnotationType = 'manual'; -export type KeyType = 'point_in_time'; +export type KeyType = 'point_in_time' | 'range'; -export interface StyleProps { +export interface PointStyleProps { label: string; color?: string; icon?: string; @@ -20,10 +21,30 @@ export interface StyleProps { isHidden?: boolean; } -export type EventAnnotationConfig = { +export type PointInTimeEventAnnotationConfig = { id: string; key: { - type: KeyType; + type: 'point_in_time'; timestamp: string; }; -} & StyleProps; +} & PointStyleProps; + +export interface RangeStyleProps { + label: string; + color?: string; + outside?: boolean; + isHidden?: boolean; +} + +export type RangeEventAnnotationConfig = { + id: string; + key: { + type: 'range'; + timestamp: string; + endTimestamp: string; + }; +} & RangeStyleProps; + +export type StyleProps = PointStyleProps & RangeStyleProps; + +export type EventAnnotationConfig = PointInTimeEventAnnotationConfig | RangeEventAnnotationConfig; diff --git a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts index aed33da840574..8eb3d05309ec1 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts @@ -5,5 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import { euiLightVars } from '@kbn/ui-theme'; +import { EventAnnotationConfig, RangeEventAnnotationConfig } from '../../common'; export const defaultAnnotationColor = euiLightVars.euiColorAccent; +export const defaultAnnotationRangeColor = `#F04E981A`; // defaultAnnotationColor with opacity 0.1 + +export const defaultAnnotationLabel = i18n.translate( + 'eventAnnotation.manualAnnotation.defaultAnnotationLabel', + { + defaultMessage: 'Event', + } +); + +export const isRangeAnnotation = ( + annotation?: EventAnnotationConfig +): annotation is RangeEventAnnotationConfig => { + return Boolean(annotation && annotation?.key.type === 'range'); +}; diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index 3d81ea6a3e3a6..4770c1c182af6 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -7,43 +7,70 @@ */ import { EventAnnotationServiceType } from './types'; -import { defaultAnnotationColor } from './helpers'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + defaultAnnotationLabel, +} from './helpers'; +import { EventAnnotationConfig } from '../../common'; +import { RangeEventAnnotationConfig } from '../../common/types'; export function hasIcon(icon: string | undefined): icon is string { return icon != null && icon !== 'empty'; } +const isRangeAnnotation = ( + annotation?: EventAnnotationConfig +): annotation is RangeEventAnnotationConfig => { + return Boolean(annotation && annotation?.key.type === 'range'); +}; + export function getEventAnnotationService(): EventAnnotationServiceType { return { - toExpression: ({ - label, - isHidden, - color, - lineStyle, - lineWidth, - icon, - textVisibility, - time, - }) => { - return { - type: 'expression', - chain: [ - { - type: 'function', - function: 'manual_event_annotation', - arguments: { - time: [time], - label: [label], - color: [color || defaultAnnotationColor], - lineWidth: [lineWidth || 1], - lineStyle: [lineStyle || 'solid'], - icon: hasIcon(icon) ? [icon] : ['triangle'], - textVisibility: [textVisibility || false], - isHidden: [Boolean(isHidden)], + toExpression: (annotation) => { + if (isRangeAnnotation(annotation)) { + const { label, isHidden, color, key, outside } = annotation; + const { timestamp: time, endTimestamp: endTime } = key; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'manual_range_event_annotation', + arguments: { + time: [time], + endTime: [endTime], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationRangeColor], + outside: [Boolean(outside)], + isHidden: [Boolean(isHidden)], + }, + }, + ], + }; + } else { + const { label, isHidden, color, lineStyle, lineWidth, icon, key, textVisibility } = + annotation; + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'manual_point_event_annotation', + arguments: { + time: [key.timestamp], + label: [label || defaultAnnotationLabel], + color: [color || defaultAnnotationColor], + lineWidth: [lineWidth || 1], + lineStyle: [lineStyle || 'solid'], + icon: hasIcon(icon) ? [icon] : ['triangle'], + textVisibility: [textVisibility || false], + isHidden: [Boolean(isHidden)], + }, }, - }, - ], - }; + ], + }; + } }, }; } diff --git a/src/plugins/event_annotation/public/event_annotation_service/types.ts b/src/plugins/event_annotation/public/event_annotation_service/types.ts index c44b2d1e536d5..d5fcaa23107c8 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/types.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/types.ts @@ -7,8 +7,8 @@ */ import { ExpressionAstExpression } from '@kbn/expressions-plugin/common/ast'; -import { EventAnnotationArgs } from '../../common'; +import { EventAnnotationConfig } from '../../common'; export interface EventAnnotationServiceType { - toExpression: (props: EventAnnotationArgs) => ExpressionAstExpression; + toExpression: (props: EventAnnotationConfig) => ExpressionAstExpression; } diff --git a/src/plugins/event_annotation/public/index.ts b/src/plugins/event_annotation/public/index.ts index c15429c94cbe4..56ddc4b8a60e1 100644 --- a/src/plugins/event_annotation/public/index.ts +++ b/src/plugins/event_annotation/public/index.ts @@ -14,4 +14,8 @@ export const plugin = () => new EventAnnotationPlugin(); export type { EventAnnotationPluginSetup, EventAnnotationPluginStart } from './plugin'; export * from './event_annotation_service/types'; export { EventAnnotationService } from './event_annotation_service'; -export { defaultAnnotationColor } from './event_annotation_service/helpers'; +export { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotation, +} from './event_annotation_service/helpers'; diff --git a/src/plugins/event_annotation/public/plugin.ts b/src/plugins/event_annotation/public/plugin.ts index f3f4fcfcc60f6..9314151375f20 100644 --- a/src/plugins/event_annotation/public/plugin.ts +++ b/src/plugins/event_annotation/public/plugin.ts @@ -8,7 +8,11 @@ import { Plugin, CoreSetup } from '@kbn/core/public'; import { ExpressionsSetup } from '@kbn/expressions-plugin/public'; -import { manualEventAnnotation, eventAnnotationGroup } from '../common'; +import { + manualPointEventAnnotation, + manualRangeEventAnnotation, + eventAnnotationGroup, +} from '../common'; import { EventAnnotationService } from './event_annotation_service'; interface SetupDependencies { @@ -28,7 +32,8 @@ export class EventAnnotationPlugin private readonly eventAnnotationService = new EventAnnotationService(); public setup(core: CoreSetup, dependencies: SetupDependencies): EventAnnotationPluginSetup { - dependencies.expressions.registerFunction(manualEventAnnotation); + dependencies.expressions.registerFunction(manualPointEventAnnotation); + dependencies.expressions.registerFunction(manualRangeEventAnnotation); dependencies.expressions.registerFunction(eventAnnotationGroup); return this.eventAnnotationService; } diff --git a/src/plugins/event_annotation/server/plugin.ts b/src/plugins/event_annotation/server/plugin.ts index 0643611af9bb3..387326fcf2a21 100644 --- a/src/plugins/event_annotation/server/plugin.ts +++ b/src/plugins/event_annotation/server/plugin.ts @@ -8,7 +8,11 @@ import { CoreSetup, Plugin } from '@kbn/core/server'; import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server'; -import { manualEventAnnotation, eventAnnotationGroup } from '../common'; +import { + manualPointEventAnnotation, + eventAnnotationGroup, + manualRangeEventAnnotation, +} from '../common'; interface SetupDependencies { expressions: ExpressionsServerSetup; @@ -16,7 +20,8 @@ interface SetupDependencies { export class EventAnnotationServerPlugin implements Plugin { public setup(core: CoreSetup, dependencies: SetupDependencies) { - dependencies.expressions.registerFunction(manualEventAnnotation); + dependencies.expressions.registerFunction(manualPointEventAnnotation); + dependencies.expressions.registerFunction(manualRangeEventAnnotation); dependencies.expressions.registerFunction(eventAnnotationGroup); return {}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 3ab3633725678..7f21cf21000b1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -635,7 +635,7 @@ describe('IndexPatternDimensionEditorPanel', () => { act(() => { wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') + .find('input[data-test-subj="column-label-edit"]') .simulate('change', { target: { value: 'New Label' } }); }); @@ -739,7 +739,7 @@ describe('IndexPatternDimensionEditorPanel', () => { act(() => { wrapper - .find('input[data-test-subj="indexPattern-label-edit"]') + .find('input[data-test-subj="column-label-edit"]') .simulate('change', { target: { value: 'Sum of bytes' } }); }); diff --git a/x-pack/plugins/lens/public/mocks/index.ts b/x-pack/plugins/lens/public/mocks/index.ts index ab6f2066e8804..58ef8ce05c613 100644 --- a/x-pack/plugins/lens/public/mocks/index.ts +++ b/x-pack/plugins/lens/public/mocks/index.ts @@ -31,14 +31,14 @@ export type FrameMock = jest.Mocked; export const createMockFramePublicAPI = (): FrameMock => ({ datasourceLayers: {}, - dateRange: { fromDate: 'now-7d', toDate: 'now' }, + dateRange: { fromDate: '2022-03-17T08:25:00.000Z', toDate: '2022-04-17T08:25:00.000Z' }, }); export type FrameDatasourceMock = jest.Mocked; export const createMockFrameDatasourceAPI = (): FrameDatasourceMock => ({ datasourceLayers: {}, - dateRange: { fromDate: 'now-7d', toDate: 'now' }, + dateRange: { fromDate: '2022-03-17T08:25:00.000Z', toDate: '2022-04-17T08:25:00.000Z' }, query: { query: '', language: 'lucene' }, filters: [], }); diff --git a/x-pack/plugins/lens/public/shared_components/name_input.tsx b/x-pack/plugins/lens/public/shared_components/name_input.tsx index 0b65b26021628..9502c7df93d55 100644 --- a/x-pack/plugins/lens/public/shared_components/name_input.tsx +++ b/x-pack/plugins/lens/public/shared_components/name_input.tsx @@ -35,8 +35,9 @@ export const NameInput = ({ fullWidth > { handleInputChange(e.target.value); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 22d794e803df1..e4ff1aab1a6c4 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -525,7 +525,7 @@ export interface OperationDescriptor extends Operation { export interface VisualizationConfigProps { layerId: string; - frame: Pick; + frame: FramePublicAPI; state: T; } diff --git a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx index fe120dc71b17a..9157d310d00b0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/annotations/helpers.tsx @@ -7,7 +7,11 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { defaultAnnotationColor } from '@kbn/event-annotation-plugin/public'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotation, +} from '@kbn/event-annotation-plugin/public'; import { layerTypes } from '../../../common'; import type { FramePublicAPI, Visualization } from '../../types'; import { isHorizontalChart } from '../state_helpers'; @@ -29,6 +33,13 @@ export const defaultAnnotationLabel = i18n.translate('xpack.lens.xyChart.default defaultMessage: 'Event', }); +export const defaultRangeAnnotationLabel = i18n.translate( + 'xpack.lens.xyChart.defaultRangeAnnotationLabel', + { + defaultMessage: 'Event range', + } +); + export function getStaticDate(dataLayers: XYDataLayerConfig[], frame: FramePublicAPI) { const dataLayersId = dataLayers.map(({ layerId }) => layerId); const { activeData, dateRange } = frame; @@ -161,7 +172,9 @@ export const getAnnotationsAccessorColorConfig = (layer: XYAnnotationLayerConfig return { columnId: annotation.id, triggerIcon: annotation.isHidden ? ('invisible' as const) : ('color' as const), - color: annotation?.color || defaultAnnotationColor, + color: + annotation?.color || + (isRangeAnnotation(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor), }; }); }; 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 eeb56bd7ee115..423cc49b68cf7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -28,7 +28,7 @@ import { getReferenceLayers, getAnnotationsLayers, } from './visualization_helpers'; -import { getUniqueLabels, defaultAnnotationLabel } from './annotations/helpers'; +import { getUniqueLabels } from './annotations/helpers'; import { layerTypes } from '../../common'; export const getSortedAccessors = ( @@ -83,7 +83,7 @@ const simplifiedLayerExpression = { [layerTypes.REFERENCELINE]: (layer: XYReferenceLineLayerConfig) => ({ ...layer, hide: true, - yConfig: layer.yConfig?.map(({ lineWidth, ...rest }) => ({ + yConfig: layer.yConfig?.map(({ ...rest }) => ({ ...rest, lineWidth: 1, icon: undefined, @@ -93,12 +93,6 @@ const simplifiedLayerExpression = { [layerTypes.ANNOTATIONS]: (layer: XYAnnotationLayerConfig) => ({ ...layer, hide: true, - annotations: layer.annotations?.map(({ lineWidth, ...rest }) => ({ - ...rest, - lineWidth: 1, - icon: undefined, - textVisibility: false, - })), }), }; @@ -417,19 +411,7 @@ const annotationLayerToExpression = ( hide: [Boolean(layer.hide)], layerId: [layer.layerId], annotations: layer.annotations - ? layer.annotations.map( - (ann): Ast => - eventAnnotationService.toExpression({ - time: ann.key.timestamp, - label: ann.label || defaultAnnotationLabel, - textVisibility: ann.textVisibility, - icon: ann.icon, - lineStyle: ann.lineStyle, - lineWidth: ann.lineWidth, - color: ann.color, - isHidden: Boolean(ann.isHidden), - }) - ) + ? layer.annotations.map((ann): Ast => eventAnnotationService.toExpression(ann)) : [], }, }, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.scss b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.scss new file mode 100644 index 0000000000000..3a0f4b944aa6c --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.scss @@ -0,0 +1,12 @@ +.lnsRowCompressedMargin+.lnsRowCompressedMargin { + margin-top: $euiSizeS; +} + +.lnsConfigPanelNoPadding { + padding: 0; +} + +.lnsConfigPanelDate__label { + min-width: 56px; // makes both labels ("from" and "to") the same width + text-align: center; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx new file mode 100644 index 0000000000000..6194a7f0da120 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.test.tsx @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import { AnnotationsPanel } from '.'; +import { FramePublicAPI } from '../../../types'; +import { layerTypes } from '../../..'; +import { createMockFramePublicAPI } from '../../../mocks'; +import { State } from '../../types'; +import { Position } from '@elastic/charts'; +import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import moment from 'moment'; + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); + +const customLineStaticAnnotation = { + id: 'ann1', + key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' }, + label: 'Event', + icon: 'triangle', + color: 'red', + lineStyle: 'dashed' as const, + lineWidth: 3, +}; + +describe('AnnotationsPanel', () => { + let frame: FramePublicAPI; + + function testState(): State { + return { + legend: { isVisible: true, position: Position.Right }, + valueLabels: 'hide', + preferredSeriesType: 'bar', + layers: [ + { + layerType: layerTypes.ANNOTATIONS, + layerId: 'annotation', + annotations: [customLineStaticAnnotation], + }, + ], + }; + } + + beforeEach(() => { + frame = createMockFramePublicAPI(); + frame.datasourceLayers = {}; + }); + describe('Dimension Editor', () => { + test('shows correct options for line annotations', () => { + const state = testState(); + const component = mount( + + ); + + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-time"]').prop('selected') + ).toEqual(moment('2022-03-18T08:25:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-fromTime"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-toTime"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') + ).toEqual(false); + expect( + component.find('EuiFieldText[data-test-subj="column-label-edit"]').prop('value') + ).toEqual('Event'); + expect( + component.find('EuiComboBox[data-test-subj="lns-icon-select"]').prop('selectedOptions') + ).toEqual([{ label: 'Triangle', value: 'triangle' }]); + expect(component.find('TextDecorationSetting').exists()).toBeTruthy(); + expect(component.find('LineStyleSettings').exists()).toBeTruthy(); + expect( + component.find('EuiButtonGroup[data-test-subj="lns-xyAnnotation-fillStyle"]').exists() + ).toBeFalsy(); + }); + test('shows correct options for range annotations', () => { + const state = testState(); + state.layers[0] = { + annotations: [ + { + color: 'red', + icon: 'triangle', + id: 'ann1', + isHidden: undefined, + key: { + endTimestamp: '2022-03-21T10:49:00.000Z', + timestamp: '2022-03-18T08:25:00.000Z', + type: 'range', + }, + label: 'Event range', + lineStyle: 'dashed', + lineWidth: 3, + }, + ], + layerId: 'annotation', + layerType: 'annotations', + }; + const component = mount( + + ); + + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-fromTime"]').prop('selected') + ).toEqual(moment('2022-03-18T08:25:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-toTime"]').prop('selected') + ).toEqual(moment('2022-03-21T10:49:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-time"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') + ).toEqual(true); + expect( + component.find('EuiFieldText[data-test-subj="column-label-edit"]').prop('value') + ).toEqual('Event range'); + expect(component.find('EuiComboBox[data-test-subj="lns-icon-select"]').exists()).toBeFalsy(); + expect(component.find('TextDecorationSetting').exists()).toBeFalsy(); + expect(component.find('LineStyleSettings').exists()).toBeFalsy(); + expect(component.find('[data-test-subj="lns-xyAnnotation-fillStyle"]').exists()).toBeTruthy(); + }); + + test('calculates correct endTimstamp and transparent color when switching for range annotation and back', () => { + const state = testState(); + const setState = jest.fn(); + const component = mount( + + ); + component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); + + expect(setState).toBeCalledWith({ + ...state, + layers: [ + { + annotations: [ + { + color: '#FF00001A', + id: 'ann1', + isHidden: undefined, + label: 'Event range', + key: { + endTimestamp: '2022-03-21T10:49:00.000Z', + timestamp: '2022-03-18T08:25:00.000Z', + type: 'range', + }, + }, + ], + layerId: 'annotation', + layerType: 'annotations', + }, + ], + }); + component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); + expect(setState).toBeCalledWith({ + ...state, + layers: [ + { + annotations: [ + { + color: '#FF0000', + id: 'ann1', + isHidden: undefined, + key: { + timestamp: '2022-03-18T08:25:00.000Z', + type: 'point_in_time', + }, + label: 'Event', + }, + ], + layerId: 'annotation', + layerType: 'annotations', + }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx index 6435f4c7ba2b8..511d00f33a01c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/annotations_config_panel/index.tsx @@ -5,23 +5,115 @@ * 2.0. */ +import './index.scss'; import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDatePicker, EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; +import { + EuiDatePicker, + EuiFormRow, + EuiSwitch, + EuiSwitchEvent, + EuiButtonGroup, + EuiFormLabel, + EuiFormControlLayout, + EuiText, + transparentize, +} from '@elastic/eui'; +import { pick } from 'lodash'; import moment from 'moment'; -import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common/types'; -import type { VisualizationDimensionEditorProps } from '../../../types'; -import { State, XYState, XYAnnotationLayerConfig } from '../../types'; +import { + EventAnnotationConfig, + PointInTimeEventAnnotationConfig, + RangeEventAnnotationConfig, +} from '@kbn/event-annotation-plugin/common/types'; +import { search } from '@kbn/data-plugin/public'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotation, +} from '@kbn/event-annotation-plugin/public'; +import Color from 'color'; +import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../types'; +import { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types'; import { FormatFactory } from '../../../../common'; import { DimensionEditorSection, NameInput, useDebouncedValue } from '../../../shared_components'; import { isHorizontalChart } from '../../state_helpers'; -import { defaultAnnotationLabel } from '../../annotations/helpers'; +import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers'; import { ColorPicker } from '../color_picker'; import { IconSelectSetting, TextDecorationSetting } from '../shared/marker_decoration_settings'; import { LineStyleSettings } from '../shared/line_style_settings'; import { updateLayer } from '..'; import { annotationsIconSet } from './icon_set'; +import { getDataLayers } from '../../visualization_helpers'; + +export const toRangeAnnotationColor = (color = defaultAnnotationColor) => { + return new Color(transparentize(color, 0.1)).hexa(); +}; + +export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => { + return new Color(transparentize(color, 1)).hex(); +}; + +export const getEndTimestamp = ( + startTime: string, + { activeData, dateRange }: FramePublicAPI, + dataLayers: XYDataLayerConfig[] +) => { + const startTimeNumber = moment(startTime).valueOf(); + const dateRangeFraction = + (moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1; + const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString(); + const dataLayersId = dataLayers.map(({ layerId }) => layerId); + if ( + !dataLayersId.length || + !activeData || + Object.entries(activeData) + .filter(([key]) => dataLayersId.includes(key)) + .every(([, { rows }]) => !rows || !rows.length) + ) { + return fallbackValue; + } + const xColumn = activeData?.[dataLayersId[0]].columns.find( + (column) => column.id === dataLayers[0].xAccessor + ); + if (!xColumn) { + return fallbackValue; + } + + const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval; + if (!dateInterval) return fallbackValue; + const intervalDuration = search.aggs.parseInterval(dateInterval); + if (!intervalDuration) return fallbackValue; + return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString(); +}; + +const sanitizeProperties = (annotation: EventAnnotationConfig) => { + if (isRangeAnnotation(annotation)) { + const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [ + 'label', + 'key', + 'id', + 'isHidden', + 'color', + 'outside', + ]); + return rangeAnnotation; + } else { + const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [ + 'id', + 'label', + 'key', + 'isHidden', + 'lineStyle', + 'lineWidth', + 'color', + 'icon', + 'textVisibility', + ]); + return lineAnnotation; + } +}; export const AnnotationsPanel = ( props: VisualizationDimensionEditorProps & { @@ -29,7 +121,7 @@ export const AnnotationsPanel = ( paletteService: PaletteRegistry; } ) => { - const { state, setState, layerId, accessor } = props; + const { state, setState, layerId, accessor, frame } = props; const isHorizontal = isHorizontalChart(state.layers); const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ @@ -42,19 +134,26 @@ export const AnnotationsPanel = ( (l) => l.layerId === layerId ) as XYAnnotationLayerConfig; - const currentAnnotations = localLayer.annotations?.find((c) => c.id === accessor); + const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor); + + const isRange = isRangeAnnotation(currentAnnotation); const setAnnotations = useCallback( - (annotations: Partial | undefined) => { - if (annotations == null) { + (annotation) => { + if (annotation == null) { return; } const newConfigs = [...(localLayer.annotations || [])]; const existingIndex = newConfigs.findIndex((c) => c.id === accessor); if (existingIndex !== -1) { - newConfigs[existingIndex] = { ...newConfigs[existingIndex], ...annotations }; + newConfigs[existingIndex] = sanitizeProperties({ + ...newConfigs[existingIndex], + ...annotation, + }); } else { - return; // that should never happen because annotations are created before annotations panel is opened + throw new Error( + 'should never happen because annotation is created before config panel is opened' + ); } setLocalState(updateLayer(localState, { ...localLayer, annotations: newConfigs }, index)); }, @@ -68,21 +167,97 @@ export const AnnotationsPanel = ( defaultMessage: 'Placement', })} > - { - if (date) { - setAnnotations({ - key: { - ...(currentAnnotations?.key || { type: 'point_in_time' }), - timestamp: date.toISOString(), - }, - }); - } - }} - label={i18n.translate('xpack.lens.xyChart.annotationDate', { - defaultMessage: 'Annotation date', - })} + {isRange ? ( + <> + { + if (date) { + const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf(); + if (currentEndTime < date.valueOf()) { + const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf(); + const dif = currentEndTime - currentStartTime; + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'range' }), + timestamp: date.toISOString(), + endTimestamp: moment(date.valueOf() + dif).toISOString(), + }, + }); + } else { + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'range' }), + timestamp: date.toISOString(), + }, + }); + } + } + }} + label={i18n.translate('xpack.lens.xyChart.annotationDate', { + defaultMessage: 'Annotation date', + })} + /> + { + if (date) { + const currentStartTime = moment(currentAnnotation?.key.timestamp).valueOf(); + if (currentStartTime > date.valueOf()) { + const currentEndTime = moment(currentAnnotation?.key.endTimestamp).valueOf(); + const dif = currentEndTime - currentStartTime; + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'range' }), + endTimestamp: date.toISOString(), + timestamp: moment(date.valueOf() - dif).toISOString(), + }, + }); + } else { + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'range' }), + endTimestamp: date.toISOString(), + }, + }); + } + } + }} + /> + + ) : ( + { + if (date) { + setAnnotations({ + key: { + ...(currentAnnotation?.key || { type: 'point_in_time' }), + timestamp: date.toISOString(), + }, + }); + } + }} + /> + )} + + { setAnnotations({ label: value }); }} /> - - - + {!isRange && ( + + )} + {!isRange && ( + + )} + {!isRange && ( + + )} + + {isRange && ( + + { + setAnnotations({ + outside: id === `lens_xyChart_fillStyle_outside`, + }); + }} + isFullWidth + /> + + )} + setAnnotations({ isHidden: ev.target.checked })} /> @@ -134,25 +363,114 @@ export const AnnotationsPanel = ( ); }; -const ConfigPanelDatePicker = ({ +const ConfigPanelApplyAsRangeSwitch = ({ + annotation, + onChange, + frame, + state, +}: { + annotation?: EventAnnotationConfig; + onChange: (annotations: Partial | undefined) => void; + frame: FramePublicAPI; + state: XYState; +}) => { + const isRange = isRangeAnnotation(annotation); + return ( + + + {i18n.translate('xpack.lens.xyChart.applyAsRange', { + defaultMessage: 'Apply as range', + })} + + } + checked={isRange} + onChange={() => { + if (isRange) { + const newPointAnnotation: PointInTimeEventAnnotationConfig = { + key: { + type: 'point_in_time', + timestamp: annotation.key.timestamp, + }, + id: annotation.id, + label: + annotation.label === defaultRangeAnnotationLabel + ? defaultAnnotationLabel + : annotation.label, + color: toLineAnnotationColor(annotation.color), + isHidden: annotation.isHidden, + }; + onChange(newPointAnnotation); + } else if (annotation) { + const fromTimestamp = moment(annotation?.key.timestamp); + const dataLayers = getDataLayers(state.layers); + const newRangeAnnotation: RangeEventAnnotationConfig = { + key: { + type: 'range', + timestamp: annotation.key.timestamp, + endTimestamp: getEndTimestamp(fromTimestamp.toISOString(), frame, dataLayers), + }, + id: annotation.id, + label: + annotation.label === defaultAnnotationLabel + ? defaultRangeAnnotationLabel + : annotation.label, + color: toRangeAnnotationColor(annotation.color), + isHidden: annotation.isHidden, + }; + onChange(newRangeAnnotation); + } + }} + compressed + /> + + ); +}; + +const ConfigPanelRangeDatePicker = ({ value, label, + prependLabel, onChange, + dataTestSubj = 'lnsXY_annotation_date_picker', }: { value: moment.Moment; - label: string; + prependLabel?: string; + label?: string; onChange: (val: moment.Moment | null) => void; + dataTestSubj?: string; }) => { return ( - - + + {prependLabel ? ( + {prependLabel} + } + > + + + ) : ( + + )} ); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx index 37ee50f527d43..61e6a4f992390 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -5,23 +5,26 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; +import chroma from 'chroma-js'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiColorPicker, EuiColorPickerProps, EuiToolTip, EuiIcon } from '@elastic/eui'; +import { + EuiFormRow, + EuiColorPicker, + EuiColorPickerProps, + EuiToolTip, + EuiIcon, + euiPaletteColorBlind, +} from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; -import { defaultAnnotationColor } from '@kbn/event-annotation-plugin/public'; import type { VisualizationDimensionEditorProps } from '../../types'; -import { State, XYDataLayerConfig } from '../types'; +import { State } from '../types'; import { FormatFactory } from '../../../common'; import { getSeriesColor } from '../state_helpers'; -import { - defaultReferenceLineColor, - getAccessorColorConfig, - getColorAssignments, -} from '../color_assignment'; +import { getAccessorColorConfig, getColorAssignments } from '../color_assignment'; import { getSortedAccessors } from '../to_expression'; import { TooltipWrapper } from '../../shared_components'; -import { isReferenceLayer, isAnnotationsLayer, getDataLayers } from '../visualization_helpers'; +import { getDataLayers, isDataLayer } from '../visualization_helpers'; const tooltipContent = { auto: i18n.translate('xpack.lens.configPanel.color.tooltip.auto', { @@ -47,6 +50,8 @@ export const ColorPicker = ({ disableHelpTooltip, disabled, setConfig, + showAlpha, + defaultColor, }: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; paletteService: PaletteRegistry; @@ -54,6 +59,8 @@ export const ColorPicker = ({ disableHelpTooltip?: boolean; disabled?: boolean; setConfig: (config: { color?: string }) => void; + showAlpha?: boolean; + defaultColor?: string; }) => { const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; @@ -61,35 +68,46 @@ export const ColorPicker = ({ const overwriteColor = getSeriesColor(layer, accessor); const currentColor = useMemo(() => { if (overwriteColor || !frame.activeData) return overwriteColor; - if (isReferenceLayer(layer)) { - return defaultReferenceLineColor; - } else if (isAnnotationsLayer(layer)) { - return defaultAnnotationColor; + if (defaultColor) { + return defaultColor; } + if (isDataLayer(layer)) { + const sortedAccessors: string[] = getSortedAccessors( + frame.datasourceLayers[layer.layerId] ?? layer.accessors, + layer + ); - const dataLayer: XYDataLayerConfig = layer; - const sortedAccessors: string[] = getSortedAccessors( - frame.datasourceLayers[layer.layerId] ?? layer.accessors, - layer - ); + const colorAssignments = getColorAssignments( + getDataLayers(state.layers), + { tables: frame.activeData }, + formatFactory + ); + const mappedAccessors = getAccessorColorConfig( + colorAssignments, + frame, + { + ...layer, + accessors: sortedAccessors.filter((sorted) => layer.accessors.includes(sorted)), + }, + paletteService + ); - const colorAssignments = getColorAssignments( - getDataLayers(state.layers), - { tables: frame.activeData }, - formatFactory - ); - const mappedAccessors = getAccessorColorConfig( - colorAssignments, - frame, - { - ...dataLayer, - accessors: sortedAccessors.filter((sorted) => dataLayer.accessors.includes(sorted)), - }, - paletteService - ); + return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; + } + }, [ + overwriteColor, + frame, + paletteService, + state.layers, + accessor, + formatFactory, + layer, + defaultColor, + ]); - return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; - }, [overwriteColor, frame, paletteService, state.layers, accessor, formatFactory, layer]); + useEffect(() => { + setColor(currentColor); + }, [currentColor]); const [color, setColor] = useState(currentColor); @@ -107,8 +125,11 @@ export const ColorPicker = ({ defaultMessage: 'Series color', }); + const currentColorAlpha = color ? chroma(color).alpha() : 1; + const colorPicker = ( chroma(c).alpha(currentColorAlpha).hex()) + } /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx index d5a3e8ffd7a46..07c9675635a1f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx @@ -25,6 +25,7 @@ import { TextDecorationSetting, } from './shared/marker_decoration_settings'; import { LineStyleSettings } from './shared/line_style_settings'; +import { defaultReferenceLineColor } from '../color_assignment'; export const ReferenceLinePanel = ( props: VisualizationDimensionEditorProps & { @@ -88,6 +89,7 @@ export const ReferenceLinePanel = ( void; customIconSet?: IconSet; + defaultIcon?: string; }) => { const selectedIcon = customIconSet.find((option) => value === option.value) || - customIconSet.find((option) => option.value === 'empty')!; + customIconSet.find((option) => option.value === defaultIcon)!; return ( { onChange(selection[0].value!); }} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx index b780737877388..a52f3130029fd 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/shared/marker_decoration_settings.tsx @@ -132,10 +132,12 @@ export const IconSelectSetting = ({ currentConfig, setConfig, customIconSet, + defaultIcon = 'empty', }: { currentConfig?: MarkerDecorationConfig; setConfig: (config: MarkerDecorationConfig) => void; customIconSet?: IconSet; + defaultIcon?: string; }) => { return ( { diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 9751c7185213e..d38264150cfa5 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -655,7 +655,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async editDimensionLabel(label: string) { - await testSubjects.setValue('indexPattern-label-edit', label, { clearWithKeyboard: true }); + await testSubjects.setValue('column-label-edit', label, { clearWithKeyboard: true }); }, async editDimensionFormat(format: string) { const formatInput = await testSubjects.find('indexPattern-dimension-format'); From 185bb4ee2be361d1fdfbd512f378b268c5c0397d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 2 May 2022 17:31:41 +0200 Subject: [PATCH 05/50] [Synthetics] Prevent duplicate requests on monitor management page (#130225) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../hooks/use_monitor_list.ts | 67 +++++++++++++++++++ .../monitor_list/monitor_list_container.tsx | 66 +++++------------- .../monitor_management/monitor_management.tsx | 35 ++++------ .../service_allowed_wrapper.tsx | 21 +++--- .../state/effects/monitor_management.ts | 4 +- 5 files changed, 110 insertions(+), 83 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_monitor_list.ts diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_monitor_list.ts b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_monitor_list.ts new file mode 100644 index 0000000000000..e0899571f38b8 --- /dev/null +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/hooks/use_monitor_list.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useReducer, Reducer } from 'react'; +import { useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { getMonitors } from '../../../state/actions'; +import { ConfigKey } from '../../../../../common/constants/monitor_management'; +import { MonitorManagementListPageState } from '../monitor_list/monitor_list'; + +export function useMonitorList() { + const dispatch = useDispatch(); + + const [pageState, dispatchPageAction] = useReducer( + monitorManagementPageReducer, + { + pageIndex: 1, // saved objects page index is base 1 + pageSize: 10, + sortOrder: 'asc', + sortField: `${ConfigKey.NAME}.keyword`, + } + ); + + const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState; + + const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>(); + + useEffect(() => { + if (viewType === 'all') { + dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortOrder, sortField })); + } + }, [dispatch, pageIndex, pageSize, sortField, sortOrder, viewType, pageState]); + + return { + pageState, + dispatchPageAction, + viewType, + }; +} + +export type MonitorManagementPageAction = + | { + type: 'update'; + payload: MonitorManagementListPageState; + } + | { type: 'refresh' }; + +const monitorManagementPageReducer: Reducer< + MonitorManagementListPageState, + MonitorManagementPageAction +> = (state: MonitorManagementListPageState, action: MonitorManagementPageAction) => { + switch (action.type) { + case 'update': + return { + ...state, + ...action.payload, + }; + case 'refresh': + return { ...state }; + default: + throw new Error(`Action "${(action as MonitorManagementPageAction)?.type}" not recognizable`); + } +}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx index 7191cd71b0a36..727f4f6dee72b 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx @@ -5,32 +5,29 @@ * 2.0. */ -import React, { useEffect, useReducer, useCallback, Reducer } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { useCallback, Dispatch } from 'react'; +import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { useTrackPageview } from '@kbn/observability-plugin/public'; -import { ConfigKey } from '../../../../../common/runtime_types'; -import { getMonitors } from '../../../state/actions'; import { monitorManagementListSelector } from '../../../state/selectors'; -import { MonitorManagementListPageState } from './monitor_list'; import { MonitorAsyncError } from './monitor_async_error'; import { useInlineErrors } from '../hooks/use_inline_errors'; import { MonitorListTabs } from './list_tabs'; import { AllMonitors } from './all_monitors'; import { InvalidMonitors } from './invalid_monitors'; import { useInvalidMonitors } from '../hooks/use_invalid_monitors'; +import { MonitorManagementListPageState } from './monitor_list'; +import { MonitorManagementPageAction } from '../hooks/use_monitor_list'; -export const MonitorListContainer: React.FC = () => { - const [pageState, dispatchPageAction] = useReducer( - monitorManagementPageReducer, - { - pageIndex: 1, // saved objects page index is base 1 - pageSize: 10, - sortOrder: 'asc', - sortField: `${ConfigKey.NAME}.keyword`, - } - ); - +export const MonitorListContainer = ({ + isEnabled, + pageState, + dispatchPageAction, +}: { + isEnabled?: boolean; + pageState: MonitorManagementListPageState; + dispatchPageAction: Dispatch; +}) => { const onPageStateChange = useCallback( (state) => { dispatchPageAction({ type: 'update', payload: state }); @@ -45,11 +42,8 @@ export const MonitorListContainer: React.FC = () => { useTrackPageview({ app: 'uptime', path: 'manage-monitors' }); useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 }); - const dispatch = useDispatch(); const monitorList = useSelector(monitorManagementListSelector); - const { pageIndex, pageSize, sortField, sortOrder } = pageState as MonitorManagementListPageState; - const { type: viewType } = useParams<{ type: 'all' | 'invalid' }>(); const { errorSummaries, loading, count } = useInlineErrors({ onlyInvalidMonitors: viewType === 'invalid', @@ -57,14 +51,12 @@ export const MonitorListContainer: React.FC = () => { sortOrder: pageState.sortOrder, }); - useEffect(() => { - if (viewType === 'all') { - dispatch(getMonitors({ page: pageIndex, perPage: pageSize, sortField, sortOrder })); - } - }, [dispatch, pageState, pageIndex, pageSize, sortField, sortOrder, viewType]); - const { data: monitorSavedObjects, loading: objectsLoading } = useInvalidMonitors(errorSummaries); + if (!isEnabled && monitorList.list.total === 0) { + return null; + } + return ( <> @@ -95,27 +87,3 @@ export const MonitorListContainer: React.FC = () => { ); }; - -type MonitorManagementPageAction = - | { - type: 'update'; - payload: MonitorManagementListPageState; - } - | { type: 'refresh' }; - -const monitorManagementPageReducer: Reducer< - MonitorManagementListPageState, - MonitorManagementPageAction -> = (state: MonitorManagementListPageState, action: MonitorManagementPageAction) => { - switch (action.type) { - case 'update': - return { - ...state, - ...action.payload, - }; - case 'refresh': - return { ...state }; - default: - throw new Error(`Action "${(action as MonitorManagementPageAction)?.type}" not recognizable`); - } -}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx index 97013b9905187..ba243388c8a3b 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/monitor_management.tsx @@ -7,11 +7,9 @@ import React, { useEffect, useRef, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { EuiCallOut, EuiButton, EuiSpacer, EuiLink } from '@elastic/eui'; import { useTrackPageview } from '@kbn/observability-plugin/public'; -import { ConfigKey } from '../../../../common/runtime_types'; -import { getMonitors } from '../../state/actions'; import { monitorManagementListSelector } from '../../state/selectors'; import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs'; import { MonitorListContainer } from '../../components/monitor_management/monitor_list/monitor_list_container'; @@ -20,12 +18,12 @@ import { useEnablement } from '../../components/monitor_management/hooks/use_ena import { useLocations } from '../../components/monitor_management/hooks/use_locations'; import { Loader } from '../../components/monitor_management/loader/loader'; import { ERROR_HEADING_LABEL } from './content'; +import { useMonitorList } from '../../components/monitor_management/hooks/use_monitor_list'; export const MonitorManagementPage: React.FC = () => { useTrackPageview({ app: 'uptime', path: 'manage-monitors' }); useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 }); useMonitorManagementBreadcrumbs(); - const dispatch = useDispatch(); const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); const { @@ -40,19 +38,6 @@ export const MonitorManagementPage: React.FC = () => { const isEnabledRef = useRef(isEnabled); - useEffect(() => { - if (monitorList.total === null) { - dispatch( - getMonitors({ - page: 1, // saved objects page index is base 1 - perPage: 10, - sortOrder: 'asc', - sortField: `${ConfigKey.NAME}.keyword`, - }) - ); - } - }, [dispatch, monitorList.total]); - useEffect(() => { if (!isEnabled && isEnabledRef.current === true) { /* shift focus to enable button when enable toggle disappears. Prevent @@ -62,10 +47,14 @@ export const MonitorManagementPage: React.FC = () => { isEnabledRef.current = Boolean(isEnabled); }, [isEnabled]); + const { pageState, dispatchPageAction } = useMonitorList(); + + const showEmptyState = isEnabled !== undefined && monitorList.total === 0; + return ( <> { ) : null} - {isEnabled || (!isEnabled && monitorList.total) ? : null} + - {isEnabled !== undefined && monitorList.total === 0 && ( - - )} + {showEmptyState && } ); }; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/service_allowed_wrapper.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/service_allowed_wrapper.tsx index cda0e8a6cdbe9..ff74fd97f0b34 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/service_allowed_wrapper.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/monitor_management/service_allowed_wrapper.tsx @@ -13,15 +13,6 @@ import { useSyntheticsServiceAllowed } from '../../components/monitor_management export const ServiceAllowedWrapper: React.FC = ({ children }) => { const { isAllowed, signupUrl, loading } = useSyntheticsServiceAllowed(); - if (loading) { - return ( - } - title={

{LOADING_MONITOR_MANAGEMENT_LABEL}

} - /> - ); - } - // checking for explicit false if (isAllowed === false) { return ( @@ -37,7 +28,17 @@ export const ServiceAllowedWrapper: React.FC = ({ children }) => { ); } - return <>{children}; + return ( + <> + {loading && ( + } + title={

{LOADING_MONITOR_MANAGEMENT_LABEL}

} + /> + )} +
{children}
+ + ); }; const REQUEST_ACCESS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.requestAccess', { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/effects/monitor_management.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/effects/monitor_management.ts index 60a6676721784..b5ee599b0a4f9 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/state/effects/monitor_management.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/effects/monitor_management.ts @@ -35,7 +35,7 @@ import { import { fetchEffectFactory } from './fetch_effect'; export function* fetchMonitorManagementEffect() { - yield takeLatest( + yield takeLeading( getMonitors, fetchEffectFactory(fetchMonitorManagementList, getMonitorsSuccess, getMonitorsFailure) ); @@ -47,7 +47,7 @@ export function* fetchMonitorManagementEffect() { getServiceLocationsFailure ) ); - yield takeLatest( + yield takeLeading( getSyntheticsEnablement, fetchEffectFactory( fetchGetSyntheticsEnablement, From 2ef21c2d445d932f16e9fb9c9a7951853b2cf14d Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Mon, 2 May 2022 11:33:00 -0400 Subject: [PATCH 06/50] [Security Solution][Endpoint] Change Endpoint Dev data generator and data loaders so that `agent.id` and `elastic.agent.id` are identical (#131159) * Change generator to set `agent.id` and `elastic.agent.id` to the same value * Add `refresh: 'wait_for',` to all `es.index()` calls in the data loaders Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../index_endpoint_fleet_actions.ts | 7 ++ .../data_loaders/index_endpoint_hosts.ts | 6 ++ .../data_loaders/index_fleet_agent.ts | 2 + .../data_loaders/index_fleet_server.ts | 1 + .../common/endpoint/generate_data.ts | 5 +- .../endpoint_hosts/store/middleware.test.ts | 20 ++--- .../isometric_taxi_layout.test.ts.snap | 88 +++++++++---------- .../apps/endpoint/endpoint_list.ts | 18 ++-- 8 files changed, 82 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts index 369b1fff5fe39..04dce4e60a092 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_fleet_actions.ts @@ -71,6 +71,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_INDEX, body: action, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) @@ -98,6 +99,7 @@ export const indexEndpointAndFleetActionsForHost = async ( .index({ index: ENDPOINT_ACTIONS_INDEX, body: endpointActionsBody, + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise), ]); @@ -125,6 +127,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_RESULTS_INDEX, body: actionResponse, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) @@ -159,6 +162,7 @@ export const indexEndpointAndFleetActionsForHost = async ( .index({ index: ENDPOINT_ACTION_RESPONSES_INDEX, body: endpointActionResponseBody, + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise), ]); @@ -197,6 +201,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_INDEX, body: action, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) @@ -221,6 +226,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_INDEX, body: action1, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) @@ -230,6 +236,7 @@ export const indexEndpointAndFleetActionsForHost = async ( { index: AGENT_ACTIONS_INDEX, body: action2, + refresh: 'wait_for', }, ES_INDEX_OPTIONS ) diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index 2f304351dc435..30d75b30a11b6 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -165,6 +165,10 @@ export async function indexEndpointHostDocs({ // Update the Host metadata record with the ID of the "real" policy along with the enrolled agent id hostMetadata = { ...hostMetadata, + agent: { + ...hostMetadata.agent, + id: enrolledAgent?.id ?? hostMetadata.agent.id, + }, elastic: { ...hostMetadata.elastic, agent: { @@ -201,6 +205,7 @@ export async function indexEndpointHostDocs({ index: metadataIndex, body: hostMetadata, op_type: 'create', + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise); @@ -214,6 +219,7 @@ export async function indexEndpointHostDocs({ index: policyResponseIndex, body: hostPolicyResponse, op_type: 'create', + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts index 70b1e1c52a77b..b051eff37edc7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_agent.ts @@ -47,6 +47,7 @@ export const indexFleetAgentForHost = async ( fleetAgentGenerator: FleetAgentGenerator = defaultFleetAgentGenerator ): Promise => { const agentDoc = fleetAgentGenerator.generateEsHit({ + _id: endpointHost.agent.id, _source: { agent: { id: endpointHost.agent.id, @@ -75,6 +76,7 @@ export const indexFleetAgentForHost = async ( id: agentDoc._id, body: agentDoc._source, op_type: 'create', + refresh: 'wait_for', }) .catch(wrapErrorAndRejectPromise); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_server.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_server.ts index c11c2025ee88e..8b8a15a1164e5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_server.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_fleet_server.ts @@ -31,6 +31,7 @@ export const enableFleetServerIfNecessary = async (esClient: Client, version: st await esClient .index({ index: FLEET_SERVER_SERVERS_INDEX, + refresh: 'wait_for', body: { agent: { id: '12988155-475c-430d-ac89-84dc84b67cd1', diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index a571c7848bdf3..5a6b20550f224 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -464,16 +464,17 @@ export class EndpointDocGenerator extends BaseDataGenerator { const agentVersion = this.randomVersion(); const minCapabilitiesVersion = '7.15.0'; const capabilities = ['isolation']; + const agentId = this.seededUUIDv4(); return { agent: { version: agentVersion, - id: this.seededUUIDv4(), + id: agentId, type: 'endpoint', }, elastic: { agent: { - id: this.seededUUIDv4(), + id: agentId, }, }, host: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 57cdaca1f2147..d8dc87885a1fa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -424,16 +424,16 @@ describe('endpoint list middleware', () => { path: expect.any(String), query: { agent_ids: [ - '6db499e5-4927-4350-abb8-d8318e7d0eec', - 'c082dda9-1847-4997-8eda-f1192d95bec3', - '8aa1cd61-cc25-4783-afb5-0eefc4919c07', - '47fe24c1-7370-419a-9732-3ff38bf41272', - '0d2b2fa7-a9cd-49fc-ad5f-0252c642290e', - 'f480092d-0445-4bf3-9c96-8a3d5cb97824', - '3850e676-0940-4c4b-aaca-571bd1bc66d9', - '46efcc7a-086a-47a3-8f09-c4ecd6d2d917', - 'afa55826-b81b-4440-a2ac-0644d77a3fc6', - '25b49e50-cb5c-43df-824f-67b8cf697d9d', + '0dc3661d-6e67-46b0-af39-6f12b025fcb0', + 'a8e32a61-2685-47f0-83eb-edf157b8e616', + '37e219a8-fe16-4da9-bf34-634c5824b484', + '2484eb13-967e-4491-bf83-dffefdfe607c', + '0bc08ef6-6d6a-4113-92f2-b97811187c63', + 'f4127d87-b567-4a6e-afa6-9a1c7dc95f01', + 'f9ab5b8c-a43e-4e80-99d6-11570845a697', + '406c4b6a-ca57-4bd1-bc66-d9d999df3e70', + '2da1dd51-f7af-4f0e-b64c-e7751c74b0e7', + '89a94ea4-073c-4cb6-90a2-500805837027', ], }, }); diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap index dca100efcc835..b033febcd1ac8 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap @@ -15,11 +15,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "powershell.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "powershell.exe", + "name": "lsass.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -33,11 +33,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "powershell.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "powershell.exe", + "name": "lsass.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -58,11 +58,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "lsass.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "lsass.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -73,11 +73,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "powershell.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "powershell.exe", + "name": "lsass.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -88,11 +88,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "C", - "process.name": "iexlorer.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "A", }, "id": "C", - "name": "iexlorer.exe", + "name": "lsass.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -103,11 +103,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "I", - "process.name": "explorer.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "A", }, "id": "I", - "name": "explorer.exe", + "name": "notepad.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -118,11 +118,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "D", - "process.name": "mimikatz.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "B", }, "id": "D", - "name": "mimikatz.exe", + "name": "lsass.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -133,11 +133,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "E", - "process.name": "powershell.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "B", }, "id": "E", - "name": "powershell.exe", + "name": "mimikatz.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -148,11 +148,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "F", - "process.name": "explorer.exe", + "process.name": "powershell.exe", "process.parent.entity_id": "C", }, "id": "F", - "name": "explorer.exe", + "name": "powershell.exe", "parent": "C", "stats": Object { "byCategory": Object {}, @@ -178,11 +178,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "H", - "process.name": "lsass.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "G", }, "id": "H", - "name": "lsass.exe", + "name": "notepad.exe", "parent": "G", "stats": Object { "byCategory": Object {}, @@ -439,11 +439,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "lsass.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "lsass.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -457,11 +457,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "powershell.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "powershell.exe", + "name": "lsass.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -475,11 +475,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "C", - "process.name": "iexlorer.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "A", }, "id": "C", - "name": "iexlorer.exe", + "name": "lsass.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -493,11 +493,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "I", - "process.name": "explorer.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "A", }, "id": "I", - "name": "explorer.exe", + "name": "notepad.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -511,11 +511,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "D", - "process.name": "mimikatz.exe", + "process.name": "lsass.exe", "process.parent.entity_id": "B", }, "id": "D", - "name": "mimikatz.exe", + "name": "lsass.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -529,11 +529,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "E", - "process.name": "powershell.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "B", }, "id": "E", - "name": "powershell.exe", + "name": "mimikatz.exe", "parent": "B", "stats": Object { "byCategory": Object {}, @@ -547,11 +547,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "F", - "process.name": "explorer.exe", + "process.name": "powershell.exe", "process.parent.entity_id": "C", }, "id": "F", - "name": "explorer.exe", + "name": "powershell.exe", "parent": "C", "stats": Object { "byCategory": Object {}, @@ -583,11 +583,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "H", - "process.name": "lsass.exe", + "process.name": "notepad.exe", "process.parent.entity_id": "G", }, "id": "H", - "name": "lsass.exe", + "name": "notepad.exe", "parent": "G", "stats": Object { "byCategory": Object {}, @@ -608,11 +608,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "explorer.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "explorer.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -623,11 +623,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "iexlorer.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "iexlorer.exe", + "name": "mimikatz.exe", "parent": "A", "stats": Object { "byCategory": Object {}, @@ -661,11 +661,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "A", - "process.name": "explorer.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "", }, "id": "A", - "name": "explorer.exe", + "name": "mimikatz.exe", "parent": undefined, "stats": Object { "byCategory": Object {}, @@ -679,11 +679,11 @@ Object { "data": Object { "@timestamp": 1606234833273, "process.entity_id": "B", - "process.name": "iexlorer.exe", + "process.name": "mimikatz.exe", "process.parent.entity_id": "A", }, "id": "B", - "name": "iexlorer.exe", + "name": "mimikatz.exe", "parent": "A", "stats": Object { "byCategory": Object {}, diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 368783b0efd1c..48dc450737229 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -34,29 +34,29 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Last active', 'Actions', ], - ['Host-9fafsc3tqe', 'x', 'x', 'Warning', 'Windows', '10.231.117.28', '7.17.12', 'x', ''], [ 'Host-ku5jy6j0pw', 'x', 'x', - 'Warning', + 'Unsupported', 'Windows', - '10.246.87.11, 10.145.117.106,10.109.242.136', + '10.12.215.130, 10.130.188.228,10.19.102.141', '7.0.13', 'x', '', ], [ - 'Host-o07wj6uaa5', + 'Host-ntr4rkj24m', 'x', 'x', - 'Failure', + 'Success', 'Windows', - '10.82.134.220, 10.47.25.170', - '7.11.13', + '10.36.46.252, 10.222.152.110', + '7.4.13', 'x', '', ], + ['Host-q9qenwrl9k', 'x', 'x', 'Warning', 'Windows', '10.206.226.90', '7.11.10', 'x', ''], ]; const formattedTableData = async () => { @@ -209,9 +209,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Host-ku5jy6j0pw', 'x', 'x', - 'Warning', + 'Unsupported', 'Windows', - '10.246.87.11, 10.145.117.106,10.109.242.136', + '10.12.215.130, 10.130.188.228,10.19.102.141', '7.0.13', 'x', '', From 27d1fa1797b21a975173e89e9d65a2d029b64304 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Mon, 2 May 2022 11:36:40 -0400 Subject: [PATCH 07/50] Replace history-extra in place of standard url routing (#121440) * Replace history-extra in place of standard url routing * Fix smoke test * Fix smoke test again * Handle embeddalbe save + return * Update StoryShot --- package.json | 1 - .../renderers/embeddable/embeddable.tsx | 31 +++++++--- .../canvas/public/components/app/index.tsx | 59 ++----------------- .../canvas/public/components/home/home.tsx | 8 --- .../workpad_table.stories.storyshot | 3 + .../public/components/home_app/home_app.tsx | 6 +- .../components/routing/routing_link.tsx | 56 +++++++++++++++++- .../editor_menu/editor_menu.tsx | 6 +- .../plugins/canvas/public/lib/breadcrumbs.ts | 43 ++++++++++++-- x-pack/plugins/canvas/public/routes/index.tsx | 43 ++++++++++++-- .../workpad/workpad_presentation_helper.tsx | 8 ++- .../canvas/public/services/kibana/nav_link.ts | 2 +- .../feature_controls/canvas_security.ts | 4 +- .../canvas/feature_controls/canvas_spaces.ts | 2 +- .../test/functional/apps/canvas/smoke_test.js | 6 +- yarn.lock | 5 -- 16 files changed, 179 insertions(+), 104 deletions(-) diff --git a/package.json b/package.json index 43ab61feba638..1b785b1fcaaab 100644 --- a/package.json +++ b/package.json @@ -271,7 +271,6 @@ "handlebars": "4.7.7", "he": "^1.2.0", "history": "^4.9.0", - "history-extra": "^5.0.1", "hjson": "3.2.1", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^5.0.0", diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 8add1aa41b83e..dc164f862aa51 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React from 'react'; +import React, { FC } from 'react'; +import useObservable from 'react-use/lib/useObservable'; import ReactDOM from 'react-dom'; import { CoreStart } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; @@ -32,9 +33,28 @@ const embeddablesRegistry: { const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { const I18nContext = core.i18n.Context; + const EmbeddableRenderer: FC<{ embeddable: IEmbeddable }> = ({ embeddable }) => { + const currentAppId = useObservable(core.application.currentAppId$, undefined); - const embeddableContainerContext: EmbeddableContainerContext = { - getCurrentPath: () => window.location.hash, + if (!currentAppId) { + return null; + } + + const embeddableContainerContext: EmbeddableContainerContext = { + getCurrentPath: () => { + const urlToApp = core.application.getUrlForApp(currentAppId); + const inAppPath = window.location.pathname.replace(urlToApp, ''); + + return inAppPath + window.location.search + window.location.hash; + }, + }; + + return ( + + ); }; return (embeddableObject: IEmbeddable) => { @@ -45,10 +65,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { > - + diff --git a/x-pack/plugins/canvas/public/components/app/index.tsx b/x-pack/plugins/canvas/public/components/app/index.tsx index d9a8b5fa54695..82404f651ec31 100644 --- a/x-pack/plugins/canvas/public/components/app/index.tsx +++ b/x-pack/plugins/canvas/public/components/app/index.tsx @@ -5,14 +5,9 @@ * 2.0. */ -import React, { FC, useRef, useEffect } from 'react'; -import { Observable } from 'rxjs'; +import React, { FC, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { History } from 'history'; -// @ts-expect-error -import createHashStateHistory from 'history-extra/dist/createHashStateHistory'; import { ScopedHistory } from '@kbn/core/public'; -import { skipWhile, timeout, take } from 'rxjs/operators'; import { useNavLinkService } from '../../services'; // @ts-expect-error import { shortcutManager } from '../../lib/shortcut_manager'; @@ -33,64 +28,18 @@ class ShortcutManagerContextWrapper extends React.Component { } export const App: FC<{ history: ScopedHistory }> = ({ history }) => { - const historyRef = useRef(createHashStateHistory() as History); const { updatePath } = useNavLinkService(); useEffect(() => { - return historyRef.current.listen(({ pathname }) => { - updatePath(pathname); + return history.listen(({ pathname, search }) => { + updatePath(pathname + search); }); }); - useEffect(() => { - return history.listen(({ pathname, hash }) => { - // The scoped history could have something that triggers a url change, and that change is not seen by - // our hash router. For example, a scopedHistory.replace() as done as part of the saved object resolve - // alias match flow will do the replace on the scopedHistory, and our app doesn't react appropriately - - // So, to work around this, whenever we see a url on the scoped history, we're going to wait a beat and see - // if it shows up in our hash router. If it doesn't, then we're going to force it onto our hash router - - // I don't like this at all, and to overcome this we should switch away from hash router sooner rather than later - // and just use scopedHistory as our history object - const expectedPath = hash.substr(1); - const action = history.action; - - // Observable of all the path - const hashPaths$ = new Observable((subscriber) => { - subscriber.next(historyRef.current.location.pathname); - - const unsubscribeHashListener = historyRef.current.listen(({ pathname: newPath }) => { - subscriber.next(newPath); - }); - - return unsubscribeHashListener; - }); - - const subscription = hashPaths$ - .pipe( - skipWhile((value) => value !== expectedPath), - timeout(100), - take(1) - ) - .subscribe({ - error: (e) => { - if (action === 'REPLACE') { - historyRef.current.replace(expectedPath); - } else { - historyRef.current.push(expectedPath); - } - }, - }); - - window.setTimeout(() => subscription.unsubscribe(), 150); - }); - }, [history, historyRef]); - return (
- +
); diff --git a/x-pack/plugins/canvas/public/components/home/home.tsx b/x-pack/plugins/canvas/public/components/home/home.tsx index 6b356ada8681e..98dc99d6caaab 100644 --- a/x-pack/plugins/canvas/public/components/home/home.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.tsx @@ -7,14 +7,10 @@ import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; - -import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; import { resetWorkpad } from '../../state/actions/workpad'; import { Home as Component } from './home.component'; -import { usePlatformService } from '../../services'; export const Home = () => { - const { setBreadcrumbs } = usePlatformService(); const [isMounted, setIsMounted] = useState(false); const dispatch = useDispatch(); @@ -25,9 +21,5 @@ export const Home = () => { } }, [dispatch, isMounted, setIsMounted]); - useEffect(() => { - setBreadcrumbs([getBaseBreadcrumb()]); - }, [setBreadcrumbs]); - return ; }; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/workpad_table.stories.storyshot b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/workpad_table.stories.storyshot index 7536cc7acf7dd..d2e52dde1ad5c 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/workpad_table.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/__snapshots__/workpad_table.stories.storyshot @@ -382,6 +382,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiLink euiLink--primary" data-test-subj="canvasWorkpadTableWorkpad" href="/workpad/workpad-2" + onClick={[Function]} rel="noreferrer" > @@ -558,6 +559,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiLink euiLink--primary" data-test-subj="canvasWorkpadTableWorkpad" href="/workpad/workpad-1" + onClick={[Function]} rel="noreferrer" > @@ -734,6 +736,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = ` className="euiLink euiLink--primary" data-test-subj="canvasWorkpadTableWorkpad" href="/workpad/workpad-0" + onClick={[Function]} rel="noreferrer" > diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx index b288612450bf7..086a737d525e9 100644 --- a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx +++ b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; import { resetWorkpad } from '../../state/actions/workpad'; @@ -16,10 +17,11 @@ export const HomeApp = () => { const { setBreadcrumbs } = usePlatformService(); const dispatch = useDispatch(); const onLoad = () => dispatch(resetWorkpad()); + const history = useHistory(); useEffect(() => { - setBreadcrumbs([getBaseBreadcrumb()]); - }, [setBreadcrumbs]); + setBreadcrumbs([getBaseBreadcrumb(history)]); + }, [setBreadcrumbs, history]); return ; }; diff --git a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx index bb3123de3fec8..5f82b5050a172 100644 --- a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx +++ b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, MouseEvent } from 'react'; import { EuiLink, EuiLinkProps, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; @@ -15,13 +15,43 @@ interface RoutingProps { type RoutingLinkProps = Omit & RoutingProps; +const isModifiedEvent = (event: MouseEvent) => + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); + +const isLeftClickEvent = (event: MouseEvent) => event.button === 0; + +const isTargetBlank = (event: MouseEvent) => { + const target = (event.target as HTMLElement).getAttribute('target'); + return target && target !== '_self'; +}; + export const RoutingLink: FC = ({ to, ...rest }) => { const history = useHistory(); + const onClick = useCallback( + (event: MouseEvent) => { + if (event.defaultPrevented) { + return; + } + + // Let the browser handle links that open new tabs/windows + if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) { + return; + } + + // Prevent regular link behavior, which causes a browser refresh. + event.preventDefault(); + + // Push the route to the history. + history.push(to); + }, + [history, to] + ); + // Generate the correct link href (with basename accounted for) const href = history.createHref({ pathname: to }); - const props = { ...rest, href } as EuiLinkProps; + const props = { ...rest, href, onClick } as EuiLinkProps; return ; }; @@ -31,10 +61,30 @@ type RoutingButtonIconProps = Omit & Rou export const RoutingButtonIcon: FC = ({ to, ...rest }) => { const history = useHistory(); + const onClick = useCallback( + (event: MouseEvent) => { + if (event.defaultPrevented) { + return; + } + + // Let the browser handle links that open new tabs/windows + if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) { + return; + } + + // Prevent regular link behavior, which causes a browser refresh. + event.preventDefault(); + + // Push the route to the history. + history.push(to); + }, + [history, to] + ); + // Generate the correct link href (with basename accounted for) const href = history.createHref({ pathname: to }); - const props = { ...rest, href } as EuiButtonIconProps; + const props = { ...rest, href, onClick } as EuiButtonIconProps; return ; }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx index c02ac90f8066c..8c10efe1b10d9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -29,7 +29,7 @@ interface Props { export const EditorMenu: FC = ({ addElement }) => { const embeddablesService = useEmbeddablesService(); - const { pathname, search } = useLocation(); + const { pathname, search, hash } = useLocation(); const platformService = usePlatformService(); const stateTransferService = embeddablesService.getStateTransfer(); const visualizationsService = useVisualizationsService(); @@ -61,11 +61,11 @@ export const EditorMenu: FC = ({ addElement }) => { path, state: { originatingApp: CANVAS_APP, - originatingPath: `#/${pathname}${search}`, + originatingPath: `${pathname}${search}${hash}`, }, }); }, - [stateTransferService, pathname, search] + [stateTransferService, pathname, search, hash] ); const createNewEmbeddable = useCallback( diff --git a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts index 02036be10322d..53f0fbbfa3346 100644 --- a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts +++ b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts @@ -5,12 +5,47 @@ * 2.0. */ +import { MouseEvent } from 'react'; +import { History } from 'history'; import { ChromeBreadcrumb } from '@kbn/core/public'; -export const getBaseBreadcrumb = (): ChromeBreadcrumb => ({ - text: 'Canvas', - href: '#/', -}); +const isModifiedEvent = (event: MouseEvent) => + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); + +const isLeftClickEvent = (event: MouseEvent) => event.button === 0; + +const isTargetBlank = (event: MouseEvent) => { + const target = (event.target as HTMLElement).getAttribute('target'); + return target && target !== '_self'; +}; + +export const getBaseBreadcrumb = (history: History): ChromeBreadcrumb => { + const path = '/'; + const href = history.createHref({ pathname: path }); + + const onClick = (event: MouseEvent) => { + if (event.defaultPrevented) { + return; + } + + // Let the browser handle links that open new tabs/windows + if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) { + return; + } + + // Prevent regular link behavior, which causes a browser refresh. + event.preventDefault(); + + // Push the route to the history. + history.push(path); + }; + + return { + text: 'Canvas', + href, + onClick, + }; +}; export const getWorkpadBreadcrumb = ({ name = 'Workpad', diff --git a/x-pack/plugins/canvas/public/routes/index.tsx b/x-pack/plugins/canvas/public/routes/index.tsx index fd09aeae3fa9a..e7e9cd6541a3d 100644 --- a/x-pack/plugins/canvas/public/routes/index.tsx +++ b/x-pack/plugins/canvas/public/routes/index.tsx @@ -6,17 +6,48 @@ */ import React, { FC } from 'react'; -import { Router, Switch } from 'react-router-dom'; +import { Router, Switch, Route, RouteComponentProps, Redirect } from 'react-router-dom'; import { History } from 'history'; +import { parse, stringify } from 'query-string'; import { HomeRoute } from './home'; import { WorkpadRoute, ExportWorkpadRoute } from './workpad'; +const isHashPath = (hash: string) => { + return hash.indexOf('#/') === 0; +}; + +const mergeQueryStrings = (query: string, queryFromHash: string) => { + const queryObject = parse(query); + const hashObject = parse(queryFromHash); + + return stringify({ ...queryObject, ...hashObject }); +}; + export const CanvasRouter: FC<{ history: History }> = ({ history }) => ( - - {ExportWorkpadRoute()} - {WorkpadRoute()} - {HomeRoute()} - + { + // If it looks like the hash is a route then we will do a redirect + if (isHashPath(route.location.hash)) { + const [hashPath, hashQuery] = route.location.hash.split('?'); + let search = route.location.search || '?'; + + if (hashQuery !== undefined) { + search = mergeQueryStrings(search, `?${hashQuery}`); + } + + return 1 ? `?${search}` : ''}`} />; + } + + return ( + + {ExportWorkpadRoute()} + {WorkpadRoute()} + {HomeRoute()} + + ); + }} + /> ); diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx index 084c9d8c76b00..d6c4b1c6277f5 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { FC, useEffect } from 'react'; import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { getBaseBreadcrumb, getWorkpadBreadcrumb } from '../../lib/breadcrumbs'; // @ts-expect-error import { setDocTitle } from '../../lib/doc_title'; @@ -27,13 +28,14 @@ export const WorkpadPresentationHelper: FC = ({ children }) => { useFullscreenPresentationHelper(); useAutoplayHelper(); useRefreshHelper(); + const history = useHistory(); useEffect(() => { platformService.setBreadcrumbs([ - getBaseBreadcrumb(), + getBaseBreadcrumb(history), getWorkpadBreadcrumb({ name: workpad.name }), ]); - }, [workpad.name, workpad.id, platformService]); + }, [workpad.name, workpad.id, platformService, history]); useEffect(() => { setDocTitle(workpad.name); @@ -44,7 +46,7 @@ export const WorkpadPresentationHelper: FC = ({ children }) => { objectNoun: getWorkpadLabel(), currentObjectId: workpad.id, otherObjectId: workpad.aliasId, - otherObjectPath: `#/workpad/${workpad.aliasId}`, + otherObjectPath: `/workpad/${workpad.aliasId}`, }) : null; diff --git a/x-pack/plugins/canvas/public/services/kibana/nav_link.ts b/x-pack/plugins/canvas/public/services/kibana/nav_link.ts index cf68b69155ad7..8470c688f5b2b 100644 --- a/x-pack/plugins/canvas/public/services/kibana/nav_link.ts +++ b/x-pack/plugins/canvas/public/services/kibana/nav_link.ts @@ -20,7 +20,7 @@ export type CanvasNavLinkServiceFactory = KibanaPluginServiceFactory< export const navLinkServiceFactory: CanvasNavLinkServiceFactory = ({ coreStart, appUpdater }) => ({ updatePath: (path: string) => { appUpdater?.next(() => ({ - defaultPath: `#${path}`, + defaultPath: `${path}`, })); getSessionStorage().set(`${SESSIONSTORAGE_LASTPATH}:${coreStart.http.basePath.get()}`, path); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index 1497c85b91bad..d1a500be98ff8 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -94,7 +94,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`allows a workpad to be edited`, async () => { await PageObjects.common.navigateToActualUrl( 'canvas', - 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', { ensureCurrentUrl: true, shouldLoginIfPrompted: false, @@ -171,7 +171,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`does not allow a workpad to be edited`, async () => { await PageObjects.common.navigateToActualUrl( 'canvas', - 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', { ensureCurrentUrl: true, shouldLoginIfPrompted: false, diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 5060ac60eceae..e030cdbf5f624 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -80,7 +80,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`allows a workpad to be edited`, async () => { await PageObjects.common.navigateToActualUrl( 'canvas', - 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + '/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', { ensureCurrentUrl: true, shouldLoginIfPrompted: false, diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index 053c58e88cde1..dff3cb7ce30a6 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -48,9 +48,9 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { await retry.try(async () => { const url = await browser.getCurrentUrl(); - // remove all the search params, just compare the route - const hashRoute = new URL(url).hash.split('?')[0]; - expect(hashRoute).to.equal(`#/workpad/${testWorkpadId}/page/1`); + const path = new URL(url).pathname; + + expect(path).to.equal(`/app/canvas/workpad/${testWorkpadId}/page/1`); }); }); diff --git a/yarn.lock b/yarn.lock index afc8afb39df49..4ab577b3807dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16133,11 +16133,6 @@ highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@~10.4.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg== -history-extra@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/history-extra/-/history-extra-5.0.1.tgz#95a2e59dda526c4241d0ae1b124a77a5e4675ce8" - integrity sha512-6XV1L1lHgporVWgppa/Kq+Fnz4lhBew7iMxYCTfzVmoEywsAKJnTjdw1zOd+EGLHGYp0/V8jSVMEgqx4QbHLTw== - history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" From 264c310503e50896d8e1e7d62f835d61a3ef6bd9 Mon Sep 17 00:00:00 2001 From: Beth Richardson <83967298+elastichelix@users.noreply.github.com> Date: Mon, 2 May 2022 11:11:46 -0500 Subject: [PATCH 08/50] Add owner to hello_world plugin tutorial manifest (#131263) * Fix link to developer examples in dev env setup guide --- dev_docs/getting_started/hello_world_plugin.mdx | 5 +++++ dev_docs/getting_started/setting_up_a_development_env.mdx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dev_docs/getting_started/hello_world_plugin.mdx b/dev_docs/getting_started/hello_world_plugin.mdx index be4fcf5671a42..8fa4ea4316129 100644 --- a/dev_docs/getting_started/hello_world_plugin.mdx +++ b/dev_docs/getting_started/hello_world_plugin.mdx @@ -40,6 +40,10 @@ and add the following: "id": "helloWorld", "version": "1.0.0", "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Core", + "githubTeam": "kibana-core" + }, "ui": true } ``` @@ -77,6 +81,7 @@ And add the following to it: ``` $ mkdir public +$ cd public $ touch plugin.tsx ``` diff --git a/dev_docs/getting_started/setting_up_a_development_env.mdx b/dev_docs/getting_started/setting_up_a_development_env.mdx index ae994d6a018de..570dcb57a56b8 100644 --- a/dev_docs/getting_started/setting_up_a_development_env.mdx +++ b/dev_docs/getting_started/setting_up_a_development_env.mdx @@ -72,7 +72,7 @@ In another terminal tab/window you can start Kibana. yarn start ``` -If you include the `--run-examples` flag then all of the [developer examples](https://github.com/elastic/kibana/tree/{branch}/examples). Read more about the advanced options for [Running Kibana](https://www.elastic.co/guide/en/kibana/current/running-kibana-advanced.html). +Include developer examples](https://github.com/elastic/kibana/tree/main/examples) by adding an optional `--run-examples` flag. Read more about the advanced options for [Running Kibana](https://www.elastic.co/guide/en/kibana/current/running-kibana-advanced.html). ## Code away! From 223b49d9af93899423e48177b63ac8d9ebcdac47 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Mon, 2 May 2022 19:29:18 +0300 Subject: [PATCH 09/50] flag enabled (#131336) --- x-pack/plugins/cloud_security_posture/common/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index a351e6f271c71..1d35d6439bead 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -28,7 +28,7 @@ export const RULE_FAILED = `failed`; // A mapping of in-development features to their status. These features should be hidden from users but can be easily // activated via a simple code change in a single location. export const INTERNAL_FEATURE_FLAGS = { - showBenchmarks: false, + showBenchmarks: true, showManageRulesMock: false, showRisksMock: false, showFindingsGroupBy: false, From 848e020e82b1ee498d45f298e7a576f2576100b9 Mon Sep 17 00:00:00 2001 From: Jordan <51442161+JordanSh@users.noreply.github.com> Date: Mon, 2 May 2022 19:43:11 +0300 Subject: [PATCH 10/50] [Cloud Posture] General and Remediation tabs updates (#131294) --- .../findings_flyout/findings_flyout.tsx | 71 ++++++++++--------- .../pages/findings/findings_table.test.tsx | 9 +++ .../public/pages/findings/translations.ts | 15 ++++ .../public/pages/findings/types.ts | 11 ++- 4 files changed, 68 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx index f475c9fa5fcb4..be05e2b8418d6 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_flyout/findings_flyout.tsx @@ -15,7 +15,6 @@ import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, - EuiBadge, EuiTabs, EuiTab, EuiFlexGrid, @@ -23,8 +22,10 @@ import { EuiFlexGroup, EuiIcon, type PropsOf, + EuiMarkdownFormat, } from '@elastic/eui'; import { assertNever } from '@kbn/std'; +import moment from 'moment'; import type { CspFinding } from '../types'; import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge'; import * as TEXT from '../translations'; @@ -33,10 +34,19 @@ import k8sLogoIcon from '../../../assets/icons/k8s_logo.svg'; import { ResourceTab } from './resource_tab'; import { JsonTab } from './json_tab'; -const tabs = ['remediation', 'resource', 'general', 'json'] as const; +const tabs = [ + { title: TEXT.REMEDIATION, id: 'remediation' }, + { title: TEXT.RESOURCE, id: 'resource' }, + { title: TEXT.GENERAL, id: 'general' }, + { title: TEXT.JSON, id: 'json' }, +] as const; const CodeBlock: React.FC> = (props) => ( - + +); + +const Markdown: React.FC> = (props) => ( + ); type FindingsTab = typeof tabs[number]; @@ -74,7 +84,7 @@ const Cards = ({ data }: { data: Card[] }) => ( ); const FindingsTab = ({ tab, findings }: { findings: CspFinding; tab: FindingsTab }) => { - switch (tab) { + switch (tab.id) { case 'remediation': return ; case 'resource': @@ -89,7 +99,7 @@ const FindingsTab = ({ tab, findings }: { findings: CspFinding; tab: FindingsTab }; export const FindingsRuleFlyout = ({ onClose, findings }: FindingFlyoutProps) => { - const [tab, setTab] = useState('remediation'); + const [tab, setTab] = useState(tabs[0]); return ( @@ -109,13 +119,8 @@ export const FindingsRuleFlyout = ({ onClose, findings }: FindingFlyoutProps) => {tabs.map((v) => ( - setTab(v)} - style={{ textTransform: 'capitalize' }} - > - {v} + setTab(v)}> + {v.title} ))} @@ -127,13 +132,11 @@ export const FindingsRuleFlyout = ({ onClose, findings }: FindingFlyoutProps) => ); }; -const getGeneralCards = ({ rule }: CspFinding): Card[] => [ +const getGeneralCards = ({ rule, ...rest }: CspFinding): Card[] => [ { title: TEXT.RULE, listItems: [ - [TEXT.SEVERITY, ''], - [TEXT.INDEX, ''], - [TEXT.RULE_EVALUATED_AT, ''], + [TEXT.RULE_EVALUATED_AT, moment(rest['@timestamp']).format('MMMM D, YYYY @ HH:mm:ss.SSS')], [ TEXT.FRAMEWORK_SOURCES, @@ -145,40 +148,38 @@ const getGeneralCards = ({ rule }: CspFinding): Card[] => [ , ], - [TEXT.SECTION, ''], - [TEXT.PROFILE_APPLICABILITY, ''], - [TEXT.AUDIT, ''], + [TEXT.CIS_SECTION, rule.section], + [TEXT.PROFILE_APPLICABILITY, {rule.profile_applicability}], [TEXT.BENCHMARK, rule.benchmark.name], [TEXT.NAME, rule.name], - [TEXT.DESCRIPTION, rule.description], - [ - TEXT.TAGS, - rule.tags.map((t) => ( - - {t} - - )), - ], + [TEXT.DESCRIPTION, {rule.description}], + [TEXT.AUDIT, {rule.audit}], + [TEXT.REFERENCES, {rule.references}], ], }, ]; -const getRemediationCards = ({ result, ...rest }: CspFinding): Card[] => [ +const getRemediationCards = ({ result, rule, ...rest }: CspFinding): Card[] => [ { title: TEXT.RESULT_DETAILS, listItems: [ - [TEXT.EXPECTED, ''], + result.expected + ? [TEXT.EXPECTED, {JSON.stringify(result.expected, null, 2)}] + : ['', ''], [TEXT.EVIDENCE, {JSON.stringify(result.evidence, null, 2)}], - [TEXT.TIMESTAMP, {rest['@timestamp']}], + [ + TEXT.RULE_EVALUATED_AT, + {moment(rest['@timestamp']).format('MMMM D, YYYY @ HH:mm:ss.SSS')}, + ], ], }, { title: TEXT.REMEDIATION, listItems: [ - ['', {rest.rule.remediation}], - [TEXT.IMPACT, rest.rule.impact], - [TEXT.DEFAULT_VALUE, ''], - [TEXT.RATIONALE, ''], + ['', {rule.remediation}], + [TEXT.IMPACT, {rule.impact}], + [TEXT.DEFAULT_VALUE, {rule.default_value}], + [TEXT.RATIONALE, {rule.rationale}], ], }, ]; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_table.test.tsx index 28d14b42ae099..b287a73469d51 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_table.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/findings_table.test.tsx @@ -17,6 +17,9 @@ const chance = new Chance(); const getFakeFindings = (name: string): CspFinding & { id: string } => ({ id: chance.word(), result: { + expected: { + source: {}, + }, evaluation: chance.weighted(['passed', 'failed'], [0.5, 0.5]), evidence: { filemode: chance.word(), @@ -31,6 +34,12 @@ const getFakeFindings = (name: string): CspFinding & { id: string } => ({ name: 'CIS Kubernetes', version: '1.6.0', }, + section: chance.sentence(), + audit: chance.paragraph(), + references: chance.paragraph(), + profile_applicability: chance.sentence(), + rationale: chance.paragraph(), + default_value: chance.sentence(), tags: [], }, agent: { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts index df950e59a90db..4256f3219b3f6 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/translations.ts @@ -42,6 +42,14 @@ export const RESOURCE = i18n.translate('xpack.csp.findings.resourceLabel', { defaultMessage: 'Resource', }); +export const GENERAL = i18n.translate('xpack.csp.findings.findingsFlyout.generalTabLabel', { + defaultMessage: 'General', +}); + +export const JSON = i18n.translate('xpack.csp.findings.findingsFlyout.jsonTabLabel', { + defaultMessage: 'JSON', +}); + export const RESOURCE_ID = i18n.translate( 'xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnLabel', { @@ -141,6 +149,13 @@ export const AUDIT = i18n.translate('xpack.csp.findings.auditLabel', { defaultMessage: 'Audit', }); +export const REFERENCES = i18n.translate( + 'xpack.csp.findings.findingsFlyout.generalTab.referencesLabel', + { + defaultMessage: 'References', + } +); + export const RESULT_DETAILS = i18n.translate('xpack.csp.findings.resultLabel', { defaultMessage: 'Result Details', }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts index 405ea9f231375..158bbefc422ef 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/types.ts @@ -48,8 +48,14 @@ export interface CspFinding { interface CspRule { benchmark: { name: string; version: string }; + section: string; + audit: string; + references: string; + profile_applicability: string; description: string; impact: string; + default_value: string; + rationale: string; name: string; remediation: string; tags: string[]; @@ -57,9 +63,8 @@ interface CspRule { interface CspFindingResult { evaluation: 'passed' | 'failed'; - evidence: { - filemode: string; - }; + expected?: Record; + evidence: Record; } interface CspFindingResource { From 5f290fc0e97adf5999279e36307bf1a7b0ff9592 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Mon, 2 May 2022 09:57:33 -0700 Subject: [PATCH 11/50] [Security Solution][Alerts] Return tuple of included and excluded docs from value list exception evaluation (#130044) * Return tuple of included and excluded docs from value list exception evaluation * Types and tests * Add doc comment --- .../signals/__mocks__/es_results.ts | 35 +++- .../signals/bulk_create_ml_signals.ts | 23 +-- .../detection_engine/signals/executors/ml.ts | 12 +- .../signals/filters/filter_events.test.ts | 24 +-- .../signals/filters/filter_events.ts | 9 +- .../filter_events_against_list.test.ts | 107 ++++++------ .../filters/filter_events_against_list.ts | 46 +++-- .../detection_engine/signals/filters/types.ts | 7 +- .../signals/search_after_bulk_create.test.ts | 14 +- .../signals/search_after_bulk_create.ts | 21 +-- .../signals/send_telemetry_events.test.ts | 160 ++++++++---------- .../signals/send_telemetry_events.ts | 8 +- .../threat_mapping/build_threat_enrichment.ts | 5 +- .../threat_mapping/create_event_signal.ts | 4 +- .../enrich_signal_threat_matches.test.ts | 36 ++-- .../enrich_signal_threat_matches.ts | 22 +-- .../lib/detection_engine/signals/types.ts | 2 +- 17 files changed, 260 insertions(+), 275 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index d699e71947853..9213d6c5b278c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -910,6 +910,32 @@ export const sampleDocSearchResultsNoSortIdNoHits = ( }, }); +/** + * + * @param count Total number of hits to create + * @param guids List of _id values for the hits. If this array is smaller than count, the remaining hits will receive a default value. + * @param ips List of source.ip values for the hits. If this array is smaller than count, the remaining hits will receive a default value. + * @param destIps List of destination.ip values for the hits. If this array is smaller than count, the remaining hits will receive a default value. + * @param sortIds List of sort IDs. The same list is inserted into every hit. + * @returns Array of mock hits + */ +export const repeatedHitsWithSortId = ( + count: number, + guids: string[], + ips?: Array, + destIps?: Array, + sortIds?: string[] +): SignalSourceHit[] => { + return Array.from({ length: count }).map((x, index) => ({ + ...sampleDocWithSortId( + guids[index], + sortIds, + ips ? ips[index] : '127.0.0.1', + destIps ? destIps[index] : '127.0.0.1' + ), + })); +}; + export const repeatedSearchResultsWithSortId = ( total: number, pageSize: number, @@ -929,14 +955,7 @@ export const repeatedSearchResultsWithSortId = ( hits: { total, max_score: 100, - hits: Array.from({ length: pageSize }).map((x, index) => ({ - ...sampleDocWithSortId( - guids[index], - sortIds, - ips ? ips[index] : '127.0.0.1', - destIps ? destIps[index] : '127.0.0.1' - ), - })), + hits: repeatedHitsWithSortId(pageSize, guids, ips, destIps, sortIds), }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index 66394d4bca81d..e38ee3952cadb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -15,7 +15,7 @@ import { RuleExecutorServices, } from '@kbn/alerting-plugin/server'; import { GenericBulkCreateResponse } from '../rule_types/factories'; -import { AnomalyResults, Anomaly } from '../../machine_learning'; +import { Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; import { BulkCreate, WrapHits } from './types'; import { CompleteRule, MachineLearningRuleParams } from '../schemas/rule_schemas'; @@ -23,7 +23,7 @@ import { buildReasonMessageForMlAlert } from './reason_formatters'; import { BaseFieldsLatest } from '../../../../common/detection_engine/schemas/alerts'; interface BulkCreateMlSignalsParams { - someResult: AnomalyResults; + anomalyHits: Array>; completeRule: CompleteRule; services: RuleExecutorServices; logger: Logger; @@ -65,32 +65,23 @@ export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { }; const transformAnomalyResultsToEcs = ( - results: AnomalyResults -): estypes.SearchResponse => { - const transformedHits = results.hits.hits.map(({ _source, ...rest }) => ({ + results: Array> +): Array> => { + return results.map(({ _source, ...rest }) => ({ ...rest, _source: transformAnomalyFieldsToEcs( // @ts-expect-error @elastic/elasticsearch _source is optional _source ), })); - - // @ts-expect-error Anomaly is not assignable to EcsAnomaly - return { - ...results, - hits: { - ...results.hits, - hits: transformedHits, - }, - }; }; export const bulkCreateMlSignals = async ( params: BulkCreateMlSignalsParams ): Promise> => { - const anomalyResults = params.someResult; + const anomalyResults = params.anomalyHits; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); - const wrappedDocs = params.wrapHits(ecsResults.hits.hits, buildReasonMessageForMlAlert); + const wrappedDocs = params.wrapHits(ecsResults, buildReasonMessageForMlAlert); return params.bulkCreate(wrappedDocs); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts index 2070d487c49d0..22c11b565e909 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts @@ -97,21 +97,21 @@ export const mlExecutor = async ({ exceptionItems, }); - const filteredAnomalyResults = await filterEventsAgainstList({ + const [filteredAnomalyHits, _] = await filterEventsAgainstList({ listClient, exceptionsList: exceptionItems, logger, - eventSearchResult: anomalyResults, + events: anomalyResults.hits.hits, buildRuleMessage, }); - const anomalyCount = filteredAnomalyResults.hits.hits.length; + const anomalyCount = filteredAnomalyHits.length; if (anomalyCount) { logger.debug(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`)); } const { success, errors, bulkCreateDuration, createdItemsCount, createdItems } = await bulkCreateMlSignals({ - someResult: filteredAnomalyResults, + anomalyHits: filteredAnomalyHits, completeRule, services, logger, @@ -124,7 +124,7 @@ export const mlExecutor = async ({ // The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] } const shardFailures = ( - filteredAnomalyResults._shards as typeof filteredAnomalyResults._shards & { + anomalyResults._shards as typeof anomalyResults._shards & { failures: []; } ).failures ?? []; @@ -134,7 +134,7 @@ export const mlExecutor = async ({ return mergeReturns([ result, createSearchAfterReturnType({ - success: success && filteredAnomalyResults._shards.failed === 0, + success: success && anomalyResults._shards.failed === 0, errors: [...errors, ...searchErrors], createdSignalsCount: createdItemsCount, createdSignals: createdItems, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts index df9bb0cd59f83..bed5f96fbc233 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts @@ -9,10 +9,10 @@ import { sampleDocWithSortId } from '../__mocks__/es_results'; import { listMock } from '@kbn/lists-plugin/server/mocks'; import { getSearchListItemResponseMock } from '@kbn/lists-plugin/common/schemas/response/search_list_item_schema.mock'; -import { filterEvents } from './filter_events'; +import { partitionEvents } from './filter_events'; import { FieldSet } from './types'; -describe('filterEvents', () => { +describe('partitionEvents', () => { let listClient = listMock.getListClient(); let events = [sampleDocWithSortId('123', undefined, '1.1.1.1')]; @@ -43,11 +43,12 @@ describe('filterEvents', () => { matchedSet: new Set([JSON.stringify(['1.1.1.1'])]), }, ]; - const field = filterEvents({ + const [included, excluded] = partitionEvents({ events, fieldAndSetTuples, }); - expect([...field]).toEqual([]); + expect(included).toEqual([]); + expect(excluded).toEqual(events); }); test('it does not filter out the event if it is "excluded"', () => { @@ -59,11 +60,12 @@ describe('filterEvents', () => { matchedSet: new Set([JSON.stringify(['1.1.1.1'])]), }, ]; - const field = filterEvents({ + const [included, excluded] = partitionEvents({ events, fieldAndSetTuples, }); - expect([...field]).toEqual(events); + expect(included).toEqual(events); + expect(excluded).toEqual([]); }); test('it does NOT filter out the event if the field is not found', () => { @@ -75,11 +77,12 @@ describe('filterEvents', () => { matchedSet: new Set([JSON.stringify(['1.1.1.1'])]), }, ]; - const field = filterEvents({ + const [included, excluded] = partitionEvents({ events, fieldAndSetTuples, }); - expect([...field]).toEqual(events); + expect(included).toEqual(events); + expect(excluded).toEqual([]); }); test('it does NOT filter out the event if it is in both an inclusion and exclusion list', () => { @@ -100,10 +103,11 @@ describe('filterEvents', () => { }, ]; - const field = filterEvents({ + const [included, excluded] = partitionEvents({ events, fieldAndSetTuples, }); - expect([...field]).toEqual(events); + expect(included).toEqual(events); + expect(excluded).toEqual([]); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts index d267153a4813a..aee98b7e0ff5b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts @@ -5,6 +5,7 @@ * 2.0. */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { partition } from 'lodash'; import { FilterEventsOptions } from './types'; /** @@ -12,12 +13,14 @@ import { FilterEventsOptions } from './types'; * If the entry is in both an inclusion and exclusion list it will not be filtered out. * @param events The events to check against * @param fieldAndSetTuples The field and set tuples + * @returns A tuple where the first element is an array of alerts that should be created and second element is + * an array of alerts that matched the exception and should not be created. */ -export const filterEvents = ({ +export const partitionEvents = ({ events, fieldAndSetTuples, -}: FilterEventsOptions): Array> => { - return events.filter((item) => { +}: FilterEventsOptions): [Array>, Array>] => { + return partition(events, (item) => { return fieldAndSetTuples .map((tuple) => { const eventItem = item.fields ? item.fields[tuple.field] : undefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts index 8c33cf2ea1314..22dc5136fcded 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts @@ -8,7 +8,7 @@ import uuid from 'uuid'; import { filterEventsAgainstList } from './filter_events_against_list'; import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; -import { mockLogger, repeatedSearchResultsWithSortId } from '../__mocks__/es_results'; +import { mockLogger, repeatedHitsWithSortId } from '../__mocks__/es_results'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { listMock } from '@kbn/lists-plugin/server/mocks'; @@ -30,11 +30,11 @@ describe('filterEventsAgainstList', () => { }); it('should respond with eventSearchResult if exceptionList is empty array', async () => { - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + events: repeatedHitsWithSortId(4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', '3.3.3.3', @@ -42,15 +42,16 @@ describe('filterEventsAgainstList', () => { ]), buildRuleMessage, }); - expect(res.hits.hits.length).toEqual(4); + expect(included.length).toEqual(4); + expect(excluded.length).toEqual(0); }); it('should respond with eventSearchResult if exceptionList does not contain value list exceptions', async () => { - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [getExceptionListItemSchemaMock()], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + events: repeatedHitsWithSortId(4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', '3.3.3.3', @@ -58,7 +59,8 @@ describe('filterEventsAgainstList', () => { ]), buildRuleMessage, }); - expect(res.hits.hits.length).toEqual(4); + expect(included.length).toEqual(4); + expect(excluded.length).toEqual(0); expect((mockLogger.debug as unknown as jest.Mock).mock.calls[0][0]).toContain( 'no exception items of type list found - returning original search result' ); @@ -79,14 +81,15 @@ describe('filterEventsAgainstList', () => { }, ]; - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)), + events: repeatedHitsWithSortId(4, someGuids.slice(0, 3)), buildRuleMessage, }); - expect(res.hits.hits.length).toEqual(4); + expect(included.length).toEqual(4); + expect(excluded.length).toEqual(0); }); it('should respond with less items in the list if some values match', async () => { @@ -110,11 +113,11 @@ describe('filterEventsAgainstList', () => { })) ) ); - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + events: repeatedHitsWithSortId(4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', '3.3.3.3', @@ -126,10 +129,11 @@ describe('filterEventsAgainstList', () => { expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( 'ci-badguys.txt' ); - expect(res.hits.hits.length).toEqual(2); + expect(included.length).toEqual(2); + expect(excluded.length).toEqual(2); // @ts-expect-error - const ipVals = res.hits.hits.map((item) => item._source.source.ip); + const ipVals = included.map((item) => item._source.source.ip); expect(['3.3.3.3', '7.7.7.7']).toEqual(ipVals); }); @@ -170,11 +174,11 @@ describe('filterEventsAgainstList', () => { { ...getSearchListItemResponseMock(), value: ['6.6.6.6'] }, ]); - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem, exceptionItemAgain], - eventSearchResult: repeatedSearchResultsWithSortId(9, 9, someGuids.slice(0, 9), [ + events: repeatedHitsWithSortId(9, someGuids.slice(0, 9), [ '1.1.1.1', '2.2.2.2', '3.3.3.3', @@ -188,10 +192,11 @@ describe('filterEventsAgainstList', () => { buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); - expect(res.hits.hits.length).toEqual(6); + expect(included.length).toEqual(6); + expect(excluded.length).toEqual(3); // @ts-expect-error - const ipVals = res.hits.hits.map((item) => item._source.source.ip); + const ipVals = included.map((item) => item._source.source.ip); expect(['1.1.1.1', '3.3.3.3', '5.5.5.5', '7.7.7.7', '8.8.8.8', '9.9.9.9']).toEqual(ipVals); }); @@ -231,11 +236,11 @@ describe('filterEventsAgainstList', () => { { ...getSearchListItemResponseMock(), value: ['6.6.6.6'] }, ]); - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem, exceptionItemAgain], - eventSearchResult: repeatedSearchResultsWithSortId(9, 9, someGuids.slice(0, 9), [ + events: repeatedHitsWithSortId(9, someGuids.slice(0, 9), [ '1.1.1.1', '2.2.2.2', '3.3.3.3', @@ -250,8 +255,9 @@ describe('filterEventsAgainstList', () => { }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); // @ts-expect-error - const ipVals = res.hits.hits.map((item) => item._source.source.ip); - expect(res.hits.hits.length).toEqual(7); + const ipVals = included.map((item) => item._source.source.ip); + expect(included.length).toEqual(7); + expect(excluded.length).toEqual(2); expect(['1.1.1.1', '3.3.3.3', '4.4.4.4', '5.5.5.5', '7.7.7.7', '8.8.8.8', '9.9.9.9']).toEqual( ipVals @@ -290,12 +296,11 @@ describe('filterEventsAgainstList', () => { { ...getSearchListItemResponseMock(), value: ['4.4.4.4'] }, ]); - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem], - eventSearchResult: repeatedSearchResultsWithSortId( - 9, + events: repeatedHitsWithSortId( 9, someGuids.slice(0, 9), [ @@ -324,10 +329,11 @@ describe('filterEventsAgainstList', () => { buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); - expect(res.hits.hits.length).toEqual(8); + expect(included.length).toEqual(8); + expect(excluded.length).toEqual(1); // @ts-expect-error - const ipVals = res.hits.hits.map((item) => item._source.source.ip); + const ipVals = included.map((item) => item._source?.source?.ip); expect([ '1.1.1.1', '2.2.2.2', @@ -368,11 +374,11 @@ describe('filterEventsAgainstList', () => { { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] }, ]); - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem], - eventSearchResult: repeatedSearchResultsWithSortId(9, 9, someGuids.slice(0, 9), [ + events: repeatedHitsWithSortId(9, someGuids.slice(0, 9), [ '1.1.1.1', '2.2.2.2', '3.3.3.3', @@ -386,10 +392,11 @@ describe('filterEventsAgainstList', () => { buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); - expect(res.hits.hits.length).toEqual(9); + expect(included.length).toEqual(9); + expect(excluded.length).toEqual(0); // @ts-expect-error - const ipVals = res.hits.hits.map((item) => item._source.source.ip); + const ipVals = included.map((item) => item._source.source.ip); expect([ '1.1.1.1', '2.2.2.2', @@ -435,12 +442,11 @@ describe('filterEventsAgainstList', () => { { ...getSearchListItemResponseMock(), value: ['3.3.3.3', '4.4.4.4'] }, ]); - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem], - eventSearchResult: repeatedSearchResultsWithSortId( - 3, + events: repeatedHitsWithSortId( 3, someGuids.slice(0, 3), [ @@ -467,16 +473,17 @@ describe('filterEventsAgainstList', () => { ['2.2.2.2', '3.3.3.3'], ['3.3.3.3', '4.4.4.4'], ]); - expect(res.hits.hits.length).toEqual(2); + expect(included.length).toEqual(2); + expect(excluded.length).toEqual(1); // @ts-expect-error - const sourceIpVals = res.hits.hits.map((item) => item._source.source.ip); + const sourceIpVals = included.map((item) => item._source.source.ip); expect([ ['1.1.1.1', '1.1.1.1'], ['1.1.1.1', '2.2.2.2'], ]).toEqual(sourceIpVals); // @ts-expect-error - const destIpVals = res.hits.hits.map((item) => item._source.destination.ip); + const destIpVals = included.map((item) => item._source.destination.ip); expect([ ['1.1.1.1', '2.2.2.2'], ['2.2.2.2', '3.3.3.3'], @@ -497,14 +504,15 @@ describe('filterEventsAgainstList', () => { }, }, ]; - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)), + events: repeatedHitsWithSortId(4, someGuids.slice(0, 3)), buildRuleMessage, }); - expect(res.hits.hits.length).toEqual(0); + expect(included.length).toEqual(0); + expect(excluded.length).toEqual(4); }); it('should respond with less items in the list if some values match', async () => { @@ -528,11 +536,11 @@ describe('filterEventsAgainstList', () => { })) ) ); - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + events: repeatedHitsWithSortId(4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', '3.3.3.3', @@ -544,7 +552,8 @@ describe('filterEventsAgainstList', () => { expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( 'ci-badguys.txt' ); - expect(res.hits.hits.length).toEqual(2); + expect(included.length).toEqual(2); + expect(excluded.length).toEqual(2); }); it('should respond with the same items in the list given one exception item with two entries of type list and array of values in document', async () => { @@ -582,12 +591,11 @@ describe('filterEventsAgainstList', () => { { ...getSearchListItemResponseMock(), value: ['3.3.3.3', '4.4.4.4'] }, ]); - const res = await filterEventsAgainstList({ + const [included, excluded] = await filterEventsAgainstList({ logger: mockLogger, listClient, exceptionsList: [exceptionItem], - eventSearchResult: repeatedSearchResultsWithSortId( - 3, + events: repeatedHitsWithSortId( 3, someGuids.slice(0, 3), [ @@ -614,16 +622,17 @@ describe('filterEventsAgainstList', () => { ['2.2.2.2', '3.3.3.3'], ['3.3.3.3', '4.4.4.4'], ]); - expect(res.hits.hits.length).toEqual(2); + expect(included.length).toEqual(2); + expect(excluded.length).toEqual(1); // @ts-expect-error - const sourceIpVals = res.hits.hits.map((item) => item._source.source.ip); + const sourceIpVals = included.map((item) => item._source.source.ip); expect([ ['1.1.1.1', '2.2.2.2'], ['2.2.2.2', '3.3.3.3'], ]).toEqual(sourceIpVals); // @ts-expect-error - const destIpVals = res.hits.hits.map((item) => item._source.destination.ip); + const destIpVals = included.map((item) => item._source.destination.ip); expect([ ['2.2.2.2', '3.3.3.3'], ['3.3.3.3', '4.4.4.4'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts index 49a8ab0781eb0..7b9f6fde51842 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -5,13 +5,12 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { entriesList, ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; -import { FilterEventsAgainstListOptions } from './types'; -import { filterEvents } from './filter_events'; +import { FilterEventsAgainstListOptions, FilterEventsAgainstListReturn } from './types'; +import { partitionEvents } from './filter_events'; import { createFieldAndSetTuples } from './create_field_and_set_tuples'; /** @@ -39,9 +38,9 @@ export const filterEventsAgainstList = async ({ listClient, exceptionsList, logger, - eventSearchResult, + events, buildRuleMessage, -}: FilterEventsAgainstListOptions): Promise> => { +}: FilterEventsAgainstListOptions): Promise> => { try { const atLeastOneLargeValueList = exceptionsList.some(({ entries }) => hasLargeValueList(entries) @@ -51,46 +50,41 @@ export const filterEventsAgainstList = async ({ logger.debug( buildRuleMessage('no exception items of type list found - returning original search result') ); - return eventSearchResult; + return [events, []]; } const valueListExceptionItems = exceptionsList.filter((listItem: ExceptionListItemSchema) => { return listItem.entries.every((entry) => entriesList.is(entry)); }); - const res = await valueListExceptionItems.reduce>>>( + // Every event starts out in the 'included' list, and each value list item checks all the + // current 'included' events and moves events that match the exception to the 'excluded' list + return valueListExceptionItems.reduce>>( async ( - filteredAccum: Promise>>, + filteredAccum: Promise>, exceptionItem: ExceptionListItemSchema ) => { - const events = await filteredAccum; + const [includedEvents, excludedEvents] = await filteredAccum; const fieldAndSetTuples = await createFieldAndSetTuples({ - events, + events: includedEvents, exceptionItem, listClient, logger, buildRuleMessage, }); - const filteredEvents = filterEvents({ events, fieldAndSetTuples }); - const diff = eventSearchResult.hits.hits.length - filteredEvents.length; + const [nextIncludedEvents, nextExcludedEvents] = partitionEvents({ + events: includedEvents, + fieldAndSetTuples, + }); logger.debug( - buildRuleMessage(`Exception with id ${exceptionItem.id} filtered out ${diff} events`) + buildRuleMessage( + `Exception with id ${exceptionItem.id} filtered out ${nextExcludedEvents.length} events` + ) ); - return filteredEvents; + return [nextIncludedEvents, [...excludedEvents, ...nextExcludedEvents]]; }, - Promise.resolve>>(eventSearchResult.hits.hits) + Promise.resolve>([events, []]) ); - - return { - took: eventSearchResult.took, - timed_out: eventSearchResult.timed_out, - _shards: eventSearchResult._shards, - hits: { - total: res.length, - max_score: eventSearchResult.hits.max_score, - hits: res, - }, - }; } catch (exc) { throw new Error(`Failed to query large value based lists index. Reason: ${exc.message}`); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts index 051685c67db4a..f5d438f54bdb3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts @@ -15,10 +15,15 @@ export interface FilterEventsAgainstListOptions { listClient: ListClient; exceptionsList: ExceptionListItemSchema[]; logger: Logger; - eventSearchResult: estypes.SearchResponse; + events: Array>; buildRuleMessage: BuildRuleMessage; } +export type FilterEventsAgainstListReturn = [ + Array>, + Array> +]; + export interface CreateSetToFilterAgainstOptions { events: Array>; field: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index b96b0e6dc7148..4f3a798a327ce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -1004,16 +1004,12 @@ describe('searchAfterAndBulkCreate', () => { }); expect(mockEnrichment).toHaveBeenCalledWith( - expect.objectContaining({ - hits: expect.objectContaining({ - hits: expect.arrayContaining([ - expect.objectContaining({ - ...sampleDocWithSortId(), - _id: expect.any(String), - }), - ]), + expect.objectContaining([ + expect.objectContaining({ + ...sampleDocWithSortId(), + _id: expect.any(String), }), - }) + ]) ); expect(success).toEqual(true); expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(4); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 69c001898b217..84ef95b856a5f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -127,11 +127,11 @@ export const searchAfterAndBulkCreate = async ({ // filter out the search results that match with the values found in the list. // the resulting set are signals to be indexed, given they are not duplicates // of signals already present in the signals index. - const filteredEvents = await filterEventsAgainstList({ + const [includedEvents, _] = await filterEventsAgainstList({ listClient, exceptionsList, logger, - eventSearchResult: mergedSearchResults, + events: mergedSearchResults.hits.hits, buildRuleMessage, }); @@ -139,16 +139,11 @@ export const searchAfterAndBulkCreate = async ({ // if there isn't anything after going through the value list filter // skip the call to bulk create and proceed to the next search_after, // if there is a sort id to continue the search_after with. - if (filteredEvents.hits.hits.length !== 0) { + if (includedEvents.length !== 0) { // make sure we are not going to create more signals than maxSignals allows - if (signalsCreatedCount + filteredEvents.hits.hits.length > tuple.maxSignals) { - filteredEvents.hits.hits = filteredEvents.hits.hits.slice( - 0, - tuple.maxSignals - signalsCreatedCount - ); - } - const enrichedEvents = await enrichment(filteredEvents); - const wrappedDocs = wrapHits(enrichedEvents.hits.hits, buildReasonMessage); + const limitedEvents = includedEvents.slice(0, tuple.maxSignals - signalsCreatedCount); + const enrichedEvents = await enrichment(limitedEvents); + const wrappedDocs = wrapHits(enrichedEvents, buildReasonMessage); const { bulkCreateDuration: bulkDuration, @@ -171,9 +166,7 @@ export const searchAfterAndBulkCreate = async ({ signalsCreatedCount += createdCount; logger.debug(buildRuleMessage(`created ${createdCount} signals`)); logger.debug(buildRuleMessage(`signalsCreatedCount: ${signalsCreatedCount}`)); - logger.debug( - buildRuleMessage(`enrichedEvents.hits.hits: ${enrichedEvents.hits.hits.length}`) - ); + logger.debug(buildRuleMessage(`enrichedEvents.hits.hits: ${enrichedEvents.length}`)); sendAlertTelemetryEvents( logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts index 36bb90936620b..d598b84ea99e2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.test.ts @@ -9,100 +9,86 @@ import { selectEvents, enrichEndpointAlertsSignalID } from './send_telemetry_eve describe('sendAlertTelemetry', () => { it('selectEvents', () => { - const filteredEvents = { - took: 0, - timed_out: false, - _shards: { - total: 1, - successful: 1, - failed: 0, - skipped: 0, + const filteredEvents = [ + { + _index: 'x', + _type: 'x', + _id: 'x', + _score: 0, + _source: { + '@timestamp': 'x', + key1: 'hello', + data_stream: { + dataset: 'endpoint.events', + }, + event: { + id: 'foo', + }, + }, }, - hits: { - total: 2, - max_score: 0, - hits: [ - { - _index: 'x', - _type: 'x', - _id: 'x', - _score: 0, - _source: { - '@timestamp': 'x', - key1: 'hello', - data_stream: { - dataset: 'endpoint.events', - }, - event: { - id: 'foo', - }, - }, + { + _index: 'x', + _type: 'x', + _id: 'x', + _score: 0, + _source: { + '@timestamp': 'x', + key2: 'hello', + data_stream: { + dataset: 'endpoint.alerts', + other: 'x', + }, + event: { + id: 'bar', }, - { - _index: 'x', - _type: 'x', - _id: 'x', - _score: 0, - _source: { - '@timestamp': 'x', - key2: 'hello', - data_stream: { - dataset: 'endpoint.alerts', - other: 'x', - }, - event: { - id: 'bar', - }, - }, + }, + }, + { + _index: 'x', + _type: 'x', + _id: 'x', + _score: 0, + _source: { + '@timestamp': 'x', + key3: 'hello', + data_stream: {}, + event: { + id: 'baz', }, - { - _index: 'x', - _type: 'x', - _id: 'x', - _score: 0, - _source: { - '@timestamp': 'x', - key3: 'hello', - data_stream: {}, - event: { - id: 'baz', - }, - }, + }, + }, + { + _index: 'y', + _type: 'y', + _id: 'y', + _score: 0, + _source: { + '@timestamp': 'y', + key3: 'hello', + data_stream: { + dataset: 'endpoint.alerts', + other: 'y', }, - { - _index: 'y', - _type: 'y', - _id: 'y', - _score: 0, - _source: { - '@timestamp': 'y', - key3: 'hello', - data_stream: { - dataset: 'endpoint.alerts', - other: 'y', - }, - event: { - id: 'not-in-map', - }, - }, + event: { + id: 'not-in-map', }, - { - _index: 'z', - _type: 'z', - _id: 'z', - _score: 0, - _source: { - '@timestamp': 'z', - key3: 'no-event-id', - data_stream: { - dataset: 'endpoint.alerts', - other: 'z', - }, - }, + }, + }, + { + _index: 'z', + _type: 'z', + _id: 'z', + _score: 0, + _source: { + '@timestamp': 'z', + key3: 'no-event-id', + data_stream: { + dataset: 'endpoint.alerts', + other: 'z', }, - ], + }, }, - }; + ]; const joinMap = new Map([ ['foo', '1234'], ['bar', 'abcd'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts index 511f148f13d68..e419fdf632137 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts @@ -9,7 +9,7 @@ import { Logger } from '@kbn/core/server'; import { ITelemetryEventsSender } from '../../telemetry/sender'; import { TelemetryEvent } from '../../telemetry/types'; import { BuildRuleMessage } from './rule_messages'; -import { SignalSearchResponse, SignalSource } from './types'; +import { SignalSource, SignalSourceHit } from './types'; interface SearchResultSource { _source: SignalSource; @@ -18,9 +18,9 @@ interface SearchResultSource { type CreatedSignalId = string; type AlertId = string; -export function selectEvents(filteredEvents: SignalSearchResponse): TelemetryEvent[] { +export function selectEvents(filteredEvents: SignalSourceHit[]): TelemetryEvent[] { // @ts-expect-error @elastic/elasticsearch _source is optional - const sources: TelemetryEvent[] = filteredEvents.hits.hits.map(function ( + const sources: TelemetryEvent[] = filteredEvents.map(function ( obj: SearchResultSource ): TelemetryEvent { return obj._source; @@ -46,7 +46,7 @@ export function enrichEndpointAlertsSignalID( export function sendAlertTelemetryEvents( logger: Logger, eventsTelemetry: ITelemetryEventsSender | undefined, - filteredEvents: SignalSearchResponse, + filteredEvents: SignalSourceHit[], createdEvents: SignalSource[], buildRuleMessage: BuildRuleMessage ) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts index bc31ee660aad8..4c5391d238b31 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SignalSearchResponse, SignalsEnrichment } from '../types'; +import { SignalsEnrichment } from '../types'; import { enrichSignalThreatMatches } from './enrich_signal_threat_matches'; import { BuildThreatEnrichmentOptions, GetMatchedThreats } from './types'; import { getThreatList } from './get_threat_list'; @@ -55,6 +55,5 @@ export const buildThreatEnrichment = ({ return threatResponse.hits.hits; }; - return (signals: SignalSearchResponse): Promise => - enrichSignalThreatMatches(signals, getMatchedThreats, threatIndicatorPath); + return (signals) => enrichSignalThreatMatches(signals, getMatchedThreats, threatIndicatorPath); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts index c5d86c9ab460c..2587c76907ccb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts @@ -10,7 +10,7 @@ import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; import { buildReasonMessageForThreatMatchAlert } from '../reason_formatters'; import { CreateEventSignalOptions } from './types'; -import { SearchAfterAndBulkCreateReturnType, SignalSearchResponse } from '../types'; +import { SearchAfterAndBulkCreateReturnType, SignalSourceHit } from '../types'; import { getAllThreatListHits } from './get_threat_list'; import { enrichSignalThreatMatches, @@ -112,7 +112,7 @@ export const createEventSignal = async ({ ) ); - const threatEnrichment = (signals: SignalSearchResponse): Promise => + const threatEnrichment = (signals: SignalSourceHit[]): Promise => enrichSignalThreatMatches( signals, () => Promise.resolve(threatListHits), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index 66e44e5796eb6..b6df435c04dda 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -8,6 +8,7 @@ import { get } from 'lodash'; import { ENRICHMENT_DESTINATION_PATH } from '../../../../../common/constants'; import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; +import { SignalSourceHit } from '../types'; import { getThreatListItemMock } from './build_threat_mapping_filter.mock'; import { @@ -16,11 +17,7 @@ import { groupAndMergeSignalMatches, getSignalMatchesFromThreatList, } from './enrich_signal_threat_matches'; -import { - getNamedQueryMock, - getSignalHitMock, - getSignalsResponseMock, -} from './enrich_signal_threat_matches.mock'; +import { getNamedQueryMock, getSignalHitMock } from './enrich_signal_threat_matches.mock'; import { GetMatchedThreats, ThreatListItem, ThreatMatchNamedQuery } from './types'; import { encodeThreatMatchNamedQuery } from './utils'; @@ -507,14 +504,14 @@ describe('enrichSignalThreatMatches', () => { }); it('performs no enrichment if there are no signals', async () => { - const signals = getSignalsResponseMock([]); + const signals: SignalSourceHit[] = []; const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, indicatorPath ); - expect(enrichedSignals.hits.hits).toEqual([]); + expect(enrichedSignals).toEqual([]); }); it('preserves existing threat.enrichments objects on signals', async () => { @@ -526,13 +523,13 @@ describe('enrichSignalThreatMatches', () => { }, matched_queries: [matchedQuery], }); - const signals = getSignalsResponseMock([signalHit]); + const signals: SignalSourceHit[] = [signalHit]; const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, indicatorPath ); - const [enrichedHit] = enrichedSignals.hits.hits; + const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); expect(enrichments).toEqual([ @@ -560,13 +557,13 @@ describe('enrichSignalThreatMatches', () => { const signalHit = getSignalHitMock({ matched_queries: [matchedQuery], }); - const signals = getSignalsResponseMock([signalHit]); + const signals: SignalSourceHit[] = [signalHit]; const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, indicatorPath ); - const [enrichedHit] = enrichedSignals.hits.hits; + const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); expect(enrichments).toEqual([ @@ -598,13 +595,13 @@ describe('enrichSignalThreatMatches', () => { }, matched_queries: [matchedQuery], }); - const signals = getSignalsResponseMock([signalHit]); + const signals: SignalSourceHit[] = [signalHit]; const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, indicatorPath ); - const [enrichedHit] = enrichedSignals.hits.hits; + const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); expect(enrichments).toEqual([ @@ -637,7 +634,7 @@ describe('enrichSignalThreatMatches', () => { _source: { '@timestamp': 'mocked', threat: 'whoops' }, matched_queries: [matchedQuery], }); - const signals = getSignalsResponseMock([signalHit]); + const signals: SignalSourceHit[] = [signalHit]; await expect(() => enrichSignalThreatMatches(signals, getMatchedThreats, indicatorPath) ).rejects.toThrowError('Expected threat field to be an object, but found: whoops'); @@ -674,13 +671,13 @@ describe('enrichSignalThreatMatches', () => { }, matched_queries: [matchedQuery], }); - const signals = getSignalsResponseMock([signalHit]); + const signals: SignalSourceHit[] = [signalHit]; const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, 'custom_threat.custom_indicator' ); - const [enrichedHit] = enrichedSignals.hits.hits; + const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); expect(enrichments).toEqual([ @@ -748,16 +745,15 @@ describe('enrichSignalThreatMatches', () => { ), ], }); - const signals = getSignalsResponseMock([signalHit, otherSignalHit]); + const signals: SignalSourceHit[] = [signalHit, otherSignalHit]; const enrichedSignals = await enrichSignalThreatMatches( signals, getMatchedThreats, indicatorPath ); - expect(enrichedSignals.hits.total).toEqual(expect.objectContaining({ value: 1 })); - expect(enrichedSignals.hits.hits).toHaveLength(1); + expect(enrichedSignals).toHaveLength(1); - const [enrichedHit] = enrichedSignals.hits.hits; + const [enrichedHit] = enrichedSignals; const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); expect(enrichments).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index c1fb88176fd4c..58a486068013f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -8,7 +8,7 @@ import { get, isObject } from 'lodash'; import { ENRICHMENT_TYPES, FEED_NAME_PATH } from '../../../../../common/cti/constants'; -import type { SignalSearchResponse, SignalSourceHit } from '../types'; +import type { SignalSourceHit } from '../types'; import type { GetMatchedThreats, ThreatEnrichment, @@ -109,17 +109,16 @@ export const buildEnrichments = ({ }); export const enrichSignalThreatMatches = async ( - signals: SignalSearchResponse, + signals: SignalSourceHit[], getMatchedThreats: GetMatchedThreats, indicatorPath: string, signalMatchesArg?: SignalMatch[] -): Promise => { - const signalHits = signals.hits.hits; - if (signalHits.length === 0) { +): Promise => { + if (signals.length === 0) { return signals; } - const uniqueHits = groupAndMergeSignalMatches(signalHits); + const uniqueHits = groupAndMergeSignalMatches(signals); const signalMatches: SignalMatch[] = signalMatchesArg ? signalMatchesArg : uniqueHits.map((signalHit) => ({ @@ -177,14 +176,5 @@ export const enrichSignalThreatMatches = async ( }; }); - return { - ...signals, - hits: { - ...signals.hits, - hits: enrichedSignals, - total: isObject(signals.hits.total) - ? { ...signals.hits.total, value: enrichedSignals.length } - : enrichedSignals.length, - }, - }; + return enrichedSignals; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 235865a8b60a9..5dc19b1b257b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -277,7 +277,7 @@ export interface AlertAttributes { export type BulkResponseErrorAggregation = Record; -export type SignalsEnrichment = (signals: SignalSearchResponse) => Promise; +export type SignalsEnrichment = (signals: SignalSourceHit[]) => Promise; export type BulkCreate = ( docs: Array> From 6278939024a629b0853d41467805a5ead1930193 Mon Sep 17 00:00:00 2001 From: mgiota Date: Mon, 2 May 2022 20:30:17 +0200 Subject: [PATCH 12/50] [Actionable Observability] state containers for last response filtering (#131300) * add lastResponse filtering in the urlbar using routeParams * refactor using state containers --- .../public/pages/rules/index.tsx | 36 +++++-- .../pages/rules/state_container/index.tsx | 9 ++ .../rules/state_container/state_container.tsx | 50 ++++++++++ .../use_rules_page_state_container.tsx | 96 +++++++++++++++++++ 4 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/rules/state_container/index.tsx create mode 100644 x-pack/plugins/observability/public/pages/rules/state_container/state_container.tsx create mode 100644 x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index dd8829937a876..693c5dd11f4d1 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { capitalize, sortBy } from 'lodash'; import { EuiButton, @@ -31,6 +31,8 @@ import { } from '@kbn/triggers-actions-ui-plugin/public'; import { RuleExecutionStatus, ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { Provider, rulesPageStateContainer, useRulesPageStateContainer } from './state_container'; + import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; import { useKibana } from '../../utils/kibana_react'; import { useFetchRules } from '../../hooks/use_fetch_rules'; @@ -71,7 +73,7 @@ import { import { ExperimentalBadge } from '../../components/shared/experimental_badge'; const ENTER_KEY = 13; -export function RulesPage() { +function RulesPage() { const { ObservabilityPageTemplate, kibanaFeatures } = usePluginContext(); const { http, @@ -80,6 +82,7 @@ export function RulesPage() { application: { capabilities }, notifications: { toasts }, } = useKibana().services; + const { lastResponse, setLastResponse } = useRulesPageStateContainer(); const documentationLink = docLinks.links.observability.createAlerts; const ruleTypeRegistry = triggersActionsUi.ruleTypeRegistry; const canExecuteActions = hasExecuteActionsCapability(capabilities); @@ -90,7 +93,7 @@ export function RulesPage() { }); const [inputText, setInputText] = useState(); const [searchText, setSearchText] = useState(); - const [ruleLastResponseFilter, setRuleLastResponseFilter] = useState([]); + // const [ruleLastResponseFilter, setRuleLastResponseFilter] = useState([]); const [typesFilter, setTypesFilter] = useState([]); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); const [rulesToDelete, setRulesToDelete] = useState([]); @@ -106,7 +109,7 @@ export function RulesPage() { const { rulesState, setRulesState, reload, noData, initialLoad } = useFetchRules({ searchText, - ruleLastResponseFilter, + ruleLastResponseFilter: lastResponse, typesFilter, page, setPage, @@ -280,6 +283,13 @@ export function RulesPage() { [] ); + const setExecutionStatusFilter = useCallback( + (ids: string[]) => { + setLastResponse(ids); + }, + [setLastResponse] + ); + const getRulesTable = () => { if (noData && !rulesState.isLoading) { return authorizedToCreateAnyRules ? ( @@ -294,6 +304,10 @@ export function RulesPage() { if (initialLoad) { return ; } + + // const nextSearchParams = new URLSearchParams(history.location.search); + // const xx = [...nextSearchParams.getAll('executionStatus')] || []; + // console.log(xx, '!!'); return ( <> @@ -331,8 +345,8 @@ export function RulesPage() { setRuleLastResponseFilter(ids)} + selectedStatuses={lastResponse} + onChange={setExecutionStatusFilter} /> @@ -457,3 +471,13 @@ export function RulesPage() { ); } + +function WrappedRulesPage() { + return ( + + + + ); +} + +export { WrappedRulesPage as RulesPage }; diff --git a/x-pack/plugins/observability/public/pages/rules/state_container/index.tsx b/x-pack/plugins/observability/public/pages/rules/state_container/index.tsx new file mode 100644 index 0000000000000..7820342482035 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/state_container/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Provider, rulesPageStateContainer } from './state_container'; +export { useRulesPageStateContainer } from './use_rules_page_state_container'; diff --git a/x-pack/plugins/observability/public/pages/rules/state_container/state_container.tsx b/x-pack/plugins/observability/public/pages/rules/state_container/state_container.tsx new file mode 100644 index 0000000000000..b36ffca96972e --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/state_container/state_container.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + createStateContainer, + createStateContainerReactHelpers, +} from '@kbn/kibana-utils-plugin/public'; + +interface RulesPageContainerState { + lastResponse: string[]; +} + +const defaultState: RulesPageContainerState = { + lastResponse: [], +}; + +interface RulesPageStateTransitions { + setLastResponse: ( + state: RulesPageContainerState + ) => (lastResponse: string[]) => RulesPageContainerState; +} + +const transitions: RulesPageStateTransitions = { + setLastResponse: (state) => (lastResponse) => { + const filteredIds = lastResponse; + lastResponse.forEach((id) => { + const isPreviouslyChecked = state.lastResponse.includes(id); + if (!isPreviouslyChecked) { + filteredIds.concat(id); + } else { + filteredIds.filter((val) => { + return val !== id; + }); + } + }); + return { ...state, lastResponse: filteredIds }; + }, +}; + +const rulesPageStateContainer = createStateContainer(defaultState, transitions); + +type RulesPageStateContainer = typeof rulesPageStateContainer; +const { Provider, useContainer } = createStateContainerReactHelpers(); + +export { Provider, rulesPageStateContainer, useContainer, defaultState }; +export type { RulesPageStateContainer, RulesPageContainerState, RulesPageStateTransitions }; diff --git a/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx b/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx new file mode 100644 index 0000000000000..6b44dc8ae31d5 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/state_container/use_rules_page_state_container.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { + createKbnUrlStateStorage, + syncState, + IKbnUrlStateStorage, + useContainerSelector, +} from '@kbn/kibana-utils-plugin/public'; + +import { + useContainer, + defaultState, + RulesPageStateContainer, + RulesPageContainerState, +} from './state_container'; + +export function useRulesPageStateContainer() { + const stateContainer = useContainer(); + + useUrlStateSyncEffect(stateContainer); + + const { setLastResponse } = stateContainer.transitions; + const { lastResponse } = useContainerSelector(stateContainer, (state) => state); + + return { + lastResponse, + setLastResponse, + }; +} + +function useUrlStateSyncEffect(stateContainer: RulesPageStateContainer) { + const history = useHistory(); + + useEffect(() => { + const urlStateStorage = createKbnUrlStateStorage({ + history, + useHash: false, + useHashQuery: false, + }); + const { start, stop } = setupUrlStateSync(stateContainer, urlStateStorage); + + start(); + + syncUrlStateWithInitialContainerState(stateContainer, urlStateStorage); + + return stop; + }, [stateContainer, history]); +} + +function setupUrlStateSync( + stateContainer: RulesPageStateContainer, + stateStorage: IKbnUrlStateStorage +) { + // This handles filling the state when an incomplete URL set is provided + const setWithDefaults = (changedState: Partial | null) => { + stateContainer.set({ ...defaultState, ...changedState }); + }; + return syncState({ + storageKey: '_a', + stateContainer: { + ...stateContainer, + set: setWithDefaults, + }, + stateStorage, + }); +} + +function syncUrlStateWithInitialContainerState( + stateContainer: RulesPageStateContainer, + urlStateStorage: IKbnUrlStateStorage +) { + const urlState = urlStateStorage.get>('_a'); + + if (urlState) { + const newState = { + ...defaultState, + ...urlState, + }; + + stateContainer.set(newState); + } else { + // Reset the state container when no URL state or timefilter range is set to avoid accidentally + // re-using state set on a previous visit to the page in the same session + stateContainer.set(defaultState); + } + + urlStateStorage.set('_a', stateContainer.get()); +} From 41a1975e09ac52be49db5991ee91ad2a88f6864c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 2 May 2022 12:42:42 -0600 Subject: [PATCH 13/50] [Reporting/Maps] fix background tiles in a map panel might not load in a screenshot report (#131185) * [Reporting/Maps] fix background tiles in a map panel might not load in a screenshot report * add unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ems_vector_tile_layer.test.ts | 38 ++++++- .../ems_vector_tile_layer.tsx | 4 + .../mvt_vector_layer.test.tsx | 106 ++++++++++++++++++ .../mvt_vector_layer/mvt_vector_layer.tsx | 6 + .../mb_map/tile_status_tracker.test.ts | 4 +- .../mb_map/tile_status_tracker.ts | 33 +++++- 6 files changed, 183 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts index 8b27bacff8ecb..21c9c1f79d970 100644 --- a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts @@ -6,14 +6,18 @@ */ import { SOURCE_TYPES } from '../../../../common/constants'; -import { DataFilters, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; +import { + DataFilters, + LayerDescriptor, + XYZTMSSourceDescriptor, +} from '../../../../common/descriptor_types'; import { ILayer } from '../layer'; import { EmsVectorTileLayer } from './ems_vector_tile_layer'; import { DataRequestContext } from '../../../actions'; import { EMSTMSSource } from '../../sources/ems_tms_source'; describe('EmsVectorTileLayer', () => { - it('should correctly inject tileLayerId in meta', async () => { + test('should correctly inject tileLayerId in meta', async () => { const layer: ILayer = new EmsVectorTileLayer({ source: { getTileLayerId: () => { @@ -50,4 +54,34 @@ describe('EmsVectorTileLayer', () => { expect(actualMeta).toStrictEqual({ tileLayerId: 'myTileLayerId' }); expect(actualErrorMessage).toStrictEqual('network error'); }); + + describe('isInitialDataLoadComplete', () => { + test('should return false when tile loading has not started', () => { + const layer = new EmsVectorTileLayer({ + source: {} as unknown as EMSTMSSource, + layerDescriptor: {} as unknown as LayerDescriptor, + }); + expect(layer.isInitialDataLoadComplete()).toBe(false); + }); + + test('should return false when tiles are loading', () => { + const layer = new EmsVectorTileLayer({ + source: {} as unknown as EMSTMSSource, + layerDescriptor: { + __areTilesLoaded: false, + } as unknown as LayerDescriptor, + }); + expect(layer.isInitialDataLoadComplete()).toBe(false); + }); + + test('should return true when tiles are loaded', () => { + const layer = new EmsVectorTileLayer({ + source: {} as unknown as EMSTMSSource, + layerDescriptor: { + __areTilesLoaded: true, + } as unknown as LayerDescriptor, + }); + expect(layer.isInitialDataLoadComplete()).toBe(true); + }); + }); }); diff --git a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx index 428156165c4c3..646ccb3c09acd 100644 --- a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx @@ -67,6 +67,10 @@ export class EmsVectorTileLayer extends AbstractLayer { this._style = new TileStyle(); } + isInitialDataLoadComplete(): boolean { + return !!this._descriptor.__areTilesLoaded; + } + getSource(): EMSTMSSource { return super.getSource() as EMSTMSSource; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx index 27d377851152e..d9ee5207b29f3 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx @@ -17,6 +17,8 @@ import { shallow } from 'enzyme'; import { Feature } from 'geojson'; import { MVTSingleLayerVectorSource } from '../../../sources/mvt_single_layer_vector_source'; +import { IVectorSource } from '../../../sources/vector_source'; +import { InnerJoin } from '../../../joins/inner_join'; import { TiledSingleLayerVectorSourceDescriptor, VectorLayerDescriptor, @@ -93,3 +95,107 @@ describe('getFeatureById', () => { expect(feature).toEqual(null); }); }); + +describe('isInitialDataLoadComplete', () => { + const sourceDataRequestDescriptor = { + data: {}, + dataId: 'source', + dataRequestMeta: {}, + dataRequestMetaAtStart: undefined, + dataRequestToken: undefined, + }; + test('should return false when tile loading has not started', () => { + const layer = new MvtVectorLayer({ + customIcons: [], + layerDescriptor: { + __dataRequests: [sourceDataRequestDescriptor], + } as unknown as VectorLayerDescriptor, + source: {} as unknown as IVectorSource, + }); + expect(layer.isInitialDataLoadComplete()).toBe(false); + }); + + test('should return false when tiles are loading', () => { + const layer = new MvtVectorLayer({ + customIcons: [], + layerDescriptor: { + __areTilesLoaded: false, + __dataRequests: [sourceDataRequestDescriptor], + } as unknown as VectorLayerDescriptor, + source: {} as unknown as IVectorSource, + }); + expect(layer.isInitialDataLoadComplete()).toBe(false); + }); + + test('should return true when tiles are loaded', () => { + const layer = new MvtVectorLayer({ + customIcons: [], + layerDescriptor: { + __areTilesLoaded: true, + __dataRequests: [sourceDataRequestDescriptor], + } as unknown as VectorLayerDescriptor, + source: {} as unknown as IVectorSource, + }); + expect(layer.isInitialDataLoadComplete()).toBe(true); + }); + + test('should return false when tiles are loaded but join is loading', () => { + const layer = new MvtVectorLayer({ + customIcons: [], + joins: [ + { + hasCompleteConfig: () => { + return true; + }, + getSourceDataRequestId: () => { + return 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2'; + }, + } as unknown as InnerJoin, + ], + layerDescriptor: { + __areTilesLoaded: true, + __dataRequests: [ + sourceDataRequestDescriptor, + { + dataId: 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2', + dataRequestMetaAtStart: {}, + dataRequestToken: Symbol('join request'), + }, + ], + } as unknown as VectorLayerDescriptor, + source: {} as unknown as IVectorSource, + }); + expect(layer.isInitialDataLoadComplete()).toBe(false); + }); + + test('should return true when tiles are loaded and joins are loaded', () => { + const layer = new MvtVectorLayer({ + customIcons: [], + joins: [ + { + hasCompleteConfig: () => { + return true; + }, + getSourceDataRequestId: () => { + return 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2'; + }, + } as unknown as InnerJoin, + ], + layerDescriptor: { + __areTilesLoaded: true, + __dataRequests: [ + sourceDataRequestDescriptor, + { + data: {}, + dataId: 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2', + dataRequestMeta: {}, + dataRequestMetaAtStart: undefined, + dataRequestToken: undefined, + }, + ], + } as unknown as VectorLayerDescriptor, + source: {} as unknown as IVectorSource, + }); + expect(layer.isInitialDataLoadComplete()).toBe(true); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index a7ec941fceca3..6a1b80165433a 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -68,6 +68,12 @@ export class MvtVectorLayer extends AbstractVectorLayer { this._source = args.source as IMvtVectorSource; } + isInitialDataLoadComplete(): boolean { + return this._descriptor.__areTilesLoaded === undefined || !this._descriptor.__areTilesLoaded + ? false + : super.isInitialDataLoadComplete(); + } + async getBounds(syncContext: DataRequestContext) { // Add filter to narrow bounds to features with matching join keys let joinKeyFilter; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts index ffc6459262c8b..6485582149db7 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts @@ -113,7 +113,7 @@ describe('TileStatusTracker', () => { expect(loadedMap.get('foo')).toBe(true); expect(loadedMap.get('bar')).toBe(false); // still outstanding tile requests - expect(loadedMap.has('foobar')).toBe(true); // never received tile requests + expect(loadedMap.has('foobar')).toBe(false); // never received tile requests, status should not have been reported for layer (aa11BarTile as { tile: { aborted: boolean } })!.tile.aborted = true; // abort tile mockMbMap.emit('sourcedataloading', createMockMbDataEvent('barsource', 'af1d')); @@ -125,7 +125,7 @@ describe('TileStatusTracker', () => { expect(loadedMap.get('foo')).toBe(false); // still outstanding tile requests expect(loadedMap.get('bar')).toBe(true); // tiles were aborted or errored - expect(loadedMap.has('foobar')).toBe(true); // never received tile requests + expect(loadedMap.has('foobar')).toBe(false); // never received tile requests, status should not have been reported for layer }); test('should cleanup listeners on destroy', async () => { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts index 94a4344bac009..c349ef0ede3b6 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts @@ -29,8 +29,21 @@ interface Tile { } export class TileStatusTracker { - private _tileCache: Tile[]; - private _tileErrorCache: Record; + // Tile cache tracks active tile requests + // 'sourcedataloading' event adds tile request to cache + // 'sourcedata' and 'error' events remove tile request from cache + // Tile requests with 'aborted' status are removed from cache when reporting 'areTilesLoaded' status + private _tileCache: Tile[] = []; + + // Tile error cache tracks tile request errors per layer + // Error cache is cleared when map center tile changes + private _tileErrorCache: Record = {}; + + // Layer cache tracks layers that have requested one or more tiles + // Layer cache is used so that only a layer that has requested one or more tiles reports 'areTilesLoaded' status + // layer cache is never cleared + private _layerCache: Map = new Map(); + private _prevCenterTileKey?: string; private readonly _mbMap: MapboxMap; private readonly _updateTileStatus: ( @@ -48,6 +61,14 @@ export class TileStatusTracker { e.tile && (e.source.type === 'vector' || e.source.type === 'raster') ) { + const targetLayer = this._getCurrentLayerList().find((layer) => { + return layer.ownsMbSourceId(e.sourceId); + }); + const layerId = targetLayer ? targetLayer.getId() : undefined; + if (layerId && !this._layerCache.has(layerId)) { + this._layerCache.set(layerId, true); + } + const tracked = this._tileCache.find((tile) => { return ( tile.mbKey === (e.tile.tileID.key as unknown as string) && tile.mbSourceId === e.sourceId @@ -127,8 +148,6 @@ export class TileStatusTracker { updateTileStatus: (layer: ILayer, areTilesLoaded: boolean, errorMessage?: string) => void; getCurrentLayerList: () => ILayer[]; }) { - this._tileCache = []; - this._tileErrorCache = {}; this._updateTileStatus = updateTileStatus; this._getCurrentLayerList = getCurrentLayerList; @@ -146,6 +165,12 @@ export class TileStatusTracker { const layerList = this._getCurrentLayerList(); for (let i = 0; i < layerList.length; i++) { const layer: ILayer = layerList[i]; + + if (!this._layerCache.has(layer.getId())) { + // do not report status for layers that have not started loading tiles. + continue; + } + let atLeastOnePendingTile = false; for (let j = 0; j < this._tileCache.length; j++) { const tile = this._tileCache[j]; From 90e8795ed7d0f8b5e05da17e9a9eb22cc615b72b Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 2 May 2022 11:51:14 -0700 Subject: [PATCH 14/50] Change execution to run in Rules app (#131026) --- .../common/components/execution_duration_chart.tsx | 8 ++++---- .../application/sections/rule_details/components/rule.tsx | 2 +- .../public/application/sections/rule_form/rule_form.tsx | 2 +- .../sections/rules_list/components/rules_list.test.tsx | 2 +- .../sections/rules_list/components/rules_list.tsx | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx index 08fabc3fba2fd..140dae9dc14a5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx @@ -38,7 +38,7 @@ const NUM_EXECUTIONS_OPTIONS = [120, 60, 30, 15].map((value) => ({ text: i18n.translate( 'xpack.triggersActionsUI.sections.executionDurationChart.numberOfExecutionsOption', { - defaultMessage: '{value} executions', + defaultMessage: '{value} runs', values: { value, }, @@ -70,7 +70,7 @@ export const ExecutionDurationChart: React.FunctionComponent = ({

@@ -84,7 +84,7 @@ export const ExecutionDurationChart: React.FunctionComponent = ({ aria-label={i18n.translate( 'xpack.triggersActionsUI.sections.executionDurationChart.selectNumberOfExecutionDurationsLabel', { - defaultMessage: 'Select number of executions', + defaultMessage: 'Select number of runs', } )} onChange={onChange} @@ -161,7 +161,7 @@ export const ExecutionDurationChart: React.FunctionComponent = ({

diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index b70eaf20a051d..ad6ef32ab82be 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -122,7 +122,7 @@ export function RuleComponent({ { id: EVENT_LOG_LIST_TAB, name: i18n.translate('xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText', { - defaultMessage: 'Execution history', + defaultMessage: 'Run history', }), 'data-test-subj': 'eventLogListTab', content: suspendedComponentWithProps( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index d765fa225ef0c..1bca80a08c936 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -488,7 +488,7 @@ export const RuleForm = ({ > )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 098bef2b5af47..727898d42a076 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -459,7 +459,7 @@ describe('rules_list component with items', () => { jest.runAllTimers(); wrapper.update(); - expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last execution.'); + expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last run.'); wrapper .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 9b85334932a99..57c59f3f09782 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -418,7 +418,7 @@ export const RulesList: React.FunctionComponent = () => { content={i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.ruleExecutionPercentileTooltip', { - defaultMessage: `{percentileOrdinal} percentile of this rule's past {sampleLimit} execution durations (mm:ss).`, + defaultMessage: `{percentileOrdinal} percentile of this rule's past {sampleLimit} run durations (mm:ss).`, values: { percentileOrdinal: percentileOrdinals[selectedPercentile!], sampleLimit: MONITORING_HISTORY_LIMIT, @@ -605,7 +605,7 @@ export const RulesList: React.FunctionComponent = () => { content={i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastExecutionDateTitle', { - defaultMessage: 'Start time of the last execution.', + defaultMessage: 'Start time of the last run.', } )} > @@ -761,7 +761,7 @@ export const RulesList: React.FunctionComponent = () => { content={i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.successRatioTitle', { - defaultMessage: 'How often this rule executes successfully.', + defaultMessage: 'How often this rule runs successfully.', } )} > From 060cdf6df2e02357837c19b02bc0985b1ce55b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Mon, 2 May 2022 22:30:12 +0200 Subject: [PATCH 15/50] [`@kbn/docs-utils`] Support ConstructSignatures (#131287) --- .../buid_api_declaration.test.ts | 18 ++++++ .../build_api_declaration.ts | 7 ++- .../build_function_dec.ts | 8 ++- .../api_docs/build_api_declarations/utils.ts | 6 +- .../src/api_docs/build_api_docs_cli.ts | 15 +++-- .../api_docs/mdx/split_apis_by_folder.test.ts | 4 +- .../__fixtures__/src/plugin_a/public/index.ts | 5 ++ .../tests/snapshots/plugin_a.devdocs.json | 61 +++++++++++++++++++ .../src/api_docs/tests/snapshots/plugin_a.mdx | 4 +- .../api_docs/tests/snapshots/plugin_a_foo.mdx | 4 +- .../src/api_docs/tests/snapshots/plugin_b.mdx | 2 +- 11 files changed, 119 insertions(+), 15 deletions(-) diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/buid_api_declaration.test.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/buid_api_declaration.test.ts index 9b86db445c225..527ce59011a8b 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/buid_api_declaration.test.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/buid_api_declaration.test.ts @@ -53,6 +53,24 @@ it('Test number primitive doc def', () => { expect(def.type).toBe(TypeKind.NumberKind); }); +it('Test a constructor type declaration inside an interface', () => { + const node = nodes.find((n) => getNodeName(n) === 'ClassConstructorWithStaticProperties'); + expect(node).toBeDefined(); + const def = buildApiDeclarationTopNode(node!, { + plugins, + log, + currentPluginId: plugins[0].manifest.id, + scope: ApiScope.CLIENT, + captureReferences: false, + }); + + expect(def.type).toBe(TypeKind.InterfaceKind); + expect(def.children).toHaveLength(2); + expect(def.children![1].type).toBe(TypeKind.FunctionKind); + expect(def.children![1].label).toBe('new'); + expect(def.children![1].id).toBe('def-public.ClassConstructorWithStaticProperties.new'); +}); + it('Function type is exported as type with signature', () => { const node = nodes.find((n) => getNodeName(n) === 'FnWithGeneric'); expect(node).toBeDefined(); diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts index 809097ee73818..2e167c7a0a783 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_api_declaration.ts @@ -65,12 +65,17 @@ export function buildApiDeclaration(node: Node, opts: BuildApiDecOpts): ApiDecla Node.isMethodSignature(node) || Node.isFunctionDeclaration(node) || Node.isMethodDeclaration(node) || + Node.isConstructSignatureDeclaration(node) || Node.isConstructorDeclaration(node) ) { return buildFunctionDec(node, { ...opts, // Use "Constructor" if applicable, instead of the default "Unnamed" - name: Node.isConstructorDeclaration(node) ? 'Constructor' : node.getName() || 'Unnamed', + name: Node.isConstructSignatureDeclaration(node) + ? 'new' + : Node.isConstructorDeclaration(node) + ? 'Constructor' + : node.getName() || 'Unnamed', }); } else if ( Node.isPropertySignature(node) || diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts index 3ba688f1ee284..020ffd402366a 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/build_function_dec.ts @@ -11,6 +11,7 @@ import { MethodDeclaration, ConstructorDeclaration, MethodSignature, + ConstructSignatureDeclaration, } from 'ts-morph'; import { buildApiDecsForParameters } from './build_parameter_decs'; @@ -23,7 +24,12 @@ import { BuildApiDecOpts } from './types'; * Takes the various function-like node declaration types and converts them into an ApiDeclaration. */ export function buildFunctionDec( - node: FunctionDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature, + node: + | ConstructSignatureDeclaration + | FunctionDeclaration + | MethodDeclaration + | ConstructorDeclaration + | MethodSignature, opts: BuildApiDecOpts ): ApiDeclaration { const fn = { diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts index a57b1790b27a8..76328a314b066 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_declarations/utils.ts @@ -46,7 +46,11 @@ export function buildParentApiId(parentName: string, parentsParentApiId?: string } export function getOptsForChild(node: Node, parentOpts: BuildApiDecOpts): BuildApiDecOpts { - const name = isNamedNode(node) ? node.getName() : 'Unnamed'; + const name = Node.isConstructSignatureDeclaration(node) + ? 'new' + : isNamedNode(node) + ? node.getName() + : 'Unnamed'; return getOptsForChildWithName(name, parentOpts); } diff --git a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts index 0617e35a88615..41bb6400b92ab 100644 --- a/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts +++ b/packages/kbn-docs-utils/src/api_docs/build_api_docs_cli.ts @@ -65,11 +65,16 @@ export function runBuildApiDocsCli() { // Delete all files except the README that warns about the auto-generated nature of // the folder. const files = Fs.readdirSync(outputFolder); - files.forEach((file) => { - if (file.indexOf('README.md') < 0) { - Fs.rmSync(Path.resolve(outputFolder, file)); - } - }); + await Promise.all( + files + .filter((file) => file.indexOf('README.md') < 0) + .map( + (file) => + new Promise((resolve, reject) => + Fs.rm(Path.resolve(outputFolder, file), (err) => (err ? reject(err) : resolve())) + ) + ) + ); } const collectReferences = flags.references as boolean; diff --git a/packages/kbn-docs-utils/src/api_docs/mdx/split_apis_by_folder.test.ts b/packages/kbn-docs-utils/src/api_docs/mdx/split_apis_by_folder.test.ts index b1862f9bc2165..1a1ecb8ec3e67 100644 --- a/packages/kbn-docs-utils/src/api_docs/mdx/split_apis_by_folder.test.ts +++ b/packages/kbn-docs-utils/src/api_docs/mdx/split_apis_by_folder.test.ts @@ -38,7 +38,7 @@ beforeAll(() => { }); test('foo service has all exports', () => { - expect(doc?.client.length).toBe(37); + expect(doc?.client.length).toBe(38); const split = splitApisByFolder(doc); expect(split.length).toBe(2); @@ -47,5 +47,5 @@ test('foo service has all exports', () => { expect(fooDoc?.common.length).toBe(1); expect(fooDoc?.client.length).toBe(2); - expect(mainDoc?.client.length).toBe(35); + expect(mainDoc?.client.length).toBe(36); }); diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts index 345e85bc044b7..ad3d1204aeda9 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts @@ -24,6 +24,11 @@ export interface InterfaceWithIndexSignature { [key: string]: { foo: string }; } +export interface ClassConstructorWithStaticProperties { + staticProperty1: string; + new (config: { foo: string }): InterfaceWithIndexSignature; +} + export function plugin() { return new PluginA(); } diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.devdocs.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.devdocs.json index ff977517cb5a7..88e4043442e88 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.devdocs.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.devdocs.json @@ -712,6 +712,67 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "pluginA", + "id": "def-public.ClassConstructorWithStaticProperties", + "type": "Interface", + "tags": [], + "label": "ClassConstructorWithStaticProperties", + "description": [], + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "pluginA", + "id": "def-public.ClassConstructorWithStaticProperties.staticProperty1", + "type": "string", + "tags": [], + "label": "staticProperty1", + "description": [], + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "deprecated": false + }, + { + "parentPluginId": "pluginA", + "id": "def-public.ClassConstructorWithStaticProperties.new", + "type": "Function", + "tags": [], + "label": "new", + "description": [], + "signature": [ + "any" + ], + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "pluginA", + "id": "def-public.ClassConstructorWithStaticProperties.new.$1", + "type": "Object", + "tags": [], + "label": "config", + "description": [], + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "pluginA", + "id": "def-public.ClassConstructorWithStaticProperties.new.$1.foo", + "type": "string", + "tags": [], + "label": "foo", + "description": [], + "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/index.ts", + "deprecated": false + } + ] + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "pluginA", "id": "def-public.ExampleInterface", diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.mdx b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.mdx index ab80f1f02d0ac..6a66fa74f7c01 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.mdx +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/pluginA title: "pluginA" image: https://source.unsplash.com/400x175/?github summary: API docs for the pluginA plugin -date: 2022-02-14 +date: 2022-04-30 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 131 | 1 | 71 | 2 | +| 136 | 1 | 76 | 2 | ## Client diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.mdx b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.mdx index e9873f8223017..4082de6306895 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.mdx +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a_foo.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/pluginA-foo title: "pluginA.foo" image: https://source.unsplash.com/400x175/?github summary: API docs for the pluginA.foo plugin -date: 2022-02-14 +date: 2022-04-30 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA.foo'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact Kibana Core for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 131 | 1 | 71 | 2 | +| 136 | 1 | 76 | 2 | ## Client diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_b.mdx b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_b.mdx index 1671cd7a529d3..d69da971f7e3e 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_b.mdx +++ b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_b.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/pluginB title: "pluginB" image: https://source.unsplash.com/400x175/?github summary: API docs for the pluginB plugin -date: 2022-02-14 +date: 2022-04-30 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginB'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- From ca134dc18f056b2a7505048c929d7a11b0076811 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 2 May 2022 13:31:16 -0700 Subject: [PATCH 16/50] Cache range slider min and max to prevent resizing on selection change (#131266) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../range_slider/range_slider_popover.tsx | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx index a51b46d98ff85..1bb7501f7104f 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_popover.tsx @@ -45,6 +45,8 @@ export const RangeSliderPopover: FC = ({ fieldFormatter, }) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [rangeSliderMin, setRangeSliderMin] = useState(-Infinity); + const [rangeSliderMax, setRangeSliderMax] = useState(Infinity); const rangeRef = useRef(null); let errorMessage = ''; let helpText = ''; @@ -79,17 +81,6 @@ export const RangeSliderPopover: FC = ({ errorMessage = RangeSliderStrings.errors.getUpperLessThanLowerErrorMessage(); } - const rangeSliderMin = Math.min( - roundedMin, - isNaN(lowerBoundValue) ? Infinity : lowerBoundValue, - isNaN(upperBoundValue) ? Infinity : upperBoundValue - ); - const rangeSliderMax = Math.max( - roundedMax, - isNaN(lowerBoundValue) ? -Infinity : lowerBoundValue, - isNaN(upperBoundValue) ? -Infinity : upperBoundValue - ); - const displayedValue = [ hasLowerBoundSelection ? String(lowerBoundValue) : hasAvailableRange ? String(roundedMin) : '', hasUpperBoundSelection ? String(upperBoundValue) : hasAvailableRange ? String(roundedMax) : '', @@ -106,7 +97,27 @@ export const RangeSliderPopover: FC = ({ const button = (