From 551759859eb1e08a9dfa3e9d663ed291989e883e Mon Sep 17 00:00:00 2001 From: mgiota Date: Thu, 21 Apr 2022 15:31:37 +0200 Subject: [PATCH 01/24] remove autorefresh button from o11y rules page (#130526) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/pages/rules/index.tsx | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index 03e8ca86b2fa2..b161625379fd8 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, useEffect } from 'react'; +import React, { useState, useMemo } from 'react'; import { capitalize, sortBy } from 'lodash'; import { EuiButton, @@ -15,10 +15,8 @@ import { EuiButtonEmpty, EuiText, EuiHorizontalRule, - EuiAutoRefreshButton, EuiTableSortingType, EuiFieldSearch, - OnRefreshChangeProps, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -93,8 +91,6 @@ export function RulesPage() { }); const [inputText, setInputText] = useState(); const [searchText, setSearchText] = useState(); - const [refreshInterval, setRefreshInterval] = useState(60000); - const [isPaused, setIsPaused] = useState(false); const [ruleLastResponseFilter, setRuleLastResponseFilter] = useState([]); const [typesFilter, setTypesFilter] = useState([]); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); @@ -108,14 +104,6 @@ export function RulesPage() { setCurrentRuleToEdit(ruleItem); }; - const onRefreshChange = ({ - isPaused: isPausedChanged, - refreshInterval: refreshIntervalChanged, - }: OnRefreshChangeProps) => { - setIsPaused(isPausedChanged); - setRefreshInterval(refreshIntervalChanged); - }; - const { rulesState, setRulesState, reload, noData, initialLoad } = useFetchRules({ searchText, ruleLastResponseFilter, @@ -160,15 +148,6 @@ export function RulesPage() { (ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all ); - useEffect(() => { - const interval = setInterval(() => { - if (!isPaused) { - reload(); - } - }, refreshInterval); - return () => clearInterval(interval); - }, [refreshInterval, reload, isPaused]); - useBreadcrumbs([ { text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { @@ -368,14 +347,6 @@ export function RulesPage() { /> - - - From a3fd86c22a3f65b659b592e735e990900349d688 Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Thu, 21 Apr 2022 10:00:52 -0400 Subject: [PATCH 02/24] [Workplace Search] New Network Drive, Outlook, Teams, and Zoom Server integration tiles (#130421) --- .../apis/custom_integration/integrations.ts | 2 +- .../assets/source_icons/network_drive.svg | 1 + .../public/assets/source_icons/outlook.svg | 1 + .../public/assets/source_icons/teams.svg | 1 + .../public/assets/source_icons/zoom.svg | 1 + .../enterprise_search/server/integrations.ts | 59 +++++++++++++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/network_drive.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg create mode 100644 x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index e71cc045da321..c4fda918328f8 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be(38); + expect(resp.body.length).to.be(42); // Test for sample data card expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above( diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/network_drive.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/network_drive.svg new file mode 100644 index 0000000000000..cc07fbbc50877 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/network_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg new file mode 100644 index 0000000000000..932a3756a522b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/outlook.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg new file mode 100644 index 0000000000000..bf3aef9277eb6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/teams.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg new file mode 100644 index 0000000000000..d7498817bc62d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/zoom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index 2a5ed4be69cfa..d2d3b5d4d6829 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -162,6 +162,21 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['productivity'], }, + { + id: 'network_drive', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.networkDriveName', { + defaultMessage: 'Network Drive', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.networkDriveDescription', + { + defaultMessage: + 'Search over your files and folders stored on network drives with Enterprise Search.', + } + ), + categories: ['enterprise_search', 'file_storage'], + uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/network_drive', + }, { id: 'onedrive', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName', { @@ -176,6 +191,20 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ categories: ['file_storage'], uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/one_drive', }, + { + id: 'outlook', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.outlookName', { + defaultMessage: 'Outlook', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.outlookDescription', + { + defaultMessage: 'Search over your email and calendars with Enterprise Search.', + } + ), + categories: ['enterprise_search', 'microsoft_365', 'communications', 'productivity'], + uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/outlook', + }, { id: 'salesforce', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.salesforceName', { @@ -266,6 +295,21 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['communications'], }, + { + id: 'teams', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.teamsName', { + defaultMessage: 'Teams', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.teamsDescription', + { + defaultMessage: + 'Search over meeting recordings, chats and other communications with Enterprise Search.', + } + ), + categories: ['enterprise_search', 'microsoft_365', 'communications', 'productivity'], + uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/teams', + }, { id: 'zendesk', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName', { @@ -279,6 +323,21 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['communications'], }, + { + id: 'zoom', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zoomName', { + defaultMessage: 'Zoom', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.zoomDescription', + { + defaultMessage: + 'Search over meeting recordings, chats and other communications with Enterprise Search.', + } + ), + categories: ['enterprise_search', 'communications', 'productivity'], + uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/zoom', + }, { id: 'custom_api_source', title: i18n.translate( From 1a1adb9e3574864c0465827f19d45ab3a8253add Mon Sep 17 00:00:00 2001 From: mgiota Date: Thu, 21 Apr 2022 16:26:38 +0200 Subject: [PATCH 03/24] consume shareable status dropdown from triggers_actions_ui plugin (#130656) * consume shareable status dropdown from triggers_actions_ui plugin * remove old status_context implementation * do not hardcode isEditable --- .../public/pages/rules/components/status.tsx | 34 ------ .../pages/rules/components/status_context.tsx | 103 ------------------ .../public/pages/rules/index.tsx | 27 +++-- 3 files changed, 13 insertions(+), 151 deletions(-) delete mode 100644 x-pack/plugins/observability/public/pages/rules/components/status.tsx delete mode 100644 x-pack/plugins/observability/public/pages/rules/components/status_context.tsx diff --git a/x-pack/plugins/observability/public/pages/rules/components/status.tsx b/x-pack/plugins/observability/public/pages/rules/components/status.tsx deleted file mode 100644 index 612d6f8f30bdd..0000000000000 --- a/x-pack/plugins/observability/public/pages/rules/components/status.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo } from 'react'; -import { EuiBadge } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import { StatusProps } from '../types'; -import { statusMap } from '../config'; -import { RULES_CHANGE_STATUS } from '../translations'; - -export function Status({ type, disabled, onClick }: StatusProps) { - const props = useMemo( - () => ({ - color: statusMap[type].color, - ...(!disabled ? { onClick } : { onClick: noop }), - ...(!disabled ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}), - ...(!disabled ? { iconOnClick: onClick } : { iconOnClick: noop }), - }), - [disabled, onClick, type] - ); - return ( - - {statusMap[type].label} - - ); -} diff --git a/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx b/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx deleted file mode 100644 index c7bd29d85b17a..0000000000000 --- a/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, useCallback, useMemo } from 'react'; -import { - EuiPopover, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiLoadingSpinner, -} from '@elastic/eui'; -import { Status } from './status'; -import { RuleStatus, StatusContextProps } from '../types'; -import { statusMap } from '../config'; - -export function StatusContext({ - item, - disabled = false, - onStatusChanged, - enableRule, - disableRule, - muteRule, - unMuteRule, -}: StatusContextProps) { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); - const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - - let currentStatus: RuleStatus; - if (item.enabled) { - currentStatus = item.muteAll ? RuleStatus.snoozed : RuleStatus.enabled; - } else { - currentStatus = RuleStatus.disabled; - } - const popOverButton = useMemo( - () => , - [disabled, currentStatus, togglePopover] - ); - - const onContextMenuItemClick = useCallback( - async (status: RuleStatus) => { - togglePopover(); - if (currentStatus !== status) { - setIsUpdating(true); - - if (status === RuleStatus.enabled) { - await enableRule({ ...item, enabled: true }); - if (item.muteAll) { - await unMuteRule({ ...item, muteAll: false }); - } - } else if (status === RuleStatus.disabled) { - await disableRule({ ...item, enabled: false }); - } else if (status === RuleStatus.snoozed) { - await muteRule({ ...item, muteAll: true }); - } - setIsUpdating(false); - onStatusChanged(status); - } - }, - [ - item, - togglePopover, - enableRule, - disableRule, - muteRule, - unMuteRule, - currentStatus, - onStatusChanged, - ] - ); - - const panelItems = useMemo( - () => - Object.values(RuleStatus).map((status: RuleStatus) => ( - onContextMenuItemClick(status)} - disabled={status === RuleStatus.snoozed && currentStatus === RuleStatus.disabled} - > - {statusMap[status].label} - - )), - [currentStatus, onContextMenuItemClick] - ); - - return isUpdating ? ( - - ) : ( - setIsPopoverOpen(false)} - anchorPosition="downLeft" - isOpen={isPopoverOpen} - panelPaddingSize="none" - > - - - ); -} diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index b161625379fd8..60e74907a33f4 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -25,9 +25,9 @@ import { RuleTableItem, enableRule, disableRule, - muteRule, + snoozeRule, useLoadRuleTypes, - unmuteRule, + unsnoozeRule, } from '@kbn/triggers-actions-ui-plugin/public'; import { RuleExecutionStatus, ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -38,7 +38,6 @@ import { RulesTable } from './components/rules_table'; import { Name } from './components/name'; import { LastResponseFilter } from './components/last_response_filter'; import { TypeFilter } from './components/type_filter'; -import { StatusContext } from './components/status_context'; import { ExecutionStatus } from './components/execution_status'; import { LastRun } from './components/last_run'; import { EditRuleFlyout } from './components/edit_rule_flyout'; @@ -193,17 +192,17 @@ export function RulesPage() { name: STATUS_COLUMN_TITLE, sortable: true, render: (_enabled: boolean, item: RuleTableItem) => { - return ( - reload()} - enableRule={async () => await enableRule({ http, id: item.id })} - disableRule={async () => await disableRule({ http, id: item.id })} - muteRule={async () => await muteRule({ http, id: item.id })} - unMuteRule={async () => await unmuteRule({ http, id: item.id })} - /> - ); + return triggersActionsUi.getRuleStatusDropdown({ + rule: item, + enableRule: async () => await enableRule({ http, id: item.id }), + disableRule: async () => await disableRule({ http, id: item.id }), + onRuleChanged: () => reload(), + isEditable: item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId), + snoozeRule: async (snoozeEndTime: string | -1, interval: string | null) => { + await snoozeRule({ http, id: item.id, snoozeEndTime }); + }, + unsnoozeRule: async () => await unsnoozeRule({ http, id: item.id }), + }); }, }, { From 502a00b025e2c8412169d3e3833ccbe48f61c76b Mon Sep 17 00:00:00 2001 From: Muhammad Ibragimov <53621505+mibragimov@users.noreply.github.com> Date: Thu, 21 Apr 2022 19:49:04 +0500 Subject: [PATCH 04/24] [Console] Add Kibana APIs Support (#128562) * Add support for Kibana API requests * Fix failing tests * Support leading / and minor refactor * Resolve conflicts * Update send_request.test file * Refactor * Add functional test * Address comments * Fix typo * Resolve conflicts and refactor error handling * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Address comments * Resolve merge conflicts * Rename KIBANA_API_KEYWORD Co-authored-by: Muhammad Ibragimov Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/console/common/constants/api.ts | 1 + src/plugins/console/common/constants/index.ts | 2 +- .../console_editor/editor.test.mock.tsx | 4 +- .../legacy/console_editor/editor.test.tsx | 8 +-- .../editor/legacy/console_editor/editor.tsx | 10 +-- .../console_editor/keyboard_shortcuts.ts | 10 +-- .../console/public/application/hooks/index.ts | 2 +- .../index.ts | 2 +- .../send_request.test.ts} | 49 ++++++------- .../send_request.ts} | 72 +++++++++---------- .../track.ts | 0 .../use_send_current_request.test.tsx} | 28 ++++---- .../use_send_current_request.ts} | 6 +- .../models/sense_editor/sense_editor.test.js | 6 +- .../models/sense_editor/sense_editor.ts | 21 +++--- .../public/application/stores/request.ts | 10 +-- src/plugins/console/public/lib/es/es.ts | 49 ++++++++++--- src/plugins/console/public/lib/es/index.ts | 2 +- test/functional/apps/console/_autocomplete.ts | 2 + test/functional/apps/console/_console.ts | 16 +++++ test/functional/page_objects/console_page.ts | 1 - 21 files changed, 172 insertions(+), 129 deletions(-) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es => use_send_current_request}/index.ts (81%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es/send_request_to_es.test.ts => use_send_current_request/send_request.test.ts} (66%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es/send_request_to_es.ts => use_send_current_request/send_request.ts} (69%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es => use_send_current_request}/track.ts (100%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es/use_send_current_request_to_es.test.tsx => use_send_current_request/use_send_current_request.test.tsx} (80%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es/use_send_current_request_to_es.ts => use_send_current_request/use_send_current_request.ts} (96%) diff --git a/src/plugins/console/common/constants/api.ts b/src/plugins/console/common/constants/api.ts index aa0fad1fe4424..3c5aa11519350 100644 --- a/src/plugins/console/common/constants/api.ts +++ b/src/plugins/console/common/constants/api.ts @@ -7,3 +7,4 @@ */ export const API_BASE_PATH = '/api/console'; +export const KIBANA_API_PREFIX = 'kbn:'; diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts index d8768af8fc8d8..756a79883cbdb 100644 --- a/src/plugins/console/common/constants/index.ts +++ b/src/plugins/console/common/constants/index.ts @@ -7,4 +7,4 @@ */ export { MAJOR_VERSION } from './plugin'; -export { API_BASE_PATH } from './api'; +export { API_BASE_PATH, KIBANA_API_PREFIX } from './api'; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx index dfed86a643627..b410e240151d7 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx @@ -39,8 +39,8 @@ jest.mock('../../../../models/sense_editor', () => { }; }); -jest.mock('../../../../hooks/use_send_current_request_to_es/send_request_to_es', () => ({ - sendRequestToES: jest.fn(), +jest.mock('../../../../hooks/use_send_current_request/send_request', () => ({ + sendRequest: jest.fn(), })); jest.mock('../../../../../lib/autocomplete/get_endpoint_from_position', () => ({ getEndpointFromPosition: jest.fn(), diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx index b942a6d830217..ba5f1e78d5f01 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -25,7 +25,7 @@ import { } from '../../../../contexts'; // Mocked functions -import { sendRequestToES } from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; +import { sendRequest } from '../../../../hooks/use_send_current_request/send_request'; import { getEndpointFromPosition } from '../../../../../lib/autocomplete/get_endpoint_from_position'; import type { DevToolsSettings } from '../../../../../services'; import * as consoleMenuActions from '../console_menu_actions'; @@ -58,15 +58,15 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { sandbox.restore(); }); - it('calls send current request to ES', async () => { + it('calls send current request', async () => { (getEndpointFromPosition as jest.Mock).mockReturnValue({ patterns: [] }); - (sendRequestToES as jest.Mock).mockRejectedValue({}); + (sendRequest as jest.Mock).mockRejectedValue({}); const editor = doMount(); act(() => { editor.find('button[data-test-subj~="sendRequestButton"]').simulate('click'); }); await nextTick(); - expect(sendRequestToES).toBeCalledTimes(1); + expect(sendRequest).toBeCalledTimes(1); }); it('opens docs', () => { diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index bafe9ee6ca156..d01a40bdd44b3 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -26,7 +26,7 @@ import { ConsoleMenu } from '../../../../components'; import { useEditorReadContext, useServicesContext } from '../../../../contexts'; import { useSaveCurrentTextObject, - useSendCurrentRequestToES, + useSendCurrentRequest, useSetInputEditor, } from '../../../../hooks'; import * as senseEditor from '../../../../models/sense_editor'; @@ -72,7 +72,7 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { const { settings } = useEditorReadContext(); const setInputEditor = useSetInputEditor(); - const sendCurrentRequestToES = useSendCurrentRequestToES(); + const sendCurrentRequest = useSendCurrentRequest(); const saveCurrentTextObject = useSaveCurrentTextObject(); const editorRef = useRef(null); @@ -231,11 +231,11 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { if (!isKeyboardShortcutsDisabled) { registerCommands({ senseEditor: editorInstanceRef.current!, - sendCurrentRequestToES, + sendCurrentRequest, openDocumentation, }); } - }, [sendCurrentRequestToES, openDocumentation, settings]); + }, [openDocumentation, settings, sendCurrentRequest]); useEffect(() => { const { current: editor } = editorInstanceRef; @@ -262,7 +262,7 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { > void; + sendCurrentRequest: () => void; openDocumentation: () => void; } @@ -24,11 +24,7 @@ const COMMANDS = { GO_TO_LINE: 'gotoline', }; -export function registerCommands({ - senseEditor, - sendCurrentRequestToES, - openDocumentation, -}: Actions) { +export function registerCommands({ senseEditor, sendCurrentRequest, openDocumentation }: Actions) { const throttledAutoIndent = throttle(() => senseEditor.autoIndent(), 500, { leading: true, trailing: true, @@ -39,7 +35,7 @@ export function registerCommands({ keys: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, name: COMMANDS.SEND_TO_ELASTICSEARCH, fn: () => { - sendCurrentRequestToES(); + sendCurrentRequest(); }, }); diff --git a/src/plugins/console/public/application/hooks/index.ts b/src/plugins/console/public/application/hooks/index.ts index 1a9b4e5c472bf..1996330bef66b 100644 --- a/src/plugins/console/public/application/hooks/index.ts +++ b/src/plugins/console/public/application/hooks/index.ts @@ -8,6 +8,6 @@ export { useSetInputEditor } from './use_set_input_editor'; export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; -export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; +export { useSendCurrentRequest } from './use_send_current_request'; export { useSaveCurrentTextObject } from './use_save_current_text_object'; export { useDataInit } from './use_data_init'; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts similarity index 81% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/index.ts index df2431f1f6f43..33bdbef87f2ef 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; +export { useSendCurrentRequest } from './use_send_current_request'; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts similarity index 66% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts index 8578e271f37b3..60ced085c6891 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts @@ -8,14 +8,14 @@ import type { ContextValue } from '../../contexts'; -jest.mock('./send_request_to_es', () => ({ sendRequestToES: jest.fn(() => Promise.resolve()) })); +jest.mock('./send_request', () => ({ sendRequest: jest.fn(() => Promise.resolve()) })); -import { sendRequestToES } from './send_request_to_es'; +import { sendRequest } from './send_request'; import { serviceContextMock } from '../../contexts/services_context.mock'; -const mockedSendRequestToES = sendRequestToES as jest.Mock; +const mockedSendRequest = sendRequest as jest.Mock; -describe('sendRequestToES', () => { +describe('sendRequest', () => { let mockContextValue: ContextValue; beforeEach(() => { @@ -26,8 +26,8 @@ describe('sendRequestToES', () => { jest.resetAllMocks(); }); - it('should send request to ES', async () => { - mockedSendRequestToES.mockResolvedValue([ + it('should send request', async () => { + mockedSendRequest.mockResolvedValue([ { response: { statusCode: 200, @@ -40,17 +40,17 @@ describe('sendRequestToES', () => { http: mockContextValue.services.http, requests: [{ method: 'PUT', url: 'test', data: [] }], }; - const results = await sendRequestToES(args); + const results = await sendRequest(args); const [request] = results; expect(request.response.statusCode).toEqual(200); expect(request.response.value).toContain('"acknowledged": true'); - expect(mockedSendRequestToES).toHaveBeenCalledWith(args); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledWith(args); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); }); - it('should send multiple requests to ES', async () => { - mockedSendRequestToES.mockResolvedValue([ + it('should send multiple requests', async () => { + mockedSendRequest.mockResolvedValue([ { response: { statusCode: 200, @@ -70,17 +70,17 @@ describe('sendRequestToES', () => { { method: 'GET', url: 'test-2', data: [] }, ], }; - const results = await sendRequestToES(args); + const results = await sendRequest(args); const [firstRequest, secondRequest] = results; expect(firstRequest.response.statusCode).toEqual(200); expect(secondRequest.response.statusCode).toEqual(200); - expect(mockedSendRequestToES).toHaveBeenCalledWith(args); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledWith(args); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); }); it('should handle errors', async () => { - mockedSendRequestToES.mockRejectedValue({ + mockedSendRequest.mockRejectedValue({ response: { statusCode: 500, statusText: 'error', @@ -88,45 +88,46 @@ describe('sendRequestToES', () => { }); try { - await sendRequestToES({ + await sendRequest({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test', data: [] }], }); } catch (error) { expect(error.response.statusCode).toEqual(500); expect(error.response.statusText).toEqual('error'); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); } }); + describe('successful response value', () => { describe('with text', () => { it('should return value with lines separated', async () => { - mockedSendRequestToES.mockResolvedValue('\ntest_index-1 [] \ntest_index-2 []\n'); - const response = await sendRequestToES({ + mockedSendRequest.mockResolvedValue('\ntest_index-1 []\ntest_index-2 []\n'); + const response = await sendRequest({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test-1', data: [] }], }); expect(response).toMatchInlineSnapshot(` " - test_index-1 [] + test_index-1 [] test_index-2 [] " `); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); }); }); describe('with parsed json', () => { it('should stringify value', async () => { - mockedSendRequestToES.mockResolvedValue(JSON.stringify({ test: 'some value' })); - const response = await sendRequestToES({ + mockedSendRequest.mockResolvedValue(JSON.stringify({ test: 'some value' })); + const response = await sendRequest({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test-2', data: [] }], }); expect(typeof response).toBe('string'); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts similarity index 69% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts index 451198aaf2d2b..1247f3f78aa68 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts @@ -15,12 +15,12 @@ import { BaseResponseType } from '../../../types'; const { collapseLiteralStrings } = XJson; -export interface EsRequestArgs { +export interface RequestArgs { http: HttpSetup; requests: Array<{ url: string; method: string; data: string[] }>; } -export interface ESResponseObject { +export interface ResponseObject { statusCode: number; statusText: string; timeMs: number; @@ -28,17 +28,17 @@ export interface ESResponseObject { value: V; } -export interface ESRequestResult { +export interface RequestResult { request: { data: string; method: string; path: string }; - response: ESResponseObject; + response: ResponseObject; } let CURRENT_REQ_ID = 0; -export function sendRequestToES(args: EsRequestArgs): Promise { +export function sendRequest(args: RequestArgs): Promise { const requests = args.requests.slice(); return new Promise((resolve, reject) => { const reqId = ++CURRENT_REQ_ID; - const results: ESRequestResult[] = []; + const results: RequestResult[] = []; if (reqId !== CURRENT_REQ_ID) { return; } @@ -59,11 +59,11 @@ export function sendRequestToES(args: EsRequestArgs): Promise return; } const req = requests.shift()!; - const esPath = req.url; - const esMethod = req.method; - let esData = collapseLiteralStrings(req.data.join('\n')); - if (esData) { - esData += '\n'; + const path = req.url; + const method = req.method; + let data = collapseLiteralStrings(req.data.join('\n')); + if (data) { + data += '\n'; } // append a new line for bulk requests. const startTime = Date.now(); @@ -71,9 +71,9 @@ export function sendRequestToES(args: EsRequestArgs): Promise try { const { response, body } = await es.send({ http: args.http, - method: esMethod, - path: esPath, - data: esData, + method, + path, + data, asResponse: true, }); @@ -115,9 +115,9 @@ export function sendRequestToES(args: EsRequestArgs): Promise value, }, request: { - data: esData, - method: esMethod, - path: esPath, + data, + method, + path, }, }); @@ -127,25 +127,19 @@ export function sendRequestToES(args: EsRequestArgs): Promise } } catch (error) { let value; - let contentType: string | null = ''; + const { response, body } = error as IHttpFetchError; + const contentType = response?.headers.get('Content-Type') ?? ''; + const statusCode = response?.status ?? 500; + const statusText = error?.response?.statusText ?? 'error'; - const { response, body = {} } = error as IHttpFetchError; - if (response) { - const { status, headers } = response; - if (body) { - value = JSON.stringify(body, null, 2); // ES error should be shown - contentType = headers.get('Content-Type'); - } else { - value = 'Request failed to get to the server (status code: ' + status + ')'; - contentType = headers.get('Content-Type'); - } - - if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; - } + if (body) { + value = JSON.stringify(body, null, 2); } else { - value = - "\n\nFailed to connect to Console's backend.\nPlease check the Kibana server is up and running"; + value = 'Request failed to get to the server (status code: ' + statusCode + ')'; + } + + if (isMultiRequest) { + value = '# ' + req.method + ' ' + req.url + '\n' + value; } reject({ @@ -153,13 +147,13 @@ export function sendRequestToES(args: EsRequestArgs): Promise value, contentType, timeMs: Date.now() - startTime, - statusCode: error?.response?.status ?? 500, - statusText: error?.response?.statusText ?? 'error', + statusCode, + statusText, }, request: { - data: esData, - method: esMethod, - path: esPath, + data, + method, + path, }, }); } diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts b/src/plugins/console/public/application/hooks/use_send_current_request/track.ts similarity index 100% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/track.ts diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx similarity index 80% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx rename to src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx index e0131dc116a34..d16dc3f832d3a 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx +++ b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -jest.mock('./send_request_to_es', () => ({ sendRequestToES: jest.fn() })); +jest.mock('./send_request', () => ({ sendRequest: jest.fn() })); jest.mock('../../contexts/editor_context/editor_registry', () => ({ instance: { getInputEditor: jest.fn() }, })); @@ -21,10 +21,10 @@ import { serviceContextMock } from '../../contexts/services_context.mock'; import { useRequestActionContext } from '../../contexts/request_context'; import { instance as editorRegistry } from '../../contexts/editor_context/editor_registry'; -import { sendRequestToES } from './send_request_to_es'; -import { useSendCurrentRequestToES } from './use_send_current_request_to_es'; +import { sendRequest } from './send_request'; +import { useSendCurrentRequest } from './use_send_current_request'; -describe('useSendCurrentRequestToES', () => { +describe('useSendCurrentRequest', () => { let mockContextValue: ContextValue; let dispatch: (...args: unknown[]) => void; const contexts = ({ children }: { children: JSX.Element }) => ( @@ -41,18 +41,18 @@ describe('useSendCurrentRequestToES', () => { jest.resetAllMocks(); }); - it('calls send request to ES', async () => { + it('calls send request', async () => { // Set up mocks (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); // This request should succeed - (sendRequestToES as jest.Mock).mockResolvedValue([]); + (sendRequest as jest.Mock).mockResolvedValue([]); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); await act(() => result.current()); - expect(sendRequestToES).toHaveBeenCalledWith({ + expect(sendRequest).toHaveBeenCalledWith({ http: mockContextValue.services.http, requests: ['test'], }); @@ -64,12 +64,12 @@ describe('useSendCurrentRequestToES', () => { it('handles known errors', async () => { // Set up mocks - (sendRequestToES as jest.Mock).mockRejectedValue({ response: 'nada' }); + (sendRequest as jest.Mock).mockRejectedValue({ response: 'nada' }); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); await act(() => result.current()); // Second call should be the request failure const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls; @@ -80,12 +80,12 @@ describe('useSendCurrentRequestToES', () => { it('handles unknown errors', async () => { // Set up mocks - (sendRequestToES as jest.Mock).mockRejectedValue(NaN /* unexpected error value */); + (sendRequest as jest.Mock).mockRejectedValue(NaN /* unexpected error value */); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); await act(() => result.current()); // Second call should be the request failure const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls; @@ -100,7 +100,7 @@ describe('useSendCurrentRequestToES', () => { it('notifies the user about save to history errors once only', async () => { // Set up mocks - (sendRequestToES as jest.Mock).mockReturnValue( + (sendRequest as jest.Mock).mockReturnValue( [{ request: {} }, { request: {} }] /* two responses to save history */ ); (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); @@ -112,7 +112,7 @@ describe('useSendCurrentRequestToES', () => { getRequestsInRange: () => ['test', 'test'], })); - const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); await act(() => result.current()); expect(dispatch).toHaveBeenCalledTimes(2); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts similarity index 96% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts index e7c436c9806b3..ed08304d8d660 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts @@ -16,10 +16,10 @@ import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; import { instance as registry } from '../../contexts/editor_context/editor_registry'; import { useRequestActionContext, useServicesContext } from '../../contexts'; import { StorageQuotaError } from '../../components/storage_quota_error'; -import { sendRequestToES } from './send_request_to_es'; +import { sendRequest } from './send_request'; import { track } from './track'; -export const useSendCurrentRequestToES = () => { +export const useSendCurrentRequest = () => { const { services: { history, settings, notifications, trackUiMetric, http }, theme$, @@ -46,7 +46,7 @@ export const useSendCurrentRequestToES = () => { // Fire and forget setTimeout(() => track(requests, editor, trackUiMetric), 0); - const results = await sendRequestToES({ http, requests }); + const results = await sendRequest({ http, requests }); let saveToHistoryError: undefined | Error; const { isHistoryDisabled } = settings.toJSON(); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js index 0889b98c69388..ff9d245f61275 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js @@ -455,11 +455,11 @@ describe('Editor', () => { editorInput1, { start: { lineNumber: 7 }, end: { lineNumber: 14 } }, ` -curl -XGET "http://localhost:9200/_stats?level=shards" +curl -XGET "http://localhost:9200/_stats?level=shards" -H "kbn-xsrf: reporting" #in between comment -curl -XPUT "http://localhost:9200/index_1/type1/1" -H 'Content-Type: application/json' -d' +curl -XPUT "http://localhost:9200/index_1/type1/1" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d' { "f": 1 }'`.trim() @@ -470,7 +470,7 @@ curl -XPUT "http://localhost:9200/index_1/type1/1" -H 'Content-Type: application editorInput1, { start: { lineNumber: 29 }, end: { lineNumber: 33 } }, ` -curl -XPOST "http://localhost:9200/_sql?format=txt" -H 'Content-Type: application/json' -d' +curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d' { "query": "SELECT prenom FROM claude_index WHERE prenom = '\\''claude'\\'' ", "fetch_size": 1 diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index 5e8ca35f287b7..ac65afce2c18a 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -14,7 +14,7 @@ import RowParser from '../../../lib/row_parser'; import * as utils from '../../../lib/utils'; // @ts-ignore -import * as es from '../../../lib/es/es'; +import { constructUrl } from '../../../lib/es/es'; import { CoreEditor, Position, Range } from '../../../types'; import { createTokenIterator } from '../../factories'; @@ -467,21 +467,22 @@ export class SenseEditor { return req; } - const esPath = req.url; - const esMethod = req.method; - const esData = req.data; + const path = req.url; + const method = req.method; + const data = req.data; // this is the first url defined in elasticsearch.hosts - const url = es.constructESUrl(elasticsearchBaseUrl, esPath); + const url = constructUrl(elasticsearchBaseUrl, path); - let ret = 'curl -X' + esMethod + ' "' + url + '"'; - if (esData && esData.length) { - ret += " -H 'Content-Type: application/json' -d'\n"; - const dataAsString = collapseLiteralStrings(esData.join('\n')); + // Append 'kbn-xsrf' header to bypass (XSRF/CSRF) protections + let ret = `curl -X${method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`; + if (data && data.length) { + ret += ` -H "Content-Type: application/json" -d'\n`; + const dataAsString = collapseLiteralStrings(data.join('\n')); // We escape single quoted strings that that are wrapped in single quoted strings ret += dataAsString.replace(/'/g, "'\\''"); - if (esData.length > 1) { + if (data.length > 1) { ret += '\n'; } // end with a new line ret += "'"; diff --git a/src/plugins/console/public/application/stores/request.ts b/src/plugins/console/public/application/stores/request.ts index 099ab24326d31..8056ab5a7987f 100644 --- a/src/plugins/console/public/application/stores/request.ts +++ b/src/plugins/console/public/application/stores/request.ts @@ -10,18 +10,18 @@ import { Reducer } from 'react'; import { produce } from 'immer'; import { identity } from 'fp-ts/lib/function'; import { BaseResponseType } from '../../types/common'; -import { ESRequestResult } from '../hooks/use_send_current_request_to_es/send_request_to_es'; +import { RequestResult } from '../hooks/use_send_current_request/send_request'; export type Actions = | { type: 'sendRequest'; payload: undefined } - | { type: 'requestSuccess'; payload: { data: ESRequestResult[] } } - | { type: 'requestFail'; payload: ESRequestResult | undefined }; + | { type: 'requestSuccess'; payload: { data: RequestResult[] } } + | { type: 'requestFail'; payload: RequestResult | undefined }; export interface Store { requestInFlight: boolean; lastResult: { - data: ESRequestResult[] | null; - error?: ESRequestResult; + data: RequestResult[] | null; + error?: RequestResult; }; } diff --git a/src/plugins/console/public/lib/es/es.ts b/src/plugins/console/public/lib/es/es.ts index 2a4059d664e6c..10d0ad95b0496 100644 --- a/src/plugins/console/public/lib/es/es.ts +++ b/src/plugins/console/public/lib/es/es.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import type { HttpFetchOptions, HttpResponse, HttpSetup } from '@kbn/core/public'; -import { API_BASE_PATH } from '../../../common/constants'; +import type { HttpResponse, HttpSetup } from '@kbn/core/public'; +import { trimStart } from 'lodash'; +import { API_BASE_PATH, KIBANA_API_PREFIX } from '../../../common/constants'; const esVersion: string[] = []; @@ -20,7 +21,7 @@ export function getContentType(body: unknown) { return 'application/json'; } -interface SendProps { +interface SendConfig { http: HttpSetup; method: string; path: string; @@ -30,6 +31,8 @@ interface SendProps { asResponse?: boolean; } +type Method = 'get' | 'post' | 'delete' | 'put' | 'patch' | 'head'; + export async function send({ http, method, @@ -38,18 +41,48 @@ export async function send({ asSystemRequest = false, withProductOrigin = false, asResponse = false, -}: SendProps) { - const options: HttpFetchOptions = { +}: SendConfig) { + const kibanaRequestUrl = getKibanaRequestUrl(path); + + if (kibanaRequestUrl) { + const httpMethod = method.toLowerCase() as Method; + const url = new URL(kibanaRequestUrl); + const { pathname, searchParams } = url; + const query = Object.fromEntries(searchParams.entries()); + const body = ['post', 'put', 'patch'].includes(httpMethod) ? data : null; + + return await http[httpMethod](pathname, { + body, + query, + asResponse, + asSystemRequest, + }); + } + + return await http.post(`${API_BASE_PATH}/proxy`, { query: { path, method, ...(withProductOrigin && { withProductOrigin }) }, body: data, asResponse, asSystemRequest, - }; + }); +} + +function getKibanaRequestUrl(path: string) { + const isKibanaApiRequest = path.startsWith(KIBANA_API_PREFIX); + const kibanaBasePath = window.location.origin; - return await http.post(`${API_BASE_PATH}/proxy`, options); + if (isKibanaApiRequest) { + // window.location.origin is used as a Kibana public base path for sending requests in cURL commands. E.g. "Copy as cURL". + return `${kibanaBasePath}/${trimStart(path.replace(KIBANA_API_PREFIX, ''), '/')}`; + } } -export function constructESUrl(baseUri: string, path: string) { +export function constructUrl(baseUri: string, path: string) { + const kibanaRequestUrl = getKibanaRequestUrl(path); + + if (kibanaRequestUrl) { + return kibanaRequestUrl; + } baseUri = baseUri.replace(/\/+$/, ''); path = path.replace(/^\/+/, ''); return baseUri + '/' + path; diff --git a/src/plugins/console/public/lib/es/index.ts b/src/plugins/console/public/lib/es/index.ts index 61d34ba96ec05..f83893e93713e 100644 --- a/src/plugins/console/public/lib/es/index.ts +++ b/src/plugins/console/public/lib/es/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { send, constructESUrl, getContentType, getVersion } from './es'; +export { send, constructUrl, getContentType, getVersion } from './es'; diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index 57c59793f69f6..a1b441720d324 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should provide basic auto-complete functionality', async () => { await PageObjects.console.enterRequest(); + await PageObjects.console.pressEnter(); await PageObjects.console.enterText(`{\n\t"query": {`); await PageObjects.console.pressEnter(); await PageObjects.console.promptAutocomplete(); @@ -39,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.console.clearTextArea(); await PageObjects.console.enterRequest(); + await PageObjects.console.pressEnter(); }); it('should add a comma after previous non empty line', async () => { await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"match": {}`); diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 367f1ccb56256..52218b88be60d 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -106,5 +106,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); + + describe('with kbn: prefix in request', () => { + before(async () => { + await PageObjects.console.clearTextArea(); + }); + it('it should send successful request to Kibana API', async () => { + const expectedResponseContains = 'default space'; + await PageObjects.console.enterRequest('\n GET kbn:/api/spaces/space'); + await PageObjects.console.clickPlay(); + await retry.try(async () => { + const actualResponse = await PageObjects.console.getResponse(); + log.debug(actualResponse); + expect(actualResponse).to.contain(expectedResponseContains); + }); + }); + }); }); } diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 281c49a789acf..7aaf842f28d14 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -102,7 +102,6 @@ export class ConsolePageObject extends FtrService { public async enterRequest(request: string = '\nGET _search') { const textArea = await this.getEditorTextArea(); await textArea.pressKeys(request); - await textArea.pressKeys(Key.ENTER); } public async enterText(text: string) { From fb331872701ba65399ea403e345de5b1f3dd3ed6 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 21 Apr 2022 08:11:39 -0700 Subject: [PATCH 05/24] Adds documentation and improves migrations failing on timeouts while waiting for index yellow status (#130352) * reapply docs and doclink changes * Updates wait_for_index_yellow_status response type on response timeout, updates create_index action and model to account for the changes * Refactors clone_index action to account for new return type of waitForIndexYellow, updates model * Updates README * Updates snapshot * Updates docs * Fix import violations * imports * Extends the retry log message with an actionable item linking to the docs on every retryable migration action * Refactor retry_state and model to allow linking to specific subsections in the docs * Updates resolving saved objects migration failures docs * Calls waitForIndexStatusYellow directly in actions integration tests * Deletes comment * Update src/core/server/saved_objects/migrations/model/retry_state.test.ts Co-authored-by: Rudolf Meijering Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Rudolf Meijering --- docs/setup/upgrade.asciidoc | 6 +- .../resolving-migration-failures.asciidoc | 66 ++++++++- packages/kbn-doc-links/src/get_doc_links.ts | 3 + packages/kbn-doc-links/src/types.ts | 3 + .../server/saved_objects/migrations/README.md | 19 ++- ...grations_state_action_machine.test.ts.snap | 18 +++ .../migrations/actions/clone_index.ts | 6 +- .../migrations/actions/create_index.ts | 7 +- .../saved_objects/migrations/actions/index.ts | 8 +- .../actions/integration_tests/actions.test.ts | 48 ++++++- .../actions/wait_for_index_status_yellow.ts | 16 ++- .../migrations/initial_state.test.ts | 14 ++ .../saved_objects/migrations/initial_state.ts | 6 + .../migrations/kibana_migrator.test.ts | 2 + .../migrations/kibana_migrator.ts | 6 + .../migrations_state_action_machine.test.ts | 4 +- .../migrations/model/model.test.ts | 128 +++++++++++++++++- .../saved_objects/migrations/model/model.ts | 72 +++++++++- .../migrations/model/retry_state.test.ts | 2 +- .../migrations/model/retry_state.ts | 3 +- .../migrations/run_resilient_migrator.ts | 4 + .../server/saved_objects/migrations/state.ts | 4 + .../saved_objects_service.test.ts | 2 + .../saved_objects/saved_objects_service.ts | 10 +- src/core/server/server.ts | 1 + 25 files changed, 425 insertions(+), 33 deletions(-) diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index db9d302709092..686e10265df7b 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -46,7 +46,11 @@ Take these extra steps to ensure you are ready for migration. [float] ==== Ensure your {es} cluster is healthy -Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensure that your cluster has: +Problems with your {es} cluster can prevent {kib} upgrades from succeeding. + +During the upgrade process, {kib} creates new indices into which updated documents are written. If a cluster is approaching the low watermark, there's a high risk of {kib} not being able to create these. Reading, transforming and writing updated documents can be memory intensive, using more available heap than during routine operation. You must make sure that enough heap is available to prevent requests from timing out or throwing errors from circuit breaker exceptions. You should also ensure that all shards are replicated and assigned. + +A healthy cluster has: * Enough free disk space, at least twice the amount of storage taken up by the `.kibana` and `.kibana_task_manager` indices * Sufficient heap size diff --git a/docs/setup/upgrade/resolving-migration-failures.asciidoc b/docs/setup/upgrade/resolving-migration-failures.asciidoc index 5b590c359cc69..3cbfb4c9c2abe 100644 --- a/docs/setup/upgrade/resolving-migration-failures.asciidoc +++ b/docs/setup/upgrade/resolving-migration-failures.asciidoc @@ -99,7 +99,7 @@ object types will also log the following warning message: [source,sh] -------------------------------------------- -CHECK_UNKNOWN_DOCUMENTS Upgrades will fail for 8.0+ because documents were found for unknown saved object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete these documents from the ".kibana_7.17.0_001" index after the current upgrade completes. +CHECK_UNKNOWN_DOCUMENTS Upgrades will fail for 8.0+ because documents were found for unknown saved object types. To ensure that future upgrades will succeed, either re-enable plugins or delete these documents from the ".kibana_7.17.0_001" index after the current upgrade completes. -------------------------------------------- If you fail to remedy this, your upgrade to 8.0+ will fail with a message like: @@ -123,3 +123,67 @@ In {kib} 7.5.0 and earlier, when the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations fail. In {kib} 7.5.1 and later, the incompatible configuration setting prevents upgrade migrations from starting. + +[float] +==== Repeated time-out requests that eventually fail +Migrations get stuck in a loop of retry attempts waiting for index yellow status that's never reached. +In the CLONE_TEMP_TO_TARGET or CREATE_REINDEX_TEMP steps, you might see a log entry similar to: + +[source,sh] +-------------------------------------------- +"Action failed with [index_not_yellow_timeout] Timeout waiting for the status of the [.kibana_8.1.0_001] index to become "yellow". Retrying attempt 1 in 2 seconds." +-------------------------------------------- +The process is waiting for a yellow index status. There are two known causes: + +* Cluster hits the low watermark for disk usage +* Cluster has <> + +Before retrying the migration, inspect the output of the `_cluster/allocation/explain?index=${targetIndex}` API to identify why the index isn't yellow: + +[source,sh] +-------------------------------------------- +GET _cluster/allocation/explain +{ + "index": ".kibana_8.1.0_001", + "shard": 0, + "primary": true, +} +-------------------------------------------- +If the cluster exceeded the low watermark for disk usage, the output should contain a message similar to this: + +[source,sh] +-------------------------------------------- +"The node is above the low watermark cluster setting [cluster.routing.allocation.disk.watermark.low=85%], using more disk space than the maximum allowed [85.0%], actual free: [11.692661332965082%]" +-------------------------------------------- +Refer to the {es} guide for how to {ref}/fix-common-cluster-issues.html#_error_disk_usage_exceeded_flood_stage_watermark_index_has_read_only_allow_delete_block[fix common cluster issues]. + +If routing allocation is the issue, the `_cluster/allocation/explain` API will return an entry similar to this: + +[source,sh] +-------------------------------------------- +"allocate_explanation" : "cannot allocate because allocation is not permitted to any of the nodes" +-------------------------------------------- + +[float] +[[routing-allocation-disabled]] +==== Routing allocation disabled or restricted +Upgrade migrations fail because routing allocation is disabled or restricted (`cluster.routing.allocation.enable: none/primaries/new_primaries`), which causes {kib} to log errors such as: + +[source,sh] +-------------------------------------------- +Unable to complete saved object migrations for the [.kibana] index: The elasticsearch cluster has cluster routing allocation incorrectly set for migrations to continue. To proceed, please remove the cluster routing allocation settings with PUT /_cluster/settings {"transient": {"cluster.routing.allocation.enable": null}, "persistent": {"cluster.routing.allocation.enable": null}} +-------------------------------------------- + +To get around the issue, remove the transient and persisted routing allocation settings: +[source,sh] +-------------------------------------------- +PUT /_cluster/settings +{ + "transient": { + "cluster.routing.allocation.enable": null + }, + "persistent": { + "cluster.routing.allocation.enable": null + } +} +-------------------------------------------- diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index f4d01b8f12353..a4e788d7639ec 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -642,5 +642,8 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { legal: { privacyStatement: `${ELASTIC_WEBSITE_URL}legal/privacy-statement`, }, + kibanaUpgradeSavedObjects: { + resolveMigrationFailures: `${KIBANA_DOCS}resolve-migrations-failures.html`, + }, }); }; diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index e51ef91ac43b1..a2d05fbbe699c 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -398,4 +398,7 @@ export interface DocLinks { readonly legal: { readonly privacyStatement: string; }; + readonly kibanaUpgradeSavedObjects: { + readonly resolveMigrationFailures: string; + }; } diff --git a/src/core/server/saved_objects/migrations/README.md b/src/core/server/saved_objects/migrations/README.md index d8382e4f0e061..7dd5dacffcee6 100644 --- a/src/core/server/saved_objects/migrations/README.md +++ b/src/core/server/saved_objects/migrations/README.md @@ -181,7 +181,11 @@ and the migration source index is the index the `.kibana` alias points to. Create the target index. This operation is idempotent, if the index already exist, we wait until its status turns yellow ### New control state - → `MARK_VERSION_INDEX_READY` +1. If the action succeeds + → `MARK_VERSION_INDEX_READY` +2. If the action fails with a `index_not_yellow_timeout` + → `CREATE_NEW_TARGET` + ## LEGACY_SET_WRITE_BLOCK ### Next action @@ -209,8 +213,10 @@ Create a new `.kibana_pre6.5.0_001` index into which we can reindex the legacy index. (Since the task manager index was converted from a data index into a saved objects index in 7.4 it will be reindexed into `.kibana_pre7.4.0_001`) ### New control state +1. If the index creation succeeds → `LEGACY_REINDEX` - +2. If the index creation task failed with a `index_not_yellow_timeout` + → `LEGACY_REINDEX_WAIT_FOR_TASK` ## LEGACY_REINDEX ### Next action `reindex` @@ -257,7 +263,10 @@ Wait for the Elasticsearch cluster to be in "yellow" state. It means the index's We don't have as much data redundancy as we could have, but it's enough to start the migration. ### New control state +1. If the action succeeds → `SET_SOURCE_WRITE_BLOCK` +2. If the action fails with a `index_not_yellow_timeout` + → `WAIT_FOR_YELLOW_SOURCE` ## SET_SOURCE_WRITE_BLOCK ### Next action @@ -278,7 +287,10 @@ This operation is idempotent, if the index already exist, we wait until its stat - (Since we never query the temporary index we can potentially disable refresh to speed up indexing performance. Profile to see if gains justify complexity) ### New control state +1. If the action succeeds → `REINDEX_SOURCE_TO_TEMP_OPEN_PIT` +2. If the action fails with a `index_not_yellow_timeout` + → `CREATE_REINDEX_TEMP` ## REINDEX_SOURCE_TO_TEMP_OPEN_PIT ### Next action @@ -357,7 +369,10 @@ Ask elasticsearch to clone the temporary index into the target index. If the tar We can’t use the temporary index as our target index because one instance can complete the migration, delete a document, and then a second instance starts the reindex operation and re-creates the deleted document. By cloning the temporary index and only accepting writes/deletes from the cloned target index, we prevent lost acknowledged deletes. ### New control state +1. If the action succeeds → `OUTDATED_DOCUMENTS_SEARCH` +2. If the action fails with a `index_not_yellow_timeout` + → `CLONE_TEMP_TO_TARGET` ## OUTDATED_DOCUMENTS_SEARCH ### Next action diff --git a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap index 0ad6f18104685..b1dcd51bbdd0d 100644 --- a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap +++ b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap @@ -32,6 +32,9 @@ Object { }, ], "maxBatchSizeBytes": 100000000, + "migrationDocLinks": Object { + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + }, "outdatedDocuments": Array [], "outdatedDocumentsQuery": Object { "bool": Object { @@ -193,6 +196,9 @@ Object { }, ], "maxBatchSizeBytes": 100000000, + "migrationDocLinks": Object { + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + }, "outdatedDocuments": Array [], "outdatedDocumentsQuery": Object { "bool": Object { @@ -358,6 +364,9 @@ Object { }, ], "maxBatchSizeBytes": 100000000, + "migrationDocLinks": Object { + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + }, "outdatedDocuments": Array [], "outdatedDocumentsQuery": Object { "bool": Object { @@ -527,6 +536,9 @@ Object { }, ], "maxBatchSizeBytes": 100000000, + "migrationDocLinks": Object { + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + }, "outdatedDocuments": Array [], "outdatedDocumentsQuery": Object { "bool": Object { @@ -722,6 +734,9 @@ Object { }, ], "maxBatchSizeBytes": 100000000, + "migrationDocLinks": Object { + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + }, "outdatedDocuments": Array [ Object { "_id": "1234", @@ -894,6 +909,9 @@ Object { }, ], "maxBatchSizeBytes": 100000000, + "migrationDocLinks": Object { + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + }, "outdatedDocuments": Array [ Object { "_id": "1234", diff --git a/src/core/server/saved_objects/migrations/actions/clone_index.ts b/src/core/server/saved_objects/migrations/actions/clone_index.ts index b5aedb11b36fa..c9496ec6915ca 100644 --- a/src/core/server/saved_objects/migrations/actions/clone_index.ts +++ b/src/core/server/saved_objects/migrations/actions/clone_index.ts @@ -15,7 +15,7 @@ import { catchRetryableEsClientErrors, RetryableEsClientError, } from './catch_retryable_es_client_errors'; -import type { IndexNotFound, AcknowledgeResponse } from '.'; +import type { IndexNotFound, AcknowledgeResponse, IndexNotYellowTimeout } from '.'; import { waitForIndexStatusYellow } from './wait_for_index_status_yellow'; import { DEFAULT_TIMEOUT, @@ -49,7 +49,7 @@ export const cloneIndex = ({ target, timeout = DEFAULT_TIMEOUT, }: CloneIndexParams): TaskEither.TaskEither< - RetryableEsClientError | IndexNotFound, + RetryableEsClientError | IndexNotFound | IndexNotYellowTimeout, CloneIndexResponse > => { const cloneTask: TaskEither.TaskEither< @@ -122,7 +122,7 @@ export const cloneIndex = ({ return pipe( cloneTask, - TaskEither.chain((res) => { + TaskEither.chainW((res) => { if (res.acknowledged && res.shardsAcknowledged) { // If the cluster state was updated and all shards ackd we're done return TaskEither.right(res); diff --git a/src/core/server/saved_objects/migrations/actions/create_index.ts b/src/core/server/saved_objects/migrations/actions/create_index.ts index b0be80d1b74b6..69d47077c0606 100644 --- a/src/core/server/saved_objects/migrations/actions/create_index.ts +++ b/src/core/server/saved_objects/migrations/actions/create_index.ts @@ -22,7 +22,7 @@ import { INDEX_AUTO_EXPAND_REPLICAS, WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE, } from './constants'; -import { waitForIndexStatusYellow } from './wait_for_index_status_yellow'; +import { IndexNotYellowTimeout, waitForIndexStatusYellow } from './wait_for_index_status_yellow'; function aliasArrayToRecord(aliases: string[]): Record { const result: Record = {}; @@ -54,7 +54,10 @@ export const createIndex = ({ indexName, mappings, aliases = [], -}: CreateIndexParams): TaskEither.TaskEither => { +}: CreateIndexParams): TaskEither.TaskEither< + RetryableEsClientError | IndexNotYellowTimeout, + 'create_index_succeeded' +> => { const createIndexTask: TaskEither.TaskEither< RetryableEsClientError, AcknowledgeResponse diff --git a/src/core/server/saved_objects/migrations/actions/index.ts b/src/core/server/saved_objects/migrations/actions/index.ts index 0915a73f0d531..4db260d4c139b 100644 --- a/src/core/server/saved_objects/migrations/actions/index.ts +++ b/src/core/server/saved_objects/migrations/actions/index.ts @@ -35,8 +35,11 @@ export { removeWriteBlock } from './remove_write_block'; export type { CloneIndexResponse, CloneIndexParams } from './clone_index'; export { cloneIndex } from './clone_index'; -export type { WaitForIndexStatusYellowParams } from './wait_for_index_status_yellow'; -import { waitForIndexStatusYellow } from './wait_for_index_status_yellow'; +export type { + WaitForIndexStatusYellowParams, + IndexNotYellowTimeout, +} from './wait_for_index_status_yellow'; +import { IndexNotYellowTimeout, waitForIndexStatusYellow } from './wait_for_index_status_yellow'; export type { WaitForTaskResponse, WaitForTaskCompletionTimeout } from './wait_for_task'; import { waitForTask, WaitForTaskCompletionTimeout } from './wait_for_task'; @@ -149,6 +152,7 @@ export interface ActionErrorTypeMap { request_entity_too_large_exception: RequestEntityTooLargeException; unknown_docs_found: UnknownDocsFound; unsupported_cluster_routing_allocation: UnsupportedClusterRoutingAllocation; + index_not_yellow_timeout: IndexNotYellowTimeout; } /** diff --git a/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts index 331958e88ce83..5e840d87ea1ab 100644 --- a/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts @@ -321,8 +321,13 @@ describe('migration actions', () => { }); describe('waitForIndexStatusYellow', () => { - afterAll(async () => { - await client.indices.delete({ index: 'red_then_yellow_index' }); + afterEach(async () => { + try { + await client.indices.delete({ index: 'red_then_yellow_index' }); + await client.indices.delete({ index: 'red_index' }); + } catch (e) { + /** ignore */ + } }); it('resolves right after waiting for an index status to be yellow if the index already existed', async () => { // Create a red index @@ -366,6 +371,39 @@ describe('migration actions', () => { const yellowStatusResponse = await client.cluster.health({ index: 'red_then_yellow_index' }); expect(yellowStatusResponse.status).toBe('yellow'); }); + it('resolves left with "index_not_yellow_timeout" after waiting for an index status to be yellow timeout', async () => { + // Create a red index + await client.indices + .create({ + index: 'red_index', + timeout: '5s', + body: { + mappings: { properties: {} }, + settings: { + // Allocate no replicas so that this index stays red + number_of_replicas: '0', + // Disable all shard allocation so that the index status is red + index: { routing: { allocation: { enable: 'none' } } }, + }, + }, + }) + .catch((e) => {}); + // try to wait for index status yellow: + const task = waitForIndexStatusYellow({ + client, + index: 'red_index', + timeout: '1s', + }); + await expect(task()).resolves.toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "message": "[index_not_yellow_timeout] Timeout waiting for the status of the [red_index] index to become 'yellow'", + "type": "index_not_yellow_timeout", + }, + } + `); + }); }); describe('cloneIndex', () => { @@ -459,7 +497,7 @@ describe('migration actions', () => { } `); }); - it('resolves left with a retryable_es_client_error if clone target already exists but takes longer than the specified timeout before turning yellow', async () => { + it('resolves left with a index_not_yellow_timeout if clone target already exists but takes longer than the specified timeout before turning yellow', async () => { // Create a red index await client.indices .create({ @@ -489,8 +527,8 @@ describe('migration actions', () => { Object { "_tag": "Left", "left": Object { - "message": "Timeout waiting for the status of the [clone_red_index] index to become 'yellow'", - "type": "retryable_es_client_error", + "message": "[index_not_yellow_timeout] Timeout waiting for the status of the [clone_red_index] index to become 'yellow'", + "type": "index_not_yellow_timeout", }, } `); diff --git a/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts b/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts index dbff85ff59c23..fed28252bd2a4 100644 --- a/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts +++ b/src/core/server/saved_objects/migrations/actions/wait_for_index_status_yellow.ts @@ -21,6 +21,11 @@ export interface WaitForIndexStatusYellowParams { index: string; timeout?: string; } + +export interface IndexNotYellowTimeout { + type: 'index_not_yellow_timeout'; + message: string; +} /** * A yellow index status means the index's primary shard is allocated and the * index is ready for searching/indexing documents, but ES wasn't able to @@ -37,7 +42,10 @@ export const waitForIndexStatusYellow = client, index, timeout = DEFAULT_TIMEOUT, - }: WaitForIndexStatusYellowParams): TaskEither.TaskEither => + }: WaitForIndexStatusYellowParams): TaskEither.TaskEither< + RetryableEsClientError | IndexNotYellowTimeout, + {} + > => () => { return client.cluster .health( @@ -47,14 +55,14 @@ export const waitForIndexStatusYellow = timeout, }, // Don't reject on status code 408 so that we can handle the timeout - // explicitly and provide more context in the error message + // explicitly with a custom response type and provide more context in the error message { ignore: [408] } ) .then((res) => { if (res.timed_out === true) { return Either.left({ - type: 'retryable_es_client_error' as const, - message: `Timeout waiting for the status of the [${index}] index to become 'yellow'`, + type: 'index_not_yellow_timeout' as const, + message: `[index_not_yellow_timeout] Timeout waiting for the status of the [${index}] index to become 'yellow'`, }); } return Either.right({}); diff --git a/src/core/server/saved_objects/migrations/initial_state.test.ts b/src/core/server/saved_objects/migrations/initial_state.test.ts index 601eb7cf1ce36..0fff4ddb06895 100644 --- a/src/core/server/saved_objects/migrations/initial_state.test.ts +++ b/src/core/server/saved_objects/migrations/initial_state.test.ts @@ -8,15 +8,19 @@ import { ByteSizeValue } from '@kbn/config-schema'; import * as Option from 'fp-ts/Option'; +import { DocLinksServiceSetup } from '../../doc_links'; +import { docLinksServiceMock } from '../../mocks'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { createInitialState } from './initial_state'; describe('createInitialState', () => { let typeRegistry: SavedObjectTypeRegistry; + let docLinks: DocLinksServiceSetup; beforeEach(() => { typeRegistry = new SavedObjectTypeRegistry(); + docLinks = docLinksServiceMock.createSetupContract(); }); const migrationsConfig = { @@ -36,6 +40,7 @@ describe('createInitialState', () => { indexPrefix: '.kibana_task_manager', migrationsConfig, typeRegistry, + docLinks, }) ).toEqual({ batchSize: 1000, @@ -108,6 +113,10 @@ describe('createInitialState', () => { }, versionAlias: '.kibana_task_manager_8.1.0', versionIndex: '.kibana_task_manager_8.1.0_001', + migrationDocLinks: { + resolveMigrationFailures: + 'https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html', + }, }); }); @@ -135,6 +144,7 @@ describe('createInitialState', () => { indexPrefix: '.kibana_task_manager', migrationsConfig, typeRegistry, + docLinks, }); expect(initialState.knownTypes).toEqual(['foo', 'bar']); @@ -160,6 +170,7 @@ describe('createInitialState', () => { indexPrefix: '.kibana_task_manager', migrationsConfig, typeRegistry, + docLinks, }); expect(initialState.excludeFromUpgradeFilterHooks).toEqual({ foo: fooExcludeOnUpgradeHook }); @@ -178,6 +189,7 @@ describe('createInitialState', () => { indexPrefix: '.kibana_task_manager', migrationsConfig, typeRegistry, + docLinks, }); expect(Option.isSome(initialState.preMigrationScript)).toEqual(true); @@ -199,6 +211,7 @@ describe('createInitialState', () => { indexPrefix: '.kibana_task_manager', migrationsConfig, typeRegistry, + docLinks, }).preMigrationScript ) ).toEqual(true); @@ -216,6 +229,7 @@ describe('createInitialState', () => { indexPrefix: '.kibana_task_manager', migrationsConfig, typeRegistry, + docLinks, }).outdatedDocumentsQuery ).toMatchInlineSnapshot(` Object { diff --git a/src/core/server/saved_objects/migrations/initial_state.ts b/src/core/server/saved_objects/migrations/initial_state.ts index 21b564f8c454f..ae0fe54049505 100644 --- a/src/core/server/saved_objects/migrations/initial_state.ts +++ b/src/core/server/saved_objects/migrations/initial_state.ts @@ -13,6 +13,7 @@ import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import type { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { InitState } from './state'; import { excludeUnusedTypesQuery } from './core'; +import { DocLinksServiceStart } from '../../doc_links'; /** * Construct the initial state for the model @@ -25,6 +26,7 @@ export const createInitialState = ({ indexPrefix, migrationsConfig, typeRegistry, + docLinks, }: { kibanaVersion: string; targetMappings: IndexMapping; @@ -33,6 +35,7 @@ export const createInitialState = ({ indexPrefix: string; migrationsConfig: SavedObjectsMigrationConfigType; typeRegistry: ISavedObjectTypeRegistry; + docLinks: DocLinksServiceStart; }): InitState => { const outdatedDocumentsQuery = { bool: { @@ -64,6 +67,8 @@ export const createInitialState = ({ .filter((type) => !!type.excludeOnUpgrade) .map((type) => [type.name, type.excludeOnUpgrade!]) ); + // short key to access savedObjects entries directly from docLinks + const migrationDocLinks = docLinks.links.kibanaUpgradeSavedObjects; return { controlState: 'INIT', @@ -87,5 +92,6 @@ export const createInitialState = ({ unusedTypesQuery: excludeUnusedTypesQuery, knownTypes, excludeFromUpgradeFilterHooks: excludeFilterHooks, + migrationDocLinks, }; }; diff --git a/src/core/server/saved_objects/migrations/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana_migrator.test.ts index e5aa51f9d6b0a..77837f066a52f 100644 --- a/src/core/server/saved_objects/migrations/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.test.ts @@ -16,6 +16,7 @@ import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { SavedObjectsType } from '../types'; import { DocumentMigrator } from './core/document_migrator'; import { ByteSizeValue } from '@kbn/config-schema'; +import { docLinksServiceMock } from '../../mocks'; import { lastValueFrom } from 'rxjs'; jest.mock('./core/document_migrator', () => { @@ -287,6 +288,7 @@ const mockOptions = () => { retryAttempts: 20, }, client: elasticsearchClientMock.createElasticsearchClient(), + docLinks: docLinksServiceMock.createSetupContract(), }; return options; }; diff --git a/src/core/server/saved_objects/migrations/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana_migrator.ts index 2e2c4e2c63c04..31b9c89f4176f 100644 --- a/src/core/server/saved_objects/migrations/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana_migrator.ts @@ -29,6 +29,7 @@ import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { SavedObjectsType } from '../types'; import { runResilientMigrator } from './run_resilient_migrator'; import { migrateRawDocsSafely } from './core/migrate_raw_docs'; +import { DocLinksServiceStart } from '../../doc_links'; export interface KibanaMigratorOptions { client: ElasticsearchClient; @@ -37,6 +38,7 @@ export interface KibanaMigratorOptions { kibanaIndex: string; kibanaVersion: string; logger: Logger; + docLinks: DocLinksServiceStart; } export type IKibanaMigrator = Pick; @@ -65,6 +67,7 @@ export class KibanaMigrator { private readonly activeMappings: IndexMapping; private readonly soMigrationsConfig: SavedObjectsMigrationConfigType; public readonly kibanaVersion: string; + private readonly docLinks: DocLinksServiceStart; /** * Creates an instance of KibanaMigrator. @@ -76,6 +79,7 @@ export class KibanaMigrator { soMigrationsConfig, kibanaVersion, logger, + docLinks, }: KibanaMigratorOptions) { this.client = client; this.kibanaIndex = kibanaIndex; @@ -93,6 +97,7 @@ export class KibanaMigrator { // Building the active mappings (and associated md5sums) is an expensive // operation so we cache the result this.activeMappings = buildActiveMappings(this.mappingProperties); + this.docLinks = docLinks; } /** @@ -177,6 +182,7 @@ export class KibanaMigrator { indexPrefix: index, migrationsConfig: this.soMigrationsConfig, typeRegistry: this.typeRegistry, + docLinks: this.docLinks, }); }, }; diff --git a/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts index 3bc07c0fea0c1..93e6476f8e78c 100644 --- a/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrations/migrations_state_action_machine.test.ts @@ -8,7 +8,7 @@ import { cleanupMock } from './migrations_state_machine_cleanup.mocks'; import { migrationStateActionMachine } from './migrations_state_action_machine'; -import { loggingSystemMock, elasticsearchServiceMock } from '../../mocks'; +import { loggingSystemMock, elasticsearchServiceMock, docLinksServiceMock } from '../../mocks'; import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import * as Either from 'fp-ts/lib/Either'; import * as Option from 'fp-ts/lib/Option'; @@ -33,6 +33,7 @@ describe('migrationsStateActionMachine', () => { const mockLogger = loggingSystemMock.create(); const typeRegistry = typeRegistryMock.create(); + const docLinks = docLinksServiceMock.createSetupContract(); const initialState = createInitialState({ kibanaVersion: '7.11.0', @@ -48,6 +49,7 @@ describe('migrationsStateActionMachine', () => { retryAttempts: 5, }, typeRegistry, + docLinks, }); const next = jest.fn((s: State) => { diff --git a/src/core/server/saved_objects/migrations/model/model.test.ts b/src/core/server/saved_objects/migrations/model/model.test.ts index fc28cca8f4ddc..b80e2bceae846 100644 --- a/src/core/server/saved_objects/migrations/model/model.test.ts +++ b/src/core/server/saved_objects/migrations/model/model.test.ts @@ -94,6 +94,9 @@ describe('migrations v2 model', () => { }, knownTypes: ['dashboard', 'config'], excludeFromUpgradeFilterHooks: {}, + migrationDocLinks: { + resolveMigrationFailures: 'resolveMigrationFailures', + }, }; describe('exponential retry delays for retryable_es_client_error', () => { @@ -182,7 +185,7 @@ describe('migrations v2 model', () => { expect(newState.controlState).toEqual('FATAL'); expect(newState.reason).toMatchInlineSnapshot( - `"Unable to complete the INIT step after 15 attempts, terminating."` + `"Unable to complete the INIT step after 15 attempts, terminating. The last failure message was: snapshot_in_progress_exception"` ); }); }); @@ -560,9 +563,29 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); - // The createIndex action called by LEGACY_CREATE_REINDEX_TARGET never - // returns a left, it will always succeed or timeout. Since timeout - // failures are always retried we don't explicitly test this logic + test('LEGACY_CREATE_REINDEX_TARGET -> LEGACY_CREATE_REINDEX_TARGET if action fails with index_not_yellow_timeout', () => { + const res: ResponseType<'LEGACY_CREATE_REINDEX_TARGET'> = Either.left({ + message: '[index_not_yellow_timeout] Timeout waiting for ...', + type: 'index_not_yellow_timeout', + }); + const newState = model(legacyCreateReindexTargetState, res); + expect(newState.controlState).toEqual('LEGACY_CREATE_REINDEX_TARGET'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); + test('LEGACY_CREATE_REINDEX_TARGET -> LEGACY_REINDEX resets retry count and retry delay if action succeeds', () => { + const res: ResponseType<'LEGACY_CREATE_REINDEX_TARGET'> = + Either.right('create_index_succeeded'); + const testState = { + ...legacyCreateReindexTargetState, + retryCount: 1, + retryDelay: 2000, + }; + const newState = model(testState, res); + expect(newState.controlState).toEqual('LEGACY_REINDEX'); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + }); }); describe('LEGACY_REINDEX', () => { @@ -707,6 +730,33 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana_3'), }); }); + + test('WAIT_FOR_YELLOW_SOURCE -> WAIT_FOR_YELLOW_SOURCE if action fails with index_not_yellow_timeout', () => { + const res: ResponseType<'WAIT_FOR_YELLOW_SOURCE'> = Either.left({ + message: '[index_not_yellow_timeout] Timeout waiting for ...', + type: 'index_not_yellow_timeout', + }); + const newState = model(waitForYellowSourceState, res); + expect(newState.controlState).toEqual('WAIT_FOR_YELLOW_SOURCE'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); + + test('WAIT_FOR_YELLOW_SOURCE -> CHECK_UNKNOWN_DOCUMENTS resets retry count and delay if action succeeds', () => { + const res: ResponseType<'WAIT_FOR_YELLOW_SOURCE'> = Either.right({}); + const testState = { + ...waitForYellowSourceState, + retryCount: 1, + retryDelay: 2000, + }; + const newState = model(testState, res); + expect(newState.controlState).toEqual('CHECK_UNKNOWN_DOCUMENTS'); + + expect(newState).toMatchObject({ + controlState: 'CHECK_UNKNOWN_DOCUMENTS', + sourceIndex: Option.some('.kibana_3'), + }); + }); }); describe('CHECK_UNKNOWN_DOCUMENTS', () => { @@ -900,6 +950,28 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + it('CREATE_REINDEX_TEMP -> CREATE_REINDEX_TEMP if action fails with index_not_yellow_timeout', () => { + const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.left({ + message: '[index_not_yellow_timeout] Timeout waiting for ...', + type: 'index_not_yellow_timeout', + }); + const newState = model(state, res); + expect(newState.controlState).toEqual('CREATE_REINDEX_TEMP'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); + it('CREATE_REINDEX_TEMP -> REINDEX_SOURCE_TO_TEMP_OPEN_PIT resets retry count if action succeeds', () => { + const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.right('create_index_succeeded'); + const testState = { + ...state, + retryCount: 1, + retryDelay: 2000, + }; + const newState = model(testState, res); + expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_OPEN_PIT'); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + }); }); describe('REINDEX_SOURCE_TO_TEMP_OPEN_PIT', () => { @@ -1212,6 +1284,31 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toBe(0); expect(newState.retryDelay).toBe(0); }); + it('CLONE_TEMP_TO_TARGET -> CLONE_TEMP_TO_TARGET if action fails with index_not_yellow_timeout', () => { + const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.left({ + message: '[index_not_yellow_timeout] Timeout waiting for ...', + type: 'index_not_yellow_timeout', + }); + const newState = model(state, res); + expect(newState.controlState).toEqual('CLONE_TEMP_TO_TARGET'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); + it('CREATE_NEW_TARGET -> MARK_VERSION_INDEX_READY resets the retry count and delay', () => { + const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.right({ + acknowledged: true, + shardsAcknowledged: true, + }); + const testState = { + ...state, + retryCount: 1, + retryDelay: 2000, + }; + const newState = model(testState, res); + expect(newState.controlState).toBe('REFRESH_TARGET'); + expect(newState.retryCount).toBe(0); + expect(newState.retryDelay).toBe(0); + }); }); describe('OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT', () => { @@ -1698,6 +1795,29 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('CREATE_NEW_TARGET -> CREATE_NEW_TARGET if action fails with index_not_yellow_timeout', () => { + const res: ResponseType<'CREATE_NEW_TARGET'> = Either.left({ + message: '[index_not_yellow_timeout] Timeout waiting for ...', + type: 'index_not_yellow_timeout', + }); + const newState = model(createNewTargetState, res); + expect(newState.controlState).toEqual('CREATE_NEW_TARGET'); + expect(newState.retryCount).toEqual(1); + expect(newState.retryDelay).toEqual(2000); + }); + test('CREATE_NEW_TARGET -> MARK_VERSION_INDEX_READY resets the retry count and delay', () => { + const res: ResponseType<'CREATE_NEW_TARGET'> = Either.right('create_index_succeeded'); + const testState = { + ...createNewTargetState, + retryCount: 1, + retryDelay: 2000, + }; + + const newState = model(testState, res); + expect(newState.controlState).toEqual('MARK_VERSION_INDEX_READY'); + expect(newState.retryCount).toEqual(0); + expect(newState.retryDelay).toEqual(0); + }); }); describe('MARK_VERSION_INDEX_READY', () => { diff --git a/src/core/server/saved_objects/migrations/model/model.ts b/src/core/server/saved_objects/migrations/model/model.ts index 2e3905fd90fe1..e711f62bcd8d6 100644 --- a/src/core/server/saved_objects/migrations/model/model.ts +++ b/src/core/server/saved_objects/migrations/model/model.ts @@ -235,7 +235,21 @@ export const model = (currentState: State, resW: ResponseType): } } else if (stateP.controlState === 'LEGACY_CREATE_REINDEX_TARGET') { const res = resW as ExcludeRetryableEsError>; - if (Either.isRight(res)) { + if (Either.isLeft(res)) { + const left = res.left; + if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + // `index_not_yellow_timeout` for the LEGACY_CREATE_REINDEX_TARGET source index: + // A yellow status timeout could theoretically be temporary for a busy cluster + // that takes a long time to allocate the primary and we retry the action to see if + // we get a response. + // If the cluster hit the low watermark for disk usage the LEGACY_CREATE_REINDEX_TARGET action will + // continue to timeout and eventually lead to a failed migration. + const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.resolveMigrationFailures} for information on how to resolve the issue.`; + return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else { + return throwBadResponse(stateP, left); + } + } else if (Either.isRight(res)) { return { ...stateP, controlState: 'LEGACY_REINDEX', @@ -285,7 +299,7 @@ export const model = (currentState: State, resW: ResponseType): // After waiting for the specified timeout, the task has not yet // completed. Retry this step to see if the task has completed after an // exponential delay. We will basically keep polling forever until the - // Elasticeasrch task succeeds or fails. + // Elasticsearch task succeeds or fails. return delayRetryState(stateP, left.message, Number.MAX_SAFE_INTEGER); } else if ( isLeftTypeof(left, 'index_not_found_exception') || @@ -344,6 +358,19 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'CHECK_UNKNOWN_DOCUMENTS', }; + } else if (Either.isLeft(res)) { + const left = res.left; + if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + // A yellow status timeout could theoretically be temporary for a busy cluster + // that takes a long time to allocate the primary and we retry the action to see if + // we get a response. + // In the event of retries running out, we link to the docs to help with diagnosing + // the problem. + const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.resolveMigrationFailures} for information on how to resolve the issue.`; + return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else { + return throwBadResponse(stateP, left); + } } else { return throwBadResponse(stateP, res); } @@ -425,6 +452,20 @@ export const model = (currentState: State, resW: ResponseType): const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { ...stateP, controlState: 'REINDEX_SOURCE_TO_TEMP_OPEN_PIT' }; + } else if (Either.isLeft(res)) { + const left = res.left; + if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + // `index_not_yellow_timeout` for the CREATE_REINDEX_TEMP target temp index: + // The index status did not go yellow within the specified timeout period. + // A yellow status timeout could theoretically be temporary for a busy cluster. + // + // If there is a problem CREATE_REINDEX_TEMP action will + // continue to timeout and eventually lead to a failed migration. + const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.resolveMigrationFailures} for information on how to resolve the issue.`; + return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else { + return throwBadResponse(stateP, left); + } } else { // If the createIndex action receives an 'resource_already_exists_exception' // it will wait until the index status turns green so we don't have any @@ -645,6 +686,18 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'REFRESH_TARGET', }; + } else if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + // `index_not_yellow_timeout` for the CLONE_TEMP_TO_TARGET source -> target index: + // The target index status did not go yellow within the specified timeout period. + // The cluster could just be busy and we retry the action. + + // Once we run out of retries, the migration fails. + // Identifying the cause requires inspecting the ouput of the + // `_cluster/allocation/explain?index=${targetIndex}` API. + // Unless the root cause is identified and addressed, the request will + // continue to timeout and eventually lead to a failed migration. + const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.resolveMigrationFailures} for information on how to resolve the issue.`; + return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); } else { throwBadResponse(stateP, left); } @@ -876,7 +929,7 @@ export const model = (currentState: State, resW: ResponseType): if (isLeftTypeof(left, 'wait_for_task_completion_timeout')) { // After waiting for the specified timeout, the task has not yet // completed. Retry this step to see if the task has completed after an - // exponential delay. We will basically keep polling forever until the + // exponential delay. We will basically keep polling forever until the // Elasticsearch task succeeds or fails. return delayRetryState(stateP, res.left.message, Number.MAX_SAFE_INTEGER); } else { @@ -890,6 +943,19 @@ export const model = (currentState: State, resW: ResponseType): ...stateP, controlState: 'MARK_VERSION_INDEX_READY', }; + } else if (Either.isLeft(res)) { + const left = res.left; + if (isLeftTypeof(left, 'index_not_yellow_timeout')) { + // `index_not_yellow_timeout` for the CREATE_NEW_TARGET target index: + // The cluster might just be busy and we retry the action for a set number of times. + // If the cluster hit the low watermark for disk usage the action will continue to timeout. + // Unless the disk space is addressed, the LEGACY_CREATE_REINDEX_TARGET action will + // continue to timeout and eventually lead to a failed migration. + const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.resolveMigrationFailures} for information on how to resolve the issue.`; + return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else { + return throwBadResponse(stateP, left); + } } else { // If the createIndex action receives an 'resource_already_exists_exception' // it will wait until the index status turns green so we don't have any diff --git a/src/core/server/saved_objects/migrations/model/retry_state.test.ts b/src/core/server/saved_objects/migrations/model/retry_state.test.ts index 5a195f8597182..994f3ca2ede4b 100644 --- a/src/core/server/saved_objects/migrations/model/retry_state.test.ts +++ b/src/core/server/saved_objects/migrations/model/retry_state.test.ts @@ -109,7 +109,7 @@ describe('delayRetryState', () => { hello: 'dolly', retryCount: 5, retryDelay: 64, - reason: `Unable to complete the TEST step after 5 attempts, terminating.`, + reason: `Unable to complete the TEST step after 5 attempts, terminating. The last failure message was: some-error`, }); }); }); diff --git a/src/core/server/saved_objects/migrations/model/retry_state.ts b/src/core/server/saved_objects/migrations/model/retry_state.ts index 02057a6af2061..532cc70347916 100644 --- a/src/core/server/saved_objects/migrations/model/retry_state.ts +++ b/src/core/server/saved_objects/migrations/model/retry_state.ts @@ -18,12 +18,11 @@ export const delayRetryState = ( return { ...state, controlState: 'FATAL', - reason: `Unable to complete the ${state.controlState} step after ${maxRetryAttempts} attempts, terminating.`, + reason: `Unable to complete the ${state.controlState} step after ${maxRetryAttempts} attempts, terminating. The last failure message was: ${errorMessage}`, }; } else { const retryCount = state.retryCount + 1; const retryDelay = 1000 * Math.min(Math.pow(2, retryCount), 64); // 2s, 4s, 8s, 16s, 32s, 64s, 64s, 64s ... - return { ...state, retryCount, diff --git a/src/core/server/saved_objects/migrations/run_resilient_migrator.ts b/src/core/server/saved_objects/migrations/run_resilient_migrator.ts index bdc36b2b79ebe..dce9227719389 100644 --- a/src/core/server/saved_objects/migrations/run_resilient_migrator.ts +++ b/src/core/server/saved_objects/migrations/run_resilient_migrator.ts @@ -18,6 +18,7 @@ import { createInitialState } from './initial_state'; import { migrationStateActionMachine } from './migrations_state_action_machine'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import type { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { DocLinksServiceStart } from '../../doc_links'; /** * Migrates the provided indexPrefix index using a resilient algorithm that is @@ -35,6 +36,7 @@ export async function runResilientMigrator({ indexPrefix, migrationsConfig, typeRegistry, + docLinks, }: { client: ElasticsearchClient; kibanaVersion: string; @@ -46,6 +48,7 @@ export async function runResilientMigrator({ indexPrefix: string; migrationsConfig: SavedObjectsMigrationConfigType; typeRegistry: ISavedObjectTypeRegistry; + docLinks: DocLinksServiceStart; }): Promise { const initialState = createInitialState({ kibanaVersion, @@ -55,6 +58,7 @@ export async function runResilientMigrator({ indexPrefix, migrationsConfig, typeRegistry, + docLinks, }); return migrationStateActionMachine({ initialState, diff --git a/src/core/server/saved_objects/migrations/state.ts b/src/core/server/saved_objects/migrations/state.ts index aacc2826ccb26..6630b5ee57808 100644 --- a/src/core/server/saved_objects/migrations/state.ts +++ b/src/core/server/saved_objects/migrations/state.ts @@ -122,6 +122,10 @@ export interface BaseState extends ControlState { string, SavedObjectTypeExcludeFromUpgradeFilterHook >; + /** + * DocLinks for savedObjects. to reference online documentation + */ + readonly migrationDocLinks: Record; } export interface InitState extends BaseState { diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index a8bda95af46f9..dea9ea224512a 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -36,6 +36,7 @@ import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure import { SavedObjectsRepository } from './service/lib/repository'; import { registerCoreObjectTypes } from './object_types'; import { getSavedObjectsDeprecationsProvider } from './deprecations'; +import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; jest.mock('./service/lib/repository'); jest.mock('./object_types'); @@ -79,6 +80,7 @@ describe('SavedObjectsService', () => { return { pluginsInitialized, elasticsearch: elasticsearchServiceMock.createInternalStart(), + docLinks: docLinksServiceMock.createStartContract(), }; }; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 0729db0e04266..1598d2b1d2764 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -50,6 +50,7 @@ import { ServiceStatus } from '../status'; import { calculateStatus$ } from './status'; import { registerCoreObjectTypes } from './object_types'; import { getSavedObjectsDeprecationsProvider } from './deprecations'; +import { DocLinksServiceStart } from '../doc_links'; const kibanaIndex = '.kibana'; @@ -284,6 +285,7 @@ interface WrappedClientFactoryWrapper { export interface SavedObjectsStartDeps { elasticsearch: InternalElasticsearchServiceStart; pluginsInitialized?: boolean; + docLinks: DocLinksServiceStart; } export class SavedObjectsService @@ -383,6 +385,7 @@ export class SavedObjectsService public async start({ elasticsearch, pluginsInitialized = true, + docLinks, }: SavedObjectsStartDeps): Promise { if (!this.setupDeps || !this.config) { throw new Error('#setup() needs to be run first'); @@ -394,7 +397,8 @@ export class SavedObjectsService const migrator = this.createMigrator( this.config.migration, - elasticsearch.client.asInternalUser + elasticsearch.client.asInternalUser, + docLinks ); this.migrator$.next(migrator); @@ -509,7 +513,8 @@ export class SavedObjectsService private createMigrator( soMigrationsConfig: SavedObjectsMigrationConfigType, - client: ElasticsearchClient + client: ElasticsearchClient, + docLinks: DocLinksServiceStart ): IKibanaMigrator { return new KibanaMigrator({ typeRegistry: this.typeRegistry, @@ -518,6 +523,7 @@ export class SavedObjectsService soMigrationsConfig, kibanaIndex, client, + docLinks, }); } diff --git a/src/core/server/server.ts b/src/core/server/server.ts index dc19f5eeea198..fc215d9055818 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -316,6 +316,7 @@ export class Server { const savedObjectsStart = await this.savedObjects.start({ elasticsearch: elasticsearchStart, pluginsInitialized: this.#pluginsInitialized, + docLinks: docLinkStart, }); await this.resolveSavedObjectsStartPromise!(savedObjectsStart); From dccca6b26d6320b6c667dee2c0dbb35f5d9dbc1c Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Thu, 21 Apr 2022 16:16:27 +0100 Subject: [PATCH 06/24] [Fleet] Remove legacy component templates on package install (#130758) * remove legacy component templates as part of package install * re-work unit tests * remove unnecessary await * check if component templates are in use before deleting * add integration tests * PR feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../template/remove_legacy.test.ts | 262 ++++++++++++++++++ .../elasticsearch/template/remove_legacy.ts | 161 +++++++++++ .../services/epm/packages/_install_package.ts | 7 + .../fleet_api_integration/apis/epm/index.js | 1 + .../apis/epm/remove_legacy_templates.ts | 152 ++++++++++ 5 files changed, 583 insertions(+) create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/template/remove_legacy.test.ts create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/template/remove_legacy.ts create mode 100644 x-pack/test/fleet_api_integration/apis/epm/remove_legacy_templates.ts diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/remove_legacy.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/remove_legacy.test.ts new file mode 100644 index 0000000000000..4877d28cbead0 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/remove_legacy.test.ts @@ -0,0 +1,262 @@ +/* + * 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 type { + ClusterComponentTemplate, + IndicesGetIndexTemplateIndexTemplateItem, +} from '@elastic/elasticsearch/lib/api/types'; + +import type { Logger } from '@kbn/core/server'; + +import uuid from 'uuid'; + +import { loggingSystemMock } from '@kbn/core/server/mocks'; + +import type { InstallablePackage, RegistryDataStream } from '../../../../types'; + +import { + _getLegacyComponentTemplatesForPackage, + _getIndexTemplatesToUsedByMap, + _filterComponentTemplatesInUse, +} from './remove_legacy'; + +const mockLogger: Logger = loggingSystemMock.create().get(); + +const pickRandom = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]; +const pickRandomType = pickRandom.bind(null, ['logs', 'metrics']); +const createMockDataStream = ({ + packageName, + type, + dataset, +}: { + packageName: string; + type?: string; + dataset?: string; +}) => { + return { + type: type || pickRandomType(), + dataset: dataset || uuid.v4(), + title: packageName, + package: packageName, + path: 'some_path', + release: 'ga', + } as RegistryDataStream; +}; +const createMockComponentTemplate = ({ + name = 'templateName', + packageName, +}: { + name?: string; + packageName: string; +}) => { + return { + name, + component_template: { + _meta: { + package: { name: packageName }, + }, + template: { + settings: {}, + }, + }, + } as ClusterComponentTemplate; +}; + +const createMockTemplate = ({ name, composedOf = [] }: { name: string; composedOf?: string[] }) => + ({ + name, + index_template: { + composed_of: composedOf, + }, + } as IndicesGetIndexTemplateIndexTemplateItem); + +const makeArrayOf = (arraySize: number, fn = (i: any) => i) => { + return [...Array(arraySize)].map(fn); +}; +describe('_getLegacyComponentTemplatesForPackage', () => { + it('should handle empty templates array', () => { + const templates = [] as ClusterComponentTemplate[]; + const pkg = { name: 'testPkg', data_streams: [] as RegistryDataStream[] } as InstallablePackage; + + const result = _getLegacyComponentTemplatesForPackage(templates, pkg); + expect(result).toEqual([]); + }); + it('should return empty array if no legacy templates', () => { + const packageName = 'testPkg'; + const templates = makeArrayOf(1000, () => createMockComponentTemplate({ packageName })); + const pkg = { + name: packageName, + data_streams: makeArrayOf(100, () => createMockDataStream({ packageName })), + } as InstallablePackage; + + const result = _getLegacyComponentTemplatesForPackage(templates, pkg); + expect(result).toEqual([]); + }); + + it('should find legacy templates', () => { + const packageName = 'testPkg'; + const legacyTemplates = [ + 'logs-testPkg.dataset@settings', + 'logs-testPkg.dataset@mappings', + 'metrics-testPkg.dataset2@mappings', + 'metrics-testPkg.dataset2@settings', + ]; + const templates = [ + ...makeArrayOf(100, () => createMockComponentTemplate({ packageName })), + ...legacyTemplates.map((name) => createMockComponentTemplate({ name, packageName })), + ]; + const pkg = { + name: packageName, + data_streams: [ + ...makeArrayOf(20, () => createMockDataStream({ packageName })), + createMockDataStream({ type: 'logs', packageName, dataset: 'testPkg.dataset' }), + createMockDataStream({ type: 'metrics', packageName, dataset: 'testPkg.dataset2' }), + ], + } as InstallablePackage; + + const result = _getLegacyComponentTemplatesForPackage(templates, pkg); + expect(result).toEqual(legacyTemplates); + }); + + it('should only return templates if package name matches as well', () => { + const packageName = 'testPkg'; + const legacyTemplates = [ + 'logs-testPkg.dataset@settings', + 'logs-testPkg.dataset@mappings', + 'metrics-testPkg.dataset2@mappings', + 'metrics-testPkg.dataset2@settings', + ]; + const templates = [ + ...makeArrayOf(20, () => createMockComponentTemplate({ packageName })), + ...legacyTemplates.map((name) => + createMockComponentTemplate({ name, packageName: 'someOtherPkg' }) + ), + ]; + const pkg = { + name: packageName, + data_streams: [ + ...makeArrayOf(20, () => createMockDataStream({ packageName })), + createMockDataStream({ type: 'logs', packageName, dataset: 'testPkg.dataset' }), + createMockDataStream({ type: 'metrics', packageName, dataset: 'testPkg.dataset2' }), + ], + } as InstallablePackage; + + const result = _getLegacyComponentTemplatesForPackage(templates, pkg); + expect(result).toEqual([]); + }); +}); + +describe('_getIndexTemplatesToUsedByMap', () => { + it('should return empty map if no index templates provided', () => { + const indexTemplates = [] as IndicesGetIndexTemplateIndexTemplateItem[]; + + const result = _getIndexTemplatesToUsedByMap(indexTemplates); + + expect(result.size).toEqual(0); + }); + + it('should return empty map if no index templates have no component templates', () => { + const indexTemplates = [createMockTemplate({ name: 'tmpl1' })]; + + const result = _getIndexTemplatesToUsedByMap(indexTemplates); + + expect(result.size).toEqual(0); + }); + + it('should return correct map if templates have composedOf', () => { + const indexTemplates = [ + createMockTemplate({ name: 'tmpl1' }), + createMockTemplate({ name: 'tmpl2', composedOf: ['ctmp1'] }), + createMockTemplate({ name: 'tmpl3', composedOf: ['ctmp1', 'ctmp2'] }), + createMockTemplate({ name: 'tmpl4', composedOf: ['ctmp3'] }), + ]; + + const expectedMap = { + ctmp1: ['tmpl2', 'tmpl3'], + ctmp2: ['tmpl3'], + ctmp3: ['tmpl4'], + }; + + const result = _getIndexTemplatesToUsedByMap(indexTemplates); + + expect(Object.fromEntries(result)).toEqual(expectedMap); + }); +}); + +describe('_filterComponentTemplatesInUse', () => { + it('should return empty array if provided with empty component templates', () => { + const componentTemplateNames = [] as string[]; + const indexTemplates = [] as IndicesGetIndexTemplateIndexTemplateItem[]; + + const result = _filterComponentTemplatesInUse({ + componentTemplateNames, + indexTemplates, + logger: mockLogger, + }); + + expect(result).toHaveLength(0); + }); + + it('should remove component template used by index template ', () => { + const componentTemplateNames = ['ctmp1', 'ctmp2'] as string[]; + const indexTemplates = [ + createMockTemplate({ name: 'tmpl1', composedOf: ['ctmp1'] }), + ] as IndicesGetIndexTemplateIndexTemplateItem[]; + + const result = _filterComponentTemplatesInUse({ + componentTemplateNames, + indexTemplates, + logger: mockLogger, + }); + + expect(result).toEqual(['ctmp2']); + }); + it('should remove component templates used by one index template ', () => { + const componentTemplateNames = ['ctmp1', 'ctmp2', 'ctmp3'] as string[]; + const indexTemplates = [ + createMockTemplate({ name: 'tmpl1', composedOf: ['ctmp1', 'ctmp2'] }), + ] as IndicesGetIndexTemplateIndexTemplateItem[]; + + const result = _filterComponentTemplatesInUse({ + componentTemplateNames, + indexTemplates, + logger: mockLogger, + }); + + expect(result).toEqual(['ctmp3']); + }); + it('should remove component templates used by different index templates ', () => { + const componentTemplateNames = ['ctmp1', 'ctmp2', 'ctmp3'] as string[]; + const indexTemplates = [ + createMockTemplate({ name: 'tmpl1', composedOf: ['ctmp1'] }), + createMockTemplate({ name: 'tmpl2', composedOf: ['ctmp2'] }), + ] as IndicesGetIndexTemplateIndexTemplateItem[]; + + const result = _filterComponentTemplatesInUse({ + componentTemplateNames, + indexTemplates, + logger: mockLogger, + }); + + expect(result).toEqual(['ctmp3']); + }); + it('should remove component templates used by multiple index templates ', () => { + const componentTemplateNames = ['ctmp1', 'ctmp2', 'ctmp3'] as string[]; + const indexTemplates = [ + createMockTemplate({ name: 'tmpl1', composedOf: ['ctmp1', 'ctmp2'] }), + createMockTemplate({ name: 'tmpl2', composedOf: ['ctmp2', 'ctmp1'] }), + ] as IndicesGetIndexTemplateIndexTemplateItem[]; + + const result = _filterComponentTemplatesInUse({ + componentTemplateNames, + indexTemplates, + logger: mockLogger, + }); + + expect(result).toEqual(['ctmp3']); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/remove_legacy.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/remove_legacy.ts new file mode 100644 index 0000000000000..44b9756edc448 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/remove_legacy.ts @@ -0,0 +1,161 @@ +/* + * 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 type { + ClusterComponentTemplate, + IndicesGetIndexTemplateIndexTemplateItem, +} from '@elastic/elasticsearch/lib/api/types'; +import type { ElasticsearchClient, Logger } from '@kbn/core/server'; + +import type { InstallablePackage, RegistryDataStream } from '../../../../types'; +import { getRegistryDataStreamAssetBaseName } from '..'; +const LEGACY_TEMPLATE_SUFFIXES = ['@mappings', '@settings']; + +const getComponentTemplateWithSuffix = (dataStream: RegistryDataStream, suffix: string) => { + const baseName = getRegistryDataStreamAssetBaseName(dataStream); + + return baseName + suffix; +}; + +export const _getLegacyComponentTemplatesForPackage = ( + componentTemplates: ClusterComponentTemplate[], + installablePackage: InstallablePackage +): string[] => { + const legacyNamesLookup: Set = new Set(); + + // fill a map with all possible @mappings and @settings component + // template names for fast lookup below. + installablePackage.data_streams?.forEach((ds) => { + LEGACY_TEMPLATE_SUFFIXES.forEach((suffix) => { + legacyNamesLookup.add(getComponentTemplateWithSuffix(ds, suffix)); + }); + }); + + return componentTemplates.reduce((legacyTemplates, componentTemplate) => { + if (!legacyNamesLookup.has(componentTemplate.name)) return legacyTemplates; + + if (componentTemplate.component_template._meta?.package?.name !== installablePackage.name) { + return legacyTemplates; + } + + return legacyTemplates.concat(componentTemplate.name); + }, []); +}; + +const _deleteComponentTemplates = async (params: { + templateNames: string[]; + esClient: ElasticsearchClient; + logger: Logger; +}): Promise => { + const { templateNames, esClient, logger } = params; + const deleteResults = await Promise.allSettled( + templateNames.map((name) => esClient.cluster.deleteComponentTemplate({ name })) + ); + + const errors = deleteResults.filter((r) => r.status === 'rejected') as PromiseRejectedResult[]; + + if (errors.length) { + const prettyErrors = errors.map((e) => `"${e.reason}"`).join(', '); + logger.debug( + `Encountered ${errors.length} errors deleting legacy component templates: ${prettyErrors}` + ); + } +}; + +export const _getIndexTemplatesToUsedByMap = ( + indexTemplates: IndicesGetIndexTemplateIndexTemplateItem[] +) => { + const lookupMap: Map = new Map(); + + indexTemplates.forEach(({ name: indexTemplateName, index_template: indexTemplate }) => { + const composedOf = indexTemplate?.composed_of; + + if (!composedOf) return; + + composedOf.forEach((componentTemplateName) => { + const existingEntry = lookupMap.get(componentTemplateName) || []; + + lookupMap.set(componentTemplateName, existingEntry.concat(indexTemplateName)); + }); + }); + return lookupMap; +}; + +const _getAllComponentTemplates = async (esClient: ElasticsearchClient) => { + const { component_templates: componentTemplates } = await esClient.cluster.getComponentTemplate(); + + return componentTemplates; +}; + +const _getAllIndexTemplatesWithComposedOf = async (esClient: ElasticsearchClient) => { + const { index_templates: indexTemplates } = await esClient.indices.getIndexTemplate(); + return indexTemplates.filter((tmpl) => tmpl.index_template.composed_of?.length); +}; + +export const _filterComponentTemplatesInUse = ({ + componentTemplateNames, + indexTemplates, + logger, +}: { + componentTemplateNames: string[]; + indexTemplates: IndicesGetIndexTemplateIndexTemplateItem[]; + logger: Logger; +}): string[] => { + const usedByLookup = _getIndexTemplatesToUsedByMap(indexTemplates); + + return componentTemplateNames.filter((componentTemplateName) => { + const indexTemplatesUsingComponentTemplate = usedByLookup.get(componentTemplateName); + + if (indexTemplatesUsingComponentTemplate?.length) { + const prettyTemplates = indexTemplatesUsingComponentTemplate.join(', '); + logger.debug( + `Not deleting legacy template ${componentTemplateName} as it is in use by index templates: ${prettyTemplates}` + ); + return false; + } + + return true; + }); +}; + +export const removeLegacyTemplates = async (params: { + packageInfo: InstallablePackage; + esClient: ElasticsearchClient; + logger: Logger; +}): Promise => { + const { packageInfo, esClient, logger } = params; + + const allComponentTemplates = await _getAllComponentTemplates(esClient); + + const legacyComponentTemplateNames = _getLegacyComponentTemplatesForPackage( + allComponentTemplates, + packageInfo + ); + + if (!legacyComponentTemplateNames.length) return; + + // all index templates that are composed of at least one component template + const allIndexTemplatesWithComposedOf = await _getAllIndexTemplatesWithComposedOf(esClient); + + let templatesToDelete = legacyComponentTemplateNames; + if (allIndexTemplatesWithComposedOf.length) { + // get the component templates not in use by any index templates + templatesToDelete = _filterComponentTemplatesInUse({ + componentTemplateNames: legacyComponentTemplateNames, + indexTemplates: allIndexTemplatesWithComposedOf, + logger, + }); + } + + if (!templatesToDelete.length) return; + + await _deleteComponentTemplates({ + templateNames: templatesToDelete, + esClient, + logger, + }); +}; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index b4e673b8a9da4..34ada19685f83 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -23,6 +23,7 @@ import type { InstallablePackage, InstallSource, PackageAssetReference } from '. import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import type { AssetReference, Installation, InstallType } from '../../../types'; import { installTemplates } from '../elasticsearch/template/install'; +import { removeLegacyTemplates } from '../elasticsearch/template/remove_legacy'; import { installPipelines, isTopLevelPipeline, @@ -161,6 +162,12 @@ export async function _installPackage({ savedObjectsClient ); + try { + await removeLegacyTemplates({ packageInfo, esClient, logger }); + } catch (e) { + logger.warn(`Error removing legacy templates: ${e.message}`); + } + // update current backing indices of each data stream await updateCurrentWriteIndices(esClient, logger, installedTemplates); diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 6364b1de59fc8..ef103592dfb45 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -27,6 +27,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./update_assets')); loadTestFile(require.resolve('./data_stream')); loadTestFile(require.resolve('./package_install_complete')); + loadTestFile(require.resolve('./remove_legacy_templates')); loadTestFile(require.resolve('./install_error_rollback')); loadTestFile(require.resolve('./final_pipeline')); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/remove_legacy_templates.ts b/x-pack/test/fleet_api_integration/apis/epm/remove_legacy_templates.ts new file mode 100644 index 0000000000000..53022461244b3 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/epm/remove_legacy_templates.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import fs from 'fs'; +import { promisify } from 'util'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { setupFleetAndAgents } from '../agents/services'; +const sleep = promisify(setTimeout); + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); + const esClient = getService('es'); + + const uploadPkgName = 'apache'; + const uploadPkgVersion = '0.1.4'; + + const installUploadPackage = async () => { + const buf = fs.readFileSync(testPkgArchiveZip); + await supertest + .post(`/api/fleet/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/zip') + .send(buf) + .expect(200); + }; + + const testPkgArchiveZip = path.join( + path.dirname(__filename), + '../fixtures/direct_upload_packages/apache_0.1.4.zip' + ); + + const legacyComponentTemplates = [ + { + name: 'logs-apache.access@settings', + template: { + settings: { + index: { + lifecycle: { + name: 'idontexist', + }, + }, + }, + }, + _meta: { + package: { + name: 'apache', + }, + }, + }, + { + name: 'logs-apache.access@mappings', + template: { + mappings: { + dynamic: false, + }, + }, + _meta: { + package: { + name: 'apache', + }, + }, + }, + ]; + const createLegacyComponentTemplates = async () => + Promise.all( + legacyComponentTemplates.map((tmpl) => esClient.cluster.putComponentTemplate(tmpl)) + ); + + const deleteLegacyComponentTemplates = async () => { + esClient.cluster + .deleteComponentTemplate({ name: legacyComponentTemplates.map((t) => t.name) }) + .catch((e) => {}); + }; + + const waitUntilLegacyComponentTemplatesCreated = async () => { + const legacyTemplateNames = legacyComponentTemplates.map((t) => t.name); + for (let i = 5; i > 0; i--) { + const { component_templates: ctmps } = await esClient.cluster.getComponentTemplate(); + + const createdTemplates = ctmps.filter((tmp) => legacyTemplateNames.includes(tmp.name)); + + if (createdTemplates.length === legacyTemplateNames.length) return; + + await sleep(500); + } + + throw new Error('Legacy component templates not created after 5 attempts'); + }; + const uninstallPackage = async (pkg: string, version: string) => { + await supertest.delete(`/api/fleet/epm/packages/${pkg}/${version}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('legacy component template removal', async () => { + skipIfNoDockerRegistry(providerContext); + setupFleetAndAgents(providerContext); + + afterEach(async () => { + if (!server.enabled) return; + await deleteLegacyComponentTemplates(); + await uninstallPackage(uploadPkgName, uploadPkgVersion); + }); + + after(async () => { + await esClient.indices.deleteIndexTemplate({ name: 'testtemplate' }); + }); + + it('should remove legacy component templates if not in use by index templates', async () => { + await createLegacyComponentTemplates(); + + await waitUntilLegacyComponentTemplatesCreated(); + await installUploadPackage(); + + const { component_templates: allComponentTemplates } = + await esClient.cluster.getComponentTemplate(); + const allComponentTemplateNames = allComponentTemplates.map((t) => t.name); + + expect(allComponentTemplateNames.includes('logs-apache.access@settings')).to.equal(false); + expect(allComponentTemplateNames.includes('logs-apache.access@mappings')).to.equal(false); + }); + + it('should not remove legacy component templates if in use by index templates', async () => { + await createLegacyComponentTemplates(); + + await esClient.indices.putIndexTemplate({ + name: 'testtemplate', + index_patterns: ['nonexistentindices'], + template: {}, + composed_of: ['logs-apache.access@settings', 'logs-apache.access@mappings'], + }); + + await waitUntilLegacyComponentTemplatesCreated(); + await installUploadPackage(); + + const { component_templates: allComponentTemplates } = + await esClient.cluster.getComponentTemplate(); + const allComponentTemplateNames = allComponentTemplates.map((t) => t.name); + + expect(allComponentTemplateNames.includes('logs-apache.access@settings')).to.equal(true); + expect(allComponentTemplateNames.includes('logs-apache.access@mappings')).to.equal(true); + }); + }); +} From f41dc1fcc9bc3017315da08debcf754ca420bef1 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 21 Apr 2022 10:26:40 -0500 Subject: [PATCH 07/24] Revert "[Console] Add Kibana APIs Support (#128562)" This reverts commit 502a00b025e2c8412169d3e3833ccbe48f61c76b. --- src/plugins/console/common/constants/api.ts | 1 - src/plugins/console/common/constants/index.ts | 2 +- .../console_editor/editor.test.mock.tsx | 4 +- .../legacy/console_editor/editor.test.tsx | 8 +-- .../editor/legacy/console_editor/editor.tsx | 10 +-- .../console_editor/keyboard_shortcuts.ts | 10 ++- .../console/public/application/hooks/index.ts | 2 +- .../index.ts | 2 +- .../send_request_to_es.test.ts} | 49 +++++++------ .../send_request_to_es.ts} | 72 ++++++++++--------- .../track.ts | 0 .../use_send_current_request_to_es.test.tsx} | 28 ++++---- .../use_send_current_request_to_es.ts} | 6 +- .../models/sense_editor/sense_editor.test.js | 6 +- .../models/sense_editor/sense_editor.ts | 21 +++--- .../public/application/stores/request.ts | 10 +-- src/plugins/console/public/lib/es/es.ts | 49 +++---------- src/plugins/console/public/lib/es/index.ts | 2 +- test/functional/apps/console/_autocomplete.ts | 2 - test/functional/apps/console/_console.ts | 16 ----- test/functional/page_objects/console_page.ts | 1 + 21 files changed, 129 insertions(+), 172 deletions(-) rename src/plugins/console/public/application/hooks/{use_send_current_request => use_send_current_request_to_es}/index.ts (81%) rename src/plugins/console/public/application/hooks/{use_send_current_request/send_request.test.ts => use_send_current_request_to_es/send_request_to_es.test.ts} (66%) rename src/plugins/console/public/application/hooks/{use_send_current_request/send_request.ts => use_send_current_request_to_es/send_request_to_es.ts} (69%) rename src/plugins/console/public/application/hooks/{use_send_current_request => use_send_current_request_to_es}/track.ts (100%) rename src/plugins/console/public/application/hooks/{use_send_current_request/use_send_current_request.test.tsx => use_send_current_request_to_es/use_send_current_request_to_es.test.tsx} (80%) rename src/plugins/console/public/application/hooks/{use_send_current_request/use_send_current_request.ts => use_send_current_request_to_es/use_send_current_request_to_es.ts} (96%) diff --git a/src/plugins/console/common/constants/api.ts b/src/plugins/console/common/constants/api.ts index 3c5aa11519350..aa0fad1fe4424 100644 --- a/src/plugins/console/common/constants/api.ts +++ b/src/plugins/console/common/constants/api.ts @@ -7,4 +7,3 @@ */ export const API_BASE_PATH = '/api/console'; -export const KIBANA_API_PREFIX = 'kbn:'; diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts index 756a79883cbdb..d8768af8fc8d8 100644 --- a/src/plugins/console/common/constants/index.ts +++ b/src/plugins/console/common/constants/index.ts @@ -7,4 +7,4 @@ */ export { MAJOR_VERSION } from './plugin'; -export { API_BASE_PATH, KIBANA_API_PREFIX } from './api'; +export { API_BASE_PATH } from './api'; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx index b410e240151d7..dfed86a643627 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx @@ -39,8 +39,8 @@ jest.mock('../../../../models/sense_editor', () => { }; }); -jest.mock('../../../../hooks/use_send_current_request/send_request', () => ({ - sendRequest: jest.fn(), +jest.mock('../../../../hooks/use_send_current_request_to_es/send_request_to_es', () => ({ + sendRequestToES: jest.fn(), })); jest.mock('../../../../../lib/autocomplete/get_endpoint_from_position', () => ({ getEndpointFromPosition: jest.fn(), diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx index ba5f1e78d5f01..b942a6d830217 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -25,7 +25,7 @@ import { } from '../../../../contexts'; // Mocked functions -import { sendRequest } from '../../../../hooks/use_send_current_request/send_request'; +import { sendRequestToES } from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; import { getEndpointFromPosition } from '../../../../../lib/autocomplete/get_endpoint_from_position'; import type { DevToolsSettings } from '../../../../../services'; import * as consoleMenuActions from '../console_menu_actions'; @@ -58,15 +58,15 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { sandbox.restore(); }); - it('calls send current request', async () => { + it('calls send current request to ES', async () => { (getEndpointFromPosition as jest.Mock).mockReturnValue({ patterns: [] }); - (sendRequest as jest.Mock).mockRejectedValue({}); + (sendRequestToES as jest.Mock).mockRejectedValue({}); const editor = doMount(); act(() => { editor.find('button[data-test-subj~="sendRequestButton"]').simulate('click'); }); await nextTick(); - expect(sendRequest).toBeCalledTimes(1); + expect(sendRequestToES).toBeCalledTimes(1); }); it('opens docs', () => { diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index d01a40bdd44b3..bafe9ee6ca156 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -26,7 +26,7 @@ import { ConsoleMenu } from '../../../../components'; import { useEditorReadContext, useServicesContext } from '../../../../contexts'; import { useSaveCurrentTextObject, - useSendCurrentRequest, + useSendCurrentRequestToES, useSetInputEditor, } from '../../../../hooks'; import * as senseEditor from '../../../../models/sense_editor'; @@ -72,7 +72,7 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { const { settings } = useEditorReadContext(); const setInputEditor = useSetInputEditor(); - const sendCurrentRequest = useSendCurrentRequest(); + const sendCurrentRequestToES = useSendCurrentRequestToES(); const saveCurrentTextObject = useSaveCurrentTextObject(); const editorRef = useRef(null); @@ -231,11 +231,11 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { if (!isKeyboardShortcutsDisabled) { registerCommands({ senseEditor: editorInstanceRef.current!, - sendCurrentRequest, + sendCurrentRequestToES, openDocumentation, }); } - }, [openDocumentation, settings, sendCurrentRequest]); + }, [sendCurrentRequestToES, openDocumentation, settings]); useEffect(() => { const { current: editor } = editorInstanceRef; @@ -262,7 +262,7 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { > void; + sendCurrentRequestToES: () => void; openDocumentation: () => void; } @@ -24,7 +24,11 @@ const COMMANDS = { GO_TO_LINE: 'gotoline', }; -export function registerCommands({ senseEditor, sendCurrentRequest, openDocumentation }: Actions) { +export function registerCommands({ + senseEditor, + sendCurrentRequestToES, + openDocumentation, +}: Actions) { const throttledAutoIndent = throttle(() => senseEditor.autoIndent(), 500, { leading: true, trailing: true, @@ -35,7 +39,7 @@ export function registerCommands({ senseEditor, sendCurrentRequest, openDocument keys: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, name: COMMANDS.SEND_TO_ELASTICSEARCH, fn: () => { - sendCurrentRequest(); + sendCurrentRequestToES(); }, }); diff --git a/src/plugins/console/public/application/hooks/index.ts b/src/plugins/console/public/application/hooks/index.ts index 1996330bef66b..1a9b4e5c472bf 100644 --- a/src/plugins/console/public/application/hooks/index.ts +++ b/src/plugins/console/public/application/hooks/index.ts @@ -8,6 +8,6 @@ export { useSetInputEditor } from './use_set_input_editor'; export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; -export { useSendCurrentRequest } from './use_send_current_request'; +export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; export { useSaveCurrentTextObject } from './use_save_current_text_object'; export { useDataInit } from './use_data_init'; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/index.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts similarity index 81% rename from src/plugins/console/public/application/hooks/use_send_current_request/index.ts rename to src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts index 33bdbef87f2ef..df2431f1f6f43 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/index.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { useSendCurrentRequest } from './use_send_current_request'; +export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts similarity index 66% rename from src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts rename to src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts index 60ced085c6891..8578e271f37b3 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts @@ -8,14 +8,14 @@ import type { ContextValue } from '../../contexts'; -jest.mock('./send_request', () => ({ sendRequest: jest.fn(() => Promise.resolve()) })); +jest.mock('./send_request_to_es', () => ({ sendRequestToES: jest.fn(() => Promise.resolve()) })); -import { sendRequest } from './send_request'; +import { sendRequestToES } from './send_request_to_es'; import { serviceContextMock } from '../../contexts/services_context.mock'; -const mockedSendRequest = sendRequest as jest.Mock; +const mockedSendRequestToES = sendRequestToES as jest.Mock; -describe('sendRequest', () => { +describe('sendRequestToES', () => { let mockContextValue: ContextValue; beforeEach(() => { @@ -26,8 +26,8 @@ describe('sendRequest', () => { jest.resetAllMocks(); }); - it('should send request', async () => { - mockedSendRequest.mockResolvedValue([ + it('should send request to ES', async () => { + mockedSendRequestToES.mockResolvedValue([ { response: { statusCode: 200, @@ -40,17 +40,17 @@ describe('sendRequest', () => { http: mockContextValue.services.http, requests: [{ method: 'PUT', url: 'test', data: [] }], }; - const results = await sendRequest(args); + const results = await sendRequestToES(args); const [request] = results; expect(request.response.statusCode).toEqual(200); expect(request.response.value).toContain('"acknowledged": true'); - expect(mockedSendRequest).toHaveBeenCalledWith(args); - expect(mockedSendRequest).toHaveBeenCalledTimes(1); + expect(mockedSendRequestToES).toHaveBeenCalledWith(args); + expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); }); - it('should send multiple requests', async () => { - mockedSendRequest.mockResolvedValue([ + it('should send multiple requests to ES', async () => { + mockedSendRequestToES.mockResolvedValue([ { response: { statusCode: 200, @@ -70,17 +70,17 @@ describe('sendRequest', () => { { method: 'GET', url: 'test-2', data: [] }, ], }; - const results = await sendRequest(args); + const results = await sendRequestToES(args); const [firstRequest, secondRequest] = results; expect(firstRequest.response.statusCode).toEqual(200); expect(secondRequest.response.statusCode).toEqual(200); - expect(mockedSendRequest).toHaveBeenCalledWith(args); - expect(mockedSendRequest).toHaveBeenCalledTimes(1); + expect(mockedSendRequestToES).toHaveBeenCalledWith(args); + expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); }); it('should handle errors', async () => { - mockedSendRequest.mockRejectedValue({ + mockedSendRequestToES.mockRejectedValue({ response: { statusCode: 500, statusText: 'error', @@ -88,46 +88,45 @@ describe('sendRequest', () => { }); try { - await sendRequest({ + await sendRequestToES({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test', data: [] }], }); } catch (error) { expect(error.response.statusCode).toEqual(500); expect(error.response.statusText).toEqual('error'); - expect(mockedSendRequest).toHaveBeenCalledTimes(1); + expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); } }); - describe('successful response value', () => { describe('with text', () => { it('should return value with lines separated', async () => { - mockedSendRequest.mockResolvedValue('\ntest_index-1 []\ntest_index-2 []\n'); - const response = await sendRequest({ + mockedSendRequestToES.mockResolvedValue('\ntest_index-1 [] \ntest_index-2 []\n'); + const response = await sendRequestToES({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test-1', data: [] }], }); expect(response).toMatchInlineSnapshot(` " - test_index-1 [] + test_index-1 [] test_index-2 [] " `); - expect(mockedSendRequest).toHaveBeenCalledTimes(1); + expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); }); }); describe('with parsed json', () => { it('should stringify value', async () => { - mockedSendRequest.mockResolvedValue(JSON.stringify({ test: 'some value' })); - const response = await sendRequest({ + mockedSendRequestToES.mockResolvedValue(JSON.stringify({ test: 'some value' })); + const response = await sendRequestToES({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test-2', data: [] }], }); expect(typeof response).toBe('string'); - expect(mockedSendRequest).toHaveBeenCalledTimes(1); + expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts similarity index 69% rename from src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts rename to src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index 1247f3f78aa68..451198aaf2d2b 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -15,12 +15,12 @@ import { BaseResponseType } from '../../../types'; const { collapseLiteralStrings } = XJson; -export interface RequestArgs { +export interface EsRequestArgs { http: HttpSetup; requests: Array<{ url: string; method: string; data: string[] }>; } -export interface ResponseObject { +export interface ESResponseObject { statusCode: number; statusText: string; timeMs: number; @@ -28,17 +28,17 @@ export interface ResponseObject { value: V; } -export interface RequestResult { +export interface ESRequestResult { request: { data: string; method: string; path: string }; - response: ResponseObject; + response: ESResponseObject; } let CURRENT_REQ_ID = 0; -export function sendRequest(args: RequestArgs): Promise { +export function sendRequestToES(args: EsRequestArgs): Promise { const requests = args.requests.slice(); return new Promise((resolve, reject) => { const reqId = ++CURRENT_REQ_ID; - const results: RequestResult[] = []; + const results: ESRequestResult[] = []; if (reqId !== CURRENT_REQ_ID) { return; } @@ -59,11 +59,11 @@ export function sendRequest(args: RequestArgs): Promise { return; } const req = requests.shift()!; - const path = req.url; - const method = req.method; - let data = collapseLiteralStrings(req.data.join('\n')); - if (data) { - data += '\n'; + const esPath = req.url; + const esMethod = req.method; + let esData = collapseLiteralStrings(req.data.join('\n')); + if (esData) { + esData += '\n'; } // append a new line for bulk requests. const startTime = Date.now(); @@ -71,9 +71,9 @@ export function sendRequest(args: RequestArgs): Promise { try { const { response, body } = await es.send({ http: args.http, - method, - path, - data, + method: esMethod, + path: esPath, + data: esData, asResponse: true, }); @@ -115,9 +115,9 @@ export function sendRequest(args: RequestArgs): Promise { value, }, request: { - data, - method, - path, + data: esData, + method: esMethod, + path: esPath, }, }); @@ -127,19 +127,25 @@ export function sendRequest(args: RequestArgs): Promise { } } catch (error) { let value; - const { response, body } = error as IHttpFetchError; - const contentType = response?.headers.get('Content-Type') ?? ''; - const statusCode = response?.status ?? 500; - const statusText = error?.response?.statusText ?? 'error'; + let contentType: string | null = ''; - if (body) { - value = JSON.stringify(body, null, 2); - } else { - value = 'Request failed to get to the server (status code: ' + statusCode + ')'; - } + const { response, body = {} } = error as IHttpFetchError; + if (response) { + const { status, headers } = response; + if (body) { + value = JSON.stringify(body, null, 2); // ES error should be shown + contentType = headers.get('Content-Type'); + } else { + value = 'Request failed to get to the server (status code: ' + status + ')'; + contentType = headers.get('Content-Type'); + } - if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; + if (isMultiRequest) { + value = '# ' + req.method + ' ' + req.url + '\n' + value; + } + } else { + value = + "\n\nFailed to connect to Console's backend.\nPlease check the Kibana server is up and running"; } reject({ @@ -147,13 +153,13 @@ export function sendRequest(args: RequestArgs): Promise { value, contentType, timeMs: Date.now() - startTime, - statusCode, - statusText, + statusCode: error?.response?.status ?? 500, + statusText: error?.response?.statusText ?? 'error', }, request: { - data, - method, - path, + data: esData, + method: esMethod, + path: esPath, }, }); } diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/track.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts similarity index 100% rename from src/plugins/console/public/application/hooks/use_send_current_request/track.ts rename to src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx similarity index 80% rename from src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx rename to src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx index d16dc3f832d3a..e0131dc116a34 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -jest.mock('./send_request', () => ({ sendRequest: jest.fn() })); +jest.mock('./send_request_to_es', () => ({ sendRequestToES: jest.fn() })); jest.mock('../../contexts/editor_context/editor_registry', () => ({ instance: { getInputEditor: jest.fn() }, })); @@ -21,10 +21,10 @@ import { serviceContextMock } from '../../contexts/services_context.mock'; import { useRequestActionContext } from '../../contexts/request_context'; import { instance as editorRegistry } from '../../contexts/editor_context/editor_registry'; -import { sendRequest } from './send_request'; -import { useSendCurrentRequest } from './use_send_current_request'; +import { sendRequestToES } from './send_request_to_es'; +import { useSendCurrentRequestToES } from './use_send_current_request_to_es'; -describe('useSendCurrentRequest', () => { +describe('useSendCurrentRequestToES', () => { let mockContextValue: ContextValue; let dispatch: (...args: unknown[]) => void; const contexts = ({ children }: { children: JSX.Element }) => ( @@ -41,18 +41,18 @@ describe('useSendCurrentRequest', () => { jest.resetAllMocks(); }); - it('calls send request', async () => { + it('calls send request to ES', async () => { // Set up mocks (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); // This request should succeed - (sendRequest as jest.Mock).mockResolvedValue([]); + (sendRequestToES as jest.Mock).mockResolvedValue([]); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); await act(() => result.current()); - expect(sendRequest).toHaveBeenCalledWith({ + expect(sendRequestToES).toHaveBeenCalledWith({ http: mockContextValue.services.http, requests: ['test'], }); @@ -64,12 +64,12 @@ describe('useSendCurrentRequest', () => { it('handles known errors', async () => { // Set up mocks - (sendRequest as jest.Mock).mockRejectedValue({ response: 'nada' }); + (sendRequestToES as jest.Mock).mockRejectedValue({ response: 'nada' }); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); await act(() => result.current()); // Second call should be the request failure const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls; @@ -80,12 +80,12 @@ describe('useSendCurrentRequest', () => { it('handles unknown errors', async () => { // Set up mocks - (sendRequest as jest.Mock).mockRejectedValue(NaN /* unexpected error value */); + (sendRequestToES as jest.Mock).mockRejectedValue(NaN /* unexpected error value */); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); await act(() => result.current()); // Second call should be the request failure const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls; @@ -100,7 +100,7 @@ describe('useSendCurrentRequest', () => { it('notifies the user about save to history errors once only', async () => { // Set up mocks - (sendRequest as jest.Mock).mockReturnValue( + (sendRequestToES as jest.Mock).mockReturnValue( [{ request: {} }, { request: {} }] /* two responses to save history */ ); (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); @@ -112,7 +112,7 @@ describe('useSendCurrentRequest', () => { getRequestsInRange: () => ['test', 'test'], })); - const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); await act(() => result.current()); expect(dispatch).toHaveBeenCalledTimes(2); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts similarity index 96% rename from src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts rename to src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts index ed08304d8d660..e7c436c9806b3 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts @@ -16,10 +16,10 @@ import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; import { instance as registry } from '../../contexts/editor_context/editor_registry'; import { useRequestActionContext, useServicesContext } from '../../contexts'; import { StorageQuotaError } from '../../components/storage_quota_error'; -import { sendRequest } from './send_request'; +import { sendRequestToES } from './send_request_to_es'; import { track } from './track'; -export const useSendCurrentRequest = () => { +export const useSendCurrentRequestToES = () => { const { services: { history, settings, notifications, trackUiMetric, http }, theme$, @@ -46,7 +46,7 @@ export const useSendCurrentRequest = () => { // Fire and forget setTimeout(() => track(requests, editor, trackUiMetric), 0); - const results = await sendRequest({ http, requests }); + const results = await sendRequestToES({ http, requests }); let saveToHistoryError: undefined | Error; const { isHistoryDisabled } = settings.toJSON(); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js index ff9d245f61275..0889b98c69388 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js @@ -455,11 +455,11 @@ describe('Editor', () => { editorInput1, { start: { lineNumber: 7 }, end: { lineNumber: 14 } }, ` -curl -XGET "http://localhost:9200/_stats?level=shards" -H "kbn-xsrf: reporting" +curl -XGET "http://localhost:9200/_stats?level=shards" #in between comment -curl -XPUT "http://localhost:9200/index_1/type1/1" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d' +curl -XPUT "http://localhost:9200/index_1/type1/1" -H 'Content-Type: application/json' -d' { "f": 1 }'`.trim() @@ -470,7 +470,7 @@ curl -XPUT "http://localhost:9200/index_1/type1/1" -H "kbn-xsrf: reporting" -H " editorInput1, { start: { lineNumber: 29 }, end: { lineNumber: 33 } }, ` -curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d' +curl -XPOST "http://localhost:9200/_sql?format=txt" -H 'Content-Type: application/json' -d' { "query": "SELECT prenom FROM claude_index WHERE prenom = '\\''claude'\\'' ", "fetch_size": 1 diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index ac65afce2c18a..5e8ca35f287b7 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -14,7 +14,7 @@ import RowParser from '../../../lib/row_parser'; import * as utils from '../../../lib/utils'; // @ts-ignore -import { constructUrl } from '../../../lib/es/es'; +import * as es from '../../../lib/es/es'; import { CoreEditor, Position, Range } from '../../../types'; import { createTokenIterator } from '../../factories'; @@ -467,22 +467,21 @@ export class SenseEditor { return req; } - const path = req.url; - const method = req.method; - const data = req.data; + const esPath = req.url; + const esMethod = req.method; + const esData = req.data; // this is the first url defined in elasticsearch.hosts - const url = constructUrl(elasticsearchBaseUrl, path); + const url = es.constructESUrl(elasticsearchBaseUrl, esPath); - // Append 'kbn-xsrf' header to bypass (XSRF/CSRF) protections - let ret = `curl -X${method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`; - if (data && data.length) { - ret += ` -H "Content-Type: application/json" -d'\n`; - const dataAsString = collapseLiteralStrings(data.join('\n')); + let ret = 'curl -X' + esMethod + ' "' + url + '"'; + if (esData && esData.length) { + ret += " -H 'Content-Type: application/json' -d'\n"; + const dataAsString = collapseLiteralStrings(esData.join('\n')); // We escape single quoted strings that that are wrapped in single quoted strings ret += dataAsString.replace(/'/g, "'\\''"); - if (data.length > 1) { + if (esData.length > 1) { ret += '\n'; } // end with a new line ret += "'"; diff --git a/src/plugins/console/public/application/stores/request.ts b/src/plugins/console/public/application/stores/request.ts index 8056ab5a7987f..099ab24326d31 100644 --- a/src/plugins/console/public/application/stores/request.ts +++ b/src/plugins/console/public/application/stores/request.ts @@ -10,18 +10,18 @@ import { Reducer } from 'react'; import { produce } from 'immer'; import { identity } from 'fp-ts/lib/function'; import { BaseResponseType } from '../../types/common'; -import { RequestResult } from '../hooks/use_send_current_request/send_request'; +import { ESRequestResult } from '../hooks/use_send_current_request_to_es/send_request_to_es'; export type Actions = | { type: 'sendRequest'; payload: undefined } - | { type: 'requestSuccess'; payload: { data: RequestResult[] } } - | { type: 'requestFail'; payload: RequestResult | undefined }; + | { type: 'requestSuccess'; payload: { data: ESRequestResult[] } } + | { type: 'requestFail'; payload: ESRequestResult | undefined }; export interface Store { requestInFlight: boolean; lastResult: { - data: RequestResult[] | null; - error?: RequestResult; + data: ESRequestResult[] | null; + error?: ESRequestResult; }; } diff --git a/src/plugins/console/public/lib/es/es.ts b/src/plugins/console/public/lib/es/es.ts index 10d0ad95b0496..2a4059d664e6c 100644 --- a/src/plugins/console/public/lib/es/es.ts +++ b/src/plugins/console/public/lib/es/es.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import type { HttpResponse, HttpSetup } from '@kbn/core/public'; -import { trimStart } from 'lodash'; -import { API_BASE_PATH, KIBANA_API_PREFIX } from '../../../common/constants'; +import type { HttpFetchOptions, HttpResponse, HttpSetup } from '@kbn/core/public'; +import { API_BASE_PATH } from '../../../common/constants'; const esVersion: string[] = []; @@ -21,7 +20,7 @@ export function getContentType(body: unknown) { return 'application/json'; } -interface SendConfig { +interface SendProps { http: HttpSetup; method: string; path: string; @@ -31,8 +30,6 @@ interface SendConfig { asResponse?: boolean; } -type Method = 'get' | 'post' | 'delete' | 'put' | 'patch' | 'head'; - export async function send({ http, method, @@ -41,48 +38,18 @@ export async function send({ asSystemRequest = false, withProductOrigin = false, asResponse = false, -}: SendConfig) { - const kibanaRequestUrl = getKibanaRequestUrl(path); - - if (kibanaRequestUrl) { - const httpMethod = method.toLowerCase() as Method; - const url = new URL(kibanaRequestUrl); - const { pathname, searchParams } = url; - const query = Object.fromEntries(searchParams.entries()); - const body = ['post', 'put', 'patch'].includes(httpMethod) ? data : null; - - return await http[httpMethod](pathname, { - body, - query, - asResponse, - asSystemRequest, - }); - } - - return await http.post(`${API_BASE_PATH}/proxy`, { +}: SendProps) { + const options: HttpFetchOptions = { query: { path, method, ...(withProductOrigin && { withProductOrigin }) }, body: data, asResponse, asSystemRequest, - }); -} - -function getKibanaRequestUrl(path: string) { - const isKibanaApiRequest = path.startsWith(KIBANA_API_PREFIX); - const kibanaBasePath = window.location.origin; + }; - if (isKibanaApiRequest) { - // window.location.origin is used as a Kibana public base path for sending requests in cURL commands. E.g. "Copy as cURL". - return `${kibanaBasePath}/${trimStart(path.replace(KIBANA_API_PREFIX, ''), '/')}`; - } + return await http.post(`${API_BASE_PATH}/proxy`, options); } -export function constructUrl(baseUri: string, path: string) { - const kibanaRequestUrl = getKibanaRequestUrl(path); - - if (kibanaRequestUrl) { - return kibanaRequestUrl; - } +export function constructESUrl(baseUri: string, path: string) { baseUri = baseUri.replace(/\/+$/, ''); path = path.replace(/^\/+/, ''); return baseUri + '/' + path; diff --git a/src/plugins/console/public/lib/es/index.ts b/src/plugins/console/public/lib/es/index.ts index f83893e93713e..61d34ba96ec05 100644 --- a/src/plugins/console/public/lib/es/index.ts +++ b/src/plugins/console/public/lib/es/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { send, constructUrl, getContentType, getVersion } from './es'; +export { send, constructESUrl, getContentType, getVersion } from './es'; diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index a1b441720d324..57c59793f69f6 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -27,7 +27,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should provide basic auto-complete functionality', async () => { await PageObjects.console.enterRequest(); - await PageObjects.console.pressEnter(); await PageObjects.console.enterText(`{\n\t"query": {`); await PageObjects.console.pressEnter(); await PageObjects.console.promptAutocomplete(); @@ -40,7 +39,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.console.clearTextArea(); await PageObjects.console.enterRequest(); - await PageObjects.console.pressEnter(); }); it('should add a comma after previous non empty line', async () => { await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"match": {}`); diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 52218b88be60d..367f1ccb56256 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -106,21 +106,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); - - describe('with kbn: prefix in request', () => { - before(async () => { - await PageObjects.console.clearTextArea(); - }); - it('it should send successful request to Kibana API', async () => { - const expectedResponseContains = 'default space'; - await PageObjects.console.enterRequest('\n GET kbn:/api/spaces/space'); - await PageObjects.console.clickPlay(); - await retry.try(async () => { - const actualResponse = await PageObjects.console.getResponse(); - log.debug(actualResponse); - expect(actualResponse).to.contain(expectedResponseContains); - }); - }); - }); }); } diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 7aaf842f28d14..281c49a789acf 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -102,6 +102,7 @@ export class ConsolePageObject extends FtrService { public async enterRequest(request: string = '\nGET _search') { const textArea = await this.getEditorTextArea(); await textArea.pressKeys(request); + await textArea.pressKeys(Key.ENTER); } public async enterText(text: string) { From 4e9a1d5ed80b587efbe261827d7b007fe0c44561 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 21 Apr 2022 10:35:25 -0500 Subject: [PATCH 08/24] Revert "consume shareable status dropdown from triggers_actions_ui plugin (#130656)" This reverts commit 1a1adb9e3574864c0465827f19d45ab3a8253add. --- .../public/pages/rules/components/status.tsx | 34 ++++++ .../pages/rules/components/status_context.tsx | 103 ++++++++++++++++++ .../public/pages/rules/index.tsx | 27 ++--- 3 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/rules/components/status.tsx create mode 100644 x-pack/plugins/observability/public/pages/rules/components/status_context.tsx diff --git a/x-pack/plugins/observability/public/pages/rules/components/status.tsx b/x-pack/plugins/observability/public/pages/rules/components/status.tsx new file mode 100644 index 0000000000000..612d6f8f30bdd --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/status.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import { StatusProps } from '../types'; +import { statusMap } from '../config'; +import { RULES_CHANGE_STATUS } from '../translations'; + +export function Status({ type, disabled, onClick }: StatusProps) { + const props = useMemo( + () => ({ + color: statusMap[type].color, + ...(!disabled ? { onClick } : { onClick: noop }), + ...(!disabled ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}), + ...(!disabled ? { iconOnClick: onClick } : { iconOnClick: noop }), + }), + [disabled, onClick, type] + ); + return ( + + {statusMap[type].label} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx b/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx new file mode 100644 index 0000000000000..c7bd29d85b17a --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { + EuiPopover, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { Status } from './status'; +import { RuleStatus, StatusContextProps } from '../types'; +import { statusMap } from '../config'; + +export function StatusContext({ + item, + disabled = false, + onStatusChanged, + enableRule, + disableRule, + muteRule, + unMuteRule, +}: StatusContextProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); + + let currentStatus: RuleStatus; + if (item.enabled) { + currentStatus = item.muteAll ? RuleStatus.snoozed : RuleStatus.enabled; + } else { + currentStatus = RuleStatus.disabled; + } + const popOverButton = useMemo( + () => , + [disabled, currentStatus, togglePopover] + ); + + const onContextMenuItemClick = useCallback( + async (status: RuleStatus) => { + togglePopover(); + if (currentStatus !== status) { + setIsUpdating(true); + + if (status === RuleStatus.enabled) { + await enableRule({ ...item, enabled: true }); + if (item.muteAll) { + await unMuteRule({ ...item, muteAll: false }); + } + } else if (status === RuleStatus.disabled) { + await disableRule({ ...item, enabled: false }); + } else if (status === RuleStatus.snoozed) { + await muteRule({ ...item, muteAll: true }); + } + setIsUpdating(false); + onStatusChanged(status); + } + }, + [ + item, + togglePopover, + enableRule, + disableRule, + muteRule, + unMuteRule, + currentStatus, + onStatusChanged, + ] + ); + + const panelItems = useMemo( + () => + Object.values(RuleStatus).map((status: RuleStatus) => ( + onContextMenuItemClick(status)} + disabled={status === RuleStatus.snoozed && currentStatus === RuleStatus.disabled} + > + {statusMap[status].label} + + )), + [currentStatus, onContextMenuItemClick] + ); + + return isUpdating ? ( + + ) : ( + setIsPopoverOpen(false)} + anchorPosition="downLeft" + isOpen={isPopoverOpen} + panelPaddingSize="none" + > + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index 60e74907a33f4..b161625379fd8 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -25,9 +25,9 @@ import { RuleTableItem, enableRule, disableRule, - snoozeRule, + muteRule, useLoadRuleTypes, - unsnoozeRule, + unmuteRule, } from '@kbn/triggers-actions-ui-plugin/public'; import { RuleExecutionStatus, ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -38,6 +38,7 @@ import { RulesTable } from './components/rules_table'; import { Name } from './components/name'; import { LastResponseFilter } from './components/last_response_filter'; import { TypeFilter } from './components/type_filter'; +import { StatusContext } from './components/status_context'; import { ExecutionStatus } from './components/execution_status'; import { LastRun } from './components/last_run'; import { EditRuleFlyout } from './components/edit_rule_flyout'; @@ -192,17 +193,17 @@ export function RulesPage() { name: STATUS_COLUMN_TITLE, sortable: true, render: (_enabled: boolean, item: RuleTableItem) => { - return triggersActionsUi.getRuleStatusDropdown({ - rule: item, - enableRule: async () => await enableRule({ http, id: item.id }), - disableRule: async () => await disableRule({ http, id: item.id }), - onRuleChanged: () => reload(), - isEditable: item.isEditable && isRuleTypeEditableInContext(item.ruleTypeId), - snoozeRule: async (snoozeEndTime: string | -1, interval: string | null) => { - await snoozeRule({ http, id: item.id, snoozeEndTime }); - }, - unsnoozeRule: async () => await unsnoozeRule({ http, id: item.id }), - }); + return ( + reload()} + enableRule={async () => await enableRule({ http, id: item.id })} + disableRule={async () => await disableRule({ http, id: item.id })} + muteRule={async () => await muteRule({ http, id: item.id })} + unMuteRule={async () => await unmuteRule({ http, id: item.id })} + /> + ); }, }, { From 0923ff6d6c48e043c9943bfd3b098743fd44c67d Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 21 Apr 2022 08:59:15 -0700 Subject: [PATCH 09/24] [DOCS] Fix section titles in Cases APIs (#130737) --- docs/api/cases/cases-api-add-comment.asciidoc | 13 ++++++------- docs/api/cases/cases-api-create.asciidoc | 16 ++++++++-------- docs/api/cases/cases-api-delete-cases.asciidoc | 12 ++++++------ .../cases/cases-api-delete-comments.asciidoc | 10 +++++----- docs/api/cases/cases-api-find-cases.asciidoc | 12 ++++++------ .../cases/cases-api-find-connectors.asciidoc | 10 +++++----- docs/api/cases/cases-api-get-alerts.asciidoc | 2 +- .../cases/cases-api-get-case-activity.asciidoc | 10 +++++----- docs/api/cases/cases-api-get-case.asciidoc | 13 ++++++------- .../cases-api-get-cases-by-alert.asciidoc | 2 +- docs/api/cases/cases-api-get-comments.asciidoc | 10 +++++----- .../api/cases/cases-api-get-reporters.asciidoc | 10 +++++----- docs/api/cases/cases-api-get-status.asciidoc | 12 ++++++------ docs/api/cases/cases-api-get-tags.asciidoc | 12 ++++++------ docs/api/cases/cases-api-push.asciidoc | 2 +- .../cases/cases-api-set-configuration.asciidoc | 2 +- .../cases/cases-api-update-comment.asciidoc | 18 ++++++++---------- docs/api/cases/cases-api-update.asciidoc | 15 +++++++-------- 18 files changed, 88 insertions(+), 93 deletions(-) diff --git a/docs/api/cases/cases-api-add-comment.asciidoc b/docs/api/cases/cases-api-add-comment.asciidoc index 20b558a89c683..df63cc0ecd65f 100644 --- a/docs/api/cases/cases-api-add-comment.asciidoc +++ b/docs/api/cases/cases-api-add-comment.asciidoc @@ -6,21 +6,20 @@ Adds a comment to a case. -=== Request +=== {api-request-title} `POST :/api/cases//comments` `POST :/s//api/cases//comments` -=== Prerequisite +=== {api-prereq-title} You must have `all` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the case you're updating. - -=== Path parameters +=== {api-path-parms-title} ``:: (Required,string) The identifier for the case. To retrieve case IDs, use @@ -30,7 +29,7 @@ You must have `all` privileges for the *Cases* feature in the *Management*, (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Request body +=== {api-request-body-title} `alertId`:: (Required*, string) The alert identifier. It is required only when `type` is @@ -65,12 +64,12 @@ only when `type` is `alert`. preview:[] `type`:: (Required, string) The comment type, which must be `user` or `alert`. -=== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -=== Example +=== {api-examples-title} Add a comment to case ID `293f1bc0-74f6-11ea-b83a-553aecdb28b6`: diff --git a/docs/api/cases/cases-api-create.asciidoc b/docs/api/cases/cases-api-create.asciidoc index f08b69998321f..b7a97fc9cb1b2 100644 --- a/docs/api/cases/cases-api-create.asciidoc +++ b/docs/api/cases/cases-api-create.asciidoc @@ -6,26 +6,26 @@ Creates a case. -=== Request +=== {api-request-title} `POST :/api/cases` `POST :/s//api/cases` -=== Prerequisite +=== {api-prereq-title} You must have `all` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the case you're creating. -=== Path parameters +=== {api-path-parms-title} ``:: (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Request body +=== {api-request-body-title} `connector`:: (Required, object) An object that contains the connector configuration. @@ -107,8 +107,8 @@ For {swimlane} connectors, specify: `id`:: (Required, string) The identifier for the connector. To create a case without a -connector, use `none`. -//To retrieve connector IDs, use <>). +connector, use `none`. To retrieve connector IDs, use +<>. `name`:: (Required, string) The name of the connector. To create a case without a @@ -147,12 +147,12 @@ categorize cases. It can be an empty array. `title`:: (Required, string) A title for the case. -=== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -=== Example +=== {api-examples-title} [source,sh] -------------------------------------------------- diff --git a/docs/api/cases/cases-api-delete-cases.asciidoc b/docs/api/cases/cases-api-delete-cases.asciidoc index 5e4436806f14f..05e9fe3e2898c 100644 --- a/docs/api/cases/cases-api-delete-cases.asciidoc +++ b/docs/api/cases/cases-api-delete-cases.asciidoc @@ -6,26 +6,26 @@ Deletes one or more cases. -=== Request +=== {api-request-title} `DELETE :/api/cases?ids=["",""]` `DELETE :/s//api/cases?ids=["",""]` -=== Prerequisite +=== {api-prereq-title} You must have `all` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're deleting. -=== Path parameters +=== {api-path-parms-title} ``:: (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Query parameters +=== {api-query-parms-title} `ids`:: (Required, string) The cases that you want to remove. To retrieve case IDs, use @@ -33,12 +33,12 @@ default space is used. + NOTE: All non-ASCII characters must be URL encoded. -==== Response code +=== {api-response-codes-title} `204`:: Indicates a successful call. -=== Example +=== {api-examples-title} Delete cases with these IDs: diff --git a/docs/api/cases/cases-api-delete-comments.asciidoc b/docs/api/cases/cases-api-delete-comments.asciidoc index 0b02786e6659d..c89407fb69ab8 100644 --- a/docs/api/cases/cases-api-delete-comments.asciidoc +++ b/docs/api/cases/cases-api-delete-comments.asciidoc @@ -6,7 +6,7 @@ Deletes one or all comments from a case. -=== Request +=== {api-request-title} `DELETE :/api/cases//comments` @@ -16,14 +16,14 @@ Deletes one or all comments from a case. `DELETE :/s//api/cases//comments/` -=== Prerequisite +=== {api-prereq-title} You must have `all` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're updating. -=== Path parameters +=== {api-path-parms-title} ``:: (Required, string) The identifier for the case. To retrieve case IDs, use @@ -38,12 +38,12 @@ comments are deleted. (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Response code +=== {api-response-codes-title} `204`:: Indicates a successful call. -=== Example +=== {api-examples-title} Delete all comments from case ID `9c235210-6834-11ea-a78c-6ffb38a34414`: diff --git a/docs/api/cases/cases-api-find-cases.asciidoc b/docs/api/cases/cases-api-find-cases.asciidoc index b6e87ad502d21..abd4e186ff706 100644 --- a/docs/api/cases/cases-api-find-cases.asciidoc +++ b/docs/api/cases/cases-api-find-cases.asciidoc @@ -6,26 +6,26 @@ Retrieves a paginated subset of cases. -=== Request +=== {api-request-title} `GET :/api/cases/_find` `GET :/s//api/cases/_find` -=== Prerequisite +=== {api-prereq-title} You must have `read` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're seeking. -=== Path parameters +=== {api-path-parms-title} ``:: (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Query parameters +=== {api-query-parms-title} `defaultSearchOperator`:: (Optional, string) The default operator to use for the `simple_query_string`. @@ -84,12 +84,12 @@ Defaults to `desc`. `to`:: (Optional, string) Returns only cases that were created before a specific date. The date must be specified as a <> data range or date match expression. preview:[] -=== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -=== Example +=== {api-examples-title} Retrieve the first five cases with the `phishing` tag, in ascending order by last update time: diff --git a/docs/api/cases/cases-api-find-connectors.asciidoc b/docs/api/cases/cases-api-find-connectors.asciidoc index 8643d569c980b..0a1554cfde4f8 100644 --- a/docs/api/cases/cases-api-find-connectors.asciidoc +++ b/docs/api/cases/cases-api-find-connectors.asciidoc @@ -10,30 +10,30 @@ In particular, only the connectors that are supported for use in cases are returned. Refer to the list of supported external incident management systems in <>. -=== Request +=== {api-request-title} `GET :/api/cases/configure/connectors/_find` `GET :/s//api/cases/configure/connectors/_find` -=== Prerequisite +=== {api-prereq-title} You must have `read` privileges for the *Actions and Connectors* feature in the *Management* section of the <>. -=== Path parameters +=== {api-path-parms-title} ``:: (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -=== Example +=== {api-examples-title} [source,sh] -------------------------------------------------- diff --git a/docs/api/cases/cases-api-get-alerts.asciidoc b/docs/api/cases/cases-api-get-alerts.asciidoc index 62bca2d38ae8f..7df81f3a8974c 100644 --- a/docs/api/cases/cases-api-get-alerts.asciidoc +++ b/docs/api/cases/cases-api-get-alerts.asciidoc @@ -36,7 +36,7 @@ default space is used. `200`:: Indicates a successful call. -=== {api-example-title} +=== {api-examples-title} Return all alerts attached to case `293f1bc0-74f6-11ea-b83a-553aecdb28b6`: diff --git a/docs/api/cases/cases-api-get-case-activity.asciidoc b/docs/api/cases/cases-api-get-case-activity.asciidoc index 92b16b7862462..25d102dc11ee7 100644 --- a/docs/api/cases/cases-api-get-case-activity.asciidoc +++ b/docs/api/cases/cases-api-get-case-activity.asciidoc @@ -8,20 +8,20 @@ Returns all user activity for the specified case. deprecated::[8.1.0] -=== Request +=== {api-request-title} `GET :/api/cases//user_actions` `GET :/s//api/cases//user_actions` -=== Prerequisite +=== {api-prereq-title} You must have `read` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're seeking. -=== Path parameters +=== {api-path-parms-title} ``:: (Required, string) An identifier for the case to retrieve. Use @@ -31,12 +31,12 @@ You must have `read` privileges for the *Cases* feature in the *Management*, (Optional, string) An identifier for the space. If it is not specified, the default space is used. -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -==== Example +=== {api-examples-title} Gets all activity for case ID `a18b38a0-71b0-11ea-a0b2-c51ea50a58e2`: diff --git a/docs/api/cases/cases-api-get-case.asciidoc b/docs/api/cases/cases-api-get-case.asciidoc index 6bd255f6f8326..5abb9ecc1903b 100644 --- a/docs/api/cases/cases-api-get-case.asciidoc +++ b/docs/api/cases/cases-api-get-case.asciidoc @@ -6,20 +6,20 @@ Returns a specified case. -=== Request +=== {api-request-title} `GET :/api/cases/` `GET :/s//api/cases/` -=== Prerequisite +=== {api-prereq-title} You must have `read` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're seeking. -=== Path parameters +=== {api-path-parms-title} ``:: (Required, string) An identifier for the case to retrieve. Use @@ -29,19 +29,18 @@ You must have `read` privileges for the *Cases* feature in the *Management*, (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Query parameters +=== {api-query-parms-title} `includeComments`:: (Optional, boolean) Determines whether case comments are returned. Defaults to `true`. deprecated:[8.1.0, "The `includeComments` query parameter is deprecated and will be removed in a future release."] - -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -==== Example +=== {api-examples-title} Returns case ID `a18b38a0-71b0-11ea-a0b2-c51ea50a58e2` without comments: diff --git a/docs/api/cases/cases-api-get-cases-by-alert.asciidoc b/docs/api/cases/cases-api-get-cases-by-alert.asciidoc index fb1f7c625c5fc..3bd2e8debb3cd 100644 --- a/docs/api/cases/cases-api-get-cases-by-alert.asciidoc +++ b/docs/api/cases/cases-api-get-cases-by-alert.asciidoc @@ -43,7 +43,7 @@ cases that the user has access to read. `200`:: Indicates a successful call. -=== {api-example-title} +=== {api-examples-title} Return cases associated with the alert ID `09f0c261e39e36351d75995b78bb83673774d1bc2cca9df2d15f0e5c0a99a540`: diff --git a/docs/api/cases/cases-api-get-comments.asciidoc b/docs/api/cases/cases-api-get-comments.asciidoc index 6e88b6ffdf004..103731cd04dd7 100644 --- a/docs/api/cases/cases-api-get-comments.asciidoc +++ b/docs/api/cases/cases-api-get-comments.asciidoc @@ -6,7 +6,7 @@ Gets a comment or all comments for a case. -=== Request +=== {api-request-title} `GET :/api/cases//comments/` @@ -16,14 +16,14 @@ Gets a comment or all comments for a case. `GET :/s//api/cases//comments` deprecated:[8.1.0] -=== Prerequisite +=== {api-prereq-title} You must have `read` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases with the comments you're seeking. -=== Path parameters +=== {api-path-parms-title} ``:: (Required, string) The identifier for the case. To retrieve case IDs, use @@ -40,12 +40,12 @@ deprecated:[8.1.0,The comment identifier will no longer be optional.] (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -=== Example +=== {api-examples-title} Retrieves comment ID `71ec1870-725b-11ea-a0b2-c51ea50a58e2` from case ID `a18b38a0-71b0-11ea-a0b2-c51ea50a58e2`: diff --git a/docs/api/cases/cases-api-get-reporters.asciidoc b/docs/api/cases/cases-api-get-reporters.asciidoc index eca8d3e45173f..331be71e9123a 100644 --- a/docs/api/cases/cases-api-get-reporters.asciidoc +++ b/docs/api/cases/cases-api-get-reporters.asciidoc @@ -6,32 +6,32 @@ Returns information about the users who opened cases. -=== Request +=== {api-request-title} `GET :/api/cases/reporters` `GET :/s/api/cases/reporters` -=== Prerequisite +=== {api-prereq-title} You must have `read` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're seeking. -=== Query parameters +=== {api-query-parms-title} `owner`:: (Optional, string or array of strings) A filter to limit the retrieved reporters to a specific set of applications. If this parameter is omitted, the response will contain all reporters from cases that the user has access to read. -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -==== Example +=== {api-examples-title} Returns all case reporters: diff --git a/docs/api/cases/cases-api-get-status.asciidoc b/docs/api/cases/cases-api-get-status.asciidoc index 62a8181feba8e..f96747dcc1116 100644 --- a/docs/api/cases/cases-api-get-status.asciidoc +++ b/docs/api/cases/cases-api-get-status.asciidoc @@ -8,26 +8,26 @@ Returns the number of cases that are open, closed, and in progress. deprecated::[8.1.0] -=== Request +=== {api-request-title} `GET :/api/cases/status` `GET :/s//api/cases/status` -=== Prerequisite +=== {api-prereq-title} You must have `read` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're seeking. -=== Path parameters +=== {api-path-parms-title} :: (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Query parameters +=== {api-query-parms-title} `owner`:: (Optional, string or array of strings) A filter to limit the retrieved case @@ -35,12 +35,12 @@ statistics to a specific set of applications. Valid values are: `cases`, `observability`, and `securitySolution`. If this parameter is omitted, the response contains all cases that the user has access to read. -=== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -=== Example +=== {api-examples-title} [source,sh] -------------------------------------------------- diff --git a/docs/api/cases/cases-api-get-tags.asciidoc b/docs/api/cases/cases-api-get-tags.asciidoc index 44d2bf9fffd1f..5008411f37bdb 100644 --- a/docs/api/cases/cases-api-get-tags.asciidoc +++ b/docs/api/cases/cases-api-get-tags.asciidoc @@ -6,38 +6,38 @@ Aggregates and returns a list of case tags. -=== Request +=== {api-request-title} `GET :/api/cases/tags` `GET :/s//api/cases/tags` -=== Prerequisite +=== {api-prereq-title} You must have `read` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're seeking. -=== Path parameters +=== {api-path-parms-title} ``:: (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Query parameters +=== {api-query-parms-title} `owner`:: (Optional, string or array of strings) A filter to limit the retrieved tags to a specific set of applications. Valid values are: `cases`, `observability`, and `securitySolution`. If this parameter is omitted, the response contains tags from all cases that the user has access to read. -==== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -==== Example +=== {api-examples-title} [source,sh] -------------------------------------------------- diff --git a/docs/api/cases/cases-api-push.asciidoc b/docs/api/cases/cases-api-push.asciidoc index e837dc78ad1a4..5b3e4d7c9ef78 100644 --- a/docs/api/cases/cases-api-push.asciidoc +++ b/docs/api/cases/cases-api-push.asciidoc @@ -40,7 +40,7 @@ default space is used. `200`:: Indicates a successful call. -=== {api-example-title} +=== {api-examples-title} Push the case to an external service: diff --git a/docs/api/cases/cases-api-set-configuration.asciidoc b/docs/api/cases/cases-api-set-configuration.asciidoc index 2b0cbefc008ac..6a7a7c26c66d2 100644 --- a/docs/api/cases/cases-api-set-configuration.asciidoc +++ b/docs/api/cases/cases-api-set-configuration.asciidoc @@ -99,7 +99,7 @@ An object that contains the case settings. `200`:: Indicates a successful call. -=== {api-example-title} +=== {api-examples-title} Sets the closure type and default connector for cases in **{stack-manage-app}**: diff --git a/docs/api/cases/cases-api-update-comment.asciidoc b/docs/api/cases/cases-api-update-comment.asciidoc index 98d426cb0c86d..020fe403fa7c5 100644 --- a/docs/api/cases/cases-api-update-comment.asciidoc +++ b/docs/api/cases/cases-api-update-comment.asciidoc @@ -6,20 +6,20 @@ Updates a comment in a case. -=== Request +=== {api-request-title} `PATCH :/api/cases//comments` `PATCH :/s//api/cases//comments` -=== Prerequisite +=== {api-prereq-title} You must have `all` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the case you're updating. -=== Path parameters +=== {api-path-parms-title} ``:: The identifier for the case. To retrieve case IDs, use @@ -29,7 +29,7 @@ The identifier for the case. To retrieve case IDs, use (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Request body +=== {api-request-body-title} `alertId`:: (Required*, string) The identifier for the alert. It is required only when @@ -40,8 +40,7 @@ default space is used. `user`. `id`:: -(Required, string) The identifier for the comment. -//To retrieve comment IDs, use <>. +(Required, string) The identifier for the comment. To retrieve comment IDs, use <>. `index`:: (Required*, string) The alert index. It is required only when `type` is `alert`. @@ -75,15 +74,14 @@ The rule that is associated with the alert. It is required only when `type` is NOTE: You cannot change the comment type. `version`:: -(Required, string) The current comment version. -//To retrieve version values, use <>. +(Required, string) The current comment version. To retrieve version values, use <>. -=== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -=== Example +=== {api-examples-title} Update comment ID `8af6ac20-74f6-11ea-b83a-553aecdb28b6` (associated with case ID `293f1bc0-74f6-11ea-b83a-553aecdb28b6`): diff --git a/docs/api/cases/cases-api-update.asciidoc b/docs/api/cases/cases-api-update.asciidoc index 522300591d8b7..7a63d0e8a6a33 100644 --- a/docs/api/cases/cases-api-update.asciidoc +++ b/docs/api/cases/cases-api-update.asciidoc @@ -6,26 +6,26 @@ Updates one or more cases. -=== Request +=== {api-request-title} `PATCH :/api/cases` `PATCH :/s//api/cases` -=== Prerequisite +=== {api-prereq-title} You must have `all` privileges for the *Cases* feature in the *Management*, *{observability}*, or *Security* section of the <>, depending on the `owner` of the cases you're updating. -=== Path parameters +=== {api-path-parms-title} ``:: (Optional, string) An identifier for the space. If it is not specified, the default space is used. -=== Request body +=== {api-request-body-title} `cases`:: (Required, array of objects) Array containing one or more case objects. @@ -114,8 +114,7 @@ For {swimlane} connectors, specify: `id`:: (Required, string) The identifier for the connector. To remove the connector, -use `none`. -//To retrieve connector IDs, use <>). +use `none`. To retrieve connector IDs, use <>). `name`:: (Required, string) The name of the connector. To remove the connector, use @@ -159,12 +158,12 @@ and `open`. (Required, string) The current version of the case. To determine this value, use <> or <>. ==== -=== Response code +=== {api-response-codes-title} `200`:: Indicates a successful call. -=== Example +=== {api-examples-title} Update the description, tags, and connector of case ID `a18b38a0-71b0-11ea-a0b2-c51ea50a58e2`: From 8609411f8eb5e8878a97f1824f22ff1544a1a586 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 21 Apr 2022 09:17:23 -0700 Subject: [PATCH 10/24] [DOCS] Add trained models to ML sync API (#130626) --- docs/api/machine-learning/sync.asciidoc | 42 +++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/api/machine-learning/sync.asciidoc b/docs/api/machine-learning/sync.asciidoc index 5f19bc17ab2fb..ba7e98a12dde6 100644 --- a/docs/api/machine-learning/sync.asciidoc +++ b/docs/api/machine-learning/sync.asciidoc @@ -4,32 +4,42 @@ Sync {ml} saved objects ++++ -Synchronizes {kib} saved objects for {ml} jobs. +Synchronizes {kib} saved objects for {ml} jobs and trained models. [[machine-learning-api-sync-request]] -==== Request +==== {api-request-title} `GET :/api/ml/saved_objects/sync` `GET :/s//api/ml/saved_objects/sync` +[[machine-learning-api-sync-prereq]] +==== {api-prereq-title} + +You must have `all` privileges for the *Machine Learning* feature in the *Analytics* section of the +<>. + +[[machine-learning-api-sync-desc]] +==== {api-description-title} + +This API runs automatically when you start {kib} and periodically thereafter. [[machine-learning-api-sync-path-params]] -==== Path parameters +==== {api-path-parms-title} `space_id`:: (Optional, string) An identifier for the space. If `space_id` is not provided in the URL the default space is used. [[machine-learning-api-sync-query-params]] -==== Query parameters +==== {api-query-parms-title} `simulate`:: (Optional, boolean) When `true`, simulates the synchronization by only returning the list actions that _would_ be performed. [[machine-learning-api-sync-response-body]] -==== Response body +==== {api-response-body-title} `datafeedsAdded`:: (array) If a saved object for an {anomaly-job} is missing a {dfeed} identifier, @@ -42,23 +52,23 @@ deleted. This list contains the {dfeed} identifiers and indicates whether the synchronization was successful. `savedObjectsCreated`:: -(array) If saved objects are missing for {ml} jobs, they are created. This -list contains the job identifiers and indicates whether the synchronization was -successful. +(array) If saved objects are missing for {ml} jobs or trained models, they are +created. This list contains the job and model identifiers and indicates whether +the synchronization was successful. `savedObjectsDeleted`:: -(array) If saved objects exist for jobs that no longer exist, they are deleted. -This list contains the job identifiers and indicates whether the synchronization -was successful. +(array) If saved objects exist for {ml} jobs or trained models that no longer +exist, they are deleted. This list contains the job and model identifiers and +indicates whether the synchronization was successful. [[machine-learning-api-sync-codes]] -==== Response code +==== {api-response-codes-title} `200`:: Indicates a successful call. [[machine-learning-api-sync-example]] -==== Example +==== {api-examples-title} Retrieve the list of {ml} saved objects that require synchronization: @@ -68,12 +78,12 @@ $ curl -X GET api/ml/saved_objects/sync?simulate=true -------------------------------------------------- // KIBANA -If there are two jobs and a {dfeed} that need to be synchronized, for example, -the API returns the following: +If there are two jobs that need to be synchronized, for example, the API returns +the following response: [source,sh] -------------------------------------------------- -{{"savedObjectsCreated":{"myjob1":{"success":true},"myjob2":{"success":true}},"savedObjectsDeleted":{},"datafeedsAdded":{},"datafeedsRemoved":{"myfeed3":{"success":true}}} +{{"savedObjectsCreated":{"anomaly_detector":{"myjob1":{"success":true},"myjob2":{"success":true}}},"savedObjectsDeleted":{},"datafeedsAdded":{},"datafeedsRemoved":{}} -------------------------------------------------- To perform the synchronization, re-run the API and omit the `simulate` parameter. \ No newline at end of file From 9ce154678ff15019d3376e370892b5135332445f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 21 Apr 2022 13:11:05 -0400 Subject: [PATCH 11/24] Attempt fixing flaky recovery context test by adding missing promises (#130500) * Attempt fixing flaky test by adding missing promises * Try something different * Try something else again Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../index_threshold/alert.ts | 46 ++++++++++--------- .../index_threshold/create_test_data.ts | 5 +- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index 0c27868e3fbb6..e7e750a92b12c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -35,8 +35,7 @@ export default function ruleTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); - // Failing: See https://github.com/elastic/kibana/issues/126949 - describe.skip('rule', async () => { + describe('rule', async () => { let endDate: string; let connectorId: string; const objectRemover = new ObjectRemover(supertest); @@ -55,7 +54,7 @@ export default function ruleTests({ getService }: FtrProviderContext) { endDate = new Date(endDateMillis).toISOString(); // write documents from now to the future end date in 3 groups - createEsDocumentsInGroups(3); + await createEsDocumentsInGroups(3); }); afterEach(async () => { @@ -102,9 +101,6 @@ export default function ruleTests({ getService }: FtrProviderContext) { }); it('runs correctly: count grouped <= =>', async () => { - // create some more documents in the first group - createEsDocumentsInGroups(1); - await createRule({ name: 'never fire', aggType: 'count', @@ -125,6 +121,9 @@ export default function ruleTests({ getService }: FtrProviderContext) { threshold: [0], }); + // create some more documents in the first group + await createEsDocumentsInGroups(1); + const docs = await waitForDocs(4); let inGroup0 = 0; @@ -146,9 +145,6 @@ export default function ruleTests({ getService }: FtrProviderContext) { }); it('runs correctly: sum all between', async () => { - // create some more documents in the first group - createEsDocumentsInGroups(1); - await createRule({ name: 'never fire', aggType: 'sum', @@ -167,6 +163,9 @@ export default function ruleTests({ getService }: FtrProviderContext) { threshold: [0, 1000000], }); + // create some more documents in the first group + await createEsDocumentsInGroups(1); + const docs = await waitForDocs(2); for (const doc of docs) { const { name, message } = doc._source.params; @@ -180,9 +179,6 @@ export default function ruleTests({ getService }: FtrProviderContext) { }); it('runs correctly: avg all', async () => { - // create some more documents in the first group - createEsDocumentsInGroups(1); - // this never fires because of bad fields error await createRule({ name: 'never fire', @@ -203,6 +199,9 @@ export default function ruleTests({ getService }: FtrProviderContext) { threshold: [0], }); + // create some more documents in the first group + await createEsDocumentsInGroups(1); + const docs = await waitForDocs(4); for (const doc of docs) { const { name, message } = doc._source.params; @@ -216,9 +215,6 @@ export default function ruleTests({ getService }: FtrProviderContext) { }); it('runs correctly: max grouped', async () => { - // create some more documents in the first group - createEsDocumentsInGroups(1); - await createRule({ name: 'never fire', aggType: 'max', @@ -241,6 +237,9 @@ export default function ruleTests({ getService }: FtrProviderContext) { threshold: [0], }); + // create some more documents in the first group + await createEsDocumentsInGroups(1); + const docs = await waitForDocs(4); let inGroup2 = 0; @@ -262,9 +261,6 @@ export default function ruleTests({ getService }: FtrProviderContext) { }); it('runs correctly: min grouped', async () => { - // create some more documents in the first group - createEsDocumentsInGroups(1); - await createRule({ name: 'never fire', aggType: 'min', @@ -287,6 +283,9 @@ export default function ruleTests({ getService }: FtrProviderContext) { threshold: [0], }); + // create some more documents in the first group + await createEsDocumentsInGroups(1); + const docs = await waitForDocs(4); let inGroup0 = 0; @@ -316,9 +315,11 @@ export default function ruleTests({ getService }: FtrProviderContext) { groupBy: 'all', thresholdComparator: '<', threshold: [10], - timeWindowSize: 60, + notifyWhen: 'onActionGroupChange', }); + await waitForDocs(1); + await createEsDocumentsInGroups(1); const docs = await waitForDocs(2); @@ -334,7 +335,7 @@ export default function ruleTests({ getService }: FtrProviderContext) { expect(activeGroup).to.be('all documents'); expect(activeTitle).to.be('alert fire then recovers group all documents met threshold'); expect(activeMessage).to.match( - /alert 'fire then recovers' is active for group \'all documents\':\n\n- Value: \d+\n- Conditions Met: count is less than 10 over 60s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ + /alert 'fire then recovers' is active for group \'all documents\':\n\n- Value: \d+\n- Conditions Met: count is less than 10 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ); const recoveredDoc = docs[1]; @@ -349,7 +350,7 @@ export default function ruleTests({ getService }: FtrProviderContext) { expect(recoveredGroup).to.be('all documents'); expect(recoveredTitle).to.be('alert fire then recovers group all documents recovered'); expect(recoveredMessage).to.match( - /alert 'fire then recovers' is recovered for group \'all documents\':\n\n- Value: \d+\n- Conditions Met: count is NOT less than 10 over 60s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ + /alert 'fire then recovers' is recovered for group \'all documents\':\n\n- Value: \d+\n- Conditions Met: count is NOT less than 10 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/ ); }); @@ -383,6 +384,7 @@ export default function ruleTests({ getService }: FtrProviderContext) { termSize?: number; thresholdComparator: string; threshold: number[]; + notifyWhen?: string; } async function createRule(params: CreateRuleParams): Promise { @@ -441,7 +443,7 @@ export default function ruleTests({ getService }: FtrProviderContext) { rule_type_id: RULE_TYPE_ID, schedule: { interval: `${RULE_INTERVAL_SECONDS}s` }, actions: [action, recoveryAction], - notify_when: 'onActiveAlert', + notify_when: params.notifyWhen || 'onActiveAlert', params: { index: ES_TEST_INDEX_NAME, timeField: params.timeField || 'date', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts index ba063044d9233..943682d185cd6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts @@ -33,6 +33,7 @@ export async function createEsDocuments( ) { const endDateMillis = Date.parse(endDate) - intervalMillis / 2; + const promises: Array> = []; times(intervals, (interval) => { const date = endDateMillis - interval * intervalMillis; @@ -41,9 +42,10 @@ export async function createEsDocuments( // don't need await on these, wait at the end of the function times(groups, (group) => { - createEsDocument(es, date, testedValue + group, `group-${group}`); + promises.push(createEsDocument(es, date, testedValue + group, `group-${group}`)); }); }); + await Promise.all(promises); const totalDocuments = intervals * groups; await esTestIndexTool.waitForDocs(DOCUMENT_SOURCE, DOCUMENT_REFERENCE, totalDocuments); @@ -67,6 +69,7 @@ async function createEsDocument( const response = await es.index({ id: uuid(), index: ES_TEST_INDEX_NAME, + refresh: 'wait_for', body: document, }); // console.log(`writing document to ${ES_TEST_INDEX_NAME}:`, JSON.stringify(document, null, 4)); From 43f03123e2d66d450a3a58c5c24dfb3e5db76826 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 21 Apr 2022 20:21:54 +0300 Subject: [PATCH 12/24] [Lens] Fixes flakiness on the visualization switcher (#130783) --- x-pack/test/functional/page_objects/lens_page.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 5e7922a9b30dc..00bee191a6368 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -720,12 +720,21 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont */ async switchToVisualization(subVisualizationId: string, searchTerm?: string) { await this.openChartSwitchPopover(); - const searchInput = await testSubjects.find('lnsChartSwitchSearch'); - await searchInput.focus(); - await this.searchOnChartSwitch(subVisualizationId, searchTerm); + await this.waitForSearchInputValue(subVisualizationId, searchTerm); await testSubjects.click(`lnsChartSwitchPopover_${subVisualizationId}`); await PageObjects.header.waitUntilLoadingHasFinished(); }, + async waitForSearchInputValue(subVisualizationId: string, searchTerm?: string) { + await retry.try(async () => { + await this.searchOnChartSwitch(subVisualizationId, searchTerm); + await PageObjects.common.sleep(1000); // give time for the value to be typed + const searchInputValue = await testSubjects.getAttribute('lnsChartSwitchSearch', 'value'); + const queryTerm = searchTerm ?? subVisualizationId.substring(subVisualizationId.length - 3); + if (searchInputValue !== queryTerm) { + throw new Error('Search input value is not the expected value'); + } + }); + }, async openChartSwitchPopover() { if (await testSubjects.exists('lnsChartSwitchList')) { From cb48e118cfad02e1419712ca37b87886ac5366d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Thu, 21 Apr 2022 20:21:19 +0200 Subject: [PATCH 13/24] [Enterprise Search] Rewire overview designs. (#130066) * Rewire overview page with new designs - Fix issues caused by VS Code - Bind add content button for empty prompt - Add content icons to assets - Add text for icons and tooltip - Add link to Elasticsearch Guide - Add context menu for create button - Add some more css changes that VS Code deleted - Add Header popover to show deployment details - Filter SVG colors * Fix some issues * Review changes * Revert some more changes * Fix i18n issues * Lint fixes for sass file * Re-fix linting * Add render tests * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Update x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.tsx Co-authored-by: Scotty Bollinger * Add tests for overview content Also made some changes to reduce the churn when we implement this feature Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Scotty Bollinger --- .../elasticsearch_card/elasticsearch_card.tsx | 2 +- .../elasticsearch_guide.tsx | 2 +- .../components/layout/index.ts | 1 + .../layout/kibana_header_actions.test.tsx | 34 ++++ .../layout/kibana_header_actions.tsx | 115 +++++++++++ .../components/overview_content/index.ts | 8 + .../overview_content.test.tsx | 73 +++++++ .../overview_content/overview_content.tsx | 159 +++++++++++++++ .../add_content_empty_prompt.scss | 7 + .../add_content_empty_prompt.test.tsx | 29 +++ .../add_content_empty_prompt.tsx | 96 ++++++++++ .../shared/add_content_empty_prompt/index.ts | 8 + .../search_indices.svg | 0 .../elasticsearch_resources.test.tsx | 47 +++++ .../elasticsearch_resources.tsx | 6 +- .../elasticsearch_resources/index.ts | 0 .../getting_started_steps.test.tsx | 56 ++++++ .../getting_started_steps.tsx | 181 ++++++++++++++++++ .../getting_started_steps/icon_row.scss | 13 ++ .../shared/getting_started_steps/icon_row.tsx | 115 +++++++++++ .../shared/getting_started_steps/index.ts | 8 + .../translations/translations/fr-FR.json | 2 - 22 files changed, 955 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/overview_content.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/overview_content.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/index.ts rename x-pack/plugins/enterprise_search/public/applications/{enterprise_search_overview/components/product_selector => shared/add_content_empty_prompt}/search_indices.svg (100%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/elasticsearch_resources.test.tsx rename x-pack/plugins/enterprise_search/public/applications/{enterprise_search_overview/components => shared}/elasticsearch_resources/elasticsearch_resources.tsx (92%) rename x-pack/plugins/enterprise_search/public/applications/{enterprise_search_overview/components => shared}/elasticsearch_resources/index.ts (100%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/getting_started_steps.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/getting_started_steps.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/icon_row.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/icon_row.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_card/elasticsearch_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_card/elasticsearch_card.tsx index 0283249ac2890..fd84267ebb8e4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_card/elasticsearch_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_card/elasticsearch_card.tsx @@ -11,9 +11,9 @@ import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle, EuiSpacer } fro import { i18n } from '@kbn/i18n'; +import { ElasticsearchResources } from '../../../shared/elasticsearch_resources'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { ELASTICSEARCH_GUIDE_PATH } from '../../routes'; -import { ElasticsearchResources } from '../elasticsearch_resources'; export const ElasticsearchCard: React.FC = () => { return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_guide/elasticsearch_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_guide/elasticsearch_guide.tsx index fb1bc7a2820e2..d722cfd637cce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_guide/elasticsearch_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_guide/elasticsearch_guide.tsx @@ -24,10 +24,10 @@ import { import { i18n } from '@kbn/i18n'; import { docLinks } from '../../../shared/doc_links'; +import { ElasticsearchResources } from '../../../shared/elasticsearch_resources'; import { ElasticsearchClientInstructions } from '../elasticsearch_client_instructions'; import { ElasticsearchCloudId } from '../elasticsearch_cloud_id'; -import { ElasticsearchResources } from '../elasticsearch_resources'; // Replace FormattedMessage with i18n strings diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/index.ts index 09a1e910fbbda..ea69823f82b87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/index.ts @@ -7,3 +7,4 @@ export { useEnterpriseSearchOverviewNav } from './nav'; export { EnterpriseSearchOverviewPageTemplate } from './page_template'; +export { EnterpriseSearchOverviewHeaderActions } from './kibana_header_actions'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.test.tsx new file mode 100644 index 0000000000000..88f5da60e3627 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { mount } from 'enzyme'; + +import { EuiPopover, EuiButtonEmpty } from '@elastic/eui'; + +import { EnterpriseSearchOverviewHeaderActions } from './kibana_header_actions'; + +describe('Enterprise Search overview HeaderActions', () => { + it('renders', () => { + const wrapper = mount(); + const popover = wrapper.find(EuiPopover); + + expect(popover.find(EuiButtonEmpty).text()).toContain('Deployment details'); + expect(popover.prop('isOpen')).toBeFalsy(); + }); + + it('opens popover when clicked', () => { + const wrapper = mount(); + + expect(wrapper.find(EuiPopover).prop('isOpen')).toBeFalsy(); + wrapper.find(EuiPopover).find(EuiButtonEmpty).simulate('click'); + wrapper.update(); + + expect(wrapper.find(EuiPopover).prop('isOpen')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.tsx new file mode 100644 index 0000000000000..0383618ecf87b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/layout/kibana_header_actions.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { + EuiHeaderLinks, + EuiIcon, + EuiCopy, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiPopover, + EuiFormRow, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiPopoverTitle, + EuiPopoverFooter, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +export const EnterpriseSearchOverviewHeaderActions: React.FC = () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + // TODO change it with actual value + // TODO make this conditional only for users on cloud, as on-prem users will not have a deployment id to show. + const clientId = 'fgdshjafghj13eshfdjag718yfhjdskf'; + + return ( + + setIsPopoverOpen(!isPopoverOpen)}> + +   + {i18n.translate('xpack.enterpriseSearch.overview.deploymentDetails', { + defaultMessage: 'Deployment details', + })} + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + > + + {i18n.translate('xpack.enterpriseSearch.overview.deploymentDetails', { + defaultMessage: 'Deployment details', + })} + + +

+ {i18n.translate('xpack.enterpriseSearch.overview.deploymentDetails.description', { + defaultMessage: + 'Send data to Elastic from your applications by referencing your deployment and Elasticsearch information.', + })} +

+
+ + + + + + + + + {(copy) => ( + + )} + + + + + + {/* TODO need link to Create and manage API keys*/} + + {i18n.translate('xpack.enterpriseSearch.overview.createAndManageButton', { + defaultMessage: 'Create and manage API keys', + })} + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/index.ts new file mode 100644 index 0000000000000..83d1f526c36cb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { OverviewContent } from './overview_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/overview_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/overview_content.test.tsx new file mode 100644 index 0000000000000..53c31eaf0498a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/overview_content.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 { setMockValues } from '../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { AddContentEmptyPrompt } from '../../../shared/add_content_empty_prompt'; +import { GettingStartedSteps } from '../../../shared/getting_started_steps'; + +import { LicenseCallout } from '../license_callout'; +import { SetupGuideCta } from '../setup_guide'; +import { TrialCallout } from '../trial_callout'; + +import { OverviewContent } from '.'; + +describe('OverviewContent', () => { + const props = { + access: {}, + isWorkplaceSearchAdmin: true, + }; + + it('renders the overview page, Getting Started steps & setup guide CTAs with no host set', () => { + setMockValues({ config: { host: '' } }); + const wrapper = shallow(); + + expect(wrapper.find(GettingStartedSteps)).toHaveLength(1); + expect(wrapper.find(AddContentEmptyPrompt)).toHaveLength(1); + expect(wrapper.find(SetupGuideCta)).toHaveLength(1); + expect(wrapper.find(LicenseCallout)).toHaveLength(0); + }); + + it('renders the trial callout', () => { + setMockValues({ config: { host: 'localhost' } }); + const wrapper = shallow(); + + expect(wrapper.find(TrialCallout)).toHaveLength(1); + }); + + // TODO Refactor this and other tests according to the search indices permissions + describe('access checks when host is set', () => { + beforeEach(() => { + setMockValues({ config: { host: 'localhost' } }); + }); + + it('renders the license callout when user has access to a product', () => { + setMockValues({ config: { host: 'localhost' } }); + const wrapper = shallow( + + ); + + expect(wrapper.find(LicenseCallout)).toHaveLength(1); + }); + + it('renders empty prompt and overview or license callout if the user does not have access', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + expect(wrapper.find(GettingStartedSteps)).toHaveLength(0); + expect(wrapper.find(AddContentEmptyPrompt)).toHaveLength(0); + expect(wrapper.find(LicenseCallout)).toHaveLength(0); + expect(wrapper.find(SetupGuideCta)).toHaveLength(0); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/overview_content.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/overview_content.tsx new file mode 100644 index 0000000000000..3824a09a0bc1c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/overview_content/overview_content.tsx @@ -0,0 +1,159 @@ +/* + * 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 { useValues } from 'kea'; + +import { + EuiButton, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiImage, + EuiLink, + EuiPageBody, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { Chat } from '@kbn/cloud-plugin/public'; +import { i18n } from '@kbn/i18n'; + +import { AddContentEmptyPrompt } from '../../../shared/add_content_empty_prompt'; +import { docLinks } from '../../../shared/doc_links'; +import { ElasticsearchResources } from '../../../shared/elasticsearch_resources'; +import { GettingStartedSteps } from '../../../shared/getting_started_steps'; +import { KibanaLogic } from '../../../shared/kibana'; +import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; + +import { EnterpriseSearchOverviewPageTemplate } from '../layout'; +import { LicenseCallout } from '../license_callout'; +import illustration from '../product_selector/lock_light.svg'; +import { SetupGuideCta } from '../setup_guide'; + +import { TrialCallout } from '../trial_callout'; + +interface OverviewContentProps { + access: { + hasAppSearchAccess?: boolean; + hasWorkplaceSearchAccess?: boolean; + }; + isWorkplaceSearchAdmin: boolean; +} + +export const OverviewContent: React.FC = ({ access }) => { + const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; + const { config } = useValues(KibanaLogic); + + // TODO Refactor this and the tests according to the search indices permissions + // If Enterprise Search has been set up and the user does not have access to either product, show a message saying they + // need to contact an administrator to get access to one of the products. + const shouldShowEnterpriseSearchOverview = + !config.host || hasAppSearchAccess || hasWorkplaceSearchAccess; + + const enterpriseSearchOverview = ( + <> + + + + + + + + + + + + + {config.host ? : } + + ); + + const insufficientAccessMessage = ( + + } + title={ +

+ {i18n.translate('xpack.enterpriseSearch.overviewContent.insufficientPermissionsTitle', { + defaultMessage: 'Insufficient permissions', + })} +

+ } + layout="horizontal" + color="plain" + body={ + <> +

+ {i18n.translate('xpack.enterpriseSearch.overviewContent.insufficientPermissionsBody', { + defaultMessage: + 'You don’t have access to view this page. If you feel this may be an error, please contact your administrator.', + })} +

+ + } + actions={ + + {i18n.translate( + 'xpack.enterpriseSearch.overviewContent.insufficientPermissionsButtonLabel', + { + defaultMessage: 'Go to the Kibana dashboard', + } + )} + + } + footer={ + <> + + + {i18n.translate( + 'xpack.enterpriseSearch.overviewContent.insufficientPermissionsFooterBody', + { + defaultMessage: 'Go to the Kibana dashboard', + } + )} + + {' '} + + {i18n.translate( + 'xpack.enterpriseSearch.overviewContent.insufficientPermissionsFooterLinkLabel', + { + defaultMessage: 'Read documentation', + } + )} + + + } + /> + ); + return ( + + + + + + {shouldShowEnterpriseSearchOverview ? enterpriseSearchOverview : insufficientAccessMessage} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.scss b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.scss new file mode 100644 index 0000000000000..c0fafb151174d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.scss @@ -0,0 +1,7 @@ +.addContentEmptyPrompt { + @include euiBreakpoint('xs','s') { + flex-direction: column-reverse; + } + + border-bottom: $euiBorderThin; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.test.tsx new file mode 100644 index 0000000000000..b9e23f00f06b5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { AddContentEmptyPrompt } from '.'; + +describe('AddContentEmptyPrompt', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders', () => { + expect(wrapper.find('h2').text()).toEqual('Add content to Enterprise Search'); + expect(wrapper.find(EuiLinkTo).prop('to')).toEqual( + '/app/enterprise_search/content/search_indices' + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.tsx new file mode 100644 index 0000000000000..0679739a7683c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/add_content_empty_prompt.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 React from 'react'; + +import { generatePath } from 'react-router-dom'; + +import { + EuiImage, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiLink, + EuiPanel, + EuiTitle, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../../../common/constants'; +import { SEARCH_INDICES_PATH } from '../../enterprise_search_content/routes'; +import { EuiLinkTo } from '../react_router_helpers'; + +import searchIndicesIllustration from './search_indices.svg'; +import './add_content_empty_prompt.scss'; + +export const AddContentEmptyPrompt: React.FC = () => { + return ( + + + + + + +

+ {i18n.translate('xpack.enterpriseSearch.overview.emptyState.heading', { + defaultMessage: 'Add content to Enterprise Search', + })} +

+
+ + +

+ {i18n.translate('xpack.enterpriseSearch.emptyState.description', { + defaultMessage: + "Data you add in Enterprise Search is called a Search index and it's searchable in both App and Workplace Search. Now you can use your connectors in App Search and your web crawlers in Workplace Search.", + })} +

+
+
+ + + + + + {i18n.translate('xpack.enterpriseSearch.overview.emptyState.buttonTitle', { + defaultMessage: 'Add content to Enterprise Search', + })} + + + + + {/* TODO need link for Learn More link*/} + + {i18n.translate('xpack.enterpriseSearch.overview.emptyState.footerLinkTitle', { + defaultMessage: 'Learn more', + })} + + + + +
+
+ + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/index.ts new file mode 100644 index 0000000000000..1ae85c30a2f5c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AddContentEmptyPrompt } from './add_content_empty_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/search_indices.svg b/x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/search_indices.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/search_indices.svg rename to x-pack/plugins/enterprise_search/public/applications/shared/add_content_empty_prompt/search_indices.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/elasticsearch_resources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/elasticsearch_resources.test.tsx new file mode 100644 index 0000000000000..3dd3b1c8ddfac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/elasticsearch_resources.test.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('../doc_links', () => ({ + docLinks: { + elasticsearchGettingStarted: 'elasticsearchGettingStarted-link', + elasticsearchCreateIndex: 'elasticsearchCreateIndex-link', + clientsGuide: 'elasticsearchClientsGuide-link', + }, +})); +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiLink } from '@elastic/eui'; + +import { ElasticsearchResources } from '.'; + +describe('ElasticsearchResources', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders', () => { + expect(wrapper.find('h4').text()).toEqual('Resources'); + + expect(wrapper.find(EuiLink).at(0).prop('href')).toEqual('elasticsearchGettingStarted-link'); + expect(wrapper.find(EuiLink).at(0).text()).toEqual('Getting started with Elasticsearch'); + + expect(wrapper.find(EuiLink).at(1).prop('href')).toEqual('elasticsearchCreateIndex-link'); + expect(wrapper.find(EuiLink).at(1).text()).toEqual('Create a new index'); + + expect(wrapper.find(EuiLink).at(2).prop('href')).toEqual('elasticsearchClientsGuide-link'); + expect(wrapper.find(EuiLink).at(2).text()).toEqual('Setup a language client'); + + expect(wrapper.find(EuiLink).at(3).prop('href')).toEqual( + 'https://github.com/elastic/search-ui/tree/master/packages/search-ui-elasticsearch-connector' + ); + expect(wrapper.find(EuiLink).at(3).text()).toEqual('Search UI for Elasticsearch'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_resources/elasticsearch_resources.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/elasticsearch_resources.tsx similarity index 92% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_resources/elasticsearch_resources.tsx rename to x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/elasticsearch_resources.tsx index b7d0390f40077..6a274e75179b1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_resources/elasticsearch_resources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/elasticsearch_resources.tsx @@ -10,10 +10,10 @@ import React from 'react'; import { EuiSpacer, EuiPanel, EuiTitle, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { docLinks } from '../../../shared/doc_links'; +import { docLinks } from '../doc_links'; export const ElasticsearchResources: React.FC = () => ( - +

{i18n.translate('xpack.enterpriseSearch.overview.elasticsearchResources.title', { @@ -40,7 +40,7 @@ export const ElasticsearchResources: React.FC = () => ( {i18n.translate( 'xpack.enterpriseSearch.overview.elasticsearchResources.elasticsearchClients', - { defaultMessage: 'Elasticsearch clients' } + { defaultMessage: 'Setup a language client' } )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_resources/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/elasticsearch_resources/index.ts rename to x-pack/plugins/enterprise_search/public/applications/shared/elasticsearch_resources/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/getting_started_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/getting_started_steps.test.tsx new file mode 100644 index 0000000000000..0595e39475b4b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/getting_started_steps.test.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('../doc_links', () => ({ + docLinks: { + elasticsearchGettingStarted: 'elasticsearchGettingStarted-link', + elasticsearchCreateIndex: 'elasticsearchCreateIndex-link', + clientsGuide: 'elasticsearchClientsGuide-link', + }, +})); +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSteps } from '@elastic/eui'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { IconRow } from './icon_row'; + +import { GettingStartedSteps } from '.'; + +describe('GettingStartedSteps', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders', () => { + const steps = wrapper + .find(EuiSteps) + .prop('steps') + .map(({ title, children, status, ...rest }) => ({ + title, + status, + children: shallow(
{children}
), + ...rest, + })); + + expect(steps[0].title).toEqual('Add your documents and data to Enterprise Search'); + expect(steps[0].status).toEqual('current'); + expect(steps[0].children.find(IconRow).length).toEqual(1); + + expect(steps[1].title).toEqual('Build a search experience'); + expect(steps[1].status).toEqual('incomplete'); + expect(steps[1].children.find(EuiLinkTo).prop('to')).toEqual('/elasticsearch_guide'); + + expect(steps[2].title).toEqual('Tune your search relevance'); + expect(steps[2].status).toEqual('incomplete'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/getting_started_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/getting_started_steps.tsx new file mode 100644 index 0000000000000..e9a3f477c7049 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/getting_started_steps.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiSpacer, + EuiSteps, + EuiText, + EuiContextMenuPanel, + EuiContextMenuItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ELASTICSEARCH_GUIDE_PATH } from '../../enterprise_search_overview/routes'; + +import { EuiLinkTo } from '../react_router_helpers'; + +import { IconRow } from './icon_row'; + +export const GettingStartedSteps: React.FC = () => { + // TODO replace with logic file + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.addData.message', + { + defaultMessage: + 'Get started by adding your data to Enterprise Search. You can use the Elastic Web Crawler, API endpoints, existing Elasticsearch indices or third party connectors like Google Drive, Microsoft Sharepoint and more.', + } + )} +

+
+ + + + ), + status: 'current', + }, + { + title: i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.buildSearchExperience.title', + { defaultMessage: 'Build a search experience' } + ), + children: ( + <> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.buildSearchExperience.message', + { + defaultMessage: + 'You can use Search Engines to build customized search experiences for your customers or internal teams with App Search or Workplace Search. Or you can use Search UI to connect directly to an Elasticsearch index to build client-side search experinces for your users.', + } + )} +

+
+ + + + setIsPopoverOpen(!isPopoverOpen)} + > + {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.createASearchEngineButton', + { defaultMessage: 'Create a search engine' } + )} + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + > + {/* TODO add onclick for these links*/} + {}}> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.createAppSearchEngine.title', + { defaultMessage: 'Create an App Search engine' } + )} +

+ {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.createAppSearchEngine.description', + { + defaultMessage: + 'All the power of Elasticsearch, without the learning curve.', + } + )} +
+ , + {}}> + +

+ {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.createWorkplaceSearchGroup.title', + { defaultMessage: 'Create a Workplace Search group' } + )} +

+ {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.createWorkplaceSearchGroup.description', + { + defaultMessage: 'A secure search experience for internal teams', + } + )} +
+
, + ]} + /> +
+
+ + + +   + {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.searchWithElasticsearchLink', + { defaultMessage: 'Search with the Elasticsearch API' } + )} + + +
+ + ), + status: 'incomplete', + }, + { + title: i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.tuneSearchExperience.title', + { defaultMessage: 'Tune your search relevance' } + ), + children: ( + +

+ {i18n.translate( + 'xpack.enterpriseSearch.overview.gettingStartedSteps.tuneSearchExperience.message', + { + defaultMessage: + "Dive into analytics and tune the result settings to help your users find exactly what they're looking for", + } + )} +

+
+ ), + status: 'incomplete', + }, + ]} + /> +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/icon_row.scss b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/icon_row.scss new file mode 100644 index 0000000000000..6992cdead2803 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/icon_row.scss @@ -0,0 +1,13 @@ +.gettingStartedSteps { + .grayscaleSvg { + filter: grayscale(1); + } + + .addManyMoreButton { + border: $euiBorderThin; + } + + .iconTooltip { + border-bottom: $euiBorderThin; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/icon_row.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/icon_row.tsx new file mode 100644 index 0000000000000..8ae50183914d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/icon_row.tsx @@ -0,0 +1,115 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiBadge, EuiToolTip, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { images } from '../../workplace_search/components/shared/assets/source_icons'; + +import './icon_row.scss'; + +const icons = [ + { + icon: 'logoElasticsearch', + title: 'Elasticsearch', + tooltip: i18n.translate('xpack.enterpriseSearch.overview.iconRow.elasticsearch.tooltip', { + defaultMessage: + 'Use App and Workplace Search Search Engines with existing Elasticsearch indices', + }), + }, + { + icon: 'desktop', + title: i18n.translate('xpack.enterpriseSearch.overview.iconRow.api.title', { + defaultMessage: 'API', + }), + tooltip: i18n.translate('xpack.enterpriseSearch.overview.iconRow.api.tooltip', { + defaultMessage: 'POST documents to an API endpoint from your own applications', + }), + }, + { + icon: 'globe', + title: i18n.translate('xpack.enterpriseSearch.overview.iconRow.crawler.title', { + defaultMessage: 'Elastic Web Crawler', + }), + tooltip: i18n.translate('xpack.enterpriseSearch.overview.iconRow.crawler.tooltip', { + defaultMessage: 'Automatically index content from your websites', + }), + }, + { + icon: images.confluence, + title: i18n.translate('xpack.enterpriseSearch.overview.iconRow.confluence.title', { + defaultMessage: 'Confluence', + }), + tooltip: i18n.translate('xpack.enterpriseSearch.overview.iconRow.confluence.tooltip', { + defaultMessage: 'Index content from Atlassian Confluence', + }), + }, + { + icon: images.googleDrive, + title: i18n.translate('xpack.enterpriseSearch.overview.iconRow.googleDrive.title', { + defaultMessage: 'Google Drive', + }), + tooltip: i18n.translate('xpack.enterpriseSearch.overview.iconRow.googleDrive.tooltip', { + defaultMessage: 'Index documents from Google Drive', + }), + }, + { + icon: images.sharePoint, + title: i18n.translate('xpack.enterpriseSearch.overview.iconRow.sharePoint.title', { + defaultMessage: 'Microsoft SharePoint', + }), + tooltip: i18n.translate('xpack.enterpriseSearch.overview.iconRow.sharePoint.tooltip', { + defaultMessage: 'Index content from Microsoft SharePoint', + }), + }, + { + icon: images.github, + title: i18n.translate('xpack.enterpriseSearch.overview.iconRow.github.title', { + defaultMessage: 'GitHub', + }), + tooltip: i18n.translate('xpack.enterpriseSearch.overview.iconRow.github.tooltip', { + defaultMessage: 'Index issues, pull requests, and more from GitHub', + }), + }, +]; + +export const IconRow: React.FC = () => { + return ( + + + + {icons.map((item, index) => { + return ( + + +

{item.title}

+ {item.tooltip} + + } + > + +
+
+ ); + })} +
+
+ + + {i18n.translate('xpack.enterpriseSearch.overview.iconRow.manyMoreBadge', { + defaultMessage: 'And many more', + })} + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/index.ts new file mode 100644 index 0000000000000..71ad56d84cb09 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/getting_started_steps/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { GettingStartedSteps } from './getting_started_steps'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 73780164049a5..32bb864b582e4 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11419,8 +11419,6 @@ "xpack.enterpriseSearch.overview.productCard.setupButton": "Configurer {productName}", "xpack.enterpriseSearch.overview.productName": "Enterprise Search", "xpack.enterpriseSearch.overview.setupCta.description": "Ajoutez des fonctions de recherche à votre application ou à votre organisation interne avec Elastic App Search et Workplace Search. Regardez la vidéo pour savoir ce qu'il est possible de faire lorsque la recherche est facilitée.", - "xpack.enterpriseSearch.overview.setupHeading": "Choisissez un produit à configurer et lancez-vous.", - "xpack.enterpriseSearch.overview.subheading": "Ajoutez une fonction de recherche à votre application ou à votre organisation.", "xpack.enterpriseSearch.productSelectorCalloutTitle": "Des fonctionnalités de niveau entreprise pour les grandes et petites équipes", "xpack.enterpriseSearch.readOnlyMode.warning": "Enterprise Search est en mode de lecture seule. Vous ne pourrez pas effectuer de changements tels que création, modification ou suppression.", "xpack.enterpriseSearch.roleMapping.addRoleMappingButtonLabel": "Ajouter un mapping", From ef3bd049793c97031cf3b0b7b0b4c935a3bec762 Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Thu, 21 Apr 2022 20:28:42 +0200 Subject: [PATCH 14/24] [Security Solution][Endpoint] Port trusted apps to `ArtifactlsListPage` component (#129208) * move validations to artifacts fixes elastic/security-team/issues/3092 * use ArtifactsListPage fixes elastic/security-team/issues/3092 * Remove redundant files fixes elastic/security-team/issues/3092 * Update trusted app list ftr test fixes elastic/security-team/issues/3092 * fix test mock fixes elastic/security-team/issues/3092 * add trusted app form tests fixes elastic/security-team/issues/3092 * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * default page index to 1 when set to 0 refs elastic/kibana/pull/129099/commits/b688d505b460964117ba1a303b417760b13a2888 * add tests for new routing methods refs elastic/kibana/pull/129099/ * review changes fixes elastic/security-team/issues/3092 * update fleet integration test without any trusted app entries, it should see the trusted empty page * update translation again after merge refs 6a792fc8d52d7107bcfce49630a892b1d6450cb6 * add a test for search field KQL review suggestions * remove redundant flyout size defaults to `m` Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/schema/trusted_apps.ts | 2 +- .../validations.ts | 0 .../public/common/store/actions.ts | 2 - .../public/management/common/routing.test.ts | 224 - .../public/management/common/routing.ts | 48 +- .../artifact_list_page_routing.test.ts | 232 + .../management/common/url_routing/utils.ts | 4 +- .../components/artifact_flyout.test.tsx | 13 +- .../components/artifact_list_page/mocks.tsx | 2 +- .../view/components/blocklist_form.tsx | 2 +- .../pages/policy/test_utils/mocks.ts | 57 +- .../policy_artifacts_empty_unassigned.tsx | 6 +- .../policy_artifacts_empty_unexisting.tsx | 6 +- .../empty/use_policy_artifacts_empty_hooks.ts | 6 +- .../layout/policy_artifacts_layout.tsx | 6 +- .../artifacts/list/policy_artifacts_list.tsx | 6 +- .../endpoint_package_custom_extension.tsx | 2 +- .../endpoint_policy_edit_extension.tsx | 2 +- .../pages/policy/view/tabs/policy_tabs.tsx | 2 +- .../management/pages/trusted_apps/index.tsx | 12 +- .../pages/trusted_apps/service/api_client.ts | 65 + .../pages/trusted_apps/service/index.ts | 2 +- .../service/trusted_apps_api_client.ts | 30 - .../service/trusted_apps_http_service.ts | 288 - .../validate_trusted_app_http_request_body.ts | 60 - .../pages/trusted_apps/state/index.ts | 9 - .../state/trusted_apps_list_page_state.ts | 72 - .../pages/trusted_apps/state/type_guards.ts | 16 - .../pages/trusted_apps/store/action.ts | 95 - .../pages/trusted_apps/store/builders.ts | 60 - .../trusted_apps/store/middleware.test.ts | 459 - .../pages/trusted_apps/store/middleware.ts | 477 - .../pages/trusted_apps/store/mocks.ts | 17 - .../pages/trusted_apps/store/reducer.test.ts | 235 - .../pages/trusted_apps/store/reducer.ts | 240 - .../trusted_apps/store/selectors.test.ts | 388 - .../pages/trusted_apps/store/selectors.ts | 277 - .../pages/trusted_apps/test_utils/index.ts | 168 - .../trusted_app_deletion_dialog.test.tsx.snap | 414 - .../components/create_trusted_app_flyout.tsx | 283 - .../components/create_trusted_app_form.tsx | 623 - ...rusted_app_form.test.tsx => form.test.tsx} | 478 +- .../trusted_apps/view/components/form.tsx | 519 + .../__snapshots__/index.test.tsx.snap | 12717 ---------------- .../trusted_apps_grid/index.stories.tsx | 99 - .../trusted_apps_grid/index.test.tsx | 150 - .../components/trusted_apps_grid/index.tsx | 210 - .../__snapshots__/index.test.tsx.snap | 53 - .../view_type_toggle/index.stories.tsx | 28 - .../view_type_toggle/index.test.tsx | 37 - .../components/view_type_toggle/index.tsx | 57 - .../pages/trusted_apps/view/hooks.ts | 53 - .../pages/trusted_apps/view/index.ts | 8 - .../pages/trusted_apps/view/translations.ts | 103 +- .../view/trusted_app_deletion_dialog.test.tsx | 127 - .../view/trusted_app_deletion_dialog.tsx | 136 - .../view/trusted_apps_list.test.tsx | 62 + .../trusted_apps/view/trusted_apps_list.tsx | 123 + .../view/trusted_apps_notifications.test.tsx | 98 - .../view/trusted_apps_notifications.tsx | 115 - .../view/trusted_apps_page.test.tsx | 885 -- .../trusted_apps/view/trusted_apps_page.tsx | 188 - .../public/management/store/middleware.ts | 7 +- .../public/management/store/reducer.ts | 5 - .../public/management/types.ts | 2 - .../validators/blocklist_validator.ts | 2 +- .../validators/trusted_app_validator.ts | 2 +- .../translations/translations/fr-FR.json | 40 +- .../translations/translations/ja-JP.json | 37 - .../translations/translations/zh-CN.json | 38 - .../apps/endpoint/fleet_integrations.ts | 2 +- .../apps/endpoint/trusted_apps_list.ts | 24 +- .../page_objects/trusted_apps_page.ts | 8 +- 73 files changed, 1464 insertions(+), 19861 deletions(-) rename x-pack/plugins/security_solution/common/endpoint/service/{trusted_apps => artifacts}/validations.ts (100%) delete mode 100644 x-pack/plugins/security_solution/public/management/common/routing.test.ts create mode 100644 x-pack/plugins/security_solution/public/management/common/url_routing/artifact_list_page_routing.test.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_http_service.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/validate_trusted_app_http_request_body.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx rename x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/{create_trusted_app_form.test.tsx => form.test.tsx} (52%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/index.ts delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index a5476159a03ac..317ee194ccc20 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; import { TrustedAppConditionEntry } from '../types'; -import { getDuplicateFields, isValidHash } from '../service/trusted_apps/validations'; +import { getDuplicateFields, isValidHash } from '../service/artifacts/validations'; export const DeleteTrustedAppsRequestSchema = { params: schema.object({ diff --git a/x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/validations.ts similarity index 100% rename from x-pack/plugins/security_solution/common/endpoint/service/trusted_apps/validations.ts rename to x-pack/plugins/security_solution/common/endpoint/service/artifacts/validations.ts diff --git a/x-pack/plugins/security_solution/public/common/store/actions.ts b/x-pack/plugins/security_solution/public/common/store/actions.ts index 1987edc0e7307..585fdb98a0323 100644 --- a/x-pack/plugins/security_solution/public/common/store/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/actions.ts @@ -7,7 +7,6 @@ import { EndpointAction } from '../../management/pages/endpoint_hosts/store/action'; import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details'; -import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store/action'; import { EventFiltersPageAction } from '../../management/pages/event_filters/store/action'; export { appActions } from './app'; @@ -20,5 +19,4 @@ export type AppAction = | EndpointAction | RoutingAction | PolicyDetailsAction - | TrustedAppsPageAction | EventFiltersPageAction; diff --git a/x-pack/plugins/security_solution/public/management/common/routing.test.ts b/x-pack/plugins/security_solution/public/management/common/routing.test.ts deleted file mode 100644 index 99afd30688983..0000000000000 --- a/x-pack/plugins/security_solution/public/management/common/routing.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { extractTrustedAppsListPageLocation, getTrustedAppsListPath } from './routing'; -import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from './constants'; -import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state'; - -describe('routing', () => { - describe('extractListPaginationParams()', () => { - it('extracts default page index when not provided', () => { - expect(extractTrustedAppsListPageLocation({}).page_index).toBe(MANAGEMENT_DEFAULT_PAGE); - }); - - it('extracts default page index when too small value provided', () => { - expect(extractTrustedAppsListPageLocation({ page_index: '-1' }).page_index).toBe( - MANAGEMENT_DEFAULT_PAGE - ); - }); - - it('extracts default page index when not a number provided', () => { - expect(extractTrustedAppsListPageLocation({ page_index: 'a' }).page_index).toBe( - MANAGEMENT_DEFAULT_PAGE - ); - }); - - it('extracts only last page index when multiple values provided', () => { - expect(extractTrustedAppsListPageLocation({ page_index: ['1', '2'] }).page_index).toBe(2); - }); - - it('extracts proper page index when single valid value provided', () => { - expect(extractTrustedAppsListPageLocation({ page_index: '2' }).page_index).toBe(2); - }); - - it('extracts default page size when not provided', () => { - expect(extractTrustedAppsListPageLocation({}).page_size).toBe(MANAGEMENT_DEFAULT_PAGE_SIZE); - }); - - it('extracts default page size when invalid option provided', () => { - expect(extractTrustedAppsListPageLocation({ page_size: '25' }).page_size).toBe( - MANAGEMENT_DEFAULT_PAGE_SIZE - ); - }); - - it('extracts default page size when not a number provided', () => { - expect(extractTrustedAppsListPageLocation({ page_size: 'a' }).page_size).toBe( - MANAGEMENT_DEFAULT_PAGE_SIZE - ); - }); - - it('extracts only last page size when multiple values provided', () => { - expect(extractTrustedAppsListPageLocation({ page_size: ['10', '20'] }).page_size).toBe(20); - }); - - it('extracts proper page size when single valid value provided', () => { - expect(extractTrustedAppsListPageLocation({ page_size: '20' }).page_size).toBe(20); - }); - - it('extracts proper "show" when single valid value provided', () => { - expect(extractTrustedAppsListPageLocation({ show: 'create' }).show).toBe('create'); - }); - - it('extracts only last "show" when multiple values provided', () => { - expect(extractTrustedAppsListPageLocation({ show: ['invalid', 'create'] }).show).toBe( - 'create' - ); - }); - - it('extracts default "show" when no value provided', () => { - expect(extractTrustedAppsListPageLocation({}).show).toBeUndefined(); - }); - - it('extracts default "show" when single invalid value provided', () => { - expect(extractTrustedAppsListPageLocation({ show: 'invalid' }).show).toBeUndefined(); - }); - - it('extracts proper view type when single valid value provided', () => { - expect(extractTrustedAppsListPageLocation({ view_type: 'list' }).view_type).toBe('list'); - }); - - it('extracts only last view type when multiple values provided', () => { - expect(extractTrustedAppsListPageLocation({ view_type: ['grid', 'list'] }).view_type).toBe( - 'list' - ); - }); - - it('extracts default view type when no value provided', () => { - expect(extractTrustedAppsListPageLocation({}).view_type).toBe('grid'); - }); - - it('extracts default view type when single invalid value provided', () => { - expect(extractTrustedAppsListPageLocation({ view_type: 'invalid' }).view_type).toBe('grid'); - }); - }); - - describe('getTrustedAppsListPath()', () => { - it('builds proper path when no parameters provided', () => { - expect(getTrustedAppsListPath()).toEqual('/administration/trusted_apps'); - }); - - it('builds proper path when empty parameters provided', () => { - expect(getTrustedAppsListPath({})).toEqual('/administration/trusted_apps'); - }); - - it('builds proper path when only page size provided', () => { - const pageSize = 20; - expect(getTrustedAppsListPath({ page_size: pageSize })).toEqual( - `/administration/trusted_apps?page_size=${pageSize}` - ); - }); - - it('builds proper path when only page index provided', () => { - const pageIndex = 2; - expect(getTrustedAppsListPath({ page_index: pageIndex })).toEqual( - `/administration/trusted_apps?page_index=${pageIndex}` - ); - }); - - it('builds proper path when only "show" provided', () => { - const show = 'create'; - expect(getTrustedAppsListPath({ show })).toEqual(`/administration/trusted_apps?show=${show}`); - }); - - it('builds proper path when only view type provided', () => { - const viewType = 'list'; - expect(getTrustedAppsListPath({ view_type: viewType })).toEqual( - `/administration/trusted_apps?view_type=${viewType}` - ); - }); - - it('builds proper path when all params provided', () => { - const location: TrustedAppsListPageLocation = { - page_index: 2, - page_size: 20, - show: 'create', - view_type: 'list', - filter: 'test', - included_policies: 'globally', - }; - - expect(getTrustedAppsListPath(location)).toEqual( - `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&view_type=${location.view_type}&show=${location.show}&filter=${location.filter}&included_policies=${location.included_policies}` - ); - }); - - it('builds proper path when page index is equal to default', () => { - const location: TrustedAppsListPageLocation = { - page_index: MANAGEMENT_DEFAULT_PAGE, - page_size: 20, - show: 'create', - view_type: 'list', - filter: '', - included_policies: '', - }; - const path = getTrustedAppsListPath(location); - - expect(path).toEqual( - `/administration/trusted_apps?page_size=${location.page_size}&view_type=${location.view_type}&show=${location.show}` - ); - }); - - it('builds proper path when page size is equal to default', () => { - const location: TrustedAppsListPageLocation = { - page_index: 2, - page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, - show: 'create', - view_type: 'list', - filter: '', - included_policies: '', - }; - const path = getTrustedAppsListPath(location); - - expect(path).toEqual( - `/administration/trusted_apps?page_index=${location.page_index}&view_type=${location.view_type}&show=${location.show}` - ); - }); - - it('builds proper path when "show" is equal to default', () => { - const location: TrustedAppsListPageLocation = { - page_index: 2, - page_size: 20, - show: undefined, - view_type: 'list', - filter: '', - included_policies: '', - }; - const path = getTrustedAppsListPath(location); - - expect(path).toEqual( - `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&view_type=${location.view_type}` - ); - }); - - it('builds proper path when view type is equal to default', () => { - const location: TrustedAppsListPageLocation = { - page_index: 2, - page_size: 20, - show: 'create', - view_type: 'grid', - filter: '', - included_policies: '', - }; - const path = getTrustedAppsListPath(location); - - expect(path).toEqual( - `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&show=${location.show}` - ); - }); - - it('builds proper path when params are equal to default', () => { - const path = getTrustedAppsListPath({ - page_index: MANAGEMENT_DEFAULT_PAGE, - page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, - show: undefined, - view_type: 'grid', - }); - - expect(path).toEqual('/administration/trusted_apps'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 40d17dc766690..828be47d9d0dd 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -16,7 +16,6 @@ import { paginationFromUrlParams } from '../components/hooks/use_url_pagination' import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types'; import { EventFiltersPageLocation } from '../pages/event_filters/types'; import { PolicyDetailsArtifactsPageLocation } from '../pages/policy/types'; -import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state'; import { AdministrationSubTab } from '../types'; import { MANAGEMENT_DEFAULT_PAGE, @@ -150,30 +149,6 @@ export const getPolicyEventFiltersPath = ( )}`; }; -const normalizeTrustedAppsPageLocation = ( - location?: Partial -): Partial => { - if (location) { - return { - ...(!isDefaultOrMissing(location.page_index, MANAGEMENT_DEFAULT_PAGE) - ? { page_index: location.page_index } - : {}), - ...(!isDefaultOrMissing(location.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE) - ? { page_size: location.page_size } - : {}), - ...(!isDefaultOrMissing(location.view_type, 'grid') ? { view_type: location.view_type } : {}), - ...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}), - ...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}), - ...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''), - ...(!isDefaultOrMissing(location.included_policies, '') - ? { included_policies: location.included_policies } - : ''), - }; - } else { - return {}; - } -}; - const normalizePolicyDetailsArtifactsListPageLocation = ( location?: Partial ): Partial => { @@ -266,31 +241,12 @@ export const extractArtifactsListPaginationParams = (query: querystring.ParsedUr included_policies: extractIncludedPolicies(query), }); -export const extractTrustedAppsListPageLocation = ( - query: querystring.ParsedUrlQuery -): TrustedAppsListPageLocation => { - const showParamValue = extractFirstParamValue( - query, - 'show' - ) as TrustedAppsListPageLocation['show']; - - return { - ...extractTrustedAppsListPaginationParams(query), - view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid', - show: - showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined, - id: extractFirstParamValue(query, 'id'), - }; -}; - -export const getTrustedAppsListPath = (location?: Partial): string => { +export const getTrustedAppsListPath = (location?: Partial): string => { const path = generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, { tabName: AdministrationSubTab.trustedApps, }); - return `${path}${appendSearch( - querystring.stringify(normalizeTrustedAppsPageLocation(location)) - )}`; + return getArtifactListPageUrlPath(path, location); }; export const extractPolicyDetailsArtifactsListPageLocation = ( diff --git a/x-pack/plugins/security_solution/public/management/common/url_routing/artifact_list_page_routing.test.ts b/x-pack/plugins/security_solution/public/management/common/url_routing/artifact_list_page_routing.test.ts new file mode 100644 index 0000000000000..efbed1e2e14fc --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/common/url_routing/artifact_list_page_routing.test.ts @@ -0,0 +1,232 @@ +/* + * 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 { ArtifactListPageUrlParams } from '../../components/artifact_list_page'; +import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../constants'; +import { + getArtifactListPageUrlPath, + extractArtifactListPageUrlSearchParams, +} from './artifact_list_page_routing'; +import { + getTrustedAppsListPath, + getBlocklistsListPath, + getHostIsolationExceptionsListPath, +} from '../routing'; + +describe('routing', () => { + describe('extractListPaginationParams()', () => { + it('extracts default page when not provided', () => { + expect(extractArtifactListPageUrlSearchParams({}).page).toBe(1); + }); + + it('extracts default page when too small value provided', () => { + expect(extractArtifactListPageUrlSearchParams({ page: '-1' }).page).toBe(1); + }); + + it('extracts default page when not a number provided', () => { + expect(extractArtifactListPageUrlSearchParams({ page: 'a' }).page).toBe(1); + }); + + it('extracts only last page when multiple values provided', () => { + expect(extractArtifactListPageUrlSearchParams({ page: ['1', '2'] }).page).toBe(2); + }); + + it('extracts proper page when single valid value provided', () => { + expect(extractArtifactListPageUrlSearchParams({ page: '2' }).page).toBe(2); + }); + + it('extracts default page size when not provided', () => { + expect(extractArtifactListPageUrlSearchParams({}).pageSize).toBe( + MANAGEMENT_DEFAULT_PAGE_SIZE + ); + }); + + it('extracts default page size when invalid option provided', () => { + expect(extractArtifactListPageUrlSearchParams({ pageSize: '25' }).pageSize).toBe( + MANAGEMENT_DEFAULT_PAGE_SIZE + ); + }); + + it('extracts default page size when not a number provided', () => { + expect(extractArtifactListPageUrlSearchParams({ pageSize: 'a' }).pageSize).toBe( + MANAGEMENT_DEFAULT_PAGE_SIZE + ); + }); + + it('extracts only last page size when multiple values provided', () => { + expect(extractArtifactListPageUrlSearchParams({ pageSize: ['10', '20'] }).pageSize).toBe(20); + }); + + it('extracts proper page size when single valid value provided', () => { + expect(extractArtifactListPageUrlSearchParams({ pageSize: '20' }).pageSize).toBe(20); + }); + + it('extracts proper "show" when single valid value provided', () => { + expect(extractArtifactListPageUrlSearchParams({ show: 'create' }).show).toBe('create'); + }); + + it('extracts only last "show" when multiple values provided', () => { + expect(extractArtifactListPageUrlSearchParams({ show: ['invalid', 'create'] }).show).toBe( + 'create' + ); + }); + + it('extracts default "show" when no value provided', () => { + expect(extractArtifactListPageUrlSearchParams({}).show).toBeUndefined(); + }); + + it('extracts default "show" when single invalid value provided', () => { + expect(extractArtifactListPageUrlSearchParams({ show: 'invalid' }).show).toBeUndefined(); + }); + }); + + describe('getArtifactListPageUrlPath()', () => { + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when no parameters provided', (path, getPath) => { + expect(getArtifactListPageUrlPath(getPath())).toEqual(`/administration/${path}`); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when only page size provided', (path, getPath) => { + const pageSize = 20; + expect(getArtifactListPageUrlPath(getPath({ pageSize }))).toEqual( + `/administration/${path}?pageSize=${pageSize}` + ); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when only page index provided', (path, getPath) => { + const pageIndex = 2; + expect(getArtifactListPageUrlPath(getPath({ page: pageIndex }))).toEqual( + `/administration/${path}?page=${pageIndex}` + ); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when only "show" provided', (path, getPath) => { + const show = 'create'; + expect(getArtifactListPageUrlPath(getPath({ show }))).toEqual( + `/administration/${path}?show=${show}` + ); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when all params provided', (path, getPath) => { + const location: ArtifactListPageUrlParams = { + page: 2, + pageSize: 20, + show: 'create', + filter: 'test', + includedPolicies: 'globally', + }; + + expect(getPath(location)).toEqual( + `/administration/${path}?page=${location.page}&pageSize=${location.pageSize}&show=${location.show}&filter=${location.filter}&includedPolicies=${location.includedPolicies}` + ); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when page index is equal to default', (path, getPath) => { + const location: ArtifactListPageUrlParams = { + page: MANAGEMENT_DEFAULT_PAGE, + pageSize: 20, + show: 'create', + filter: '', + includedPolicies: '', + }; + + expect(getPath(location)).toEqual( + `/administration/${path}?pageSize=${location.pageSize}&show=${location.show}` + ); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when page size is equal to default', (path, getPath) => { + const location: ArtifactListPageUrlParams = { + page: 2, + pageSize: MANAGEMENT_DEFAULT_PAGE_SIZE, + show: 'create', + filter: '', + includedPolicies: '', + }; + + expect(getPath(location)).toEqual( + `/administration/${path}?page=${location.page}&show=${location.show}` + ); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when "show" is equal to default', (path, getPath) => { + const location: ArtifactListPageUrlParams = { + page: 2, + pageSize: 20, + show: undefined, + filter: '', + includedPolicies: '', + }; + + expect(getPath(location)).toEqual( + `/administration/${path}?page=${location.page}&pageSize=${location.pageSize}` + ); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when view type is equal to default', (path, getPath) => { + const location: ArtifactListPageUrlParams = { + page: 2, + pageSize: 20, + show: 'create', + filter: '', + includedPolicies: '', + }; + + expect(getPath(location)).toEqual( + `/administration/${path}?page=${location.page}&pageSize=${location.pageSize}&show=${location.show}` + ); + }); + + it.each([ + ['trusted_apps', getTrustedAppsListPath], + ['blocklist', getBlocklistsListPath], + ['host_isolation_exceptions', getHostIsolationExceptionsListPath], + ])('builds proper path for %s when params are equal to default', (path, getPath) => { + const location: ArtifactListPageUrlParams = { + page: MANAGEMENT_DEFAULT_PAGE, + pageSize: MANAGEMENT_DEFAULT_PAGE_SIZE, + }; + + expect(getPath(location)).toEqual(`/administration/${path}`); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/common/url_routing/utils.ts b/x-pack/plugins/security_solution/public/management/common/url_routing/utils.ts index b9ec8b2fb7a82..afff61e94b06d 100644 --- a/x-pack/plugins/security_solution/public/management/common/url_routing/utils.ts +++ b/x-pack/plugins/security_solution/public/management/common/url_routing/utils.ts @@ -14,8 +14,8 @@ import { MANAGEMENT_DEFAULT_PAGE_SIZE, MANAGEMENT_PAGE_SIZE_OPTIONS } from '../c * @param value * @param defaultValue */ -export const isDefaultOrMissing = (value: T | undefined, defaultValue: T) => { - return value === undefined || value === defaultValue; +export const isDefaultOrMissing = (value: T | undefined | 0, defaultValue: T) => { + return value === undefined || value === defaultValue || value === 0; }; /** diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx index 81dc91dbae32c..b21579d25dedb 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/components/artifact_flyout.test.tsx @@ -16,6 +16,8 @@ import { getEndpointPrivilegesInitialStateMock } from '../../../../common/compon import { AppContextTestRender } from '../../../../common/mock/endpoint'; import { trustedAppsAllHttpMocks } from '../../../pages/mocks'; import { useUserPrivileges as _useUserPrivileges } from '../../../../common/components/user_privileges'; +import { entriesToConditionEntries } from '../../../../common/utils/exception_list_items/mappers'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; jest.mock('../../../../common/components/user_privileges'); const useUserPrivileges = _useUserPrivileges as jest.Mock; @@ -357,12 +359,19 @@ describe('When the flyout is opened in the ArtifactListPage component', () => { }); }); - expect(getLastFormComponentProps().item).toEqual({ + const expectedProps = { ...mockedApi.responseProvider.trustedApp({ query: { item_id: '123' }, } as unknown as HttpFetchOptionsWithPath), created_at: expect.any(String), - }); + }; + + // map process.hash entries to have * as suffix + expectedProps.entries = entriesToConditionEntries( + expectedProps.entries + ) as ExceptionListItemSchema['entries']; + + expect(getLastFormComponentProps().item).toEqual(expectedProps); }); it('should show error toast and close flyout if item for edit does not exist', async () => { diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx index d4f92bd2717f2..ebf98c64d7dc4 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_list_page/mocks.tsx @@ -14,7 +14,7 @@ import { ArtifactFormComponentProps } from './types'; import { ArtifactListPage, ArtifactListPageProps } from './artifact_list_page'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; import { trustedAppsAllHttpMocks } from '../../pages/mocks'; -import { TrustedAppsApiClient } from '../../pages/trusted_apps/service/trusted_apps_api_client'; +import { TrustedAppsApiClient } from '../../pages/trusted_apps/service/api_client'; import { artifactListPageLabels } from './translations'; export const getFormComponentMock = (): { diff --git a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx index 3926d42fb36cf..ccb2ec9055fc8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/blocklist/view/components/blocklist_form.tsx @@ -61,7 +61,7 @@ import { BY_POLICY_ARTIFACT_TAG_PREFIX, } from '../../../../../../common/endpoint/service/artifacts/constants'; import { useLicense } from '../../../../../common/hooks/use_license'; -import { isValidHash } from '../../../../../../common/endpoint/service/trusted_apps/validations'; +import { isValidHash } from '../../../../../../common/endpoint/service/artifacts/validations'; import { isArtifactGlobal } from '../../../../../../common/endpoint/service/artifacts'; import type { PolicyData } from '../../../../../../common/endpoint/types'; import { isGlobalPolicyEffected } from '../../../../components/effected_policy_select/utils'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/mocks.ts index c1b30b6fad10c..e3641ba66e15b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/mocks.ts @@ -5,8 +5,61 @@ * 2.0. */ -import { GetTrustedAppsListResponse } from '../../../../../common/endpoint/types'; -import { createSampleTrustedApps } from '../../trusted_apps/test_utils'; +import { OperatingSystem } from '@kbn/securitysolution-utils'; +import { GetTrustedAppsListResponse, TrustedApp } from '../../../../../common/endpoint/types'; +import { + MANAGEMENT_DEFAULT_PAGE, + MANAGEMENT_DEFAULT_PAGE_SIZE, + MANAGEMENT_PAGE_SIZE_OPTIONS, +} from '../../../common/constants'; + +interface Pagination { + pageIndex: number; + pageSize: number; + totalItemCount: number; + pageSizeOptions: number[]; +} + +const OPERATING_SYSTEMS: OperatingSystem[] = [ + OperatingSystem.WINDOWS, + OperatingSystem.MAC, + OperatingSystem.LINUX, +]; + +const generate = (count: number, generator: (i: number) => T) => + [...new Array(count).keys()].map(generator); + +const createSampleTrustedApp = (i: number, longTexts?: boolean): TrustedApp => { + return { + id: String(i), + version: 'abc123', + name: generate(longTexts ? 10 : 1, () => `trusted app ${i}`).join(' '), + description: generate(longTexts ? 10 : 1, () => `Trusted App ${i}`).join(' '), + created_at: '1 minute ago', + created_by: 'someone', + updated_at: '1 minute ago', + updated_by: 'someone', + os: OPERATING_SYSTEMS[i % 3], + entries: [], + effectScope: { type: 'global' }, + }; +}; + +const createDefaultPagination = (): Pagination => ({ + pageIndex: MANAGEMENT_DEFAULT_PAGE, + pageSize: MANAGEMENT_DEFAULT_PAGE_SIZE, + totalItemCount: 200, + pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], +}); + +const createSampleTrustedApps = ( + pagination: Partial, + longTexts?: boolean +): TrustedApp[] => { + const fullPagination = { ...createDefaultPagination(), ...pagination }; + + return generate(fullPagination.pageSize, (i: number) => createSampleTrustedApp(i, longTexts)); +}; export const getMockListResponse: () => GetTrustedAppsListResponse = () => ({ data: createSampleTrustedApps({}), diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unassigned.tsx index 4809ee53b0d7b..2bee159dc6e4d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unassigned.tsx @@ -12,7 +12,6 @@ import { useGetLinkTo } from './use_policy_artifacts_empty_hooks'; import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS } from './translations'; import { EventFiltersPageLocation } from '../../../../event_filters/types'; -import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; import { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page'; interface CommonProps { policyId: string; @@ -21,10 +20,7 @@ interface CommonProps { labels: typeof POLICY_ARTIFACT_EMPTY_UNASSIGNED_LABELS; getPolicyArtifactsPath: (policyId: string) => string; getArtifactPath: ( - location?: - | Partial - | Partial - | Partial + location?: Partial | Partial ) => string; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unexisting.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unexisting.tsx index eb0133823ea99..7d12389048753 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unexisting.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/policy_artifacts_empty_unexisting.tsx @@ -10,7 +10,6 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate } from '@elastic/eui'; import { useGetLinkTo } from './use_policy_artifacts_empty_hooks'; import { POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS } from './translations'; import { EventFiltersPageLocation } from '../../../../event_filters/types'; -import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; import { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page'; interface CommonProps { @@ -19,10 +18,7 @@ interface CommonProps { labels: typeof POLICY_ARTIFACT_EMPTY_UNEXISTING_LABELS; getPolicyArtifactsPath: (policyId: string) => string; getArtifactPath: ( - location?: - | Partial - | Partial - | Partial + location?: Partial | Partial ) => string; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/use_policy_artifacts_empty_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/use_policy_artifacts_empty_hooks.ts index 723988a9de468..b2ee8e7dd17ba 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/use_policy_artifacts_empty_hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/empty/use_policy_artifacts_empty_hooks.ts @@ -11,7 +11,6 @@ import { useNavigateToAppEventHandler } from '../../../../../../common/hooks/end import { useAppUrl } from '../../../../../../common/lib/kibana/hooks'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { EventFiltersPageLocation } from '../../../../event_filters/types'; -import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; import { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page'; export const useGetLinkTo = ( @@ -19,10 +18,7 @@ export const useGetLinkTo = ( policyName: string, getPolicyArtifactsPath: (policyId: string) => string, getArtifactPath: ( - location?: - | Partial - | Partial - | Partial + location?: Partial | Partial ) => string, location?: Partial<{ show: 'create' }> ) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx index 92b0edc690a44..ac3cf9e536b9a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/layout/policy_artifacts_layout.tsx @@ -32,7 +32,6 @@ import { PolicyArtifactsFlyout } from '../flyout'; import { PolicyArtifactsPageLabels, policyArtifactsPageLabels } from '../translations'; import { PolicyArtifactsDeleteModal } from '../delete_modal'; import { EventFiltersPageLocation } from '../../../../event_filters/types'; -import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; import { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page'; interface PolicyArtifactsLayoutProps { @@ -42,10 +41,7 @@ interface PolicyArtifactsLayoutProps { getExceptionsListApiClient: () => ExceptionsListApiClient; searchableFields: readonly string[]; getArtifactPath: ( - location?: - | Partial - | Partial - | Partial + location?: Partial | Partial ) => string; getPolicyArtifactsPath: (policyId: string) => string; /** A boolean to check extra privileges for restricted actions, true when it's allowed, false when not */ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx index 863b7de4a4815..445725915899b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/artifacts/list/policy_artifacts_list.tsx @@ -29,7 +29,6 @@ import { ExceptionsListApiClient } from '../../../../../services/exceptions_list import { useListArtifact } from '../../../../../hooks/artifacts'; import { POLICY_ARTIFACT_LIST_LABELS } from './translations'; import { EventFiltersPageLocation } from '../../../../event_filters/types'; -import { TrustedAppsListPageLocation } from '../../../../trusted_apps/state'; import { ArtifactListPageUrlParams } from '../../../../../components/artifact_list_page'; interface PolicyArtifactsListProps { @@ -37,10 +36,7 @@ interface PolicyArtifactsListProps { apiClient: ExceptionsListApiClient; searchableFields: string[]; getArtifactPath: ( - location?: - | Partial - | Partial - | Partial + location?: Partial | Partial ) => string; getPolicyArtifactsPath: (policyId: string) => string; labels: typeof POLICY_ARTIFACT_LIST_LABELS; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx index e6984087bef5a..7700c1f71fbab 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/endpoint_package_custom_extension.tsx @@ -10,7 +10,7 @@ import { EuiSpacer } from '@elastic/eui'; import { PackageCustomExtensionComponentProps } from '@kbn/fleet-plugin/public'; import { useHttp } from '../../../../../../common/lib/kibana'; import { useCanSeeHostIsolationExceptionsMenu } from '../../../../host_isolation_exceptions/view/hooks'; -import { TrustedAppsApiClient } from '../../../../trusted_apps/service/trusted_apps_api_client'; +import { TrustedAppsApiClient } from '../../../../trusted_apps/service/api_client'; import { EventFiltersApiClient } from '../../../../event_filters/service/event_filters_api_client'; import { HostIsolationExceptionsApiClient } from '../../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; import { BlocklistsApiClient } from '../../../../blocklist/services'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index 87537014e6b0f..dfb2677ecb594 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -36,7 +36,7 @@ import { FleetIntegrationArtifactsCard } from './endpoint_package_custom_extensi import { BlocklistsApiClient } from '../../../blocklist/services'; import { HostIsolationExceptionsApiClient } from '../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; import { EventFiltersApiClient } from '../../../event_filters/service/event_filters_api_client'; -import { TrustedAppsApiClient } from '../../../trusted_apps/service/trusted_apps_api_client'; +import { TrustedAppsApiClient } from '../../../trusted_apps/service/api_client'; import { SEARCHABLE_FIELDS as BLOCKLIST_SEARCHABLE_FIELDS } from '../../../blocklist/constants'; import { SEARCHABLE_FIELDS as HOST_ISOLATION_EXCEPTIONS_SEARCHABLE_FIELDS } from '../../../host_isolation_exceptions/constants'; import { SEARCHABLE_FIELDS as EVENT_FILTERS_SEARCHABLE_FIELDS } from '../../../event_filters/constants'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx index 62778bb164d98..f3a20a1abfd66 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/tabs/policy_tabs.tsx @@ -41,7 +41,7 @@ import { POLICY_ARTIFACT_EVENT_FILTERS_LABELS } from './event_filters_translatio import { POLICY_ARTIFACT_TRUSTED_APPS_LABELS } from './trusted_apps_translations'; import { POLICY_ARTIFACT_HOST_ISOLATION_EXCEPTIONS_LABELS } from './host_isolation_exceptions_translations'; import { POLICY_ARTIFACT_BLOCKLISTS_LABELS } from './blocklists_translations'; -import { TrustedAppsApiClient } from '../../../trusted_apps/service/trusted_apps_api_client'; +import { TrustedAppsApiClient } from '../../../trusted_apps/service/api_client'; import { EventFiltersApiClient } from '../../../event_filters/service/event_filters_api_client'; import { BlocklistsApiClient } from '../../../blocklist/services/blocklists_api_client'; import { HostIsolationExceptionsApiClient } from '../../../host_isolation_exceptions/host_isolation_exceptions_api_client'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/index.tsx index 018987c81b94d..d9edd9986e156 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/index.tsx @@ -6,16 +6,18 @@ */ import { Switch, Route } from 'react-router-dom'; -import React from 'react'; -import { TrustedAppsPage } from './view'; +import React, { memo } from 'react'; +import { TrustedAppsList } from './view/trusted_apps_list'; import { MANAGEMENT_ROUTING_TRUSTED_APPS_PATH } from '../../common/constants'; import { NotFoundPage } from '../../../app/404'; -export function TrustedAppsContainer() { +export const TrustedAppsContainer = memo(() => { return ( - + ); -} +}); + +TrustedAppsContainer.displayName = 'TrustedAppsContainer'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts new file mode 100644 index 0000000000000..e66c16717e428 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/api_client.ts @@ -0,0 +1,65 @@ +/* + * 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 { + CreateExceptionListItemSchema, + ExceptionListItemSchema, + UpdateExceptionListItemSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; + +import { HttpStart } from '@kbn/core/public'; +import { ConditionEntry } from '../../../../../common/endpoint/types'; +import { + conditionEntriesToEntries, + entriesToConditionEntries, +} from '../../../../common/utils/exception_list_items'; +import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; +import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../constants'; + +function readTransform(item: ExceptionListItemSchema): ExceptionListItemSchema { + return { + ...item, + entries: entriesToConditionEntries(item.entries) as ExceptionListItemSchema['entries'], + }; +} + +function writeTransform( + item: T +): T { + return { + ...item, + entries: conditionEntriesToEntries(item.entries as ConditionEntry[], true), + } as T; +} + +/** + * Trusted Apps exceptions Api client class using ExceptionsListApiClient as base class + * It follows the Singleton pattern. + * Please, use the getInstance method instead of creating a new instance when using this implementation. + */ +export class TrustedAppsApiClient extends ExceptionsListApiClient { + constructor(http: HttpStart) { + super( + http, + ENDPOINT_TRUSTED_APPS_LIST_ID, + TRUSTED_APPS_EXCEPTION_LIST_DEFINITION, + readTransform, + writeTransform + ); + } + + public static getInstance(http: HttpStart): ExceptionsListApiClient { + return super.getInstance( + http, + ENDPOINT_TRUSTED_APPS_LIST_ID, + TRUSTED_APPS_EXCEPTION_LIST_DEFINITION, + readTransform, + writeTransform + ); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts index 3c0d58043dd02..4b2dee13ce70a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './trusted_apps_http_service'; +export { TrustedAppsApiClient } from './api_client'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts deleted file mode 100644 index 574f32dca6b33..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_api_client.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; -import { HttpStart } from '@kbn/core/public'; -import { ExceptionsListApiClient } from '../../../services/exceptions_list/exceptions_list_api_client'; -import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../constants'; - -/** - * Trusted apps Api client class using ExceptionsListApiClient as base class - * It follow the Singleton pattern. - * Please, use the getInstance method instead of creating a new instance when using this implementation. - */ -export class TrustedAppsApiClient extends ExceptionsListApiClient { - constructor(http: HttpStart) { - super(http, ENDPOINT_TRUSTED_APPS_LIST_ID, TRUSTED_APPS_EXCEPTION_LIST_DEFINITION); - } - - public static getInstance(http: HttpStart): ExceptionsListApiClient { - return super.getInstance( - http, - ENDPOINT_TRUSTED_APPS_LIST_ID, - TRUSTED_APPS_EXCEPTION_LIST_DEFINITION - ); - } -} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_http_service.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_http_service.ts deleted file mode 100644 index a16ccd9169dbe..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/trusted_apps_http_service.ts +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - ExceptionListItemSchema, - ExceptionListSchema, - ExceptionListSummarySchema, - FoundExceptionListItemSchema, -} from '@kbn/securitysolution-io-ts-list-types'; -import { - ENDPOINT_TRUSTED_APPS_LIST_ID, - EXCEPTION_LIST_ITEM_URL, - EXCEPTION_LIST_URL, -} from '@kbn/securitysolution-list-constants'; -import { HttpStart } from '@kbn/core/public'; -import pMap from 'p-map'; -import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; -import { - DeleteTrustedAppsRequestParams, - GetOneTrustedAppRequestParams, - GetOneTrustedAppResponse, - GetTrustedAppsListRequest, - GetTrustedAppsListResponse, - MaybeImmutable, - PostTrustedAppCreateRequest, - PostTrustedAppCreateResponse, - PutTrustedAppsRequestParams, - PutTrustedAppUpdateRequest, - PutTrustedAppUpdateResponse, - TrustedApp, -} from '../../../../../common/endpoint/types'; -import { sendGetEndpointSpecificPackagePolicies } from '../../../services/policies/policies'; -import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../constants'; -import { isGlobalEffectScope } from '../state/type_guards'; -import { - exceptionListItemToTrustedApp, - newTrustedAppToCreateExceptionListItem, - updatedTrustedAppToUpdateExceptionListItem, -} from './mappers'; -import { validateTrustedAppHttpRequestBody } from './validate_trusted_app_http_request_body'; - -export interface TrustedAppsService { - getTrustedApp(params: GetOneTrustedAppRequestParams): Promise; - - getTrustedAppsList(request: GetTrustedAppsListRequest): Promise; - - deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise; - - createTrustedApp(request: PostTrustedAppCreateRequest): Promise; - - updateTrustedApp( - params: PutTrustedAppsRequestParams, - request: PutTrustedAppUpdateRequest - ): Promise; - - getPolicyList( - options?: Parameters[1] - ): ReturnType; - - assignPolicyToTrustedApps( - policyId: string, - trustedApps: MaybeImmutable - ): Promise; - - removePolicyFromTrustedApps( - policyId: string, - trustedApps: MaybeImmutable - ): Promise; -} - -const P_MAP_OPTIONS = Object.freeze({ - concurrency: 5, - /** When set to false, instead of stopping when a promise rejects, it will wait for all the promises to settle - * and then reject with an aggregated error containing all the errors from the rejected promises. */ - stopOnError: false, -}); - -export class TrustedAppsHttpService implements TrustedAppsService { - private readonly getHttpService: () => Promise; - - constructor(http: HttpStart) { - let ensureListExists: Promise; - - this.getHttpService = async () => { - if (!ensureListExists) { - ensureListExists = http - .post(EXCEPTION_LIST_URL, { - body: JSON.stringify(TRUSTED_APPS_EXCEPTION_LIST_DEFINITION), - }) - .then(() => {}) - .catch((err) => { - if (err.response.status !== 409) { - return Promise.reject(err); - } - }); - } - - await ensureListExists; - return http; - }; - } - - private async getExceptionListItem(itemId: string): Promise { - return (await this.getHttpService()).get(EXCEPTION_LIST_ITEM_URL, { - query: { - item_id: itemId, - namespace_type: 'agnostic', - }, - }); - } - - async getTrustedApp(params: GetOneTrustedAppRequestParams) { - const exceptionItem = await this.getExceptionListItem(params.id); - - return { - data: exceptionListItemToTrustedApp(exceptionItem), - }; - } - - async getTrustedAppsList({ - page = 1, - per_page: perPage = 10, - kuery, - }: GetTrustedAppsListRequest): Promise { - const itemListResults = await ( - await this.getHttpService() - ).get(`${EXCEPTION_LIST_ITEM_URL}/_find`, { - query: { - page, - per_page: perPage, - filter: kuery, - sort_field: 'name', - sort_order: 'asc', - list_id: [ENDPOINT_TRUSTED_APPS_LIST_ID], - namespace_type: ['agnostic'], - }, - }); - - return { - ...itemListResults, - data: itemListResults.data.map(exceptionListItemToTrustedApp), - }; - } - - async deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise { - await ( - await this.getHttpService() - ).delete(EXCEPTION_LIST_ITEM_URL, { - query: { - item_id: request.id, - namespace_type: 'agnostic', - }, - }); - } - - async createTrustedApp(request: PostTrustedAppCreateRequest) { - await validateTrustedAppHttpRequestBody(await this.getHttpService(), request); - - const newTrustedAppException = newTrustedAppToCreateExceptionListItem(request); - const createdExceptionItem = await ( - await this.getHttpService() - ).post(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify(newTrustedAppException), - }); - - return { - data: exceptionListItemToTrustedApp(createdExceptionItem), - }; - } - - async updateTrustedApp( - params: PutTrustedAppsRequestParams, - updatedTrustedApp: PutTrustedAppUpdateRequest - ) { - const [currentExceptionListItem] = await Promise.all([ - await this.getExceptionListItem(params.id), - await validateTrustedAppHttpRequestBody(await this.getHttpService(), updatedTrustedApp), - ]); - - const updatedExceptionListItem = await ( - await this.getHttpService() - ).put(EXCEPTION_LIST_ITEM_URL, { - body: JSON.stringify( - updatedTrustedAppToUpdateExceptionListItem(currentExceptionListItem, updatedTrustedApp) - ), - }); - - return { - data: exceptionListItemToTrustedApp(updatedExceptionListItem), - }; - } - - async getTrustedAppsSummary(filter?: string) { - return (await this.getHttpService()).get( - `${EXCEPTION_LIST_URL}/summary`, - { - query: { - filter, - list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, - namespace_type: 'agnostic', - }, - } - ); - } - - async getPolicyList(options?: Parameters[1]) { - return sendGetEndpointSpecificPackagePolicies(await this.getHttpService(), options); - } - - /** - * Assign a policy to trusted apps. Note that Trusted Apps MUST NOT be global - * - * @param policyId - * @param trustedApps[] - */ - assignPolicyToTrustedApps( - policyId: string, - trustedApps: MaybeImmutable - ): Promise { - return this._handleAssignOrRemovePolicyId('assign', policyId, trustedApps); - } - - /** - * Remove a policy from trusted apps. Note that Trusted Apps MUST NOT be global - * - * @param policyId - * @param trustedApps[] - */ - removePolicyFromTrustedApps( - policyId: string, - trustedApps: MaybeImmutable - ): Promise { - return this._handleAssignOrRemovePolicyId('remove', policyId, trustedApps); - } - - private _handleAssignOrRemovePolicyId( - action: 'assign' | 'remove', - policyId: string, - trustedApps: MaybeImmutable - ): Promise { - if (policyId.trim() === '') { - throw new Error('policy ID is required'); - } - - if (trustedApps.length === 0) { - throw new Error('at least one trusted app is required'); - } - - return pMap( - trustedApps, - async (trustedApp) => { - if (isGlobalEffectScope(trustedApp.effectScope)) { - throw new Error( - `Unable to update trusted app [${trustedApp.id}] policy assignment. It's effectScope is 'global'` - ); - } - - const policies: string[] = !isGlobalEffectScope(trustedApp.effectScope) - ? [...trustedApp.effectScope.policies] - : []; - - const indexOfPolicyId = policies.indexOf(policyId); - - if (action === 'assign' && indexOfPolicyId === -1) { - policies.push(policyId); - } else if (action === 'remove' && indexOfPolicyId !== -1) { - policies.splice(indexOfPolicyId, 1); - } - - return this.updateTrustedApp( - { id: trustedApp.id }, - { - ...toUpdateTrustedApp(trustedApp), - effectScope: { - type: 'policy', - policies, - }, - } - ); - }, - P_MAP_OPTIONS - ); - } -} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/validate_trusted_app_http_request_body.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/validate_trusted_app_http_request_body.ts deleted file mode 100644 index 69b281e88fc9b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/validate_trusted_app_http_request_body.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpStart } from '@kbn/core/public'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; -import { - PostTrustedAppCreateRequest, - PutTrustedAppUpdateRequest, -} from '../../../../../common/endpoint/types'; -import { HttpRequestValidationError } from './errors'; -import { sendGetAgentPolicyList } from '../../../services/policies/ingest'; - -/** - * Validates that the Trusted App is valid for sending to the API (`POST` + 'PUT') - * - * @throws - */ -export const validateTrustedAppHttpRequestBody = async ( - http: HttpStart, - trustedApp: PostTrustedAppCreateRequest | PutTrustedAppUpdateRequest -): Promise => { - const failedValidations: string[] = []; - - // Validate that the Policy IDs are still valid - if (trustedApp.effectScope.type === 'policy' && trustedApp.effectScope.policies.length) { - const policyIds = trustedApp.effectScope.policies; - - // We can't search against the Package Policy API by ID because there is no way to do that. - // So, as a work-around, we use the Agent Policy API and check for those Agent Policies that - // have these package policies in it. For endpoint, these are 1-to-1. - const agentPoliciesFound = await sendGetAgentPolicyList(http, { - query: { - kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies: (${policyIds.join(' or ')})`, - }, - }); - - if (!agentPoliciesFound.items.length) { - failedValidations.push(`Invalid Policy Id(s) [${policyIds.join(', ')}]`); - } else { - const missingPolicies = policyIds.filter( - (policyId) => - !agentPoliciesFound.items.find(({ package_policies: packagePolicies }) => - (packagePolicies as string[]).includes(policyId) - ) - ); - - if (missingPolicies.length) { - failedValidations.push(`Invalid Policy Id(s) [${missingPolicies.join(', ')}]`); - } - } - } - - if (failedValidations.length) { - throw new HttpRequestValidationError(failedValidations); - } -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts deleted file mode 100644 index a06fceab29d4c..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from '../../../state/async_resource_state'; -export * from './trusted_apps_list_page_state'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts deleted file mode 100644 index 292b982eebb47..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; -import { AsyncResourceState } from '../../../state/async_resource_state'; -import { GetPolicyListResponse } from '../../policy/types'; - -export interface Pagination { - pageIndex: number; - pageSize: number; - totalItemCount: number; - pageSizeOptions: number[]; -} - -export interface TrustedAppsListData { - items: TrustedApp[]; - pageIndex: number; - pageSize: number; - timestamp: number; - totalItemsCount: number; - filter: string; - includedPolicies: string; -} - -export type ViewType = 'list' | 'grid'; - -export interface TrustedAppsListPageLocation { - page_index: number; - page_size: number; - view_type: ViewType; - show?: 'create' | 'edit'; - /** Used for editing. The ID of the selected trusted app */ - id?: string; - filter: string; - // A string with comma dlimetered list of included policy IDs - included_policies: string; -} - -export interface TrustedAppsListPageState { - /** Represents if trusted apps entries exist, regardless of whether the list is showing results - * or not (which could use filtering in the future) - */ - entriesExist: AsyncResourceState; - listView: { - listResourceState: AsyncResourceState; - freshDataTimestamp: number; - }; - deletionDialog: { - entry?: TrustedApp; - confirmed: boolean; - submissionResourceState: AsyncResourceState; - }; - creationDialog: { - formState?: { - entry: NewTrustedApp; - isValid: boolean; - }; - /** The trusted app to be edited (when in edit mode) */ - editItem?: AsyncResourceState; - confirmed: boolean; - submissionResourceState: AsyncResourceState; - }; - /** A list of all available polices for use in associating TA to policies */ - policies: AsyncResourceState; - location: TrustedAppsListPageLocation; - active: boolean; - forceRefresh: boolean; -} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 1a28e6f3bfecf..239255b641bf4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -8,11 +8,7 @@ import { ConditionEntryField } from '@kbn/securitysolution-utils'; import { TrustedAppConditionEntry, - EffectScope, - GlobalEffectScope, MacosLinuxConditionEntry, - MaybeImmutable, - PolicyEffectScope, WindowsConditionEntry, } from '../../../../../common/endpoint/types'; @@ -27,15 +23,3 @@ export const isMacosLinuxTrustedAppCondition = ( ): condition is MacosLinuxConditionEntry => { return condition.field !== ConditionEntryField.SIGNER; }; - -export const isGlobalEffectScope = ( - effectedScope: MaybeImmutable -): effectedScope is GlobalEffectScope => { - return effectedScope.type === 'global'; -}; - -export const isPolicyEffectScope = ( - effectedScope: MaybeImmutable -): effectedScope is PolicyEffectScope => { - return effectedScope.type === 'policy'; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts deleted file mode 100644 index f9965da4a2256..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Action } from 'redux'; - -import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; -import { AsyncResourceState, TrustedAppsListData } from '../state'; -import { GetPolicyListResponse } from '../../policy/types'; - -export type TrustedAppsListDataOutdated = Action<'trustedAppsListDataOutdated'>; - -interface ResourceStateChanged extends Action { - payload: { newState: AsyncResourceState }; -} - -export type TrustedAppsListResourceStateChanged = ResourceStateChanged< - 'trustedAppsListResourceStateChanged', - TrustedAppsListData ->; - -export type TrustedAppDeletionSubmissionResourceStateChanged = - ResourceStateChanged<'trustedAppDeletionSubmissionResourceStateChanged'>; - -export type TrustedAppDeletionDialogStarted = Action<'trustedAppDeletionDialogStarted'> & { - payload: { - entry: TrustedApp; - }; -}; - -export type TrustedAppDeletionDialogConfirmed = Action<'trustedAppDeletionDialogConfirmed'>; - -export type TrustedAppDeletionDialogClosed = Action<'trustedAppDeletionDialogClosed'>; - -export type TrustedAppCreationSubmissionResourceStateChanged = ResourceStateChanged< - 'trustedAppCreationSubmissionResourceStateChanged', - TrustedApp ->; - -export type TrustedAppCreationDialogStarted = Action<'trustedAppCreationDialogStarted'> & { - payload: { - entry: NewTrustedApp; - }; -}; - -export type TrustedAppCreationDialogFormStateUpdated = - Action<'trustedAppCreationDialogFormStateUpdated'> & { - payload: { - entry: NewTrustedApp; - isValid: boolean; - }; - }; - -export type TrustedAppCreationEditItemStateChanged = - Action<'trustedAppCreationEditItemStateChanged'> & { - payload: AsyncResourceState; - }; - -export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialogConfirmed'>; - -export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>; - -export type TrustedAppsExistResponse = Action<'trustedAppsExistStateChanged'> & { - payload: AsyncResourceState; -}; - -export type TrustedAppsPoliciesStateChanged = Action<'trustedAppsPoliciesStateChanged'> & { - payload: AsyncResourceState; -}; - -export type TrustedAppForceRefresh = Action<'trustedAppForceRefresh'> & { - payload: { - forceRefresh: boolean; - }; -}; - -export type TrustedAppsPageAction = - | TrustedAppsListDataOutdated - | TrustedAppsListResourceStateChanged - | TrustedAppDeletionSubmissionResourceStateChanged - | TrustedAppDeletionDialogStarted - | TrustedAppDeletionDialogConfirmed - | TrustedAppDeletionDialogClosed - | TrustedAppCreationSubmissionResourceStateChanged - | TrustedAppCreationEditItemStateChanged - | TrustedAppCreationDialogStarted - | TrustedAppCreationDialogFormStateUpdated - | TrustedAppCreationDialogConfirmed - | TrustedAppsExistResponse - | TrustedAppsPoliciesStateChanged - | TrustedAppCreationDialogClosed - | TrustedAppForceRefresh; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts deleted file mode 100644 index f0fab98188927..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/builders.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; -import { TrustedAppConditionEntry, NewTrustedApp } from '../../../../../common/endpoint/types'; - -import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; - -import { TrustedAppsListPageState } from '../state'; - -export const defaultConditionEntry = (): TrustedAppConditionEntry => ({ - field: ConditionEntryField.HASH, - operator: 'included', - type: 'match', - value: '', -}); - -export const defaultNewTrustedApp = (): NewTrustedApp => ({ - name: '', - os: OperatingSystem.WINDOWS, - entries: [defaultConditionEntry()], - description: '', - effectScope: { type: 'global' }, -}); - -export const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({ - confirmed: false, - submissionResourceState: { type: 'UninitialisedResourceState' }, -}); - -export const initialCreationDialogState = (): TrustedAppsListPageState['creationDialog'] => ({ - confirmed: false, - submissionResourceState: { type: 'UninitialisedResourceState' }, -}); - -export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ - entriesExist: { type: 'UninitialisedResourceState' }, - listView: { - listResourceState: { type: 'UninitialisedResourceState' }, - freshDataTimestamp: Date.now(), - }, - deletionDialog: initialDeletionDialogState(), - creationDialog: initialCreationDialogState(), - policies: { type: 'UninitialisedResourceState' }, - location: { - page_index: MANAGEMENT_DEFAULT_PAGE, - page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, - show: undefined, - id: undefined, - view_type: 'grid', - filter: '', - included_policies: '', - }, - active: false, - forceRefresh: false, -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts deleted file mode 100644 index 4455baddb047c..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { applyMiddleware, createStore } from 'redux'; - -import { createSpyMiddleware } from '../../../../common/store/test_utils'; - -import { - createDefaultPagination, - createListLoadedResourceState, - createLoadedListViewWithPagination, - createSampleTrustedApp, - createSampleTrustedApps, - createServerApiError, - createUninitialisedResourceState, - createUserChangedUrlAction, -} from '../test_utils'; - -import { TrustedAppsService } from '../service'; -import { Pagination, TrustedAppsListPageLocation, TrustedAppsListPageState } from '../state'; -import { initialTrustedAppsPageState } from './builders'; -import { trustedAppsPageReducer } from './reducer'; -import { createTrustedAppsPageMiddleware } from './middleware'; -import { Immutable } from '../../../../../common/endpoint/types'; -import { getGeneratedPolicyResponse } from './mocks'; - -const initialNow = 111111; -const dateNowMock = jest.fn(); -dateNowMock.mockReturnValue(initialNow); - -Date.now = dateNowMock; - -const initialState: Immutable = initialTrustedAppsPageState(); - -const createGetTrustedListAppsResponse = (pagination: Partial) => { - const fullPagination = { ...createDefaultPagination(), ...pagination }; - - return { - data: createSampleTrustedApps(pagination), - page: fullPagination.pageIndex, - per_page: fullPagination.pageSize, - total: fullPagination.totalItemCount, - }; -}; - -const createTrustedAppsServiceMock = (): jest.Mocked => ({ - getTrustedAppsList: jest.fn(), - deleteTrustedApp: jest.fn(), - createTrustedApp: jest.fn(), - getPolicyList: jest.fn(), - updateTrustedApp: jest.fn(), - getTrustedApp: jest.fn(), - assignPolicyToTrustedApps: jest.fn(), - removePolicyFromTrustedApps: jest.fn(), -}); - -const createStoreSetup = (trustedAppsService: TrustedAppsService) => { - const spyMiddleware = createSpyMiddleware(); - - return { - spyMiddleware, - store: createStore( - trustedAppsPageReducer, - applyMiddleware( - createTrustedAppsPageMiddleware(trustedAppsService), - spyMiddleware.actionSpyMiddleware - ) - ), - }; -}; - -describe('middleware', () => { - type TrustedAppsEntriesExistState = Pick; - const entriesExistLoadedState = (): TrustedAppsEntriesExistState => { - return { - entriesExist: { - data: true, - type: 'LoadedResourceState', - }, - }; - }; - const entriesExistLoadingState = (): TrustedAppsEntriesExistState => { - return { - entriesExist: { - previousState: { - type: 'UninitialisedResourceState', - }, - type: 'LoadingResourceState', - }, - }; - }; - - const createLocationState = ( - params?: Partial - ): TrustedAppsListPageLocation => { - return { - ...initialState.location, - ...(params ?? {}), - }; - }; - - beforeEach(() => { - dateNowMock.mockReturnValue(initialNow); - }); - - describe('initial state', () => { - it('sets initial state properly', async () => { - expect(createStoreSetup(createTrustedAppsServiceMock()).store.getState()).toStrictEqual( - initialState - ); - }); - }); - - describe('refreshing list resource state', () => { - it('refreshes the list when location changes and data gets outdated', async () => { - const pagination = { pageIndex: 2, pageSize: 50 }; - const location = createLocationState({ - page_index: 2, - page_size: 50, - }); - const service = createTrustedAppsServiceMock(); - const { store, spyMiddleware } = createStoreSetup(service); - - service.getTrustedAppsList.mockResolvedValue(createGetTrustedListAppsResponse(pagination)); - - store.dispatch( - createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') - ); - - expect(store.getState()).toStrictEqual({ - ...initialState, - listView: { - listResourceState: { - type: 'LoadingResourceState', - previousState: createUninitialisedResourceState(), - }, - freshDataTimestamp: initialNow, - }, - active: true, - location, - }); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - expect(store.getState()).toStrictEqual({ - ...initialState, - ...entriesExistLoadingState(), - listView: createLoadedListViewWithPagination(initialNow, pagination), - active: true, - location, - }); - }); - - it('does not refresh the list when location changes and data does not get outdated', async () => { - const pagination = { pageIndex: 2, pageSize: 50 }; - const location = createLocationState({ - page_index: 2, - page_size: 50, - }); - const service = createTrustedAppsServiceMock(); - const { store, spyMiddleware } = createStoreSetup(service); - - service.getTrustedAppsList.mockResolvedValue(createGetTrustedListAppsResponse(pagination)); - - store.dispatch( - createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') - ); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - store.dispatch( - createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') - ); - - expect(service.getTrustedAppsList).toBeCalledTimes(2); - expect(store.getState()).toStrictEqual({ - ...initialState, - ...entriesExistLoadingState(), - listView: createLoadedListViewWithPagination(initialNow, pagination), - active: true, - location, - }); - }); - - it('refreshes the list when data gets outdated with and outdate action', async () => { - const newNow = 222222; - const pagination = { pageIndex: 0, pageSize: 10 }; - const location = createLocationState(); - const service = createTrustedAppsServiceMock(); - const { store, spyMiddleware } = createStoreSetup(service); - const policiesResponse = getGeneratedPolicyResponse(); - - service.getTrustedAppsList.mockResolvedValue(createGetTrustedListAppsResponse(pagination)); - service.getPolicyList.mockResolvedValue(policiesResponse); - - store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - dateNowMock.mockReturnValue(newNow); - - store.dispatch({ type: 'trustedAppsListDataOutdated' }); - - expect(store.getState()).toStrictEqual({ - ...initialState, - ...entriesExistLoadingState(), - listView: { - listResourceState: { - type: 'LoadingResourceState', - previousState: createListLoadedResourceState(pagination, initialNow), - }, - freshDataTimestamp: newNow, - }, - active: true, - location, - }); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - await spyMiddleware.waitForAction('trustedAppsPoliciesStateChanged'); - - expect(store.getState()).toStrictEqual({ - ...initialState, - ...entriesExistLoadedState(), - policies: { - data: policiesResponse, - type: 'LoadedResourceState', - }, - listView: createLoadedListViewWithPagination(newNow, pagination), - active: true, - location, - }); - }); - - it('set list resource state to failed when failing to load data', async () => { - const service = createTrustedAppsServiceMock(); - const { store, spyMiddleware } = createStoreSetup(service); - - service.getTrustedAppsList.mockRejectedValue({ - body: createServerApiError('Internal Server Error'), - }); - - store.dispatch( - createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') - ); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - expect(store.getState()).toStrictEqual({ - ...initialState, - ...entriesExistLoadingState(), - listView: { - listResourceState: { - type: 'FailedResourceState', - error: createServerApiError('Internal Server Error'), - lastLoadedState: undefined, - }, - freshDataTimestamp: initialNow, - }, - active: true, - location: createLocationState({ - page_index: 2, - page_size: 50, - }), - }); - - const infiniteLoopTest = async () => { - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - }; - - await expect(infiniteLoopTest).rejects.not.toBeNull(); - }); - }); - - describe('submitting deletion dialog', () => { - const newNow = 222222; - const entry = createSampleTrustedApp(3); - const notFoundError = createServerApiError('Not Found'); - const pagination = { pageIndex: 0, pageSize: 10 }; - const location = createLocationState(); - const getTrustedAppsListResponse = createGetTrustedListAppsResponse(pagination); - const listView = createLoadedListViewWithPagination(initialNow, pagination); - const listViewNew = createLoadedListViewWithPagination(newNow, pagination); - const testStartState = { - ...initialState, - ...entriesExistLoadingState(), - listView, - active: true, - location, - }; - - it('does not submit when entry is undefined', async () => { - const service = createTrustedAppsServiceMock(); - const { store, spyMiddleware } = createStoreSetup(service); - - service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); - service.deleteTrustedApp.mockResolvedValue(); - - store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); - - expect(store.getState()).toStrictEqual({ - ...testStartState, - deletionDialog: { ...testStartState.deletionDialog, confirmed: true }, - }); - }); - - it('submits successfully when entry is defined', async () => { - const service = createTrustedAppsServiceMock(); - const { store, spyMiddleware } = createStoreSetup(service); - const policiesResponse = getGeneratedPolicyResponse(); - - service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); - service.deleteTrustedApp.mockResolvedValue(); - service.getPolicyList.mockResolvedValue(policiesResponse); - - store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - dateNowMock.mockReturnValue(newNow); - - store.dispatch({ type: 'trustedAppDeletionDialogStarted', payload: { entry } }); - store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); - - expect(store.getState()).toStrictEqual({ - ...testStartState, - deletionDialog: { - entry, - confirmed: true, - submissionResourceState: { - type: 'LoadingResourceState', - previousState: { type: 'UninitialisedResourceState' }, - }, - }, - }); - - await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - expect(store.getState()).toStrictEqual({ - ...testStartState, - ...entriesExistLoadedState(), - policies: { - data: policiesResponse, - type: 'LoadedResourceState', - }, - listView: listViewNew, - }); - expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); - expect(service.deleteTrustedApp).toBeCalledTimes(1); - }); - - it('does not submit twice', async () => { - const service = createTrustedAppsServiceMock(); - const { store, spyMiddleware } = createStoreSetup(service); - const policiesResponse = getGeneratedPolicyResponse(); - - service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); - service.deleteTrustedApp.mockResolvedValue(); - service.getPolicyList.mockResolvedValue(policiesResponse); - - store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - dateNowMock.mockReturnValue(newNow); - - store.dispatch({ type: 'trustedAppDeletionDialogStarted', payload: { entry } }); - store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); - store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); - - expect(store.getState()).toStrictEqual({ - ...testStartState, - deletionDialog: { - entry, - confirmed: true, - submissionResourceState: { - type: 'LoadingResourceState', - previousState: { type: 'UninitialisedResourceState' }, - }, - }, - }); - - await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - expect(store.getState()).toStrictEqual({ - ...testStartState, - ...entriesExistLoadedState(), - policies: { - data: policiesResponse, - type: 'LoadedResourceState', - }, - listView: listViewNew, - }); - expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); - expect(service.deleteTrustedApp).toBeCalledTimes(1); - }); - - it('does not submit when server response with failure', async () => { - const service = createTrustedAppsServiceMock(); - const { store, spyMiddleware } = createStoreSetup(service); - const policiesResponse = getGeneratedPolicyResponse(); - - service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); - service.deleteTrustedApp.mockRejectedValue({ body: notFoundError }); - service.getPolicyList.mockResolvedValue(policiesResponse); - - store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); - - await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - - store.dispatch({ type: 'trustedAppDeletionDialogStarted', payload: { entry } }); - store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); - - expect(store.getState()).toStrictEqual({ - ...testStartState, - deletionDialog: { - entry, - confirmed: true, - submissionResourceState: { - type: 'LoadingResourceState', - previousState: { type: 'UninitialisedResourceState' }, - }, - }, - }); - - await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); - await spyMiddleware.waitForAction('trustedAppsPoliciesStateChanged'); - - expect(store.getState()).toStrictEqual({ - ...testStartState, - ...entriesExistLoadedState(), - policies: { - data: policiesResponse, - type: 'LoadedResourceState', - }, - deletionDialog: { - entry, - confirmed: true, - submissionResourceState: { - type: 'FailedResourceState', - error: notFoundError, - lastLoadedState: undefined, - }, - }, - }); - expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); - expect(service.deleteTrustedApp).toBeCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts deleted file mode 100644 index cd39ee27353e9..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ /dev/null @@ -1,477 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { - Immutable, - PostTrustedAppCreateRequest, - TrustedApp, -} from '../../../../../common/endpoint/types'; -import { AppAction } from '../../../../common/store/actions'; -import { - ImmutableMiddleware, - ImmutableMiddlewareAPI, - ImmutableMiddlewareFactory, -} from '../../../../common/store'; - -import { TrustedAppsHttpService, TrustedAppsService } from '../service'; - -import { - AsyncResourceState, - getLastLoadedResourceState, - isLoadedResourceState, - isLoadingResourceState, - isStaleResourceState, - isUninitialisedResourceState, - StaleResourceState, - TrustedAppsListData, - TrustedAppsListPageState, -} from '../state'; - -import { defaultNewTrustedApp } from './builders'; - -import { - TrustedAppCreationSubmissionResourceStateChanged, - TrustedAppDeletionSubmissionResourceStateChanged, - TrustedAppsListResourceStateChanged, -} from './action'; - -import { - getListResourceState, - getDeletionDialogEntry, - getDeletionSubmissionResourceState, - getLastLoadedListResourceState, - getCurrentLocationPageIndex, - getCurrentLocationPageSize, - getCurrentLocationFilter, - needsRefreshOfListData, - getCreationSubmissionResourceState, - getCreationDialogFormEntry, - isCreationDialogLocation, - isCreationDialogFormValid, - entriesExist, - getListTotalItemsCount, - trustedAppsListPageActive, - entriesExistState, - policiesState, - isEdit, - isFetchingEditTrustedAppItem, - editItemId, - editingTrustedApp, - getListItems, - getCurrentLocationIncludedPolicies, -} from './selectors'; -import { parsePoliciesToKQL, parseQueryFilterToKQL } from '../../../common/utils'; -import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; -import { SEARCHABLE_FIELDS } from '../constants'; - -const createTrustedAppsListResourceStateChangedAction = ( - newState: Immutable> -): Immutable => ({ - type: 'trustedAppsListResourceStateChanged', - payload: { newState }, -}); - -const refreshListIfNeeded = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - if (needsRefreshOfListData(store.getState())) { - store.dispatch({ type: 'trustedAppForceRefresh', payload: { forceRefresh: false } }); - store.dispatch( - createTrustedAppsListResourceStateChangedAction({ - type: 'LoadingResourceState', - // need to think on how to avoid the casting - previousState: getListResourceState(store.getState()) as Immutable< - StaleResourceState - >, - }) - ); - - try { - const pageIndex = getCurrentLocationPageIndex(store.getState()); - const pageSize = getCurrentLocationPageSize(store.getState()); - const filter = getCurrentLocationFilter(store.getState()); - const includedPolicies = getCurrentLocationIncludedPolicies(store.getState()); - - const kuery = []; - - const filterKuery = parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined; - if (filterKuery) kuery.push(filterKuery); - - const policiesKuery = - parsePoliciesToKQL(includedPolicies ? includedPolicies.split(',') : []) || undefined; - if (policiesKuery) kuery.push(policiesKuery); - - const response = await trustedAppsService.getTrustedAppsList({ - page: pageIndex + 1, - per_page: pageSize, - kuery: kuery.join(' AND ') || undefined, - }); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction({ - type: 'LoadedResourceState', - data: { - items: response.data, - pageIndex, - pageSize, - totalItemsCount: response.total, - timestamp: Date.now(), - filter, - includedPolicies, - }, - }) - ); - } catch (error) { - store.dispatch( - createTrustedAppsListResourceStateChangedAction({ - type: 'FailedResourceState', - error: error.body, - lastLoadedState: getLastLoadedListResourceState(store.getState()), - }) - ); - } - } -}; - -const updateCreationDialogIfNeeded = ( - store: ImmutableMiddlewareAPI -) => { - const newEntry = getCreationDialogFormEntry(store.getState()); - const shouldShow = isCreationDialogLocation(store.getState()); - - if (shouldShow && !newEntry) { - store.dispatch({ - type: 'trustedAppCreationDialogStarted', - payload: { entry: defaultNewTrustedApp() }, - }); - } else if (!shouldShow && newEntry) { - store.dispatch({ - type: 'trustedAppCreationDialogClosed', - }); - } -}; - -const createTrustedAppCreationSubmissionResourceStateChanged = ( - newState: Immutable> -): Immutable => ({ - type: 'trustedAppCreationSubmissionResourceStateChanged', - payload: { newState }, -}); - -const submitCreationIfNeeded = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - const currentState = store.getState(); - const submissionResourceState = getCreationSubmissionResourceState(currentState); - const isValid = isCreationDialogFormValid(currentState); - const entry = getCreationDialogFormEntry(currentState); - const editMode = isEdit(currentState); - - if (isStaleResourceState(submissionResourceState) && entry !== undefined && isValid) { - store.dispatch( - createTrustedAppCreationSubmissionResourceStateChanged({ - type: 'LoadingResourceState', - previousState: submissionResourceState, - }) - ); - - try { - let responseTrustedApp: TrustedApp; - - if (editMode) { - responseTrustedApp = ( - await trustedAppsService.updateTrustedApp( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - { id: editItemId(currentState)! }, - // TODO: try to remove the cast - entry as PostTrustedAppCreateRequest - ) - ).data; - } else { - // TODO: try to remove the cast - responseTrustedApp = ( - await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest) - ).data; - } - - store.dispatch( - createTrustedAppCreationSubmissionResourceStateChanged({ - type: 'LoadedResourceState', - data: responseTrustedApp, - }) - ); - store.dispatch({ - type: 'trustedAppsListDataOutdated', - }); - } catch (error) { - store.dispatch( - createTrustedAppCreationSubmissionResourceStateChanged({ - type: 'FailedResourceState', - error: error.body, - lastLoadedState: getLastLoadedResourceState(submissionResourceState), - }) - ); - } - } -}; - -const createTrustedAppDeletionSubmissionResourceStateChanged = ( - newState: Immutable -): Immutable => ({ - type: 'trustedAppDeletionSubmissionResourceStateChanged', - payload: { newState }, -}); - -const submitDeletionIfNeeded = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - const submissionResourceState = getDeletionSubmissionResourceState(store.getState()); - const entry = getDeletionDialogEntry(store.getState()); - - if (isStaleResourceState(submissionResourceState) && entry !== undefined) { - store.dispatch( - createTrustedAppDeletionSubmissionResourceStateChanged({ - type: 'LoadingResourceState', - previousState: submissionResourceState, - }) - ); - - try { - await trustedAppsService.deleteTrustedApp({ id: entry.id }); - - store.dispatch( - createTrustedAppDeletionSubmissionResourceStateChanged({ - type: 'LoadedResourceState', - data: null, - }) - ); - store.dispatch({ - type: 'trustedAppDeletionDialogClosed', - }); - store.dispatch({ - type: 'trustedAppsListDataOutdated', - }); - } catch (error) { - store.dispatch( - createTrustedAppDeletionSubmissionResourceStateChanged({ - type: 'FailedResourceState', - error: error.body, - lastLoadedState: getLastLoadedResourceState(submissionResourceState), - }) - ); - } - } -}; - -const checkTrustedAppsExistIfNeeded = async ( - store: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - const currentState = store.getState(); - const currentEntriesExistState = entriesExistState(currentState); - - if ( - trustedAppsListPageActive(currentState) && - !isLoadingResourceState(currentEntriesExistState) - ) { - const currentListTotal = getListTotalItemsCount(currentState); - const currentDoEntriesExist = entriesExist(currentState); - - if ( - !isLoadedResourceState(currentEntriesExistState) || - (currentListTotal === 0 && currentDoEntriesExist) || - (currentListTotal > 0 && !currentDoEntriesExist) - ) { - store.dispatch({ - type: 'trustedAppsExistStateChanged', - payload: { type: 'LoadingResourceState', previousState: currentEntriesExistState }, - }); - - let doTheyExist: boolean; - try { - const { total } = await trustedAppsService.getTrustedAppsList({ - page: 1, - per_page: 1, - }); - doTheyExist = total > 0; - } catch (e) { - // If a failure occurs, lets assume entries exits so that the UI is not blocked to the user - doTheyExist = true; - } - - store.dispatch({ - type: 'trustedAppsExistStateChanged', - payload: { type: 'LoadedResourceState', data: doTheyExist }, - }); - } - } -}; - -export const retrieveListOfPoliciesIfNeeded = async ( - { getState, dispatch }: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - const currentState = getState(); - const currentPoliciesState = policiesState(currentState); - const isLoading = isLoadingResourceState(currentPoliciesState); - const isPageActive = trustedAppsListPageActive(currentState); - const isCreateFlow = isCreationDialogLocation(currentState); - const isUninitialized = isUninitialisedResourceState(currentPoliciesState); - - if (isPageActive && ((isCreateFlow && !isLoading) || isUninitialized)) { - dispatch({ - type: 'trustedAppsPoliciesStateChanged', - payload: { - type: 'LoadingResourceState', - previousState: currentPoliciesState, - } as TrustedAppsListPageState['policies'], - }); - - try { - const policyList = await trustedAppsService.getPolicyList({ - query: { - page: 1, - perPage: 1000, - }, - }); - - dispatch({ - type: 'trustedAppsPoliciesStateChanged', - payload: { - type: 'LoadedResourceState', - data: policyList, - }, - }); - } catch (error) { - dispatch({ - type: 'trustedAppsPoliciesStateChanged', - payload: { - type: 'FailedResourceState', - error: error.body || error, - lastLoadedState: getLastLoadedResourceState(policiesState(getState())), - }, - }); - } - } -}; - -const fetchEditTrustedAppIfNeeded = async ( - { getState, dispatch }: ImmutableMiddlewareAPI, - trustedAppsService: TrustedAppsService -) => { - const currentState = getState(); - const isPageActive = trustedAppsListPageActive(currentState); - const isEditFlow = isEdit(currentState); - const isAlreadyFetching = isFetchingEditTrustedAppItem(currentState); - const editTrustedAppId = editItemId(currentState); - - if (isPageActive && isEditFlow && !isAlreadyFetching) { - if (!editTrustedAppId) { - const errorMessage = i18n.translate( - 'xpack.securitySolution.trustedapps.middleware.editIdMissing', - { - defaultMessage: 'No id provided', - } - ); - - dispatch({ - type: 'trustedAppCreationEditItemStateChanged', - payload: { - type: 'FailedResourceState', - error: Object.assign(new Error(errorMessage), { statusCode: 404, error: errorMessage }), - }, - }); - return; - } - - let trustedAppForEdit = editingTrustedApp(currentState); - - // If Trusted App is already loaded, then do nothing - if (trustedAppForEdit && trustedAppForEdit.id === editTrustedAppId) { - return; - } - - // See if we can get the Trusted App record from the current list of Trusted Apps being displayed - trustedAppForEdit = getListItems(currentState).find((ta) => ta.id === editTrustedAppId); - - try { - // Retrieve Trusted App record via API if it was not in the list data. - // This would be the case when linking from another place or using an UUID for a Trusted App - // that is not currently displayed on the list view. - if (!trustedAppForEdit) { - dispatch({ - type: 'trustedAppCreationEditItemStateChanged', - payload: { - type: 'LoadingResourceState', - }, - }); - - trustedAppForEdit = (await trustedAppsService.getTrustedApp({ id: editTrustedAppId })).data; - } - - dispatch({ - type: 'trustedAppCreationEditItemStateChanged', - payload: { - type: 'LoadedResourceState', - data: trustedAppForEdit, - }, - }); - - dispatch({ - type: 'trustedAppCreationDialogFormStateUpdated', - payload: { - entry: toUpdateTrustedApp(trustedAppForEdit), - isValid: true, - }, - }); - } catch (e) { - dispatch({ - type: 'trustedAppCreationEditItemStateChanged', - payload: { - type: 'FailedResourceState', - error: e, - }, - }); - } - } -}; - -export const createTrustedAppsPageMiddleware = ( - trustedAppsService: TrustedAppsService -): ImmutableMiddleware => { - return (store) => (next) => async (action) => { - next(action); - - // TODO: need to think if failed state is a good condition to consider need for refresh - if (action.type === 'userChangedUrl' || action.type === 'trustedAppsListDataOutdated') { - await refreshListIfNeeded(store, trustedAppsService); - await checkTrustedAppsExistIfNeeded(store, trustedAppsService); - } - - if (action.type === 'userChangedUrl') { - updateCreationDialogIfNeeded(store); - retrieveListOfPoliciesIfNeeded(store, trustedAppsService); - fetchEditTrustedAppIfNeeded(store, trustedAppsService); - } - - if (action.type === 'trustedAppCreationDialogConfirmed') { - await submitCreationIfNeeded(store, trustedAppsService); - } - - if (action.type === 'trustedAppDeletionDialogConfirmed') { - await submitDeletionIfNeeded(store, trustedAppsService); - } - }; -}; - -export const trustedAppsPageMiddlewareFactory: ImmutableMiddlewareFactory< - TrustedAppsListPageState -> = (coreStart) => createTrustedAppsPageMiddleware(new TrustedAppsHttpService(coreStart.http)); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts deleted file mode 100644 index c97dd37db6bbf..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GetPolicyListResponse } from '../../policy/types'; - -import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; - -export const getGeneratedPolicyResponse = (): GetPolicyListResponse => ({ - items: [new EndpointDocGenerator('seed').generatePolicyPackagePolicy()], - total: 1, - perPage: 1, - page: 1, -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts deleted file mode 100644 index 5047608cbb4ec..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AsyncResourceState } from '../state'; -import { initialTrustedAppsPageState } from './builders'; -import { trustedAppsPageReducer } from './reducer'; -import { - createSampleTrustedApp, - createListLoadedResourceState, - createLoadedListViewWithPagination, - createUserChangedUrlAction, - createTrustedAppsListResourceStateChangedAction, -} from '../test_utils'; - -const initialNow = 111111; -const dateNowMock = jest.fn(); -dateNowMock.mockReturnValue(initialNow); - -Date.now = dateNowMock; - -const initialState = initialTrustedAppsPageState(); - -describe('reducer', () => { - describe('UserChangedUrl', () => { - it('makes page state active and extracts all parameters', () => { - const result = trustedAppsPageReducer( - initialState, - createUserChangedUrlAction( - '/administration/trusted_apps', - '?page_index=5&page_size=50&show=create&view_type=list&filter=test&included_policies=global' - ) - ); - - expect(result).toStrictEqual({ - ...initialState, - location: { - page_index: 5, - page_size: 50, - show: 'create', - view_type: 'list', - id: undefined, - filter: 'test', - included_policies: 'global', - }, - active: true, - }); - }); - - it('extracts default pagination parameters when invalid provided', () => { - const result = trustedAppsPageReducer( - { - ...initialState, - location: { - page_index: 5, - page_size: 50, - view_type: 'grid', - filter: '', - included_policies: '', - }, - }, - createUserChangedUrlAction( - '/administration/trusted_apps', - '?page_index=b&page_size=60&show=a&view_type=c' - ) - ); - - expect(result).toStrictEqual({ ...initialState, active: true }); - }); - - it('extracts default pagination parameters when none provided', () => { - const result = trustedAppsPageReducer( - { - ...initialState, - location: { - page_index: 5, - page_size: 50, - view_type: 'grid', - filter: '', - included_policies: '', - }, - }, - createUserChangedUrlAction('/administration/trusted_apps') - ); - - expect(result).toStrictEqual({ ...initialState, active: true }); - }); - - it('makes page state inactive and resets list to uninitialised state when navigating away', () => { - const result = trustedAppsPageReducer( - { ...initialState, listView: createLoadedListViewWithPagination(initialNow), active: true }, - createUserChangedUrlAction('/administration/endpoints') - ); - - expect(result).toStrictEqual(initialState); - }); - }); - - describe('TrustedAppsListResourceStateChanged', () => { - it('sets the current list resource state', () => { - const listResourceState = createListLoadedResourceState( - { pageIndex: 3, pageSize: 50 }, - initialNow - ); - const result = trustedAppsPageReducer( - initialState, - createTrustedAppsListResourceStateChangedAction(listResourceState) - ); - - expect(result).toStrictEqual({ - ...initialState, - listView: { ...initialState.listView, listResourceState }, - }); - }); - }); - - describe('TrustedAppsListDataOutdated', () => { - it('sets the list view freshness timestamp', () => { - const newNow = 222222; - dateNowMock.mockReturnValue(newNow); - - const result = trustedAppsPageReducer(initialState, { type: 'trustedAppsListDataOutdated' }); - - expect(result).toStrictEqual({ - ...initialState, - listView: { ...initialState.listView, freshDataTimestamp: newNow }, - }); - }); - }); - - describe('TrustedAppDeletionSubmissionResourceStateChanged', () => { - it('sets the deletion dialog submission resource state', () => { - const submissionResourceState: AsyncResourceState = { - type: 'LoadedResourceState', - data: null, - }; - const result = trustedAppsPageReducer(initialState, { - type: 'trustedAppDeletionSubmissionResourceStateChanged', - payload: { newState: submissionResourceState }, - }); - - expect(result).toStrictEqual({ - ...initialState, - deletionDialog: { ...initialState.deletionDialog, submissionResourceState }, - }); - }); - }); - - describe('TrustedAppDeletionDialogStarted', () => { - it('sets the deletion dialog state to started', () => { - const entry = createSampleTrustedApp(3); - const result = trustedAppsPageReducer(initialState, { - type: 'trustedAppDeletionDialogStarted', - payload: { entry }, - }); - - expect(result).toStrictEqual({ - ...initialState, - deletionDialog: { ...initialState.deletionDialog, entry }, - }); - }); - }); - - describe('TrustedAppDeletionDialogConfirmed', () => { - it('sets the deletion dialog state to confirmed', () => { - const entry = createSampleTrustedApp(3); - const result = trustedAppsPageReducer( - { - ...initialState, - deletionDialog: { - entry, - confirmed: false, - submissionResourceState: { type: 'UninitialisedResourceState' }, - }, - }, - { type: 'trustedAppDeletionDialogConfirmed' } - ); - - expect(result).toStrictEqual({ - ...initialState, - deletionDialog: { - entry, - confirmed: true, - submissionResourceState: { type: 'UninitialisedResourceState' }, - }, - }); - }); - }); - - describe('TrustedAppDeletionDialogClosed', () => { - it('sets the deletion dialog state to confirmed', () => { - const result = trustedAppsPageReducer( - { - ...initialState, - deletionDialog: { - entry: createSampleTrustedApp(3), - confirmed: true, - submissionResourceState: { type: 'UninitialisedResourceState' }, - }, - }, - { type: 'trustedAppDeletionDialogClosed' } - ); - - expect(result).toStrictEqual(initialState); - }); - }); - - describe('TrustedAppsForceRefresh', () => { - it('sets the force refresh state to true', () => { - const result = trustedAppsPageReducer( - { - ...initialState, - forceRefresh: false, - }, - { type: 'trustedAppForceRefresh', payload: { forceRefresh: true } } - ); - - expect(result).toStrictEqual({ ...initialState, forceRefresh: true }); - }); - it('sets the force refresh state to false', () => { - const result = trustedAppsPageReducer( - { - ...initialState, - forceRefresh: true, - }, - { type: 'trustedAppForceRefresh', payload: { forceRefresh: false } } - ); - - expect(result).toStrictEqual({ ...initialState, forceRefresh: false }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts deleted file mode 100644 index 07409a46156db..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// eslint-disable-next-line import/no-nodejs-modules -import { parse } from 'querystring'; -import { matchPath } from 'react-router-dom'; -import { ImmutableReducer } from '../../../../common/store'; -import { AppLocation, Immutable } from '../../../../../common/endpoint/types'; -import { UserChangedUrl } from '../../../../common/store/routing/action'; -import { AppAction } from '../../../../common/store/actions'; -import { extractTrustedAppsListPageLocation } from '../../../common/routing'; - -import { MANAGEMENT_ROUTING_TRUSTED_APPS_PATH } from '../../../common/constants'; - -import { - TrustedAppDeletionDialogClosed, - TrustedAppDeletionDialogConfirmed, - TrustedAppDeletionDialogStarted, - TrustedAppDeletionSubmissionResourceStateChanged, - TrustedAppCreationSubmissionResourceStateChanged, - TrustedAppsListDataOutdated, - TrustedAppsListResourceStateChanged, - TrustedAppCreationDialogStarted, - TrustedAppCreationDialogFormStateUpdated, - TrustedAppCreationDialogConfirmed, - TrustedAppCreationDialogClosed, - TrustedAppsExistResponse, - TrustedAppsPoliciesStateChanged, - TrustedAppCreationEditItemStateChanged, - TrustedAppForceRefresh, -} from './action'; - -import { TrustedAppsListPageState } from '../state'; -import { - initialCreationDialogState, - initialDeletionDialogState, - initialTrustedAppsPageState, -} from './builders'; -import { entriesExistState, trustedAppsListPageActive } from './selectors'; - -type StateReducer = ImmutableReducer; -type CaseReducer = ( - state: Immutable, - action: Immutable -) => Immutable; - -const isTrustedAppsPageLocation = (location: Immutable) => { - return ( - matchPath(location.pathname ?? '', { - path: MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, - exact: true, - }) !== null - ); -}; - -const trustedAppsListDataOutdated: CaseReducer = (state, action) => { - return { ...state, listView: { ...state.listView, freshDataTimestamp: Date.now() } }; -}; - -const trustedAppsListResourceStateChanged: CaseReducer = ( - state, - action -) => { - return { ...state, listView: { ...state.listView, listResourceState: action.payload.newState } }; -}; - -const trustedAppDeletionSubmissionResourceStateChanged: CaseReducer< - TrustedAppDeletionSubmissionResourceStateChanged -> = (state, action) => { - return { - ...state, - deletionDialog: { ...state.deletionDialog, submissionResourceState: action.payload.newState }, - }; -}; - -const trustedAppDeletionDialogStarted: CaseReducer = ( - state, - action -) => { - return { ...state, deletionDialog: { ...initialDeletionDialogState(), ...action.payload } }; -}; - -const trustedAppDeletionDialogConfirmed: CaseReducer = ( - state -) => { - return { ...state, deletionDialog: { ...state.deletionDialog, confirmed: true } }; -}; - -const trustedAppDeletionDialogClosed: CaseReducer = (state) => { - return { ...state, deletionDialog: initialDeletionDialogState() }; -}; - -const trustedAppCreationSubmissionResourceStateChanged: CaseReducer< - TrustedAppCreationSubmissionResourceStateChanged -> = (state, action) => { - return { - ...state, - creationDialog: { ...state.creationDialog, submissionResourceState: action.payload.newState }, - }; -}; - -const trustedAppCreationDialogStarted: CaseReducer = ( - state, - action -) => { - return { - ...state, - creationDialog: { - ...initialCreationDialogState(), - formState: { ...action.payload, isValid: false }, - }, - }; -}; - -const trustedAppCreationDialogFormStateUpdated: CaseReducer< - TrustedAppCreationDialogFormStateUpdated -> = (state, action) => { - return { - ...state, - creationDialog: { ...state.creationDialog, formState: { ...action.payload } }, - }; -}; - -const handleUpdateToEditItemState: CaseReducer = ( - state, - action -) => { - return { - ...state, - creationDialog: { ...state.creationDialog, editItem: action.payload }, - }; -}; - -const trustedAppCreationDialogConfirmed: CaseReducer = ( - state -) => { - return { ...state, creationDialog: { ...state.creationDialog, confirmed: true } }; -}; - -const trustedAppCreationDialogClosed: CaseReducer = (state) => { - return { ...state, creationDialog: initialCreationDialogState() }; -}; - -const userChangedUrl: CaseReducer = (state, action) => { - if (isTrustedAppsPageLocation(action.payload)) { - const location = extractTrustedAppsListPageLocation(parse(action.payload.search.slice(1))); - - return { ...state, active: true, location }; - } else { - return initialTrustedAppsPageState(); - } -}; - -const updateEntriesExists: CaseReducer = (state, { payload }) => { - if (entriesExistState(state) !== payload) { - return { - ...state, - entriesExist: payload, - }; - } - return state; -}; - -const updatePolicies: CaseReducer = (state, { payload }) => { - if (trustedAppsListPageActive(state)) { - return { - ...state, - policies: payload, - }; - } - return state; -}; - -const forceRefresh: CaseReducer = (state, { payload }) => { - return { - ...state, - forceRefresh: payload.forceRefresh, - }; -}; - -export const trustedAppsPageReducer: StateReducer = ( - state = initialTrustedAppsPageState(), - action -) => { - switch (action.type) { - case 'trustedAppsListDataOutdated': - return trustedAppsListDataOutdated(state, action); - - case 'trustedAppsListResourceStateChanged': - return trustedAppsListResourceStateChanged(state, action); - - case 'trustedAppDeletionSubmissionResourceStateChanged': - return trustedAppDeletionSubmissionResourceStateChanged(state, action); - - case 'trustedAppDeletionDialogStarted': - return trustedAppDeletionDialogStarted(state, action); - - case 'trustedAppDeletionDialogConfirmed': - return trustedAppDeletionDialogConfirmed(state, action); - - case 'trustedAppDeletionDialogClosed': - return trustedAppDeletionDialogClosed(state, action); - - case 'trustedAppCreationSubmissionResourceStateChanged': - return trustedAppCreationSubmissionResourceStateChanged(state, action); - - case 'trustedAppCreationDialogStarted': - return trustedAppCreationDialogStarted(state, action); - - case 'trustedAppCreationDialogFormStateUpdated': - return trustedAppCreationDialogFormStateUpdated(state, action); - - case 'trustedAppCreationEditItemStateChanged': - return handleUpdateToEditItemState(state, action); - - case 'trustedAppCreationDialogConfirmed': - return trustedAppCreationDialogConfirmed(state, action); - - case 'trustedAppCreationDialogClosed': - return trustedAppCreationDialogClosed(state, action); - - case 'userChangedUrl': - return userChangedUrl(state, action); - - case 'trustedAppsExistStateChanged': - return updateEntriesExists(state, action); - - case 'trustedAppsPoliciesStateChanged': - return updatePolicies(state, action); - - case 'trustedAppForceRefresh': - return forceRefresh(state, action); - } - - return state; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts deleted file mode 100644 index 4468d044827c0..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - AsyncResourceState, - TrustedAppsListPageLocation, - TrustedAppsListPageState, -} from '../state'; -import { initialTrustedAppsPageState } from './builders'; -import { - getListResourceState, - getLastLoadedListResourceState, - getCurrentLocationPageIndex, - getCurrentLocationPageSize, - getListErrorMessage, - getListItems, - getListTotalItemsCount, - isListLoading, - needsRefreshOfListData, - isDeletionDialogOpen, - isDeletionInProgress, - isDeletionSuccessful, - getDeletionError, - getDeletionDialogEntry, - getDeletionSubmissionResourceState, -} from './selectors'; - -import { - createDefaultPagination, - createListComplexLoadingResourceState, - createListFailedResourceState, - createListLoadedResourceState, - createLoadedListViewWithPagination, - createSampleTrustedApp, - createSampleTrustedApps, - createServerApiError, - createUninitialisedResourceState, -} from '../test_utils'; - -const initialNow = 111111; -const dateNowMock = jest.fn(); -dateNowMock.mockReturnValue(initialNow); - -Date.now = dateNowMock; - -const initialState = initialTrustedAppsPageState(); - -const createStateWithDeletionSubmissionResourceState = ( - submissionResourceState: AsyncResourceState -): TrustedAppsListPageState => ({ - ...initialState, - deletionDialog: { ...initialState.deletionDialog, submissionResourceState }, -}); - -describe('selectors', () => { - describe('needsRefreshOfListData()', () => { - it('returns false for outdated resource state and inactive state', () => { - expect(needsRefreshOfListData(initialState)).toBe(false); - }); - - it('returns true for outdated resource state and active state', () => { - expect(needsRefreshOfListData({ ...initialState, active: true })).toBe(true); - }); - - it('returns true when current loaded page index is outdated', () => { - const listView = createLoadedListViewWithPagination(initialNow, { pageIndex: 1 }); - - expect(needsRefreshOfListData({ ...initialState, listView, active: true })).toBe(true); - }); - - it('returns true when current loaded page size is outdated', () => { - const listView = createLoadedListViewWithPagination(initialNow, { pageSize: 50 }); - - expect(needsRefreshOfListData({ ...initialState, listView, active: true })).toBe(true); - }); - - it('returns true when current loaded data timestamp is outdated', () => { - const listView = { - ...createLoadedListViewWithPagination(111111), - freshDataTimestamp: 222222, - }; - - expect(needsRefreshOfListData({ ...initialState, listView, active: true })).toBe(true); - }); - - it('returns false when current loaded data is up to date', () => { - const listView = createLoadedListViewWithPagination(initialNow); - const location: TrustedAppsListPageLocation = { - page_index: 0, - page_size: 10, - view_type: 'grid', - filter: '', - included_policies: '', - }; - - expect(needsRefreshOfListData({ ...initialState, listView, active: true, location })).toBe( - false - ); - }); - }); - - describe('getListResourceState()', () => { - it('returns current list resource state', () => { - expect(getListResourceState(initialState)).toStrictEqual(createUninitialisedResourceState()); - }); - }); - - describe('getLastLoadedListResourceState()', () => { - it('returns last loaded list resource state', () => { - const state = { - ...initialState, - listView: { - listResourceState: createListComplexLoadingResourceState( - createDefaultPagination(), - initialNow - ), - freshDataTimestamp: initialNow, - }, - }; - - expect(getLastLoadedListResourceState(state)).toStrictEqual( - createListLoadedResourceState(createDefaultPagination(), initialNow) - ); - }); - }); - - describe('getListItems()', () => { - it('returns empty list when no valid data loaded', () => { - expect(getListItems(initialState)).toStrictEqual([]); - }); - - it('returns last loaded list items', () => { - const state = { - ...initialState, - listView: { - listResourceState: createListComplexLoadingResourceState( - createDefaultPagination(), - initialNow - ), - freshDataTimestamp: initialNow, - }, - }; - - expect(getListItems(state)).toStrictEqual(createSampleTrustedApps(createDefaultPagination())); - }); - }); - - describe('getListTotalItemsCount()', () => { - it('returns 0 when no valid data loaded', () => { - expect(getListTotalItemsCount(initialState)).toBe(0); - }); - - it('returns last loaded total items count', () => { - const state = { - ...initialState, - listView: { - listResourceState: createListComplexLoadingResourceState( - createDefaultPagination(), - initialNow - ), - freshDataTimestamp: initialNow, - }, - }; - - expect(getListTotalItemsCount(state)).toBe(200); - }); - }); - - describe('getListCurrentPageIndex()', () => { - it('returns page index', () => { - const location: TrustedAppsListPageLocation = { - page_index: 3, - page_size: 10, - view_type: 'grid', - filter: '', - included_policies: '', - }; - - expect(getCurrentLocationPageIndex({ ...initialState, location })).toBe(3); - }); - }); - - describe('getListCurrentPageSize()', () => { - it('returns page size', () => { - const location: TrustedAppsListPageLocation = { - page_index: 0, - page_size: 20, - view_type: 'grid', - filter: '', - included_policies: '', - }; - - expect(getCurrentLocationPageSize({ ...initialState, location })).toBe(20); - }); - }); - - describe('getListErrorMessage()', () => { - it('returns undefined when not in failed state', () => { - const state = { - ...initialState, - listView: { - listResourceState: createListComplexLoadingResourceState( - createDefaultPagination(), - initialNow - ), - freshDataTimestamp: initialNow, - }, - }; - - expect(getListErrorMessage(state)).toBeUndefined(); - }); - - it('returns message when not in failed state', () => { - const state = { - ...initialState, - listView: { - listResourceState: createListFailedResourceState('Internal Server Error'), - freshDataTimestamp: initialNow, - }, - }; - - expect(getListErrorMessage(state)).toBe('Internal Server Error'); - }); - }); - - describe('isListLoading()', () => { - it('returns false when no loading is happening', () => { - expect(isListLoading(initialState)).toBe(false); - }); - - it('returns true when loading is in progress', () => { - const state = { - ...initialState, - listView: { - listResourceState: createListComplexLoadingResourceState( - createDefaultPagination(), - initialNow - ), - freshDataTimestamp: initialNow, - }, - }; - - expect(isListLoading(state)).toBe(true); - }); - }); - - describe('isDeletionDialogOpen()', () => { - it('returns false when no entry is set', () => { - expect(isDeletionDialogOpen(initialState)).toBe(false); - }); - - it('returns true when entry is set', () => { - const state = { - ...initialState, - deletionDialog: { - ...initialState.deletionDialog, - entry: createSampleTrustedApp(5), - }, - }; - - expect(isDeletionDialogOpen(state)).toBe(true); - }); - }); - - describe('isDeletionInProgress()', () => { - it('returns false when resource state is uninitialised', () => { - expect(isDeletionInProgress(initialState)).toBe(false); - }); - - it('returns true when resource state is loading', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'LoadingResourceState', - previousState: { type: 'UninitialisedResourceState' }, - }); - - expect(isDeletionInProgress(state)).toBe(true); - }); - - it('returns false when resource state is loaded', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'LoadedResourceState', - data: null, - }); - - expect(isDeletionInProgress(state)).toBe(false); - }); - - it('returns false when resource state is failed', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'FailedResourceState', - error: createServerApiError('Not Found'), - }); - - expect(isDeletionInProgress(state)).toBe(false); - }); - }); - - describe('isDeletionSuccessful()', () => { - it('returns false when resource state is uninitialised', () => { - expect(isDeletionSuccessful(initialState)).toBe(false); - }); - - it('returns false when resource state is loading', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'LoadingResourceState', - previousState: { type: 'UninitialisedResourceState' }, - }); - - expect(isDeletionSuccessful(state)).toBe(false); - }); - - it('returns true when resource state is loaded', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'LoadedResourceState', - data: null, - }); - - expect(isDeletionSuccessful(state)).toBe(true); - }); - - it('returns false when resource state is failed', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'FailedResourceState', - error: createServerApiError('Not Found'), - }); - - expect(isDeletionSuccessful(state)).toBe(false); - }); - }); - - describe('getDeletionError()', () => { - it('returns undefined when resource state is uninitialised', () => { - expect(getDeletionError(initialState)).toBeUndefined(); - }); - - it('returns undefined when resource state is loading', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'LoadingResourceState', - previousState: { type: 'UninitialisedResourceState' }, - }); - - expect(getDeletionError(state)).toBeUndefined(); - }); - - it('returns undefined when resource state is loaded', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'LoadedResourceState', - data: null, - }); - - expect(getDeletionError(state)).toBeUndefined(); - }); - - it('returns error when resource state is failed', () => { - const state = createStateWithDeletionSubmissionResourceState({ - type: 'FailedResourceState', - error: createServerApiError('Not Found'), - }); - - expect(getDeletionError(state)).toStrictEqual(createServerApiError('Not Found')); - }); - }); - - describe('getDeletionSubmissionResourceState()', () => { - it('returns submission resource state', () => { - expect(getDeletionSubmissionResourceState(initialState)).toStrictEqual({ - type: 'UninitialisedResourceState', - }); - }); - }); - - describe('getDeletionDialogEntry()', () => { - it('returns undefined when no entry is set', () => { - expect(getDeletionDialogEntry(initialState)).toBeUndefined(); - }); - - it('returns entry when entry is set', () => { - const entry = createSampleTrustedApp(5); - const state = { ...initialState, deletionDialog: { ...initialState.deletionDialog, entry } }; - - expect(getDeletionDialogEntry(state)).toStrictEqual(entry); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts deleted file mode 100644 index 743d8b84760ba..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createSelector } from 'reselect'; -import { ServerApiError } from '../../../../common/types'; -import { - Immutable, - NewTrustedApp, - PolicyData, - TrustedApp, -} from '../../../../../common/endpoint/types'; -import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; - -import { - AsyncResourceState, - getCurrentResourceError, - getLastLoadedResourceState, - isFailedResourceState, - isLoadedResourceState, - isLoadingResourceState, - isOutdatedResourceState, - LoadedResourceState, - Pagination, - TrustedAppsListData, - TrustedAppsListPageLocation, - TrustedAppsListPageState, -} from '../state'; -import { GetPolicyListResponse } from '../../policy/types'; - -export const needsRefreshOfListData = (state: Immutable): boolean => { - const freshDataTimestamp = state.listView.freshDataTimestamp; - const currentPage = state.listView.listResourceState; - const location = state.location; - const forceRefresh = state.forceRefresh; - return ( - Boolean(state.active) && - (forceRefresh || - isOutdatedResourceState(currentPage, (data) => { - return ( - data.pageIndex === location.page_index && - data.pageSize === location.page_size && - data.timestamp >= freshDataTimestamp - ); - })) - ); -}; - -export const getListResourceState = ( - state: Immutable -): Immutable> | undefined => { - return state.listView.listResourceState; -}; - -export const getLastLoadedListResourceState = ( - state: Immutable -): Immutable> | undefined => { - return getLastLoadedResourceState(state.listView.listResourceState); -}; - -export const getListItems = ( - state: Immutable -): Immutable => { - return getLastLoadedResourceState(state.listView.listResourceState)?.data.items || []; -}; - -export const getCurrentLocationPageIndex = (state: Immutable): number => { - return state.location.page_index; -}; - -export const getCurrentLocationPageSize = (state: Immutable): number => { - return state.location.page_size; -}; - -export const getCurrentLocationFilter = (state: Immutable): string => { - return state.location.filter; -}; - -export const getCurrentLocationIncludedPolicies = ( - state: Immutable -): string => { - return state.location.included_policies; -}; - -export const getListTotalItemsCount = (state: Immutable): number => { - return getLastLoadedResourceState(state.listView.listResourceState)?.data.totalItemsCount || 0; -}; - -export const getListPagination = (state: Immutable): Pagination => { - const lastLoadedResourceState = getLastLoadedResourceState(state.listView.listResourceState); - - return { - pageIndex: state.location.page_index, - pageSize: state.location.page_size, - totalItemCount: lastLoadedResourceState?.data.totalItemsCount || 0, - pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], - }; -}; - -export const getCurrentLocation = ( - state: Immutable -): TrustedAppsListPageLocation => state.location; - -export const getListErrorMessage = ( - state: Immutable -): string | undefined => { - return getCurrentResourceError(state.listView.listResourceState)?.message; -}; - -export const isListLoading = (state: Immutable): boolean => { - return isLoadingResourceState(state.listView.listResourceState); -}; - -export const isDeletionDialogOpen = (state: Immutable): boolean => { - return state.deletionDialog.entry !== undefined; -}; - -export const isDeletionInProgress = (state: Immutable): boolean => { - return isLoadingResourceState(state.deletionDialog.submissionResourceState); -}; - -export const isDeletionSuccessful = (state: Immutable): boolean => { - return isLoadedResourceState(state.deletionDialog.submissionResourceState); -}; - -export const getDeletionError = ( - state: Immutable -): Immutable | undefined => { - const submissionResourceState = state.deletionDialog.submissionResourceState; - - return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined; -}; - -export const getDeletionSubmissionResourceState = ( - state: Immutable -): AsyncResourceState => { - return state.deletionDialog.submissionResourceState; -}; - -export const getDeletionDialogEntry = ( - state: Immutable -): Immutable | undefined => { - return state.deletionDialog.entry; -}; - -export const isCreationDialogLocation = (state: Immutable): boolean => { - return !!state.location.show; -}; - -export const getCreationSubmissionResourceState = ( - state: Immutable -): Immutable> => { - return state.creationDialog.submissionResourceState; -}; - -export const getCreationDialogFormEntry = ( - state: Immutable -): Immutable | undefined => { - return state.creationDialog.formState?.entry; -}; - -export const isCreationDialogFormValid = (state: Immutable): boolean => { - return state.creationDialog.formState?.isValid || false; -}; - -export const isCreationInProgress = (state: Immutable): boolean => { - return isLoadingResourceState(state.creationDialog.submissionResourceState); -}; - -export const isCreationSuccessful = (state: Immutable): boolean => { - return isLoadedResourceState(state.creationDialog.submissionResourceState); -}; - -export const getCreationError = ( - state: Immutable -): Immutable | undefined => { - const submissionResourceState = state.creationDialog.submissionResourceState; - - return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined; -}; - -export const entriesExistState: ( - state: Immutable -) => Immutable = (state) => state.entriesExist; - -export const checkingIfEntriesExist: (state: Immutable) => boolean = - createSelector(entriesExistState, (doEntriesExists) => { - return !isLoadedResourceState(doEntriesExists); - }); - -export const entriesExist: (state: Immutable) => boolean = createSelector( - entriesExistState, - (doEntriesExists) => { - return isLoadedResourceState(doEntriesExists) && doEntriesExists.data; - } -); - -export const prevEntriesExist: (state: Immutable) => boolean = - createSelector(entriesExistState, (doEntriesExists) => { - return ( - isLoadingResourceState(doEntriesExists) && !!getLastLoadedResourceState(doEntriesExists)?.data - ); - }); - -export const trustedAppsListPageActive: (state: Immutable) => boolean = ( - state -) => state.active; - -export const policiesState = ( - state: Immutable -): Immutable => state.policies; - -export const loadingPolicies: (state: Immutable) => boolean = - createSelector(policiesState, (policies) => isLoadingResourceState(policies)); - -export const listOfPolicies: ( - state: Immutable -) => Immutable = createSelector(policiesState, (policies) => { - return isLoadedResourceState(policies) ? policies.data.items : []; -}); - -export const isLoadingListOfPolicies: (state: Immutable) => boolean = - createSelector(policiesState, (policies) => { - return isLoadingResourceState(policies); - }); - -export const getMapOfPoliciesById: ( - state: Immutable -) => Immutable>> = createSelector( - listOfPolicies, - (policies) => { - return policies.reduce>>((mapById, policy) => { - mapById[policy.id] = policy; - return mapById; - }, {}) as Immutable>>; - } -); - -export const isEdit: (state: Immutable) => boolean = createSelector( - getCurrentLocation, - ({ show }) => { - return show === 'edit'; - } -); - -export const editItemId: (state: Immutable) => string | undefined = - createSelector(getCurrentLocation, ({ id }) => { - return id; - }); - -export const editItemState: ( - state: Immutable -) => Immutable['creationDialog']['editItem'] = (state) => { - return state.creationDialog.editItem; -}; - -export const isFetchingEditTrustedAppItem: (state: Immutable) => boolean = - createSelector(editItemState, (editTrustedAppState) => { - return editTrustedAppState ? isLoadingResourceState(editTrustedAppState) : false; - }); - -export const editTrustedAppFetchError: ( - state: Immutable -) => ServerApiError | undefined = createSelector(editItemState, (itemForEditState) => { - return itemForEditState && getCurrentResourceError(itemForEditState); -}); - -export const editingTrustedApp: ( - state: Immutable -) => undefined | Immutable = createSelector(editItemState, (editTrustedAppState) => { - if (editTrustedAppState && isLoadedResourceState(editTrustedAppState)) { - return editTrustedAppState.data; - } -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts deleted file mode 100644 index 32e1867db567c..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { combineReducers, createStore } from 'redux'; -import { OperatingSystem } from '@kbn/securitysolution-utils'; -import { TrustedApp } from '../../../../../common/endpoint/types'; -import { RoutingAction } from '../../../../common/store/routing'; - -import { - MANAGEMENT_DEFAULT_PAGE, - MANAGEMENT_DEFAULT_PAGE_SIZE, - MANAGEMENT_PAGE_SIZE_OPTIONS, - MANAGEMENT_STORE_GLOBAL_NAMESPACE, - MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, -} from '../../../common/constants'; - -import { - AsyncResourceState, - FailedResourceState, - LoadedResourceState, - LoadingResourceState, - Pagination, - StaleResourceState, - TrustedAppsListData, - TrustedAppsListPageState, - UninitialisedResourceState, -} from '../state'; - -import { trustedAppsPageReducer } from '../store/reducer'; -import { TrustedAppsListResourceStateChanged } from '../store/action'; - -const OPERATING_SYSTEMS: OperatingSystem[] = [ - OperatingSystem.WINDOWS, - OperatingSystem.MAC, - OperatingSystem.LINUX, -]; - -const generate = (count: number, generator: (i: number) => T) => - [...new Array(count).keys()].map(generator); - -export const createSampleTrustedApp = (i: number, longTexts?: boolean): TrustedApp => { - return { - id: String(i), - version: 'abc123', - name: generate(longTexts ? 10 : 1, () => `trusted app ${i}`).join(' '), - description: generate(longTexts ? 10 : 1, () => `Trusted App ${i}`).join(' '), - created_at: '1 minute ago', - created_by: 'someone', - updated_at: '1 minute ago', - updated_by: 'someone', - os: OPERATING_SYSTEMS[i % 3], - entries: [], - effectScope: { type: 'global' }, - }; -}; - -export const createSampleTrustedApps = ( - pagination: Partial, - longTexts?: boolean -): TrustedApp[] => { - const fullPagination = { ...createDefaultPagination(), ...pagination }; - - return generate(fullPagination.pageSize, (i: number) => createSampleTrustedApp(i, longTexts)); -}; - -export const createTrustedAppsListData = ( - pagination: Partial, - timestamp: number, - longTexts?: boolean -) => { - const fullPagination = { ...createDefaultPagination(), ...pagination }; - - return { - items: createSampleTrustedApps(fullPagination, longTexts), - pageSize: fullPagination.pageSize, - pageIndex: fullPagination.pageIndex, - totalItemsCount: fullPagination.totalItemCount, - timestamp, - filter: '', - includedPolicies: '', - }; -}; - -export const createServerApiError = (message: string) => ({ - statusCode: 500, - error: 'Internal Server Error', - message, -}); - -export const createUninitialisedResourceState = (): UninitialisedResourceState => ({ - type: 'UninitialisedResourceState', -}); - -export const createListLoadedResourceState = ( - pagination: Partial, - timestamp: number, - longTexts?: boolean -): LoadedResourceState => ({ - type: 'LoadedResourceState', - data: createTrustedAppsListData(pagination, timestamp, longTexts), -}); - -export const createListFailedResourceState = ( - message: string, - lastLoadedState?: LoadedResourceState -): FailedResourceState => ({ - type: 'FailedResourceState', - error: createServerApiError(message), - lastLoadedState, -}); - -export const createListLoadingResourceState = ( - previousState: StaleResourceState = createUninitialisedResourceState() -): LoadingResourceState => ({ - type: 'LoadingResourceState', - previousState, -}); - -export const createListComplexLoadingResourceState = ( - pagination: Partial, - timestamp: number -): LoadingResourceState => - createListLoadingResourceState( - createListFailedResourceState( - 'Internal Server Error', - createListLoadedResourceState(pagination, timestamp) - ) - ); - -export const createDefaultPagination = (): Pagination => ({ - pageIndex: MANAGEMENT_DEFAULT_PAGE, - pageSize: MANAGEMENT_DEFAULT_PAGE_SIZE, - totalItemCount: 200, - pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], -}); - -export const createLoadedListViewWithPagination = ( - freshDataTimestamp: number, - pagination: Partial = createDefaultPagination() -): TrustedAppsListPageState['listView'] => ({ - listResourceState: createListLoadedResourceState(pagination, freshDataTimestamp), - freshDataTimestamp, -}); - -export const createUserChangedUrlAction = (path: string, search: string = ''): RoutingAction => { - return { type: 'userChangedUrl', payload: { pathname: path, search, hash: '' } }; -}; - -export const createTrustedAppsListResourceStateChangedAction = ( - newState: AsyncResourceState -): TrustedAppsListResourceStateChanged => ({ - type: 'trustedAppsListResourceStateChanged', - payload: { newState }, -}); - -export const createGlobalNoMiddlewareStore = () => { - return createStore( - combineReducers({ - [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({ - [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer, - }), - }) - ); -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap deleted file mode 100644 index 070f1b9eabe23..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap +++ /dev/null @@ -1,414 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TrustedAppDeletionDialog renders correctly initially 1`] = ` - -
- -`; - -exports[`TrustedAppDeletionDialog renders correctly when deletion failed 1`] = ` - -
-
-
-
- -
-
-
- Delete " - - trusted app 3 - - " -
-
-
-
-
-
-
-
-
-

- Deleting this entry will remove it from all associated policies. -

-
-
-
-
-
-

- This action cannot be undone. Are you sure you wish to continue? -

-
-
-
-
- - -
-
-
-
-
- -`; - -exports[`TrustedAppDeletionDialog renders correctly when deletion is in progress 1`] = ` - -
-
-
-
- -
-
-
- Delete " - - trusted app 3 - - " -
-
-
-
-
-
-
-
-
-

- Deleting this entry will remove it from all associated policies. -

-
-
-
-
-
-

- This action cannot be undone. Are you sure you wish to continue? -

-
-
-
-
- - -
-
-
-
-
- -`; - -exports[`TrustedAppDeletionDialog renders correctly when dialog started 1`] = ` - -
-
-
-
- -
-
-
- Delete " - - trusted app 3 - - " -
-
-
-
-
-
-
-
-
-

- Deleting this entry will remove it from all associated policies. -

-
-
-
-
-
-

- This action cannot be undone. Are you sure you wish to continue? -

-
-
-
-
- - -
-
-
-
-
- -`; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx deleted file mode 100644 index f76ac89474e7b..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiLink, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import React, { memo, useCallback, useEffect, useState, useMemo } from 'react'; -import { EuiFlyoutProps } from '@elastic/eui/src/components/flyout/flyout'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; -import { CreateTrustedAppForm, CreateTrustedAppFormProps } from './create_trusted_app_form'; -import { - editTrustedAppFetchError, - getCreationDialogFormEntry, - getCreationError, - getCurrentLocation, - isCreationDialogFormValid, - isCreationInProgress, - isCreationSuccessful, - isEdit, - listOfPolicies, - loadingPolicies, -} from '../../store/selectors'; -import { AppAction } from '../../../../../common/store/actions'; -import { useTrustedAppsSelector } from '../hooks'; - -import { ABOUT_TRUSTED_APPS } from '../translations'; -import { defaultNewTrustedApp } from '../../store/builders'; -import { getTrustedAppsListPath } from '../../../../common/routing'; -import { useKibana, useToasts } from '../../../../../common/lib/kibana'; -import { useTestIdGenerator } from '../../../../components/hooks/use_test_id_generator'; -import { useLicense } from '../../../../../common/hooks/use_license'; -import { isGlobalEffectScope } from '../../state/type_guards'; -import { NewTrustedApp } from '../../../../../../common/endpoint/types'; - -export type CreateTrustedAppFlyoutProps = Omit; -export const CreateTrustedAppFlyout = memo( - ({ onClose, ...flyoutProps }) => { - const dispatch = useDispatch<(action: AppAction) => void>(); - const history = useHistory(); - const toasts = useToasts(); - - const creationInProgress = useTrustedAppsSelector(isCreationInProgress); - const creationErrors = useTrustedAppsSelector(getCreationError); - const creationSuccessful = useTrustedAppsSelector(isCreationSuccessful); - const isFormValid = useTrustedAppsSelector(isCreationDialogFormValid); - const isLoadingPolicies = useTrustedAppsSelector(loadingPolicies); - const policyList = useTrustedAppsSelector(listOfPolicies); - const isEditMode = useTrustedAppsSelector(isEdit); - const trustedAppFetchError = useTrustedAppsSelector(editTrustedAppFetchError); - const formValues = useTrustedAppsSelector(getCreationDialogFormEntry) || defaultNewTrustedApp(); - const location = useTrustedAppsSelector(getCurrentLocation); - const isPlatinumPlus = useLicense().isPlatinumPlus(); - const docLinks = useKibana().services.docLinks; - const [isFormDirty, setIsFormDirty] = useState(false); - - const dataTestSubj = flyoutProps['data-test-subj']; - - const policies = useMemo(() => { - return { - // Casting is needed due to the use of `Immutable<>` on the return value from the selector above - options: policyList as CreateTrustedAppFormProps['policies']['options'], - isLoading: isLoadingPolicies, - }; - }, [isLoadingPolicies, policyList]); - - const creationErrorsMessage = useMemo(() => { - return creationErrors?.message ?? []; - }, [creationErrors]); - - const getTestId = useTestIdGenerator(dataTestSubj); - - const handleCancelClick = useCallback(() => { - if (creationInProgress) { - return; - } - onClose(); - }, [onClose, creationInProgress]); - - const handleSaveClick = useCallback( - () => dispatch({ type: 'trustedAppCreationDialogConfirmed' }), - [dispatch] - ); - - const handleFormOnChange = useCallback( - (newFormState) => { - dispatch({ - type: 'trustedAppCreationDialogFormStateUpdated', - payload: { entry: newFormState.item, isValid: newFormState.isValid }, - }); - if (_.isEqual(formValues, newFormState.item) === false) { - setIsFormDirty(true); - } - }, - - [dispatch, formValues] - ); - - const [wasByPolicy, setWasByPolicy] = useState(!isGlobalEffectScope(formValues.effectScope)); - // set initial state of `wasByPolicy` that checks if the initial state of the exception was by policy or not - useEffect(() => { - if (!isFormDirty && formValues.effectScope) { - setWasByPolicy(!isGlobalEffectScope(formValues.effectScope)); - } - }, [isFormDirty, formValues.effectScope]); - - const isGlobal = useMemo(() => { - return isGlobalEffectScope((formValues as NewTrustedApp).effectScope); - }, [formValues]); - - const showExpiredLicenseBanner = useMemo(() => { - return !isPlatinumPlus && isEditMode && wasByPolicy && (!isGlobal || isFormDirty); - }, [isPlatinumPlus, isEditMode, isGlobal, isFormDirty, wasByPolicy]); - - // If there was a failure trying to retrieve the Trusted App for edit item, - // then redirect back to the list ++ show toast message. - useEffect(() => { - if (trustedAppFetchError) { - // Replace the current URL route so that user does not keep hitting this page via browser back/fwd buttons - history.replace( - getTrustedAppsListPath({ - ...location, - show: undefined, - id: undefined, - }) - ); - - toasts.addWarning( - i18n.translate( - 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.notFoundToastMessage', - { - defaultMessage: 'Unable to edit trusted application ({apiMsg})', - values: { - apiMsg: trustedAppFetchError.message, - }, - } - ) - ); - } - }, [history, location, toasts, trustedAppFetchError]); - - // If it was created, then close flyout - useEffect(() => { - if (creationSuccessful) { - onClose(); - } - }, [onClose, creationSuccessful]); - - return ( - - - -

- {isEditMode ? ( - - ) : ( - - )} -

-
-
- {showExpiredLicenseBanner && ( - - - - - - - )} - - -

- {i18n.translate('xpack.securitySolution.trustedApps.detailsSectionTitle', { - defaultMessage: 'Details', - })} -

-
- - {!isEditMode && ( - <> - -

{ABOUT_TRUSTED_APPS}

-
- - - )} - -
- - - - - - - - - - - {isEditMode ? ( - - ) : ( - - )} - - - - -
- ); - } -); - -CreateTrustedAppFlyout.displayName = 'NewTrustedAppFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx deleted file mode 100644 index 8e7246962b5ee..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ /dev/null @@ -1,623 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { ChangeEventHandler, memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { - EuiFieldText, - EuiForm, - EuiFormRow, - EuiHorizontalRule, - EuiSuperSelect, - EuiSuperSelectOption, - EuiTextArea, - EuiText, - EuiSpacer, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - hasSimpleExecutableName, - isPathValid, - ConditionEntryField, - OperatingSystem, -} from '@kbn/securitysolution-utils'; -import { EuiFormProps } from '@elastic/eui/src/components/form/form'; - -import { - TrustedAppConditionEntry, - EffectScope, - MacosLinuxConditionEntry, - MaybeImmutable, - NewTrustedApp, -} from '../../../../../../common/endpoint/types'; -import { - isValidHash, - getDuplicateFields, -} from '../../../../../../common/endpoint/service/trusted_apps/validations'; - -import { - isGlobalEffectScope, - isMacosLinuxTrustedAppCondition, - isPolicyEffectScope, - isWindowsTrustedAppCondition, -} from '../../state/type_guards'; -import { defaultConditionEntry } from '../../store/builders'; -import { CONDITION_FIELD_TITLE, OS_TITLES } from '../translations'; -import { LogicalConditionBuilder, LogicalConditionBuilderProps } from './logical_condition'; -import { useTestIdGenerator } from '../../../../components/hooks/use_test_id_generator'; -import { useLicense } from '../../../../../common/hooks/use_license'; -import { - EffectedPolicySelect, - EffectedPolicySelection, - EffectedPolicySelectProps, -} from '../../../../components/effected_policy_select'; - -const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ - OperatingSystem.MAC, - OperatingSystem.WINDOWS, - OperatingSystem.LINUX, -]; - -interface FieldValidationState { - /** If this fields state is invalid. Drives display of errors on the UI */ - isInvalid: boolean; - errors: React.ReactNode[]; - warnings: React.ReactNode[]; -} -interface ValidationResult { - /** Overall indicator if form is valid */ - isValid: boolean; - - /** Individual form field validations */ - result: Partial<{ - [key in keyof NewTrustedApp]: FieldValidationState; - }>; -} - -const addResultToValidation = ( - validation: ValidationResult, - field: keyof NewTrustedApp, - type: 'warnings' | 'errors', - resultValue: React.ReactNode -) => { - if (!validation.result[field]) { - validation.result[field] = { - isInvalid: false, - errors: [], - warnings: [], - }; - } - const errorMarkup: React.ReactNode = type === 'warnings' ?
{resultValue}
: resultValue; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - validation.result[field]![type].push(errorMarkup); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - validation.result[field]!.isInvalid = true; -}; - -const validateFormValues = (values: MaybeImmutable): ValidationResult => { - let isValid: ValidationResult['isValid'] = true; - const validation: ValidationResult = { - isValid, - result: {}, - }; - - // Name field - if (!values.name.trim()) { - isValid = false; - addResultToValidation( - validation, - 'name', - 'errors', - i18n.translate('xpack.securitySolution.trustedapps.create.nameRequiredMsg', { - defaultMessage: 'Name is required', - }) - ); - } - - if (!values.os) { - isValid = false; - addResultToValidation( - validation, - 'os', - 'errors', - i18n.translate('xpack.securitySolution.trustedapps.create.osRequiredMsg', { - defaultMessage: 'Operating System is required', - }) - ); - } - - if (!values.entries.length) { - isValid = false; - addResultToValidation( - validation, - 'entries', - 'errors', - i18n.translate('xpack.securitySolution.trustedapps.create.conditionRequiredMsg', { - defaultMessage: 'At least one Field definition is required', - }) - ); - } else { - const duplicated = getDuplicateFields(values.entries as TrustedAppConditionEntry[]); - if (duplicated.length) { - isValid = false; - duplicated.forEach((field) => { - addResultToValidation( - validation, - 'entries', - 'errors', - i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldDuplicatedMsg', { - defaultMessage: '{field} cannot be added more than once', - values: { field: CONDITION_FIELD_TITLE[field] }, - }) - ); - }); - } - values.entries.forEach((entry, index) => { - const isValidPathEntry = isPathValid({ - os: values.os, - field: entry.field, - type: entry.type, - value: entry.value, - }); - - if (!entry.field || !entry.value.trim()) { - isValid = false; - addResultToValidation( - validation, - 'entries', - 'errors', - i18n.translate( - 'xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg', - { - defaultMessage: '[{row}] Field entry must have a value', - values: { row: index + 1 }, - } - ) - ); - } else if (entry.field === ConditionEntryField.HASH && !isValidHash(entry.value)) { - isValid = false; - addResultToValidation( - validation, - 'entries', - 'errors', - i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldInvalidHashMsg', { - defaultMessage: '[{row}] Invalid hash value', - values: { row: index + 1 }, - }) - ); - } else if (!isValidPathEntry) { - addResultToValidation( - validation, - 'entries', - 'warnings', - i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldInvalidPathMsg', { - defaultMessage: '[{row}] Path may be formed incorrectly; verify value', - values: { row: index + 1 }, - }) - ); - } else if ( - isValidPathEntry && - !hasSimpleExecutableName({ os: values.os, value: entry.value, type: entry.type }) - ) { - addResultToValidation( - validation, - 'entries', - 'warnings', - i18n.translate( - 'xpack.securitySolution.trustedapps.create.conditionFieldDegradedPerformanceMsg', - { - defaultMessage: `[{row}] A wildcard in the filename will affect the endpoint's performance`, - values: { row: index + 1 }, - } - ) - ); - } - }); - } - - validation.isValid = isValid; - return validation; -}; - -export interface TrustedAppFormState { - isValid: boolean; - item: NewTrustedApp; -} - -export type CreateTrustedAppFormProps = Pick< - EuiFormProps, - 'className' | 'data-test-subj' | 'isInvalid' | 'error' | 'invalidCallout' -> & { - /** The trusted app values that will be passed to the form */ - trustedApp: MaybeImmutable; - isEditMode: boolean; - isDirty: boolean; - wasByPolicy: boolean; - onChange: (state: TrustedAppFormState) => void; - /** Setting passed on to the EffectedPolicySelect component */ - policies: Pick; - /** if form should be shown full width of parent container */ - fullWidth?: boolean; -}; -export const CreateTrustedAppForm = memo( - ({ - fullWidth, - isEditMode, - isDirty, - wasByPolicy, - onChange, - trustedApp: _trustedApp, - policies = { options: [] }, - ...formProps - }) => { - const trustedApp = _trustedApp as NewTrustedApp; - - const dataTestSubj = formProps['data-test-subj']; - - const isPlatinumPlus = useLicense().isPlatinumPlus(); - - const isGlobal = useMemo(() => { - return isGlobalEffectScope(trustedApp.effectScope); - }, [trustedApp]); - - const showAssignmentSection = useMemo(() => { - return isPlatinumPlus || (isEditMode && (!isGlobal || (wasByPolicy && isGlobal && isDirty))); - }, [isEditMode, isGlobal, isDirty, isPlatinumPlus, wasByPolicy]); - - const osOptions: Array> = useMemo( - () => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })), - [] - ); - - // We create local state for the list of policies because we want the selected policies to - // persist while the user is on the form and possibly toggling between global/non-global - const [selectedPolicies, setSelectedPolicies] = useState({ - isGlobal, - selected: [], - }); - - const [validationResult, setValidationResult] = useState(() => - validateFormValues(trustedApp) - ); - - const [wasVisited, setWasVisited] = useState< - Partial<{ - [key in keyof NewTrustedApp]: boolean; - }> - >({}); - - const getTestId = useTestIdGenerator(dataTestSubj); - - const notifyOfChange = useCallback( - (updatedFormValues: TrustedAppFormState['item']) => { - const updatedValidationResult = validateFormValues(updatedFormValues); - - setValidationResult(updatedValidationResult); - - onChange({ - item: updatedFormValues, - isValid: updatedValidationResult.isValid, - }); - }, - [onChange] - ); - - const handleAndClick = useCallback(() => { - if (trustedApp.os === OperatingSystem.WINDOWS) { - notifyOfChange({ - ...trustedApp, - entries: [...trustedApp.entries, defaultConditionEntry()].filter( - isWindowsTrustedAppCondition - ), - }); - } else { - notifyOfChange({ - ...trustedApp, - entries: [ - ...trustedApp.entries.filter(isMacosLinuxTrustedAppCondition), - defaultConditionEntry(), - ], - }); - } - }, [notifyOfChange, trustedApp]); - - const handleDomChangeEvents = useCallback< - ChangeEventHandler - >( - ({ target: { name, value } }) => { - notifyOfChange({ - ...trustedApp, - [name]: value, - }); - }, - [notifyOfChange, trustedApp] - ); - - // Handles keeping track if an input form field has been visited - const handleDomBlurEvents = useCallback>( - ({ target: { name } }) => { - setWasVisited((prevState) => { - return { - ...prevState, - [name]: true, - }; - }); - }, - [] - ); - - const handleOsChange = useCallback<(v: OperatingSystem) => void>( - (newOsValue) => { - setWasVisited((prevState) => { - return { - ...prevState, - os: true, - }; - }); - - const updatedState: NewTrustedApp = { - ...trustedApp, - entries: [], - os: newOsValue, - }; - if (updatedState.os !== OperatingSystem.WINDOWS) { - updatedState.entries.push( - ...(trustedApp.entries.filter((entry) => - isMacosLinuxTrustedAppCondition(entry) - ) as MacosLinuxConditionEntry[]) - ); - if (updatedState.entries.length === 0) { - updatedState.entries.push(defaultConditionEntry()); - } - } else { - updatedState.entries.push(...trustedApp.entries); - } - - notifyOfChange(updatedState); - }, - [notifyOfChange, trustedApp] - ); - - const handleEntryRemove = useCallback( - (entry: NewTrustedApp['entries'][0]) => { - notifyOfChange({ - ...trustedApp, - entries: trustedApp.entries.filter((item) => item !== entry), - } as NewTrustedApp); - }, - [notifyOfChange, trustedApp] - ); - - const handleEntryChange = useCallback( - (newEntry, oldEntry) => { - if (trustedApp.os === OperatingSystem.WINDOWS) { - notifyOfChange({ - ...trustedApp, - entries: trustedApp.entries.map((item) => { - if (item === oldEntry) { - return newEntry; - } - return item; - }), - } as NewTrustedApp); - } else { - notifyOfChange({ - ...trustedApp, - entries: trustedApp.entries.map((item) => { - if (item === oldEntry) { - return newEntry; - } - return item; - }), - } as NewTrustedApp); - } - }, - [notifyOfChange, trustedApp] - ); - - const handleConditionBuilderOnVisited: LogicalConditionBuilderProps['onVisited'] = - useCallback(() => { - setWasVisited((prevState) => { - return { - ...prevState, - entries: true, - }; - }); - }, []); - - const handlePolicySelectChange: EffectedPolicySelectProps['onChange'] = useCallback( - (selection) => { - setSelectedPolicies(() => selection); - - let newEffectedScope: EffectScope; - - if (selection.isGlobal) { - newEffectedScope = { - type: 'global', - }; - } else { - newEffectedScope = { - type: 'policy', - policies: selection.selected.map((policy) => policy.id), - }; - } - - notifyOfChange({ - ...trustedApp, - effectScope: newEffectedScope, - }); - }, - [notifyOfChange, trustedApp] - ); - - // Anytime the form values change, re-validate - useEffect(() => { - setValidationResult((prevState) => { - const newResults = validateFormValues(trustedApp); - - // Only notify if the overall validation result is different - if (newResults.isValid !== prevState.isValid) { - notifyOfChange(trustedApp); - } - - return newResults; - }); - }, [notifyOfChange, trustedApp]); - - // Anytime the TrustedApp has an effective scope of `policies`, then ensure that - // those polices are selected in the UI while at the same time preserving prior - // selections (UX requirement) - useEffect(() => { - setSelectedPolicies((currentSelection) => { - if (isPolicyEffectScope(trustedApp.effectScope) && policies.options.length > 0) { - const missingSelectedPolicies: EffectedPolicySelectProps['selected'] = []; - - for (const policyId of trustedApp.effectScope.policies) { - if ( - !currentSelection.selected.find( - (currentlySelectedPolicyItem) => currentlySelectedPolicyItem.id === policyId - ) - ) { - const newSelectedPolicy = policies.options.find((policy) => policy.id === policyId); - if (newSelectedPolicy) { - missingSelectedPolicies.push(newSelectedPolicy); - } - } - } - - if (missingSelectedPolicies.length) { - return { - ...currentSelection, - selected: [...currentSelection.selected, ...missingSelectedPolicies], - }; - } - } - - return currentSelection; - }); - }, [policies.options, trustedApp.effectScope]); - - return ( - - - - - - - - - -

- {i18n.translate('xpack.securitySolution.trustedApps.conditionsSectionTitle', { - defaultMessage: 'Conditions', - })} -

-
- - -

- {i18n.translate('xpack.securitySolution.trustedApps.conditionsSectionDescription', { - defaultMessage: - 'Select an operating system and add conditions. Availability of conditions may depend on your chosen OS.', - })} -

-
- - - - - - - - {showAssignmentSection ? ( - <> - - - - - - ) : null} -
- ); - } -); - -CreateTrustedAppForm.displayName = 'NewTrustedAppForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.test.tsx similarity index 52% rename from x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx rename to x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.test.tsx index 68dd43fa41152..dca86557f6309 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.test.tsx @@ -6,25 +6,30 @@ */ import React from 'react'; -import * as reactTestingLibrary from '@testing-library/react'; -import { fireEvent, getByTestId } from '@testing-library/dom'; +import { screen, cleanup, act, fireEvent, getByTestId } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { + TrustedAppEntryTypes, + OperatingSystem, + ConditionEntryField, +} from '@kbn/securitysolution-utils'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; -import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; -import { NewTrustedApp } from '../../../../../../common/endpoint/types'; +import { TrustedAppsForm } from './form'; +import { + ArtifactFormComponentOnChangeCallbackProps, + ArtifactFormComponentProps, +} from '../../../../components/artifact_list_page'; import { AppContextTestRender, createAppRootMockRenderer, } from '../../../../../common/mock/endpoint'; - -import { CreateTrustedAppForm, CreateTrustedAppFormProps } from './create_trusted_app_form'; -import { defaultNewTrustedApp } from '../../store/builders'; -import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; -import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { INPUT_ERRORS } from '../translations'; import { licenseService } from '../../../../../common/hooks/use_license'; import { forceHTMLElementOffsetWidth } from '../../../../components/effected_policy_select/test_utils'; +import type { PolicyData, TrustedAppConditionEntry } from '../../../../../../common/endpoint/types'; -jest.mock('../../../../../common/hooks/use_experimental_features'); -const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; +import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; jest.mock('../../../../../common/hooks/use_license', () => { const licenseServiceInstance = { @@ -38,55 +43,108 @@ jest.mock('../../../../../common/hooks/use_license', () => { }; }); -describe('When using the Trusted App Form', () => { - const dataTestSubjForForm = 'createForm'; +describe('Trusted apps form', () => { + const formPrefix = 'trustedApps-form'; const generator = new EndpointDocGenerator('effected-policy-select'); - let resetHTMLElementOffsetWidth: ReturnType; + let formProps: jest.Mocked; let mockedContext: AppContextTestRender; - let formProps: jest.Mocked; let renderResult: ReturnType; + let latestUpdatedItem: ArtifactFormComponentProps['item']; - // As the form's `onChange()` callback is executed, this variable will - // hold the latest updated trusted app. Use it to re-render - let latestUpdatedTrustedApp: NewTrustedApp; - - const getUI = () => ; + const getUI = () => ; const render = () => { return (renderResult = mockedContext.render(getUI())); }; const rerender = () => renderResult.rerender(getUI()); - const rerenderWithLatestTrustedApp = () => { - formProps.trustedApp = latestUpdatedTrustedApp; + const rerenderWithLatestProps = () => { + formProps.item = latestUpdatedItem; rerender(); }; + function createEntry( + field: T, + type: TrustedAppEntryTypes, + value: string + ): TrustedAppConditionEntry { + return { + field, + type, + operator: 'included', + value, + }; + } + + function createItem( + overrides: Partial = {} + ): ArtifactFormComponentProps['item'] { + const defaults: ArtifactFormComponentProps['item'] = { + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + name: '', + description: '', + os_types: [OperatingSystem.WINDOWS], + entries: [createEntry(ConditionEntryField.HASH, 'match', '')], + type: 'simple', + tags: ['policy:all'], + }; + return { + ...defaults, + ...overrides, + }; + } + + function createOnChangeArgs( + overrides: Partial + ): ArtifactFormComponentOnChangeCallbackProps { + const defaults = { + item: createItem(), + isValid: false, + }; + return { + ...defaults, + ...overrides, + }; + } + + function createPolicies(): PolicyData[] { + const policies = [ + generator.generatePolicyPackagePolicy(), + generator.generatePolicyPackagePolicy(), + ]; + policies.map((p, i) => { + p.id = `id-${i}`; + p.name = `some-policy-${Math.random().toString(36).split('.').pop()}`; + return p; + }); + return policies; + } + // Some helpers const setTextFieldValue = (textField: HTMLInputElement | HTMLTextAreaElement, value: string) => { - reactTestingLibrary.act(() => { + act(() => { fireEvent.change(textField, { target: { value }, }); fireEvent.blur(textField); }); }; - const getNameField = (dataTestSub: string = dataTestSubjForForm): HTMLInputElement => { + const getDetailsBlurb = (dataTestSub: string = formPrefix): HTMLInputElement => { + return renderResult.queryByTestId(`${dataTestSub}-about`) as HTMLInputElement; + }; + const getNameField = (dataTestSub: string = formPrefix): HTMLInputElement => { return renderResult.getByTestId(`${dataTestSub}-nameTextField`) as HTMLInputElement; }; - const getOsField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => { + const getOsField = (dataTestSub: string = formPrefix): HTMLButtonElement => { return renderResult.getByTestId(`${dataTestSub}-osSelectField`) as HTMLButtonElement; }; - const getDescriptionField = (dataTestSub: string = dataTestSubjForForm): HTMLTextAreaElement => { + const getDescriptionField = (dataTestSub: string = formPrefix): HTMLTextAreaElement => { return renderResult.getByTestId(`${dataTestSub}-descriptionField`) as HTMLTextAreaElement; }; - const getCondition = ( - index: number = 0, - dataTestSub: string = dataTestSubjForForm - ): HTMLElement => { + const getCondition = (index: number = 0, dataTestSub: string = formPrefix): HTMLElement => { return renderResult.getByTestId(`${dataTestSub}-conditionsBuilder-group1-entry${index}`); }; - const getAllConditions = (dataTestSub: string = dataTestSubjForForm): HTMLElement[] => { + const getAllConditions = (dataTestSub: string = formPrefix): HTMLElement[] => { return Array.from( renderResult.getByTestId(`${dataTestSub}-conditionsBuilder-group1-entries`).children ) as HTMLElement[]; @@ -97,19 +155,16 @@ describe('When using the Trusted App Form', () => { const getConditionFieldSelect = (condition: HTMLElement): HTMLButtonElement => { return getByTestId(condition, `${condition.dataset.testSubj}-field`) as HTMLButtonElement; }; + const getConditionValue = (condition: HTMLElement): HTMLInputElement => { return getByTestId(condition, `${condition.dataset.testSubj}-value`) as HTMLInputElement; }; - const getConditionBuilderAndButton = ( - dataTestSub: string = dataTestSubjForForm - ): HTMLButtonElement => { + const getConditionBuilderAndButton = (dataTestSub: string = formPrefix): HTMLButtonElement => { return renderResult.getByTestId( `${dataTestSub}-conditionsBuilder-group1-AndButton` ) as HTMLButtonElement; }; - const getConditionBuilderAndConnectorBadge = ( - dataTestSub: string = dataTestSubjForForm - ): HTMLElement => { + const getConditionBuilderAndConnectorBadge = (dataTestSub: string = formPrefix): HTMLElement => { return renderResult.getByTestId(`${dataTestSub}-conditionsBuilder-group1-andConnector`); }; const getAllValidationErrors = (): HTMLElement[] => { @@ -121,42 +176,44 @@ describe('When using the Trusted App Form', () => { beforeEach(() => { resetHTMLElementOffsetWidth = forceHTMLElementOffsetWidth(); - useIsExperimentalFeatureEnabledMock.mockReturnValue(true); (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); - mockedContext = createAppRootMockRenderer(); - - latestUpdatedTrustedApp = defaultNewTrustedApp(); + latestUpdatedItem = createItem(); formProps = { - 'data-test-subj': dataTestSubjForForm, - trustedApp: latestUpdatedTrustedApp, - isEditMode: false, - isDirty: false, - wasByPolicy: false, + item: latestUpdatedItem, + mode: 'create', + disabled: false, + error: undefined, + policiesIsLoading: false, onChange: jest.fn((updates) => { - latestUpdatedTrustedApp = updates.item; + latestUpdatedItem = updates.item; }), - policies: { - options: [], - }, + policies: [], }; }); afterEach(() => { resetHTMLElementOffsetWidth(); - reactTestingLibrary.cleanup(); + cleanup(); }); - describe('and the form is rendered', () => { + describe('Details and Conditions', () => { beforeEach(() => render()); - it('should show Name as required after blur', () => { - expect(getNameField().required).toBe(false); - reactTestingLibrary.act(() => { - fireEvent.blur(getNameField()); - }); - expect(getNameField().required).toBe(true); + it('should NOT initially show any inline validation errors', () => { + expect(renderResult.container.querySelectorAll('.euiFormErrorText').length).toBe(0); + }); + + it('should hide details text when in edit mode', () => { + formProps.mode = 'edit'; + rerenderWithLatestProps(); + expect(getDetailsBlurb()).toBeNull(); + }); + + it('should show name required name blur', () => { + setTextFieldValue(getNameField(), ' '); + expect(renderResult.getByText(INPUT_ERRORS.name)).toBeTruthy(); }); it('should default OS to Windows', () => { @@ -165,42 +222,68 @@ describe('When using the Trusted App Form', () => { it('should allow user to select between 3 OSs', () => { const osField = getOsField(); - reactTestingLibrary.act(() => { - fireEvent.click(osField, { button: 1 }); - }); + userEvent.click(osField, { button: 1 }); const options = Array.from( renderResult.baseElement.querySelectorAll( '.euiSuperSelect__listbox button.euiSuperSelect__item' ) ).map((button) => button.textContent); - expect(options).toEqual(['Mac', 'Windows', 'Linux']); + expect(options).toEqual(['Linux', 'Mac', 'Windows']); }); it('should show Description as optional', () => { expect(getDescriptionField().required).toBe(false); }); - it('should NOT initially show any inline validation errors', () => { - expect(renderResult.container.querySelectorAll('.euiFormErrorText').length).toBe(0); + it('should be invalid if no name', () => { + const emptyName = ' '; + setTextFieldValue(getNameField(), emptyName); + const expected = createOnChangeArgs({ + item: createItem({ + name: emptyName, + }), + }); + expect(formProps.onChange).toHaveBeenCalledWith(expected); }); - it('should show top-level Errors', () => { - formProps.isInvalid = true; - formProps.error = 'a top level error'; - rerender(); - expect(renderResult.queryByText(formProps.error as string)).not.toBeNull(); + it('should correctly edit name', () => { + setTextFieldValue(getNameField(), 'z'); + const expected = createOnChangeArgs({ + item: createItem({ + name: 'z', + }), + }); + expect(formProps.onChange).toHaveBeenCalledWith(expected); + }); + + it('should correctly edit description', () => { + setTextFieldValue(getDescriptionField(), 'describe ta'); + const expected = createOnChangeArgs({ + item: createItem({ description: 'describe ta' }), + }); + expect(formProps.onChange).toHaveBeenCalledWith(expected); + }); + + it('should correctly change OS', () => { + userEvent.click(getOsField()); + userEvent.click(screen.getByRole('option', { name: 'Linux' })); + const expected = createOnChangeArgs({ + item: createItem({ os_types: [OperatingSystem.LINUX] }), + }); + expect(formProps.onChange).toHaveBeenCalledWith(expected); }); }); - describe('the condition builder component', () => { + describe('ConditionBuilder', () => { beforeEach(() => render()); - it('should show an initial condition entry with labels', () => { + it('should default to hash entry field', () => { const defaultCondition = getCondition(); const labels = Array.from(defaultCondition.querySelectorAll('.euiFormRow__labelWrapper')).map( (label) => (label.textContent || '').trim() ); expect(labels).toEqual(['Field', 'Operator', 'Value', '']); + expect(formProps.onChange).not.toHaveBeenCalled(); }); it('should not allow the entry to be removed if its the only one displayed', () => { @@ -210,9 +293,7 @@ describe('When using the Trusted App Form', () => { it('should display 3 options for Field for Windows', () => { const conditionFieldSelect = getConditionFieldSelect(getCondition()); - reactTestingLibrary.act(() => { - fireEvent.click(conditionFieldSelect, { button: 1 }); - }); + userEvent.click(conditionFieldSelect, { button: 1 }); const options = Array.from( renderResult.baseElement.querySelectorAll( '.euiSuperSelect__listbox button.euiSuperSelect__item' @@ -228,12 +309,37 @@ describe('When using the Trusted App Form', () => { it('should show the value field as required after blur', () => { expect(getConditionValue(getCondition()).required).toEqual(false); - reactTestingLibrary.act(() => { + act(() => { fireEvent.blur(getConditionValue(getCondition())); }); expect(getConditionValue(getCondition()).required).toEqual(true); }); + it('should show path malformed warning', () => { + render(); + expect(screen.queryByText(INPUT_ERRORS.pathWarning(0))).toBeNull(); + + const propsItem: Partial = { + entries: [createEntry(ConditionEntryField.PATH, 'match', 'malformed-path')], + }; + formProps.item = { ...formProps.item, ...propsItem }; + render(); + expect(screen.getByText(INPUT_ERRORS.pathWarning(0))).not.toBeNull(); + }); + + it('should show wildcard in path warning', () => { + render(); + expect(screen.queryByText(INPUT_ERRORS.wildcardPathWarning(0))).toBeNull(); + + const propsItem: Partial = { + os_types: [OperatingSystem.LINUX], + entries: [createEntry(ConditionEntryField.PATH, 'wildcard', '/sys/wil*/*.app')], + }; + formProps.item = { ...formProps.item, ...propsItem }; + render(); + expect(screen.getByText(INPUT_ERRORS.wildcardPathWarning(0))).not.toBeNull(); + }); + it('should display the `AND` button', () => { const andButton = getConditionBuilderAndButton(); expect(andButton.textContent).toEqual('AND'); @@ -243,11 +349,9 @@ describe('When using the Trusted App Form', () => { describe('and when the AND button is clicked', () => { beforeEach(() => { const andButton = getConditionBuilderAndButton(); - reactTestingLibrary.act(() => { - fireEvent.click(andButton, { button: 1 }); - }); + userEvent.click(andButton, { button: 1 }); // re-render with updated `newTrustedApp` - formProps.trustedApp = formProps.onChange.mock.calls[0][0].item; + formProps.item = formProps.onChange.mock.calls[0][0].item; rerender(); }); @@ -270,127 +374,105 @@ describe('When using the Trusted App Form', () => { describe('the Policy Selection area', () => { beforeEach(() => { - const policy = generator.generatePolicyPackagePolicy(); - policy.name = 'test policy A'; - policy.id = '123'; - - formProps.policies.options = [policy]; + formProps.policies = createPolicies(); }); it('should have `global` switch on if effective scope is global and policy options hidden', () => { render(); const globalButton = renderResult.getByTestId( - `${dataTestSubjForForm}-effectedPolicies-global` + `${formPrefix}-effectedPolicies-global` ) as HTMLButtonElement; expect(globalButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); - expect(renderResult.queryByTestId('policy-123')).toBeNull(); + expect( + renderResult.queryByTestId(`${formPrefix}-effectedPolicies-policiesSelectable`) + ).toBeNull(); + expect(renderResult.queryByTestId('policy-id-0')).toBeNull(); }); it('should have policy options visible and specific policies checked if scope is per-policy', () => { - (formProps.trustedApp as NewTrustedApp).effectScope = { - type: 'policy', - policies: ['123'], - }; + formProps.item.tags = [formProps.policies.map((p) => `policy:${p.id}`)[0]]; render(); + const perPolicyButton = renderResult.getByTestId( - `${dataTestSubjForForm}-effectedPolicies-perPolicy` + `${formPrefix}-effectedPolicies-perPolicy` ) as HTMLButtonElement; expect(perPolicyButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual('false'); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-checked')).toEqual('true'); + expect(renderResult.getByTestId('policy-id-0').getAttribute('aria-disabled')).toEqual( + 'false' + ); + expect(renderResult.getByTestId('policy-id-0-checkbox')).toBeChecked(); }); + it('should show loader when setting `policies.isLoading` to true and scope is per-policy', () => { - formProps.policies.isLoading = true; - (formProps.trustedApp as NewTrustedApp).effectScope = { - type: 'policy', - policies: ['123'], - }; + formProps.policiesIsLoading = true; + formProps.item.tags = [formProps.policies.map((p) => `policy:${p.id}`)[0]]; render(); expect(renderResult.queryByTestId('loading-spinner')).not.toBeNull(); }); }); - describe('the Policy Selection area under feature flag', () => { - it("shouldn't display the policiy selection area ", () => { - useIsExperimentalFeatureEnabledMock.mockReturnValue(false); - render(); - expect( - renderResult.queryByText('Apply trusted application globally') - ).not.toBeInTheDocument(); - }); - }); - describe('the Policy Selection area when the license downgrades to gold or below', () => { beforeEach(() => { - // select per policy for trusted app - const policy = generator.generatePolicyPackagePolicy(); - policy.name = 'test policy A'; - policy.id = '123'; - - formProps.policies.options = [policy]; - - (formProps.trustedApp as NewTrustedApp).effectScope = { - type: 'policy', - policies: ['123'], - }; - - formProps.isEditMode = true; - + const policies = createPolicies(); + formProps.policies = policies; + formProps.item.tags = [policies.map((p) => `policy:${p.id}`)[0]]; + formProps.mode = 'edit'; // downgrade license (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + render(); }); it('maintains policy configuration but does not allow the user to edit add/remove individual policies in edit mode', () => { - render(); const perPolicyButton = renderResult.getByTestId( - `${dataTestSubjForForm}-effectedPolicies-perPolicy` + `${formPrefix}-effectedPolicies-perPolicy` ) as HTMLButtonElement; - expect(perPolicyButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); - expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual('true'); - expect(renderResult.getByTestId('policy-123-checkbox')).toBeChecked(); + expect(renderResult.getByTestId('policy-id-0').getAttribute('aria-disabled')).toEqual('true'); + expect(renderResult.getByTestId('policy-id-0-checkbox')).toBeChecked(); }); it("allows the user to set the trusted app entry to 'Global' in the edit option", () => { - render(); const globalButtonInput = renderResult.getByTestId('globalPolicy') as HTMLButtonElement; - - reactTestingLibrary.act(() => { + act(() => { fireEvent.click(globalButtonInput); }); - expect(formProps.onChange.mock.calls[0][0].item.effectScope.type).toBe('global'); + const policyItem = formProps.onChange.mock.calls[0][0].item.tags + ? formProps.onChange.mock.calls[0][0].item.tags[0] + : ''; + expect(policyItem).toBe('policy:all'); }); + it('hides the policy assignment section if the TA is set to global', () => { - (formProps.trustedApp as NewTrustedApp).effectScope = { - type: 'global', - }; - expect(renderResult.queryByTestId(`${dataTestSubjForForm}-effectedPolicies`)).toBeNull(); + formProps.item.tags = ['policy:all']; + rerender(); + expect(renderResult.queryByTestId(`${formPrefix}-effectedPolicies`)).toBeNull(); }); it('hides the policy assignment section if the user is adding a new TA', () => { - formProps.isEditMode = false; - expect(renderResult.queryByTestId(`${dataTestSubjForForm}-effectedPolicies`)).toBeNull(); + formProps.mode = 'create'; + rerender(); + expect(renderResult.queryByTestId(`${formPrefix}-effectedPolicies`)).toBeNull(); }); }); describe('and the user visits required fields but does not fill them out', () => { beforeEach(() => { render(); - reactTestingLibrary.act(() => { + act(() => { fireEvent.blur(getNameField()); }); - reactTestingLibrary.act(() => { + act(() => { fireEvent.blur(getConditionValue(getCondition())); }); }); it('should show Name validation error', () => { - expect(renderResult.getByText('Name is required')); + expect(renderResult.getByText(INPUT_ERRORS.name)).not.toBeNull(); }); it('should show Condition validation error', () => { - expect(renderResult.getByText('[1] Field entry must have a value')); + expect(renderResult.getByText(INPUT_ERRORS.mustHaveValue(0))); }); it('should NOT display any other errors', () => { @@ -403,129 +485,107 @@ describe('When using the Trusted App Form', () => { it('should validate that Name has a non empty space value', () => { setTextFieldValue(getNameField(), ' '); - expect(renderResult.getByText('Name is required')); + expect(renderResult.getByText(INPUT_ERRORS.name)); }); it('should validate invalid Hash value', () => { setTextFieldValue(getConditionValue(getCondition()), 'someHASH'); - expect(renderResult.getByText('[1] Invalid hash value')); + expect(renderResult.getByText(INPUT_ERRORS.invalidHash(0))); }); it('should validate that a condition value has a non empty space value', () => { setTextFieldValue(getConditionValue(getCondition()), ' '); - expect(renderResult.getByText('[1] Field entry must have a value')); + expect(renderResult.getByText(INPUT_ERRORS.mustHaveValue(0))); }); it('should validate all condition values (when multiples exist) have non empty space value', () => { const andButton = getConditionBuilderAndButton(); - reactTestingLibrary.act(() => { - fireEvent.click(andButton, { button: 1 }); - }); - rerenderWithLatestTrustedApp(); + userEvent.click(andButton, { button: 1 }); + rerenderWithLatestProps(); setTextFieldValue(getConditionValue(getCondition()), 'someHASH'); - rerenderWithLatestTrustedApp(); + rerenderWithLatestProps(); - expect(renderResult.getByText('[2] Field entry must have a value')); + expect(renderResult.getByText(INPUT_ERRORS.mustHaveValue(1))); }); it('should validate duplicated conditions', () => { const andButton = getConditionBuilderAndButton(); - reactTestingLibrary.act(() => { - fireEvent.click(andButton, { button: 1 }); - }); + userEvent.click(andButton, { button: 1 }); setTextFieldValue(getConditionValue(getCondition()), ''); - rerenderWithLatestTrustedApp(); + rerenderWithLatestProps(); - expect(renderResult.getByText('Hash cannot be added more than once')); + expect(renderResult.getByText(INPUT_ERRORS.noDuplicateField(ConditionEntryField.HASH))); }); it('should validate multiple errors in form', () => { const andButton = getConditionBuilderAndButton(); - reactTestingLibrary.act(() => { - fireEvent.click(andButton, { button: 1 }); - }); - rerenderWithLatestTrustedApp(); + userEvent.click(andButton, { button: 1 }); + rerenderWithLatestProps(); setTextFieldValue(getConditionValue(getCondition()), 'someHASH'); - rerenderWithLatestTrustedApp(); - expect(renderResult.getByText('[1] Invalid hash value')); - expect(renderResult.getByText('[2] Field entry must have a value')); + rerenderWithLatestProps(); + expect(renderResult.getByText(INPUT_ERRORS.invalidHash(0))); + expect(renderResult.getByText(INPUT_ERRORS.mustHaveValue(1))); }); }); describe('and all required data passes validation', () => { it('should call change callback with isValid set to true and contain the new item', () => { + const propsItem: Partial = { + os_types: [OperatingSystem.LINUX], + name: 'Some process', + description: 'some description', + entries: [ + createEntry(ConditionEntryField.HASH, 'match', 'e50fb1a0e5fff590ece385082edc6c41'), + ], + }; + formProps.item = { ...formProps.item, ...propsItem }; render(); - - setTextFieldValue(getNameField(), 'Some Process'); - rerenderWithLatestTrustedApp(); - - setTextFieldValue(getConditionValue(getCondition()), 'e50fb1a0e5fff590ece385082edc6c41'); - rerenderWithLatestTrustedApp(); - - setTextFieldValue(getDescriptionField(), 'some description'); - rerenderWithLatestTrustedApp(); + act(() => { + fireEvent.blur(getNameField()); + }); expect(getAllValidationErrors()).toHaveLength(0); - expect(formProps.onChange).toHaveBeenLastCalledWith({ + const expected = createOnChangeArgs({ isValid: true, - item: { - name: 'Some Process', - description: 'some description', - os: OperatingSystem.WINDOWS, - effectScope: { type: 'global' }, - entries: [ - { - field: ConditionEntryField.HASH, - operator: 'included', - type: 'match', - value: 'e50fb1a0e5fff590ece385082edc6c41', - }, - ], - }, + item: createItem(propsItem), }); + expect(formProps.onChange).toHaveBeenCalledWith(expected); }); it('should not validate form to true if name input is empty', () => { - const props = { + const propsItem: Partial = { name: 'some name', - description: '', - effectScope: { - type: 'global', - }, - os: OperatingSystem.WINDOWS, - entries: [ - { field: ConditionEntryField.PATH, operator: 'included', type: 'wildcard', value: 'x' }, - ], - } as NewTrustedApp; - - formProps.trustedApp = props; + os_types: [OperatingSystem.LINUX], + entries: [createEntry(ConditionEntryField.PATH, 'wildcard', '/sys/usr*/doc.app')], + }; + formProps.item = { ...formProps.item, ...propsItem }; render(); + expect(getAllValidationErrors()).toHaveLength(0); + expect(getAllValidationWarnings()).toHaveLength(0); - formProps.trustedApp = { - ...props, - name: '', - }; + formProps.item.name = ''; rerender(); - - expect(getAllValidationErrors()).toHaveLength(0); - expect(getAllValidationWarnings()).toHaveLength(1); + act(() => { + fireEvent.blur(getNameField()); + }); + expect(getAllValidationErrors()).toHaveLength(1); + expect(getAllValidationWarnings()).toHaveLength(0); expect(formProps.onChange).toHaveBeenLastCalledWith({ isValid: false, item: { + ...formProps.item, name: '', - description: '', - os: OperatingSystem.WINDOWS, - effectScope: { type: 'global' }, + os_types: [OperatingSystem.LINUX], entries: [ { field: ConditionEntryField.PATH, operator: 'included', type: 'wildcard', - value: 'x', + value: '/sys/usr*/doc.app', }, ], }, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.tsx new file mode 100644 index 0000000000000..9656b5821b756 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/form.tsx @@ -0,0 +1,519 @@ +/* + * 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, { ChangeEventHandler, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { + EuiFieldText, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiSuperSelect, + EuiSuperSelectOption, + EuiTextArea, + EuiText, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { + hasSimpleExecutableName, + isPathValid, + ConditionEntryField, + OperatingSystem, + AllConditionEntryFields, + EntryTypes, +} from '@kbn/securitysolution-utils'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import { TrustedAppConditionEntry, NewTrustedApp } from '../../../../../../common/endpoint/types'; +import { + isValidHash, + getDuplicateFields, +} from '../../../../../../common/endpoint/service/artifacts/validations'; + +import { + isArtifactGlobal, + getPolicyIdsFromArtifact, +} from '../../../../../../common/endpoint/service/artifacts'; +import { + isMacosLinuxTrustedAppCondition, + isWindowsTrustedAppCondition, +} from '../../state/type_guards'; + +import { + CONDITIONS_HEADER, + CONDITIONS_HEADER_DESCRIPTION, + DETAILS_HEADER, + DETAILS_HEADER_DESCRIPTION, + DESCRIPTION_LABEL, + INPUT_ERRORS, + NAME_LABEL, + POLICY_SELECT_DESCRIPTION, + SELECT_OS_LABEL, +} from '../translations'; +import { OS_TITLES } from '../../../../common/translations'; +import { LogicalConditionBuilder, LogicalConditionBuilderProps } from './logical_condition'; +import { useTestIdGenerator } from '../../../../components/hooks/use_test_id_generator'; +import { useLicense } from '../../../../../common/hooks/use_license'; +import { + EffectedPolicySelect, + EffectedPolicySelection, +} from '../../../../components/effected_policy_select'; +import { + GLOBAL_ARTIFACT_TAG, + BY_POLICY_ARTIFACT_TAG_PREFIX, +} from '../../../../../../common/endpoint/service/artifacts/constants'; +import type { PolicyData } from '../../../../../../common/endpoint/types'; +import { ArtifactFormComponentProps } from '../../../../components/artifact_list_page'; +import { isGlobalPolicyEffected } from '../../../../components/effected_policy_select/utils'; + +interface FieldValidationState { + /** If this fields state is invalid. Drives display of errors on the UI */ + isInvalid: boolean; + errors: React.ReactNode[]; + warnings: React.ReactNode[]; +} +interface ValidationResult { + /** Overall indicator if form is valid */ + isValid: boolean; + + /** Individual form field validations */ + result: Partial<{ + [key in keyof NewTrustedApp]: FieldValidationState; + }>; +} + +const addResultToValidation = ( + validation: ValidationResult, + field: keyof NewTrustedApp, + type: 'warnings' | 'errors', + resultValue: React.ReactNode +) => { + if (!validation.result[field]) { + validation.result[field] = { + isInvalid: false, + errors: [], + warnings: [], + }; + } + const errorMarkup: React.ReactNode = type === 'warnings' ?
{resultValue}
: resultValue; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + validation.result[field]![type].push(errorMarkup); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + validation.result[field]!.isInvalid = true; +}; + +const validateValues = (values: ArtifactFormComponentProps['item']): ValidationResult => { + let isValid: ValidationResult['isValid'] = true; + const validation: ValidationResult = { + isValid, + result: {}, + }; + + // Name field + if (!values.name.trim()) { + isValid = false; + addResultToValidation(validation, 'name', 'errors', INPUT_ERRORS.name); + } + + if (!values.os_types) { + isValid = false; + addResultToValidation(validation, 'os', 'errors', INPUT_ERRORS.os); + } + + const os = ((values.os_types ?? [])[0] as OperatingSystem) ?? OperatingSystem.WINDOWS; + if (!values.entries.length) { + isValid = false; + addResultToValidation(validation, 'entries', 'errors', INPUT_ERRORS.field); + } else { + const duplicated = getDuplicateFields(values.entries as TrustedAppConditionEntry[]); + if (duplicated.length) { + isValid = false; + duplicated.forEach((field: ConditionEntryField) => { + addResultToValidation( + validation, + 'entries', + 'errors', + INPUT_ERRORS.noDuplicateField(field) + ); + }); + } + values.entries.forEach((entry, index) => { + const isValidPathEntry = isPathValid({ + os, + field: entry.field as AllConditionEntryFields, + type: entry.type as EntryTypes, + value: (entry as TrustedAppConditionEntry).value, + }); + + if (!entry.field || !(entry as TrustedAppConditionEntry).value.trim()) { + isValid = false; + addResultToValidation(validation, 'entries', 'errors', INPUT_ERRORS.mustHaveValue(index)); + } else if ( + entry.field === ConditionEntryField.HASH && + !isValidHash((entry as TrustedAppConditionEntry).value) + ) { + isValid = false; + addResultToValidation(validation, 'entries', 'errors', INPUT_ERRORS.invalidHash(index)); + } else if (!isValidPathEntry) { + addResultToValidation(validation, 'entries', 'warnings', INPUT_ERRORS.pathWarning(index)); + } else if ( + isValidPathEntry && + !hasSimpleExecutableName({ + os, + value: (entry as TrustedAppConditionEntry).value, + type: entry.type as EntryTypes, + }) + ) { + addResultToValidation( + validation, + 'entries', + 'warnings', + INPUT_ERRORS.wildcardPathWarning(index) + ); + } + }); + } + + validation.isValid = isValid; + return validation; +}; + +const defaultConditionEntry = (): TrustedAppConditionEntry => ({ + field: ConditionEntryField.HASH, + operator: 'included', + type: 'match', + value: '', +}); + +export const TrustedAppsForm = memo( + ({ item, policies, policiesIsLoading, onChange, mode }) => { + const getTestId = useTestIdGenerator('trustedApps-form'); + const [visited, setVisited] = useState< + Partial<{ + [key in keyof NewTrustedApp]: boolean; + }> + >({}); + + const [selectedPolicies, setSelectedPolicies] = useState([]); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const isGlobal = useMemo(() => isArtifactGlobal(item as ExceptionListItemSchema), [item]); + const [wasByPolicy, setWasByPolicy] = useState(!isGlobalPolicyEffected(item.tags)); + const [hasFormChanged, setHasFormChanged] = useState(false); + + useEffect(() => { + if (!hasFormChanged && item.tags) { + setWasByPolicy(!isGlobalPolicyEffected(item.tags)); + } + }, [item.tags, hasFormChanged]); + + // select policies if editing + useEffect(() => { + if (hasFormChanged) { + return; + } + const policyIds = item.tags ? getPolicyIdsFromArtifact({ tags: item.tags }) : []; + if (!policyIds.length) { + return; + } + const policiesData = policies.filter((policy) => policyIds.includes(policy.id)); + + setSelectedPolicies(policiesData); + }, [hasFormChanged, item, policies]); + + const showAssignmentSection = useMemo(() => { + return ( + isPlatinumPlus || + (mode === 'edit' && (!isGlobal || (wasByPolicy && isGlobal && hasFormChanged))) + ); + }, [mode, isGlobal, hasFormChanged, isPlatinumPlus, wasByPolicy]); + + const [validationResult, setValidationResult] = useState(() => + validateValues(item) + ); + + const processChanged = useCallback( + (updatedFormValues: ArtifactFormComponentProps['item']) => { + const updatedValidationResult = validateValues(updatedFormValues); + setValidationResult(updatedValidationResult); + onChange({ + item: updatedFormValues, + isValid: updatedValidationResult.isValid, + }); + }, + [onChange] + ); + + const handleOnPolicyChange = useCallback( + (change: EffectedPolicySelection) => { + const tags = change.isGlobal + ? [GLOBAL_ARTIFACT_TAG] + : change.selected.map((policy) => `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policy.id}`); + + const nextItem = { ...item, tags }; + // Preserve old selected policies when switching to global + if (!change.isGlobal) { + setSelectedPolicies(change.selected); + } + processChanged(nextItem); + setHasFormChanged(true); + }, + [item, processChanged] + ); + + const handleOnNameOrDescriptionChange = useCallback< + ChangeEventHandler + >( + (event: React.ChangeEvent) => { + const nextItem = { + ...item, + [event.target.name]: event.target.value, + }; + + processChanged(nextItem); + setHasFormChanged(true); + }, + [item, processChanged] + ); + + const handleOnNameBlur = useCallback( + ({ target: { name } }) => { + processChanged(item); + setVisited((prevVisited) => ({ ...prevVisited, [name]: true })); + }, + [item, processChanged] + ); + + const osOptions: Array> = useMemo( + () => + [OperatingSystem.LINUX, OperatingSystem.MAC, OperatingSystem.WINDOWS].map((os) => ({ + value: os, + inputDisplay: OS_TITLES[os], + })), + [] + ); + + const handleOnOsChange = useCallback( + (os: OperatingSystem) => { + setVisited((prevVisited) => { + return { + ...prevVisited, + os: true, + }; + }); + + const nextItem: ArtifactFormComponentProps['item'] = { + ...item, + os_types: [os], + entries: [] as ArtifactFormComponentProps['item']['entries'], + }; + + if (os !== OperatingSystem.WINDOWS) { + const macOsLinuxConditionEntry = item.entries.filter((entry) => + isMacosLinuxTrustedAppCondition(entry as TrustedAppConditionEntry) + ); + nextItem.entries.push(...macOsLinuxConditionEntry); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + } else { + nextItem.entries.push(...item.entries); + } + + processChanged(nextItem); + setHasFormChanged(true); + }, + [item, processChanged] + ); + + const handleConditionBuilderOnVisited: LogicalConditionBuilderProps['onVisited'] = + useCallback(() => { + setVisited((prevState) => { + return { + ...prevState, + entries: true, + }; + }); + }, []); + + const handleEntryChange = useCallback( + (newEntry, oldEntry) => { + const nextItem: ArtifactFormComponentProps['item'] = { + ...item, + entries: item.entries.map((e) => { + if (e === oldEntry) { + return newEntry; + } + return e; + }), + }; + + processChanged(nextItem); + setHasFormChanged(true); + }, + [item, processChanged] + ); + + const handleEntryRemove = useCallback( + (entry: NewTrustedApp['entries'][0]) => { + const nextItem: ArtifactFormComponentProps['item'] = { + ...item, + entries: item.entries.filter((e) => e !== entry), + }; + + processChanged(nextItem); + setHasFormChanged(true); + }, + [item, processChanged] + ); + + const handleAndClick = useCallback(() => { + const nextItem: ArtifactFormComponentProps['item'] = { + ...item, + entries: [], + }; + const os = ((item.os_types ?? [])[0] as OperatingSystem) ?? OperatingSystem.WINDOWS; + if (os === OperatingSystem.WINDOWS) { + nextItem.entries = [...item.entries, defaultConditionEntry()].filter((entry) => + isWindowsTrustedAppCondition(entry as TrustedAppConditionEntry) + ); + } else { + nextItem.entries = [ + ...item.entries.filter((entry) => + isMacosLinuxTrustedAppCondition(entry as TrustedAppConditionEntry) + ), + defaultConditionEntry(), + ]; + } + processChanged(nextItem); + setHasFormChanged(true); + }, [item, processChanged]); + + const selectedOs = useMemo((): OperatingSystem => { + if (!item?.os_types?.length) { + return OperatingSystem.WINDOWS; + } + return item.os_types[0] as OperatingSystem; + }, [item?.os_types]); + + const trustedApp = useMemo(() => { + const ta = item; + + ta.entries = item.entries.length + ? (item.entries as TrustedAppConditionEntry[]) + : [defaultConditionEntry()]; + + return ta; + }, [item]); + + return ( + + +

{DETAILS_HEADER}

+
+ + {mode === 'create' && ( + +

{DETAILS_HEADER_DESCRIPTION}

+
+ )} + + + + + + + + + +

{CONDITIONS_HEADER}

+
+ + {CONDITIONS_HEADER_DESCRIPTION} + + + + + + + + {showAssignmentSection ? ( + <> + + + + + + ) : null} +
+ ); + } +); + +TrustedAppsForm.displayName = 'TrustedAppsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 8b496dcb519b9..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,12717 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TrustedAppsGrid renders correctly initially 1`] = ` -.c1 { - position: relative; - padding-top: 4px; -} - -.c1 .body { - min-height: 40px; -} - -.c1 .body-content { - position: relative; -} - -.c0 .trusted-app + .trusted-app { - margin-top: 24px; -} - -
-
-
-
-
-
-
-
-
- - No items found - -
-
-
-
-
-
-
-
-
-`; - -exports[`TrustedAppsGrid renders correctly when failed loading data for the first time 1`] = ` -.c1 { - position: relative; - padding-top: 4px; -} - -.c1 .body { - min-height: 40px; -} - -.c1 .body-content { - position: relative; -} - -.c0 .trusted-app + .trusted-app { - margin-top: 24px; -} - -
-
-
-
-
-
-
-
- - - Intenal Server Error -
-
-
-
-
-
-
-
-`; - -exports[`TrustedAppsGrid renders correctly when failed loading data for the second time 1`] = ` -.c1 { - position: relative; - padding-top: 4px; -} - -.c1 .body { - min-height: 40px; -} - -.c1 .body-content { - position: relative; -} - -.c0 .trusted-app + .trusted-app { - margin-top: 24px; -} - -
-
-
-
-
-
-
-
- - - Intenal Server Error -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-`; - -exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` -.c1 { - position: relative; - padding-top: 4px; -} - -.c1 .body { - min-height: 40px; -} - -.c1 .body-content { - position: relative; -} - -.c6 { - padding-top: 2px; -} - -.c5 { - margin-bottom: 4px !important; -} - -.c7 { - margin: 6px; -} - -.c3.artifactEntryCard + .c2.artifactEntryCard { - margin-top: 24px; -} - -.c4 { - padding: 32px; -} - -.c4.top-section { - padding-bottom: 24px; -} - -.c4.bottom-section { - padding-top: 24px; -} - -.c4.artifact-entry-collapsible-card { - padding: 24px !important; -} - -.c0 .trusted-app + .trusted-app { - margin-top: 24px; -} - -
-
-
-
-
-
-
-
-
-
-
-

- trusted app 0 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 0 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 1 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 1 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 2 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 2 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 3 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 3 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 4 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 4 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 5 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 5 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 6 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 6 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 7 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 7 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 8 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 8 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 9 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 9 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-`; - -exports[`TrustedAppsGrid renders correctly when loading data for the first time 1`] = ` -.c1 { - position: relative; - padding-top: 4px; -} - -.c1 .body { - min-height: 40px; -} - -.c1 .body-content { - position: relative; -} - -.c0 .trusted-app + .trusted-app { - margin-top: 24px; -} - -
-
-
-
-
-
-
-
-
-
-`; - -exports[`TrustedAppsGrid renders correctly when loading data for the second time 1`] = ` -.c1 { - position: relative; - padding-top: 4px; -} - -.c1 .body { - min-height: 40px; -} - -.c1 .body-content { - position: relative; -} - -.c6 { - padding-top: 2px; -} - -.c5 { - margin-bottom: 4px !important; -} - -.c7 { - margin: 6px; -} - -.c3.artifactEntryCard + .c2.artifactEntryCard { - margin-top: 24px; -} - -.c4 { - padding: 32px; -} - -.c4.top-section { - padding-bottom: 24px; -} - -.c4.bottom-section { - padding-top: 24px; -} - -.c4.artifact-entry-collapsible-card { - padding: 24px !important; -} - -.c0 .trusted-app + .trusted-app { - margin-top: 24px; -} - -
-
-
-
-
-
-
-
-
-
-
-
-

- trusted app 0 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 0 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 1 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 1 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 2 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 2 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 3 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 3 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 4 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 4 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 5 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 5 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 6 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 6 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 7 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 7 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 8 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 8 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 9 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 9 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-`; - -exports[`TrustedAppsGrid renders correctly when new page and page size set (not loading yet) 1`] = ` -.c1 { - position: relative; - padding-top: 4px; -} - -.c1 .body { - min-height: 40px; -} - -.c1 .body-content { - position: relative; -} - -.c6 { - padding-top: 2px; -} - -.c5 { - margin-bottom: 4px !important; -} - -.c7 { - margin: 6px; -} - -.c3.artifactEntryCard + .c2.artifactEntryCard { - margin-top: 24px; -} - -.c4 { - padding: 32px; -} - -.c4.top-section { - padding-bottom: 24px; -} - -.c4.bottom-section { - padding-top: 24px; -} - -.c4.artifact-entry-collapsible-card { - padding: 24px !important; -} - -.c0 .trusted-app + .trusted-app { - margin-top: 24px; -} - -
-
-
-
-
-
-
-
-
-
-
-

- trusted app 0 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 0 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 1 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 1 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 2 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 2 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 3 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 3 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 4 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 4 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 5 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 5 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 6 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 6 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 7 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 7 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Mac - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 8 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 8 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Linux - - - -
-
-
-
-
-
-
-
-
-
-

- trusted app 9 -

-
-
-
-
-
-
- -
-
-
-
-
- Last updated -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- Created -
-
-
-
- - 1 minute ago - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- - - - Created by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
- - - - Updated by - - - -
-
-
-
- -
-
-
- someone -
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Applied globally -
-
-
-
-
-
-
- Trusted App 9 -
-
-
-
-
-
- - - - - - OS - - - - - IS - - - - Windows - - - -
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-`; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx deleted file mode 100644 index 682d689c7e82c..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { ThemeProvider } from 'styled-components'; -import { storiesOf } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-theme'; -import { EuiHorizontalRule } from '@elastic/eui'; - -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; - -import { - createGlobalNoMiddlewareStore, - createListFailedResourceState, - createListLoadedResourceState, - createListLoadingResourceState, - createTrustedAppsListResourceStateChangedAction, -} from '../../../test_utils'; - -import { TrustedAppsGrid } from '.'; - -const now = 111111; - -const renderGrid = (store: ReturnType) => ( - - 'MMM D, YYYY @ HH:mm:ss.SSS' } }}> - ({ eui: euiLightVars, darkMode: false })}> - - - - - - - - -); - -storiesOf('TrustedApps/TrustedAppsGrid', module) - .add('default', () => { - return renderGrid(createGlobalNoMiddlewareStore()); - }) - .add('loading', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction(createListLoadingResourceState()) - ); - - return renderGrid(store); - }) - .add('error', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListFailedResourceState('Intenal Server Error') - ) - ); - - return renderGrid(store); - }) - .add('loaded', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 10 }, now) - ) - ); - - return renderGrid(store); - }) - .add('loading second time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadingResourceState(createListLoadedResourceState({ pageSize: 10 }, now)) - ) - ); - - return renderGrid(store); - }) - .add('long texts', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 10 }, now, true) - ) - ); - - return renderGrid(store); - }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.test.tsx deleted file mode 100644 index 6cc292b5a6d3e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.test.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render, act } from '@testing-library/react'; -import React from 'react'; -import { Provider } from 'react-redux'; - -import { - createSampleTrustedApp, - createListFailedResourceState, - createListLoadedResourceState, - createListLoadingResourceState, - createTrustedAppsListResourceStateChangedAction, - createUserChangedUrlAction, - createGlobalNoMiddlewareStore, -} from '../../../test_utils'; -import { TrustedAppsGrid } from '.'; -import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; - -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => 'mockId', -})); - -jest.mock('../../../../../../common/lib/kibana'); - -const now = 111111; - -const renderList = (store: ReturnType) => { - const Wrapper: React.FC = ({ children }) => ( - - {children} - - ); - - return render(, { wrapper: Wrapper }); -}; - -describe('TrustedAppsGrid', () => { - it('renders correctly initially', () => { - expect(renderList(createGlobalNoMiddlewareStore()).container).toMatchSnapshot(); - }); - - it('renders correctly when loading data for the first time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction(createListLoadingResourceState()) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when failed loading data for the first time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListFailedResourceState('Intenal Server Error') - ) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when loaded data', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 10 }, now) - ) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when new page and page size set (not loading yet)', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 10 }, now) - ) - ); - store.dispatch( - createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when loading data for the second time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadingResourceState(createListLoadedResourceState({ pageSize: 10 }, now)) - ) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('renders correctly when failed loading data for the second time', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListFailedResourceState( - 'Intenal Server Error', - createListLoadedResourceState({ pageSize: 10 }, now) - ) - ) - ); - - expect(renderList(store).container).toMatchSnapshot(); - }); - - it('triggers deletion dialog when delete action clicked', async () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch( - createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ pageSize: 10 }, now) - ) - ); - store.dispatch = jest.fn(); - - const renderResult = renderList(store); - - await act(async () => { - (await renderResult.findAllByTestId('trustedAppCard-header-actions-button'))[0].click(); - }); - - await act(async () => { - (await renderResult.findByTestId('deleteTrustedAppAction')).click(); - }); - - expect(store.dispatch).toBeCalledWith({ - type: 'trustedAppDeletionDialogStarted', - payload: { - entry: createSampleTrustedApp(0), - }, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx deleted file mode 100644 index a8e2187083523..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback, useMemo } from 'react'; - -import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; -import { Pagination } from '../../../state'; - -import { - getCurrentLocation, - getListErrorMessage, - getListItems, - getListPagination, - isListLoading, - getMapOfPoliciesById, - isLoadingListOfPolicies, -} from '../../../store/selectors'; - -import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from '../../hooks'; - -import { getPolicyDetailPath, getTrustedAppsListPath } from '../../../../../common/routing'; -import { - PaginatedContent, - PaginatedContentProps, -} from '../../../../../components/paginated_content'; -import { PolicyDetailsRouteState, TrustedApp } from '../../../../../../../common/endpoint/types'; -import { - ArtifactEntryCard, - ArtifactEntryCardProps, -} from '../../../../../components/artifact_entry_card'; -import { AppAction } from '../../../../../../common/store/actions'; -import { APP_UI_ID } from '../../../../../../../common/constants'; -import { useAppUrl } from '../../../../../../common/lib/kibana'; - -export interface PaginationBarProps { - pagination: Pagination; - onChange: (pagination: { size: number; index: number }) => void; -} - -type ArtifactEntryCardType = typeof ArtifactEntryCard; - -const RootWrapper = styled.div` - .trusted-app + .trusted-app { - margin-top: ${({ theme }) => theme.eui.spacerSizes.l}; - } -`; - -const BACK_TO_TRUSTED_APPS_LABEL = i18n.translate( - 'xpack.securitySolution.trustedapps.grid.policyDetailsLinkBackLabel', - { defaultMessage: 'Back to trusted applications' } -); - -const EDIT_TRUSTED_APP_ACTION_LABEL = i18n.translate( - 'xpack.securitySolution.trustedapps.grid.cardAction.edit', - { - defaultMessage: 'Edit trusted application', - } -); - -const DELETE_TRUSTED_APP_ACTION_LABEL = i18n.translate( - 'xpack.securitySolution.trustedapps.grid.cardAction.delete', - { - defaultMessage: 'Delete trusted application', - } -); - -export const TrustedAppsGrid = memo(() => { - const history = useHistory(); - const dispatch = useDispatch>(); - const { getAppUrl } = useAppUrl(); - - const pagination = useTrustedAppsSelector(getListPagination); - const listItems = useTrustedAppsSelector(getListItems); - const isLoading = useTrustedAppsSelector(isListLoading); - const error = useTrustedAppsSelector(getListErrorMessage); - const location = useTrustedAppsSelector(getCurrentLocation); - const policyListById = useTrustedAppsSelector(getMapOfPoliciesById); - const loadingPoliciesList = useTrustedAppsSelector(isLoadingListOfPolicies); - - const handlePaginationChange: PaginatedContentProps< - TrustedApp, - ArtifactEntryCardType - >['onChange'] = useTrustedAppsNavigateCallback(({ pageIndex, pageSize }) => ({ - page_index: pageIndex, - page_size: pageSize, - })); - - const artifactCardPropsPerItem = useMemo(() => { - const cachedCardProps: Record = {}; - - // Casting `listItems` below to remove the `Immutable<>` from it in order to prevent errors - // with common component's props - for (const trustedApp of listItems as TrustedApp[]) { - let policies: ArtifactEntryCardProps['policies']; - - if (trustedApp.effectScope.type === 'policy' && trustedApp.effectScope.policies.length) { - policies = trustedApp.effectScope.policies.reduce< - Required['policies'] - >((policyToNavOptionsMap, policyId) => { - const currentPagePath = getTrustedAppsListPath({ - ...location, - }); - - const policyDetailsPath = getPolicyDetailPath(policyId); - - const routeState: PolicyDetailsRouteState = { - backLink: { - label: BACK_TO_TRUSTED_APPS_LABEL, - navigateTo: [ - APP_UI_ID, - { - path: currentPagePath, - }, - ], - href: getAppUrl({ path: currentPagePath }), - }, - onCancelNavigateTo: [ - APP_UI_ID, - { - path: currentPagePath, - }, - ], - }; - - policyToNavOptionsMap[policyId] = { - navigateAppId: APP_UI_ID, - navigateOptions: { - path: policyDetailsPath, - state: routeState, - }, - href: getAppUrl({ path: policyDetailsPath }), - children: policyListById[policyId]?.name ?? policyId, - target: '_blank', - }; - return policyToNavOptionsMap; - }, {}); - } - - cachedCardProps[trustedApp.id] = { - item: trustedApp, - policies, - loadingPoliciesList, - hideComments: true, - 'data-test-subj': 'trustedAppCard', - actions: [ - { - icon: 'controlsHorizontal', - onClick: () => { - history.push( - getTrustedAppsListPath({ - ...location, - show: 'edit', - id: trustedApp.id, - }) - ); - }, - 'data-test-subj': 'editTrustedAppAction', - children: EDIT_TRUSTED_APP_ACTION_LABEL, - }, - { - icon: 'trash', - onClick: () => { - dispatch({ - type: 'trustedAppDeletionDialogStarted', - payload: { entry: trustedApp }, - }); - }, - 'data-test-subj': 'deleteTrustedAppAction', - children: DELETE_TRUSTED_APP_ACTION_LABEL, - }, - ], - hideDescription: !trustedApp.description, - }; - } - - return cachedCardProps; - }, [dispatch, getAppUrl, history, listItems, location, policyListById, loadingPoliciesList]); - - const handleArtifactCardProps = useCallback( - (trustedApp: TrustedApp) => { - return artifactCardPropsPerItem[trustedApp.id]; - }, - [artifactCardPropsPerItem] - ); - - return ( - - - items={listItems as TrustedApp[]} - onChange={handlePaginationChange} - ItemComponent={ArtifactEntryCard} - itemComponentProps={handleArtifactCardProps} - loading={isLoading} - itemId="id" - error={error} - pagination={pagination} - /> - - ); -}); - -TrustedAppsGrid.displayName = 'TrustedAppsGrid'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 09a13f11d2adb..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,53 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`view_type_toggle ViewTypeToggle should render grid selection correctly 1`] = ` - -`; - -exports[`view_type_toggle ViewTypeToggle should render list selection correctly 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx deleted file mode 100644 index 8ba70769838a3..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { ThemeProvider } from 'styled-components'; -import { storiesOf, addDecorator } from '@storybook/react'; -import { euiLightVars } from '@kbn/ui-theme'; - -import { ViewType } from '../../../state'; -import { ViewTypeToggle } from '.'; - -addDecorator((storyFn) => ( - ({ eui: euiLightVars, darkMode: false })}>{storyFn()} -)); - -const useRenderStory = (viewType: ViewType) => { - const [selectedOption, setSelectedOption] = useState(viewType); - - return ; -}; - -storiesOf('TrustedApps/ViewTypeToggle', module) - .add('grid selected', () => useRenderStory('grid')) - .add('list selected', () => useRenderStory('list')); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.test.tsx deleted file mode 100644 index d6b2bb5a2e7ef..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import { shallow } from 'enzyme'; -import React from 'react'; - -import { ViewTypeToggle } from '.'; - -describe('view_type_toggle', () => { - describe('ViewTypeToggle', () => { - it('should render grid selection correctly', () => { - const element = shallow( {}} />); - - expect(element).toMatchSnapshot(); - }); - - it('should render list selection correctly', () => { - const element = shallow( {}} />); - - expect(element).toMatchSnapshot(); - }); - - it('should trigger onToggle', async () => { - const onToggle = jest.fn(); - const element = render(); - - (await element.findAllByTestId('viewTypeToggleButton'))[0].click(); - - expect(onToggle).toBeCalledWith('grid'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.tsx deleted file mode 100644 index 700358d6beef6..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useCallback } from 'react'; -import { EuiButtonGroup } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { ViewType } from '../../../state'; -import { GRID_VIEW_TOGGLE_LABEL, LIST_VIEW_TOGGLE_LABEL } from '../../translations'; - -export interface ViewTypeToggleProps { - selectedOption: ViewType; - onToggle: (type: ViewType) => void; -} - -export const ViewTypeToggle = memo(({ selectedOption, onToggle }: ViewTypeToggleProps) => { - const handleChange = useCallback( - (id) => { - if (id === 'list' || id === 'grid') { - onToggle(id); - } - }, - [onToggle] - ); - - return ( - - ); -}); - -ViewTypeToggle.displayName = 'ViewTypeToggle'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts deleted file mode 100644 index 4c2747d74d31e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { useCallback } from 'react'; - -import { State } from '../../../../common/store'; - -import { - MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE as TRUSTED_APPS_NS, - MANAGEMENT_STORE_GLOBAL_NAMESPACE as GLOBAL_NS, -} from '../../../common/constants'; - -import { AppAction } from '../../../../common/store/actions'; -import { getTrustedAppsListPath } from '../../../common/routing'; -import { TrustedAppsListPageLocation, TrustedAppsListPageState } from '../state'; -import { getCurrentLocation } from '../store/selectors'; - -export function useTrustedAppsSelector(selector: (state: TrustedAppsListPageState) => R): R { - return useSelector((state: State) => - selector(state[GLOBAL_NS][TRUSTED_APPS_NS] as TrustedAppsListPageState) - ); -} - -export type NavigationCallback = ( - ...args: Parameters[0]> -) => Partial; - -export function useTrustedAppsNavigateCallback(callback: NavigationCallback) { - const location = useTrustedAppsSelector(getCurrentLocation); - const history = useHistory(); - - return useCallback( - (...args) => history.push(getTrustedAppsListPath({ ...location, ...callback(...args) })), - // TODO: needs more investigation, but if callback is in dependencies list memoization will never happen - // eslint-disable-next-line react-hooks/exhaustive-deps - [history, location] - ); -} - -export function useTrustedAppsStoreActionCallback( - callback: (...args: Parameters[0]>) => AppAction -) { - const dispatch = useDispatch(); - - // eslint-disable-next-line react-hooks/exhaustive-deps - return useCallback((...args) => dispatch(callback(...args)), [dispatch]); -} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/index.ts deleted file mode 100644 index 78c22fc020a45..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export * from './trusted_apps_page'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index 3d8a56ad74315..b5b92f3a686ea 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -13,14 +13,54 @@ import { OperatorFieldIds, } from '../../../../../common/endpoint/types'; -export { OS_TITLES } from '../../../common/translations'; - export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', { defaultMessage: 'Add a trusted application to improve performance or alleviate conflicts with other applications running on ' + 'your hosts.', }); +export const NAME_LABEL = i18n.translate('xpack.securitySolution.trustedApps.name.label', { + defaultMessage: 'Name', +}); + +export const DETAILS_HEADER = i18n.translate('xpack.securitySolution.trustedApps.details.header', { + defaultMessage: 'Details', +}); + +export const DETAILS_HEADER_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.trustedApps.details.header.description', + { + defaultMessage: + 'Trusted applications improve performance or alleviate conflicts with other applications running on your hosts.', + } +); + +export const DESCRIPTION_LABEL = i18n.translate( + 'xpack.securitySolution.trustedapps.create.description', + { + defaultMessage: 'Description', + } +); + +export const CONDITIONS_HEADER = i18n.translate( + 'xpack.securitySolution.trustedApps.conditions.header', + { + defaultMessage: 'Conditions', + } +); + +export const CONDITIONS_HEADER_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.trustedApps.conditions.header.description', + { + defaultMessage: + 'Select an operating system and add conditions. Availability of conditions may depend on your chosen OS.', + } +); + +export const SELECT_OS_LABEL = i18n.translate('xpack.securitySolution.trustedApps.os.label', { + defaultMessage: 'Select operating system', +}); + export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { [ConditionEntryField.HASH]: i18n.translate( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash', @@ -74,23 +114,50 @@ export const ENTRY_PROPERTY_TITLES: Readonly<{ }), }; -export const GRID_VIEW_TOGGLE_LABEL = i18n.translate( - 'xpack.securitySolution.trustedapps.view.toggle.grid', +export const POLICY_SELECT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.trustedApps.assignmentSectionDescription', { - defaultMessage: 'Grid view', + defaultMessage: + 'Assign this trusted application globally across all policies, or assign it to specific policies.', } ); -export const LIST_VIEW_TOGGLE_LABEL = i18n.translate( - 'xpack.securitySolution.trustedapps.view.toggle.list', - { - defaultMessage: 'List view', - } -); - -export const SEARCH_TRUSTED_APP_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.trustedapps.list.search.placeholder', - { - defaultMessage: 'Search on the fields below: name, description, value', - } -); +export const INPUT_ERRORS = { + name: i18n.translate('xpack.securitySolution.trustedapps.create.nameRequiredMsg', { + defaultMessage: 'Name is required', + }), + os: i18n.translate('xpack.securitySolution.trustedapps.create.osRequiredMsg', { + defaultMessage: 'Operating System is required', + }), + field: i18n.translate('xpack.securitySolution.trustedapps.create.conditionRequiredMsg', { + defaultMessage: 'At least one Field definition is required', + }), + noDuplicateField: (field: ConditionEntryField) => + i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldDuplicatedMsg', { + defaultMessage: '{field} cannot be added more than once', + values: { field: CONDITION_FIELD_TITLE[field] }, + }), + mustHaveValue: (index: number) => + i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg', { + defaultMessage: '[{row}] Field entry must have a value', + values: { row: index + 1 }, + }), + invalidHash: (index: number) => + i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldInvalidHashMsg', { + defaultMessage: '[{row}] Invalid hash value', + values: { row: index + 1 }, + }), + pathWarning: (index: number) => + i18n.translate('xpack.securitySolution.trustedapps.create.conditionFieldInvalidPathMsg', { + defaultMessage: '[{row}] Path may be formed incorrectly; verify value', + values: { row: index + 1 }, + }), + wildcardPathWarning: (index: number) => + i18n.translate( + 'xpack.securitySolution.trustedapps.create.conditionFieldDegradedPerformanceMsg', + { + defaultMessage: `[{row}] A wildcard in the filename will affect the endpoint's performance`, + values: { row: index + 1 }, + } + ), +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.test.tsx deleted file mode 100644 index 6abce21d7ccf5..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { render } from '@testing-library/react'; -import { I18nProvider } from '@kbn/i18n-react'; - -import { - createGlobalNoMiddlewareStore, - createSampleTrustedApp, - createServerApiError, -} from '../test_utils'; - -import { - TrustedAppDeletionDialogStarted, - TrustedAppDeletionSubmissionResourceStateChanged, -} from '../store/action'; - -import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; - -const renderDeletionDialog = (store: ReturnType) => { - const Wrapper: React.FC = ({ children }) => ( - - {children} - - ); - - return render(, { wrapper: Wrapper }); -}; - -const createDialogStartAction = (): TrustedAppDeletionDialogStarted => ({ - type: 'trustedAppDeletionDialogStarted', - payload: { entry: createSampleTrustedApp(3) }, -}); - -const createDialogLoadingAction = (): TrustedAppDeletionSubmissionResourceStateChanged => ({ - type: 'trustedAppDeletionSubmissionResourceStateChanged', - payload: { - newState: { - type: 'LoadingResourceState', - previousState: { type: 'UninitialisedResourceState' }, - }, - }, -}); - -const createDialogFailedAction = (): TrustedAppDeletionSubmissionResourceStateChanged => ({ - type: 'trustedAppDeletionSubmissionResourceStateChanged', - payload: { - newState: { type: 'FailedResourceState', error: createServerApiError('Not Found') }, - }, -}); - -describe('TrustedAppDeletionDialog', () => { - it('renders correctly initially', () => { - expect(renderDeletionDialog(createGlobalNoMiddlewareStore()).baseElement).toMatchSnapshot(); - }); - - it('renders correctly when dialog started', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch(createDialogStartAction()); - - expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); - }); - - it('renders correctly when deletion is in progress', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch(createDialogStartAction()); - store.dispatch(createDialogLoadingAction()); - - expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); - }); - - it('renders correctly when deletion failed', () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch(createDialogStartAction()); - store.dispatch(createDialogFailedAction()); - - expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); - }); - - it('triggers confirmation action when confirm button clicked', async () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch(createDialogStartAction()); - store.dispatch = jest.fn(); - - (await renderDeletionDialog(store).findByTestId('trustedAppDeletionConfirm')).click(); - - expect(store.dispatch).toBeCalledWith({ - type: 'trustedAppDeletionDialogConfirmed', - }); - }); - - it('triggers closing action when cancel button clicked', async () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch(createDialogStartAction()); - store.dispatch = jest.fn(); - - (await renderDeletionDialog(store).findByTestId('trustedAppDeletionCancel')).click(); - - expect(store.dispatch).toBeCalledWith({ - type: 'trustedAppDeletionDialogClosed', - }); - }); - - it('does not trigger closing action when deletion in progress and cancel button clicked', async () => { - const store = createGlobalNoMiddlewareStore(); - - store.dispatch(createDialogStartAction()); - store.dispatch(createDialogLoadingAction()); - - store.dispatch = jest.fn(); - - (await renderDeletionDialog(store).findByTestId('trustedAppDeletionCancel')).click(); - - expect(store.dispatch).not.toBeCalled(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx deleted file mode 100644 index 458a83d37322e..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonEmpty, - EuiCallOut, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import React, { memo, useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; -import { AutoFocusButton } from '../../../../common/components/autofocus_button/autofocus_button'; -import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; -import { AppAction } from '../../../../common/store/actions'; -import { isPolicyEffectScope } from '../state/type_guards'; -import { - getDeletionDialogEntry, - isDeletionDialogOpen, - isDeletionInProgress, -} from '../store/selectors'; -import { useTrustedAppsSelector } from './hooks'; - -const CANCEL_SUBJ = 'trustedAppDeletionCancel'; -const CONFIRM_SUBJ = 'trustedAppDeletionConfirm'; - -const getTranslations = (entry: Immutable | undefined) => ({ - title: ( - {entry?.name} }} - /> - ), - calloutTitle: ( - - ), - calloutMessage: ( - - ), - subMessage: ( - - ), - cancelButton: ( - - ), - confirmButton: ( - - ), -}); - -export const TrustedAppDeletionDialog = memo(() => { - const dispatch = useDispatch>(); - const isBusy = useTrustedAppsSelector(isDeletionInProgress); - const entry = useTrustedAppsSelector(getDeletionDialogEntry); - const translations = useMemo(() => getTranslations(entry), [entry]); - const onConfirm = useCallback(() => { - dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); - }, [dispatch]); - const onCancel = useCallback(() => { - if (!isBusy) { - dispatch({ type: 'trustedAppDeletionDialogClosed' }); - } - }, [dispatch, isBusy]); - - if (useTrustedAppsSelector(isDeletionDialogOpen)) { - return ( - - - {translations.title} - - - - -

{translations.calloutMessage}

-
- - -

{translations.subMessage}

-
-
- - - - {translations.cancelButton} - - - - {translations.confirmButton} - - -
- ); - } else { - return <>; - } -}); - -TrustedAppDeletionDialog.displayName = 'TrustedAppDeletionDialog'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx new file mode 100644 index 0000000000000..f778e6f471e1f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 { act, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { TRUSTED_APPS_PATH } from '../../../../../common/constants'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import { TrustedAppsList } from './trusted_apps_list'; +import { exceptionsListAllHttpMocks } from '../../mocks/exceptions_list_http_mocks'; +import { SEARCHABLE_FIELDS } from '../constants'; +import { parseQueryFilterToKQL } from '../../../common/utils'; +import { useUserPrivileges as _useUserPrivileges } from '../../../../common/components/user_privileges'; + +jest.mock('../../../../common/components/user_privileges'); + +describe('When on the trusted applications page', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let history: AppContextTestRender['history']; + let mockedContext: AppContextTestRender; + let apiMocks: ReturnType; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + ({ history } = mockedContext); + render = () => (renderResult = mockedContext.render()); + + apiMocks = exceptionsListAllHttpMocks(mockedContext.coreStart.http); + + act(() => { + history.push(TRUSTED_APPS_PATH); + }); + }); + + it('should search using expected exception item fields', async () => { + const expectedFilterString = parseQueryFilterToKQL('fooFooFoo', SEARCHABLE_FIELDS); + const { findAllByTestId } = render(); + await waitFor(async () => { + await expect(findAllByTestId('trustedAppsListPage-card')).resolves.toHaveLength(10); + }); + + apiMocks.responseProvider.exceptionsFind.mockClear(); + userEvent.type(renderResult.getByTestId('searchField'), 'fooFooFoo'); + userEvent.click(renderResult.getByTestId('searchButton')); + await waitFor(() => { + expect(apiMocks.responseProvider.exceptionsFind).toHaveBeenCalled(); + }); + + expect(apiMocks.responseProvider.exceptionsFind).toHaveBeenLastCalledWith( + expect.objectContaining({ + query: expect.objectContaining({ + filter: expectedFilterString, + }), + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx new file mode 100644 index 0000000000000..359e9d1aeb99d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { DocLinks } from '@kbn/doc-links'; +import { EuiLink } from '@elastic/eui'; + +import { useHttp } from '../../../../common/lib/kibana'; +import { ArtifactListPage, ArtifactListPageProps } from '../../../components/artifact_list_page'; +import { TrustedAppsApiClient } from '../service'; +import { TrustedAppsForm } from './components/form'; +import { SEARCHABLE_FIELDS } from '../constants'; + +const TRUSTED_APPS_PAGE_LABELS: ArtifactListPageProps['labels'] = { + pageTitle: i18n.translate('xpack.securitySolution.trustedApps.pageTitle', { + defaultMessage: 'Trusted applications', + }), + pageAboutInfo: i18n.translate('xpack.securitySolution.trustedApps.pageAboutInfo', { + defaultMessage: + 'Trusted applications improve performance or alleviate conflicts with other applications running on your hosts.', + }), + pageAddButtonTitle: i18n.translate('xpack.securitySolution.trustedApps.pageAddButtonTitle', { + defaultMessage: 'Add trusted application', + }), + getShowingCountLabel: (total) => + i18n.translate('xpack.securitySolution.trustedApps.showingTotal', { + defaultMessage: + 'Showing {total} {total, plural, one {trusted application} other {trusted applications}}', + values: { total }, + }), + cardActionEditLabel: i18n.translate('xpack.securitySolution.trustedApps.cardActionEditLabel', { + defaultMessage: 'Edit trusted application', + }), + cardActionDeleteLabel: i18n.translate( + 'xpack.securitySolution.trustedApps.cardActionDeleteLabel', + { + defaultMessage: 'Delete trusted application', + } + ), + flyoutCreateTitle: i18n.translate('xpack.securitySolution.trustedApps.flyoutCreateTitle', { + defaultMessage: 'Add trusted application', + }), + flyoutEditTitle: i18n.translate('xpack.securitySolution.trustedApps.flyoutEditTitle', { + defaultMessage: 'Edit trusted application', + }), + flyoutCreateSubmitButtonLabel: i18n.translate( + 'xpack.securitySolution.trustedApps.flyoutCreateSubmitButtonLabel', + { defaultMessage: 'Add trusted application' } + ), + flyoutCreateSubmitSuccess: ({ name }) => + i18n.translate('xpack.securitySolution.trustedApps.flyoutCreateSubmitSuccess', { + defaultMessage: '"{name}" has been added to your trusted applications.', + values: { name }, + }), + flyoutEditSubmitSuccess: ({ name }) => + i18n.translate('xpack.securitySolution.trustedApps.flyoutEditSubmitSuccess', { + defaultMessage: '"{name}" has been updated.', + values: { name }, + }), + flyoutDowngradedLicenseDocsInfo: ( + securitySolutionDocsLinks: DocLinks['securitySolution'] + ): React.ReactNode => { + return ( + <> + + + + + + ); + }, + deleteActionSuccess: (itemName) => + i18n.translate('xpack.securitySolution.trustedApps.deleteSuccess', { + defaultMessage: '"{itemName}" has been removed from trusted applications.', + values: { itemName }, + }), + emptyStateTitle: i18n.translate('xpack.securitySolution.trustedApps.emptyStateTitle', { + defaultMessage: 'Add your first trusted application', + }), + emptyStateInfo: i18n.translate('xpack.securitySolution.trustedApps.emptyStateInfo', { + defaultMessage: + 'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts.', + }), + emptyStatePrimaryButtonLabel: i18n.translate( + 'xpack.securitySolution.trustedApps.emptyStatePrimaryButtonLabel', + { defaultMessage: 'Add trusted application' } + ), + searchPlaceholderInfo: i18n.translate( + 'xpack.securitySolution.trustedApps.searchPlaceholderInfo', + { + defaultMessage: 'Search on the fields below: name, description, value', + } + ), +}; + +export const TrustedAppsList = memo(() => { + const http = useHttp(); + const trustedAppsApiClient = TrustedAppsApiClient.getInstance(http); + + return ( + + ); +}); + +TrustedAppsList.displayName = 'TrustedAppsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx deleted file mode 100644 index ebd1e06abdc4f..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { render } from '@testing-library/react'; - -import { NotificationsStart } from '@kbn/core/public'; - -import { coreMock } from '@kbn/core/public/mocks'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public/context'; - -import { - createGlobalNoMiddlewareStore, - createSampleTrustedApp, - createServerApiError, -} from '../test_utils'; - -import { TrustedAppsNotifications } from './trusted_apps_notifications'; - -const mockNotifications = () => coreMock.createStart({ basePath: '/mock' }).notifications; - -const renderNotifications = ( - store: ReturnType, - notifications: NotificationsStart -) => { - const Wrapper: React.FC = ({ children }) => ( - - {children} - - ); - - return render(, { wrapper: Wrapper }); -}; - -describe('TrustedAppsNotifications', () => { - it('renders correctly initially', () => { - const notifications = mockNotifications(); - - renderNotifications(createGlobalNoMiddlewareStore(), notifications); - - expect(notifications.toasts.addSuccess).not.toBeCalled(); - expect(notifications.toasts.addDanger).not.toBeCalled(); - }); - - it('shows success notification when deletion successful', () => { - const store = createGlobalNoMiddlewareStore(); - const notifications = mockNotifications(); - - renderNotifications(store, notifications); - - store.dispatch({ - type: 'trustedAppDeletionDialogStarted', - payload: { entry: createSampleTrustedApp(3) }, - }); - store.dispatch({ - type: 'trustedAppDeletionSubmissionResourceStateChanged', - payload: { newState: { type: 'LoadedResourceState', data: null } }, - }); - store.dispatch({ - type: 'trustedAppDeletionDialogClosed', - }); - - expect(notifications.toasts.addSuccess).toBeCalledWith({ - text: '"trusted app 3" has been removed from the trusted applications list.', - title: 'Successfully removed', - }); - expect(notifications.toasts.addDanger).not.toBeCalled(); - }); - - it('shows error notification when deletion fails', () => { - const store = createGlobalNoMiddlewareStore(); - const notifications = mockNotifications(); - - renderNotifications(store, notifications); - - store.dispatch({ - type: 'trustedAppDeletionDialogStarted', - payload: { entry: createSampleTrustedApp(3) }, - }); - store.dispatch({ - type: 'trustedAppDeletionSubmissionResourceStateChanged', - payload: { - newState: { type: 'FailedResourceState', error: createServerApiError('Not Found') }, - }, - }); - - expect(notifications.toasts.addSuccess).not.toBeCalled(); - expect(notifications.toasts.addDanger).toBeCalledWith({ - text: 'Unable to remove "trusted app 3" from the trusted applications list. Reason: Not Found', - title: 'Removal failure', - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx deleted file mode 100644 index dd03636d7cc66..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useState } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { ServerApiError } from '../../../../common/types'; -import { Immutable, NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; -import { - getCreationDialogFormEntry, - getDeletionDialogEntry, - getDeletionError, - isCreationSuccessful, - isDeletionSuccessful, - isEdit, -} from '../store/selectors'; - -import { useToasts } from '../../../../common/lib/kibana'; -import { useTrustedAppsSelector } from './hooks'; - -const getDeletionErrorMessage = (error: ServerApiError, entry: Immutable) => { - return { - title: i18n.translate('xpack.securitySolution.trustedapps.deletionError.title', { - defaultMessage: 'Removal failure', - }), - text: i18n.translate('xpack.securitySolution.trustedapps.deletionError.text', { - defaultMessage: - 'Unable to remove "{name}" from the trusted applications list. Reason: {message}', - values: { name: entry.name, message: error.message }, - }), - }; -}; - -const getDeletionSuccessMessage = (entry: Immutable) => { - return { - title: i18n.translate('xpack.securitySolution.trustedapps.deletionSuccess.title', { - defaultMessage: 'Successfully removed', - }), - text: i18n.translate('xpack.securitySolution.trustedapps.deletionSuccess.text', { - defaultMessage: '"{name}" has been removed from the trusted applications list.', - values: { name: entry?.name }, - }), - }; -}; - -const getCreationSuccessMessage = (entry: Immutable) => { - return { - title: i18n.translate('xpack.securitySolution.trustedapps.creationSuccess.title', { - defaultMessage: 'Success!', - }), - text: i18n.translate( - 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle', - { - defaultMessage: '"{name}" has been added to the trusted applications list.', - values: { name: entry.name }, - } - ), - }; -}; - -const getUpdateSuccessMessage = (entry: Immutable) => { - return { - title: i18n.translate('xpack.securitySolution.trustedapps.updateSuccess.title', { - defaultMessage: 'Success!', - }), - text: i18n.translate( - 'xpack.securitySolution.trustedapps.createTrustedAppFlyout.updateSuccessToastTitle', - { - defaultMessage: '"{name}" has been updated.', - values: { name: entry.name }, - } - ), - }; -}; - -export const TrustedAppsNotifications = memo(() => { - const deletionError = useTrustedAppsSelector(getDeletionError); - const deletionDialogEntry = useTrustedAppsSelector(getDeletionDialogEntry); - const deletionSuccessful = useTrustedAppsSelector(isDeletionSuccessful); - const creationDialogNewEntry = useTrustedAppsSelector(getCreationDialogFormEntry); - const creationSuccessful = useTrustedAppsSelector(isCreationSuccessful); - const editMode = useTrustedAppsSelector(isEdit); - const toasts = useToasts(); - - const [wasAlreadyHandled] = useState(new WeakSet()); - - if (deletionError && deletionDialogEntry) { - toasts.addDanger(getDeletionErrorMessage(deletionError, deletionDialogEntry)); - } - - if (deletionSuccessful && deletionDialogEntry) { - toasts.addSuccess(getDeletionSuccessMessage(deletionDialogEntry)); - } - - if ( - creationSuccessful && - creationDialogNewEntry && - !wasAlreadyHandled.has(creationDialogNewEntry) - ) { - wasAlreadyHandled.add(creationDialogNewEntry); - - toasts.addSuccess( - (editMode && getUpdateSuccessMessage(creationDialogNewEntry)) || - getCreationSuccessMessage(creationDialogNewEntry) - ); - } - - return <>; -}); - -TrustedAppsNotifications.displayName = 'TrustedAppsNotifications'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx deleted file mode 100644 index d3bf8fe6ed46a..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ /dev/null @@ -1,885 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import * as reactTestingLibrary from '@testing-library/react'; -import { TrustedAppsPage } from './trusted_apps_page'; -import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; -import { fireEvent } from '@testing-library/dom'; -import { MiddlewareActionSpyHelper } from '../../../../common/store/test_utils'; -import { ConditionEntryField, OperatingSystem } from '@kbn/securitysolution-utils'; -import { TrustedApp } from '../../../../../common/endpoint/types'; -import { HttpFetchOptions, HttpFetchOptionsWithPath } from '@kbn/core/public'; -import { isFailedResourceState, isLoadedResourceState } from '../state'; -import { forceHTMLElementOffsetWidth } from '../../../components/effected_policy_select/test_utils'; -import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; -import { licenseService } from '../../../../common/hooks/use_license'; -import { FoundExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; -import { trustedAppsAllHttpMocks } from '../../mocks'; -import { waitFor } from '@testing-library/react'; - -jest.mock('../../../../common/hooks/use_license', () => { - const licenseServiceInstance = { - isPlatinumPlus: jest.fn(), - }; - return { - licenseService: licenseServiceInstance, - useLicense: () => { - return licenseServiceInstance; - }, - }; -}); - -jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); - -describe('When on the Trusted Apps Page', () => { - const expectedAboutInfo = - 'Add a trusted application to improve performance or alleviate conflicts with other ' + - 'applications running on your hosts.'; - - let mockedContext: AppContextTestRender; - let history: AppContextTestRender['history']; - let coreStart: AppContextTestRender['coreStart']; - let waitForAction: MiddlewareActionSpyHelper['waitForAction']; - let render: () => ReturnType; - let renderResult: ReturnType; - let mockedApis: ReturnType; - let getFakeTrustedApp = jest.fn(); - - const originalScrollTo = window.scrollTo; - const act = reactTestingLibrary.act; - const waitForListUI = async (): Promise => { - await waitFor(() => { - expect(renderResult.getByTestId('trustedAppsListPageContent')).toBeTruthy(); - }); - }; - - beforeAll(() => { - window.scrollTo = () => {}; - }); - - afterAll(() => { - window.scrollTo = originalScrollTo; - }); - - beforeEach(() => { - mockedContext = createAppRootMockRenderer(); - getFakeTrustedApp = jest.fn( - (): TrustedApp => ({ - id: '2d95bec3-b48f-4db7-9622-a2b061cc031d', - version: 'abc123', - name: 'Generated Exception (3xnng)', - os: OperatingSystem.WINDOWS, - created_at: '2021-01-04T13:55:00.561Z', - created_by: 'me', - updated_at: '2021-01-04T13:55:00.561Z', - updated_by: 'me', - description: 'created by ExceptionListItemGenerator', - effectScope: { type: 'global' }, - entries: [ - { - field: ConditionEntryField.PATH, - value: 'one/two', - operator: 'included', - type: 'match', - }, - ], - }) - ); - - history = mockedContext.history; - coreStart = mockedContext.coreStart; - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); - waitForAction = mockedContext.middlewareSpy.waitForAction; - mockedApis = trustedAppsAllHttpMocks(coreStart.http); - render = () => (renderResult = mockedContext.render()); - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps'); - }); - window.scrollTo = jest.fn(); - }); - - afterEach(() => reactTestingLibrary.cleanup()); - - describe('and there are trusted app entries', () => { - const renderWithListData = async () => { - render(); - await act(async () => { - await waitForListUI(); - }); - - return renderResult; - }; - - it('should display subtitle info about trusted apps', async () => { - const { getByTestId } = await renderWithListData(); - expect(getByTestId('header-panel-subtitle').textContent).toEqual(expectedAboutInfo); - }); - - it('should display a Add Trusted App button', async () => { - const { getByTestId } = await renderWithListData(); - const addButton = getByTestId('trustedAppsListAddButton'); - expect(addButton.textContent).toBe('Add trusted application'); - }); - - it('should display the searchExceptions', async () => { - await renderWithListData(); - expect(await renderResult.findByTestId('searchExceptions')).not.toBeNull(); - }); - - describe('and the Grid view is being displayed', () => { - const renderWithListDataAndClickOnEditCard = async () => { - await renderWithListData(); - - await act(async () => { - // The 3rd Trusted app to be rendered will be a policy specific one - (await renderResult.findAllByTestId('trustedAppCard-header-actions-button'))[2].click(); - }); - - act(() => { - fireEvent.click(renderResult.getByTestId('editTrustedAppAction')); - }); - }; - - const renderWithListDataAndClickAddButton = async (): Promise< - ReturnType - > => { - await renderWithListData(); - - act(() => { - const addButton = renderResult.getByTestId('trustedAppsListAddButton'); - fireEvent.click(addButton, { button: 1 }); - }); - - // Wait for the policies to be loaded - await act(async () => { - await waitForAction('trustedAppsPoliciesStateChanged', { - validate: (action) => { - return isLoadedResourceState(action.payload); - }, - }); - }); - - return renderResult; - }; - - describe('the license is downgraded to gold or below and the user is editing a per policy TA', () => { - beforeEach(async () => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - - const originalFakeTrustedAppProvider = getFakeTrustedApp.getMockImplementation(); - getFakeTrustedApp.mockImplementation(() => { - return { - ...originalFakeTrustedAppProvider!(), - effectScope: { - type: 'policy', - policies: ['abc123'], - }, - }; - }); - await renderWithListDataAndClickOnEditCard(); - }); - - it('shows a message at the top of the flyout to inform the user their license is expired', () => { - expect( - renderResult.queryByTestId('addTrustedAppFlyout-expired-license-callout') - ).toBeTruthy(); - }); - }); - - describe('the license is downgraded to gold or below and the user is adding a new TA', () => { - beforeEach(async () => { - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); - - const originalFakeTrustedAppProvider = getFakeTrustedApp.getMockImplementation(); - getFakeTrustedApp.mockImplementation(() => { - return { - ...originalFakeTrustedAppProvider!(), - effectScope: { - type: 'policy', - policies: ['abc123'], - }, - }; - }); - await renderWithListDataAndClickAddButton(); - }); - it('does not show the expired license message at the top of the flyout', async () => { - expect( - renderResult.queryByTestId('addTrustedAppFlyout-expired-license-callout') - ).toBeNull(); - }); - }); - - describe('and the edit trusted app button is clicked', () => { - beforeEach(async () => { - await renderWithListDataAndClickOnEditCard(); - }); - - it('should persist edit params to url', () => { - expect(history.location.search).toEqual( - '?show=edit&id=bec3b48f-ddb7-4622-a2b0-61cc031d17eb' - ); - }); - - it('should display the Edit flyout', () => { - expect(renderResult.getByTestId('addTrustedAppFlyout')); - }); - - it('should NOT display the about info for trusted apps', () => { - expect(renderResult.queryByTestId('addTrustedAppFlyout-about')).toBeNull(); - }); - - it('should show correct flyout title', () => { - expect(renderResult.getByTestId('addTrustedAppFlyout-headerTitle').textContent).toBe( - 'Edit trusted application' - ); - }); - - it('should display the expected text for the Save button', () => { - expect(renderResult.getByTestId('addTrustedAppFlyout-createButton').textContent).toEqual( - 'Save' - ); - }); - - it('should display trusted app data for edit', async () => { - const formNameInput = renderResult.getByTestId( - 'addTrustedAppFlyout-createForm-nameTextField' - ) as HTMLInputElement; - const formDescriptionInput = renderResult.getByTestId( - 'addTrustedAppFlyout-createForm-descriptionField' - ) as HTMLTextAreaElement; - - expect(formNameInput.value).toEqual('Generated Exception (nng74)'); - expect(formDescriptionInput.value).toEqual('created by ExceptionListItemGenerator'); - }); - - describe('and when Save is clicked', () => { - it('should call the correct api (PUT)', async () => { - await act(async () => { - fireEvent.click(renderResult.getByTestId('addTrustedAppFlyout-createButton')); - await waitForAction('trustedAppCreationSubmissionResourceStateChanged', { - validate({ payload }) { - return isLoadedResourceState(payload.newState); - }, - }); - }); - - expect(coreStart.http.put).toHaveBeenCalledTimes(1); - - const lastCallToPut = coreStart.http.put.mock.calls[0] as unknown as [ - string, - HttpFetchOptions - ]; - - expect(lastCallToPut[0]).toEqual('/api/exception_lists/items'); - - expect(JSON.parse(lastCallToPut[1].body as string)).toEqual({ - _version: '9zawi', - name: 'Generated Exception (nng74)', - description: 'created by ExceptionListItemGenerator', - entries: [ - { - field: 'process.hash.md5', - operator: 'included', - type: 'match', - value: '741462ab431a22233c787baab9b653c7', - }, - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: 'c:\\fol\\bin.exe', - }, - ], - os_types: ['windows'], - tags: [ - 'policy:ddf6570b-9175-4a6d-b288-61a09771c647', - 'policy:b8e616ae-44fc-4be7-846c-ce8fa5c082dd', - ], - id: '05b5e350-0cad-4dc3-a61d-6e6796b0af39', - comments: [], - item_id: 'bec3b48f-ddb7-4622-a2b0-61cc031d17eb', - namespace_type: 'agnostic', - type: 'simple', - }); - }); - }); - }); - - describe('and attempting to show Edit panel based on URL params', () => { - const renderAndWaitForGetApi = async () => { - // the store action watcher is setup prior to render because `renderWithListData()` - // also awaits API calls and this action could be missed. - const apiResponseForEditTrustedApp = waitForAction( - 'trustedAppCreationEditItemStateChanged', - { - validate({ payload }) { - return isLoadedResourceState(payload) || isFailedResourceState(payload); - }, - } - ); - - await renderWithListData(); - - await reactTestingLibrary.act(async () => { - await apiResponseForEditTrustedApp; - }); - - return renderResult; - }; - - beforeEach(() => { - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps?show=edit&id=9999-edit-8888'); - }); - }); - - it('should retrieve trusted app via API using url `id`', async () => { - await renderAndWaitForGetApi(); - - expect(coreStart.http.get.mock.calls).toContainEqual([ - EXCEPTION_LIST_ITEM_URL, - { - query: { - item_id: '9999-edit-8888', - namespace_type: 'agnostic', - }, - }, - ]); - - expect( - ( - renderResult.getByTestId( - 'addTrustedAppFlyout-createForm-nameTextField' - ) as HTMLInputElement - ).value - ).toEqual('Generated Exception (u6kh2)'); - }); - - it('should redirect to list and show toast message if `id` is missing from URL', async () => { - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps?show=edit&id='); - }); - - await renderAndWaitForGetApi(); - - expect(history.location.search).toEqual(''); - expect(coreStart.notifications.toasts.addWarning.mock.calls[0][0]).toEqual( - 'Unable to edit trusted application (No id provided)' - ); - }); - - it('should redirect to list and show toast message on API error for GET of `id`', async () => { - // Mock the API GET for the trusted application - mockedApis.responseProvider.trustedApp.mockImplementation(() => { - throw new Error('test: api error response'); - }); - - await renderAndWaitForGetApi(); - - expect(history.location.search).toEqual(''); - expect(coreStart.notifications.toasts.addWarning.mock.calls[0][0]).toEqual( - 'Unable to edit trusted application (test: api error response)' - ); - }); - }); - }); - }); - - describe('and the Add Trusted App button is clicked', () => { - const renderAndClickAddButton = async (): Promise< - ReturnType - > => { - render(); - await act(async () => { - await Promise.all([ - waitForAction('trustedAppsListResourceStateChanged'), - waitForAction('trustedAppsExistStateChanged', { - validate({ payload }) { - return isLoadedResourceState(payload); - }, - }), - ]); - }); - - act(() => { - const addButton = renderResult.getByTestId('trustedAppsListAddButton'); - fireEvent.click(addButton, { button: 1 }); - }); - - // Wait for the policies to be loaded - await act(async () => { - await waitForAction('trustedAppsPoliciesStateChanged', { - validate: (action) => { - return isLoadedResourceState(action.payload); - }, - }); - }); - - return renderResult; - }; - - it('should display the create flyout', async () => { - const { getByTestId } = await renderAndClickAddButton(); - const flyout = getByTestId('addTrustedAppFlyout'); - expect(flyout).not.toBeNull(); - - const flyoutTitle = getByTestId('addTrustedAppFlyout-headerTitle'); - expect(flyoutTitle.textContent).toBe('Add trusted application'); - - expect(getByTestId('addTrustedAppFlyout-about')); - }); - - it('should update the URL to indicate the flyout is opened', async () => { - await renderAndClickAddButton(); - expect(/show\=create/.test(history.location.search)).toBe(true); - }); - - it('should preserve other URL search params', async () => { - const createListResponse = - mockedApis.responseProvider.trustedAppsList.getMockImplementation()!; - mockedApis.responseProvider.trustedAppsList.mockImplementation((...args) => { - const response = createListResponse(...args); - response.total = 100; // Trigger the UI to show pagination - return response; - }); - - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps?page_index=2&page_size=20'); - }); - await renderAndClickAddButton(); - expect(history.location.search).toBe('?page_index=2&page_size=20&show=create'); - }); - - it('should display create form', async () => { - const { queryByTestId } = await renderAndClickAddButton(); - expect(queryByTestId('addTrustedAppFlyout-createForm')).not.toBeNull(); - }); - - it('should have list of policies populated', async () => { - const resetEnv = forceHTMLElementOffsetWidth(); - await renderAndClickAddButton(); - act(() => { - fireEvent.click(renderResult.getByTestId('perPolicy')); - }); - expect(renderResult.getByTestId('policy-ddf6570b-9175-4a6d-b288-61a09771c647')); - resetEnv(); - }); - - it('should initially have the flyout Add button disabled', async () => { - const { getByTestId } = await renderAndClickAddButton(); - expect((getByTestId('addTrustedAppFlyout-createButton') as HTMLButtonElement).disabled).toBe( - true - ); - }); - - it('should close flyout if cancel button is clicked', async () => { - const { getByTestId, queryByTestId } = await renderAndClickAddButton(); - const cancelButton = getByTestId('addTrustedAppFlyout-cancelButton'); - await reactTestingLibrary.act(async () => { - fireEvent.click(cancelButton, { button: 1 }); - await waitForAction('trustedAppCreationDialogClosed'); - }); - expect(history.location.search).toBe(''); - expect(queryByTestId('addTrustedAppFlyout')).toBeNull(); - }); - - it('should close flyout if flyout close button is clicked', async () => { - const { getByTestId, queryByTestId } = await renderAndClickAddButton(); - const flyoutCloseButton = getByTestId('euiFlyoutCloseButton'); - await reactTestingLibrary.act(async () => { - fireEvent.click(flyoutCloseButton, { button: 1 }); - await waitForAction('trustedAppCreationDialogClosed'); - }); - expect(queryByTestId('addTrustedAppFlyout')).toBeNull(); - expect(history.location.search).toBe(''); - }); - - describe('and when the form data is valid', () => { - const fillInCreateForm = async () => { - mockedContext.store.dispatch({ - type: 'trustedAppCreationDialogFormStateUpdated', - payload: { - isValid: true, - entry: toUpdateTrustedApp(getFakeTrustedApp()), - }, - }); - }; - - it('should enable the Flyout Add button', async () => { - await renderAndClickAddButton(); - await fillInCreateForm(); - - const flyoutAddButton = renderResult.getByTestId( - 'addTrustedAppFlyout-createButton' - ) as HTMLButtonElement; - - expect(flyoutAddButton.disabled).toBe(false); - }); - - describe('and the Flyout Add button is clicked', () => { - let releasePostCreateApi: () => void; - - beforeEach(async () => { - // Add a delay to the create api response provider and expose a function that allows - // us to release it at the right time. - mockedApis.responseProvider.trustedAppCreate.mockDelay.mockReturnValue( - new Promise((resolve) => { - releasePostCreateApi = resolve as typeof releasePostCreateApi; - }) - ); - - await renderAndClickAddButton(); - await fillInCreateForm(); - - const userClickedSaveActionWatcher = waitForAction('trustedAppCreationDialogConfirmed'); - reactTestingLibrary.act(() => { - fireEvent.click(renderResult.getByTestId('addTrustedAppFlyout-createButton'), { - button: 1, - }); - }); - - await reactTestingLibrary.act(async () => { - await userClickedSaveActionWatcher; - }); - }); - - afterEach(() => releasePostCreateApi()); - - it('should display info about Trusted Apps', async () => { - expect(renderResult.getByTestId('addTrustedAppFlyout-about').textContent).toEqual( - expectedAboutInfo - ); - }); - - it('should disable the Cancel button', async () => { - expect( - (renderResult.getByTestId('addTrustedAppFlyout-cancelButton') as HTMLButtonElement) - .disabled - ).toBe(true); - releasePostCreateApi(); - }); - - it('should hide the dialog close button', async () => { - expect(renderResult.queryByTestId('euiFlyoutCloseButton')).toBeNull(); - }); - - it('should disable the flyout Add button and set it to loading', async () => { - const saveButton = renderResult.getByTestId( - 'addTrustedAppFlyout-createButton' - ) as HTMLButtonElement; - expect(saveButton.disabled).toBe(true); - expect(saveButton.querySelector('.euiLoadingSpinner')).not.toBeNull(); - }); - - describe('and if create was successful', () => { - beforeEach(async () => { - await reactTestingLibrary.act(async () => { - const serverResponseAction = waitForAction( - 'trustedAppCreationSubmissionResourceStateChanged' - ); - - coreStart.http.get.mockClear(); - releasePostCreateApi(); - await serverResponseAction; - }); - }); - - it('should close the flyout', () => { - expect(renderResult.queryByTestId('addTrustedAppFlyout')).toBeNull(); - }); - - it('should show success toast notification', () => { - expect(coreStart.notifications.toasts.addSuccess.mock.calls[0][0]).toEqual({ - text: '"Generated Exception (3xnng)" has been added to the trusted applications list.', - title: 'Success!', - }); - }); - - it('should trigger the List to reload', () => { - const isCalled = coreStart.http.get.mock.calls.some( - (call) => call[0].toString() === `${EXCEPTION_LIST_ITEM_URL}/_find` - ); - expect(isCalled).toEqual(true); - }); - }); - - describe('and if create failed', () => { - const ServerErrorResponseBodyMock = class extends Error { - public readonly body: { message: string }; - constructor(message = 'Test - Bad Call') { - super(message); - this.body = { - message, - }; - } - }; - beforeEach(async () => { - const failedCreateApiResponse = new ServerErrorResponseBodyMock(); - - mockedApis.responseProvider.trustedAppCreate.mockImplementation(() => { - throw failedCreateApiResponse; - }); - - await reactTestingLibrary.act(async () => { - const serverResponseAction = waitForAction( - 'trustedAppCreationSubmissionResourceStateChanged', - { - validate({ payload }) { - return isFailedResourceState(payload.newState); - }, - } - ); - - releasePostCreateApi(); - await serverResponseAction; - }); - }); - - it('should continue to show the flyout', () => { - expect(renderResult.getByTestId('addTrustedAppFlyout')).not.toBeNull(); - }); - - it('should enable the Cancel Button', () => { - expect( - (renderResult.getByTestId('addTrustedAppFlyout-cancelButton') as HTMLButtonElement) - .disabled - ).toBe(false); - }); - - it('should show the dialog close button', () => { - expect(renderResult.getByTestId('euiFlyoutCloseButton')).not.toBeNull(); - }); - - it('should enable the flyout Add button and remove loading indicating', () => { - expect( - (renderResult.getByTestId('addTrustedAppFlyout-createButton') as HTMLButtonElement) - .disabled - ).toBe(false); - }); - - it('should show API errors in the form', () => { - expect(renderResult.container.querySelector('.euiForm__errors')).not.toBeNull(); - }); - }); - }); - }); - - describe('and when the form data is not valid', () => { - it('should not enable the Flyout Add button with an invalid hash', async () => { - await renderAndClickAddButton(); - const { getByTestId } = renderResult; - - reactTestingLibrary.act(() => { - fireEvent.change(getByTestId('addTrustedAppFlyout-createForm-nameTextField'), { - target: { value: 'trusted app A' }, - }); - - fireEvent.change( - getByTestId('addTrustedAppFlyout-createForm-conditionsBuilder-group1-entry0-value'), - { target: { value: 'invalid hash' } } - ); - }); - - const flyoutAddButton = getByTestId( - 'addTrustedAppFlyout-createButton' - ) as HTMLButtonElement; - expect(flyoutAddButton.disabled).toBe(true); - }); - }); - }); - - describe('and there are no trusted apps', () => { - const releaseExistsResponse = jest.fn((): FoundExceptionListItemSchema => { - return { - data: [], - total: 0, - page: 1, - per_page: 1, - }; - }); - const releaseListResponse = jest.fn((): FoundExceptionListItemSchema => { - return { - data: [], - total: 0, - page: 1, - per_page: 20, - }; - }); - - beforeEach(() => { - mockedApis.responseProvider.trustedAppsList.mockImplementation(({ query }) => { - const { page, per_page: perPage } = query as { page: number; per_page: number }; - - if (page === 1 && perPage === 1) { - return releaseExistsResponse(); - } else { - return releaseListResponse(); - } - }); - }); - - afterEach(() => { - releaseExistsResponse.mockClear(); - releaseListResponse.mockClear(); - }); - - it('should show a loader until trusted apps existence can be confirmed', async () => { - render(); - expect(await renderResult.findByTestId('trustedAppsListLoader')).not.toBeNull(); - }); - - it('should show Empty Prompt if not entries exist', async () => { - render(); - await act(async () => { - await waitForAction('trustedAppsExistStateChanged'); - }); - expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull(); - }); - - it('should hide empty prompt and show list after one trusted app is added', async () => { - render(); - await act(async () => { - await waitForAction('trustedAppsExistStateChanged'); - }); - expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull(); - releaseListResponse.mockReturnValueOnce({ - data: [mockedApis.responseProvider.trustedApp({ query: {} } as HttpFetchOptionsWithPath)], - total: 1, - page: 1, - per_page: 20, - }); - releaseExistsResponse.mockReturnValueOnce({ - data: [mockedApis.responseProvider.trustedApp({ query: {} } as HttpFetchOptionsWithPath)], - total: 1, - page: 1, - per_page: 1, - }); - - await act(async () => { - mockedContext.store.dispatch({ - type: 'trustedAppsListDataOutdated', - }); - await waitForAction('trustedAppsListResourceStateChanged'); - }); - - expect(await renderResult.findByTestId('trustedAppsListPageContent')).not.toBeNull(); - }); - - it('should should show empty prompt once the last trusted app entry is deleted', async () => { - releaseListResponse.mockReturnValueOnce({ - data: [mockedApis.responseProvider.trustedApp({ query: {} } as HttpFetchOptionsWithPath)], - total: 1, - page: 1, - per_page: 20, - }); - releaseExistsResponse.mockReturnValueOnce({ - data: [mockedApis.responseProvider.trustedApp({ query: {} } as HttpFetchOptionsWithPath)], - total: 1, - page: 1, - per_page: 1, - }); - - render(); - - await act(async () => { - await waitForAction('trustedAppsExistStateChanged'); - }); - - expect(await renderResult.findByTestId('trustedAppsListPageContent')).not.toBeNull(); - - await act(async () => { - mockedContext.store.dispatch({ - type: 'trustedAppsListDataOutdated', - }); - await waitForAction('trustedAppsListResourceStateChanged'); - }); - - expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull(); - }); - - it('should not display the searchExceptions', async () => { - render(); - await act(async () => { - await waitForAction('trustedAppsExistStateChanged'); - }); - expect(renderResult.queryByTestId('searchExceptions')).toBeNull(); - }); - }); - - describe('and the search is dispatched', () => { - beforeEach(async () => { - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps?filter=test'); - }); - render(); - await act(async () => { - await waitForListUI(); - }); - }); - - it('search bar is filled with query params', () => { - expect(renderResult.getByDisplayValue('test')).not.toBeNull(); - }); - - it('search action is dispatched', async () => { - await act(async () => { - fireEvent.click(renderResult.getByTestId('searchButton')); - expect(await waitForAction('userChangedUrl')).not.toBeNull(); - }); - }); - }); - - describe('and the back button is present', () => { - beforeEach(async () => { - render(); - await act(async () => { - await waitForListUI(); - }); - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps', { - onBackButtonNavigateTo: [{ appId: 'appId' }], - backButtonLabel: 'back to fleet', - backButtonUrl: '/fleet', - }); - }); - }); - - it('back button is present', () => { - const button = renderResult.queryByTestId('backToOrigin'); - expect(button).not.toBeNull(); - expect(button).toHaveAttribute('href', '/fleet'); - }); - - it('back button is present after push history', () => { - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps'); - }); - const button = renderResult.queryByTestId('backToOrigin'); - expect(button).not.toBeNull(); - expect(button).toHaveAttribute('href', '/fleet'); - }); - }); - - describe('and the back button is not present', () => { - beforeEach(async () => { - render(); - await act(async () => { - await waitForAction('trustedAppsListResourceStateChanged'); - }); - reactTestingLibrary.act(() => { - history.push('/administration/trusted_apps'); - }); - }); - - it('back button is not present when missing history params', () => { - const button = renderResult.queryByTestId('backToOrigin'); - expect(button).toBeNull(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx deleted file mode 100644 index 6b3f59f44ce12..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo, useMemo, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; -import { useLocation } from 'react-router-dom'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { - checkingIfEntriesExist, - entriesExist, - getCurrentLocation, - getListTotalItemsCount, - listOfPolicies, - prevEntriesExist, -} from '../store/selectors'; -import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from './hooks'; -import { AdministrationListPage } from '../../../components/administration_list_page'; -import { CreateTrustedAppFlyout } from './components/create_trusted_app_flyout'; -import { TrustedAppsGrid } from './components/trusted_apps_grid'; -import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; -import { TrustedAppsNotifications } from './trusted_apps_notifications'; -import { AppAction } from '../../../../common/store/actions'; -import { ABOUT_TRUSTED_APPS, SEARCH_TRUSTED_APP_PLACEHOLDER } from './translations'; -import { EmptyState } from './components/empty_state'; -import { SearchExceptions } from '../../../components/search_exceptions'; -import { BackToExternalAppSecondaryButton } from '../../../components/back_to_external_app_secondary_button'; -import { BackToExternalAppButton } from '../../../components/back_to_external_app_button'; -import { ListPageRouteState } from '../../../../../common/endpoint/types'; -import { ManagementPageLoader } from '../../../components/management_page_loader'; -import { useMemoizedRouteState } from '../../../common/hooks'; - -export const TrustedAppsPage = memo(() => { - const dispatch = useDispatch>(); - const { state: routeState } = useLocation(); - const location = useTrustedAppsSelector(getCurrentLocation); - const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount); - const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist); - const policyList = useTrustedAppsSelector(listOfPolicies); - const doEntriesExist = useTrustedAppsSelector(entriesExist); - const didEntriesExist = useTrustedAppsSelector(prevEntriesExist); - const navigationCallbackQuery = useTrustedAppsNavigateCallback( - (query: string, includedPolicies?: string) => ({ - filter: query, - included_policies: includedPolicies, - }) - ); - - const memoizedRouteState = useMemoizedRouteState(routeState); - - const backButtonEmptyComponent = useMemo(() => { - if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { - return ; - } - }, [memoizedRouteState]); - - const backButtonHeaderComponent = useMemo(() => { - if (memoizedRouteState && memoizedRouteState.onBackButtonNavigateTo) { - return ; - } - }, [memoizedRouteState]); - - const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({ - show: 'create', - id: undefined, - })); - const handleAddFlyoutClose = useTrustedAppsNavigateCallback(() => ({ - show: undefined, - id: undefined, - })); - - const handleOnSearch = useCallback( - (query: string, includedPolicies?: string) => { - dispatch({ type: 'trustedAppForceRefresh', payload: { forceRefresh: true } }); - navigationCallbackQuery(query, includedPolicies); - }, - [dispatch, navigationCallbackQuery] - ); - - const showCreateFlyout = !!location.show; - - const canDisplayContent = useCallback( - () => doEntriesExist || (isCheckingIfEntriesExists && didEntriesExist), - [didEntriesExist, doEntriesExist, isCheckingIfEntriesExists] - ); - - const addButton = ( - - - - ); - - const content = ( - <> - - - {showCreateFlyout && ( - - )} - - {canDisplayContent() ? ( - <> - - - - - - {i18n.translate('xpack.securitySolution.trustedapps.list.totalCount', { - defaultMessage: - 'Showing {totalItemsCount, plural, one {# trusted application} other {# trusted applications}}', - values: { totalItemsCount }, - })} - - - - - - - - - - ) : ( - - )} - - ); - - return ( - - } - subtitle={ABOUT_TRUSTED_APPS} - actions={addButton} - hideHeader={!canDisplayContent()} - > - - - {isCheckingIfEntriesExists && !didEntriesExist ? ( - - ) : ( - content - )} - - ); -}); - -TrustedAppsPage.displayName = 'TrustedAppsPage'; diff --git a/x-pack/plugins/security_solution/public/management/store/middleware.ts b/x-pack/plugins/security_solution/public/management/store/middleware.ts index d011a9dcb91a7..86a5ade340058 100644 --- a/x-pack/plugins/security_solution/public/management/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/store/middleware.ts @@ -14,12 +14,10 @@ import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_GLOBAL_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, - MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, } from '../common/constants'; import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware'; -import { trustedAppsPageMiddlewareFactory } from '../pages/trusted_apps/store/middleware'; import { eventFiltersPageMiddlewareFactory } from '../pages/event_filters/store/middleware'; type ManagementSubStateKey = keyof State[typeof MANAGEMENT_STORE_GLOBAL_NAMESPACE]; @@ -42,10 +40,7 @@ export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( createSubStateSelector(MANAGEMENT_STORE_ENDPOINTS_NAMESPACE), endpointMiddlewareFactory(coreStart, depsStart) ), - substateMiddlewareFactory( - createSubStateSelector(MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE), - trustedAppsPageMiddlewareFactory(coreStart, depsStart) - ), + substateMiddlewareFactory( createSubStateSelector(MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE), eventFiltersPageMiddlewareFactory(coreStart, depsStart) diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index 662d2b4322bcb..2fd20129ddca8 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -13,15 +13,12 @@ import { import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, - MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, } from '../common/constants'; import { ImmutableCombineReducers } from '../../common/store'; import { Immutable } from '../../../common/endpoint/types'; import { ManagementState } from '../types'; import { endpointListReducer } from '../pages/endpoint_hosts/store/reducer'; -import { initialTrustedAppsPageState } from '../pages/trusted_apps/store/builders'; -import { trustedAppsPageReducer } from '../pages/trusted_apps/store/reducer'; import { initialEventFiltersPageState } from '../pages/event_filters/store/builders'; import { eventFiltersPageReducer } from '../pages/event_filters/store/reducer'; import { initialEndpointPageState } from '../pages/endpoint_hosts/store/builders'; @@ -34,7 +31,6 @@ const immutableCombineReducers: ImmutableCombineReducers = combineReducers; export const mockManagementState: Immutable = { [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointPageState(), - [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(), [MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: initialEventFiltersPageState(), }; @@ -44,6 +40,5 @@ export const mockManagementState: Immutable = { export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: endpointListReducer, - [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer, [MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE]: eventFiltersPageReducer, }); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index 65063fce96e67..0ad0f2e757c00 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -9,7 +9,6 @@ import { CombinedState } from 'redux'; import { SecurityPageName } from '../app/types'; import { PolicyDetailsState } from './pages/policy/types'; import { EndpointState } from './pages/endpoint_hosts/types'; -import { TrustedAppsListPageState } from './pages/trusted_apps/state'; import { EventFiltersListPageState } from './pages/event_filters/types'; /** @@ -21,7 +20,6 @@ export type ManagementStoreGlobalNamespace = 'management'; export type ManagementState = CombinedState<{ policyDetails: PolicyDetailsState; endpoints: EndpointState; - trustedApps: TrustedAppsListPageState; eventFilters: EventFiltersListPageState; }>; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts index 45d080ab956ee..5510f352a414e 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/blocklist_validator.ts @@ -16,7 +16,7 @@ import { } from '@kbn/lists-plugin/server'; import { BaseValidator } from './base_validator'; import { ExceptionItemLikeOptions } from '../types'; -import { isValidHash } from '../../../../common/endpoint/service/trusted_apps/validations'; +import { isValidHash } from '../../../../common/endpoint/service/artifacts/validations'; import { EndpointArtifactExceptionValidationError } from './errors'; const allowedHashes: Readonly = ['file.hash.md5', 'file.hash.sha1', 'file.hash.sha256']; diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts index b83e536cdee1e..af98ba7076535 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -19,7 +19,7 @@ import { TrustedAppConditionEntry as ConditionEntry } from '../../../../common/e import { getDuplicateFields, isValidHash, -} from '../../../../common/endpoint/service/trusted_apps/validations'; +} from '../../../../common/endpoint/service/artifacts/validations'; import { EndpointArtifactExceptionValidationError } from './errors'; const ProcessHashField = schema.oneOf([ diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 32bb864b582e4..123ce3c2a15be 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -26809,8 +26809,6 @@ "xpack.securitySolution.trustedApps.assignmentSectionDescription": "Affectez cette application de confiance globalement à toutes les politiques, ou de façon spécifique à certaines politiques.", "xpack.securitySolution.trustedapps.card.operator.is": "est", "xpack.securitySolution.trustedapps.card.operator.matches": "correspond à", - "xpack.securitySolution.trustedApps.conditionsSectionDescription": "Sélectionnez un système d'exploitation et ajoutez des conditions. La disponibilité des conditions peut dépendre de votre système d’exploitation.", - "xpack.securitySolution.trustedApps.conditionsSectionTitle": "Conditions", "xpack.securitySolution.trustedapps.create.conditionFieldDegradedPerformanceMsg": "[{row}] L'utilisation d'un caractère générique dans le nom de fichier affectera les performances du point de terminaison", "xpack.securitySolution.trustedapps.create.conditionFieldDuplicatedMsg": "{field} ne peut pas être ajouté plus d'une fois", "xpack.securitySolution.trustedapps.create.conditionFieldInvalidHashMsg": "[{row}] Valeur de hachage non valide", @@ -26818,41 +26816,10 @@ "xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg": "[{row}] L'entrée de champ doit inclure une valeur", "xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "Au moins une définition de champ est requise", "xpack.securitySolution.trustedapps.create.description": "Description", - "xpack.securitySolution.trustedapps.create.name": "Nommer votre application de confiance", "xpack.securitySolution.trustedapps.create.nameRequiredMsg": "Le nom est requis", - "xpack.securitySolution.trustedapps.create.os": "Sélectionner un système d'exploitation", "xpack.securitySolution.trustedapps.create.osRequiredMsg": "Le système d'exploitation est requis", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.cancelButton": "Annuler", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.createSaveButton": "Ajouter une application de confiance", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.createTitle": "Ajouter une application de confiance", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.editSaveButton": "Enregistrer", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.editTitle": "Modifier une application de confiance", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.expiredLicenseMessage": "Votre licence Kibana est passée à une version inférieure. Les futures configurations de politiques seront désormais globalement affectées à toutes les politiques. Pour en savoir plus, consultez notre ", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.expiredLicenseTitle": "Licence expirée", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.notFoundToastMessage": "Impossible de modifier l'application de confiance ({apiMsg})", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle": "\"{name}\" a été ajouté à la liste d'applications de confiance.", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.updateSuccessToastTitle": "\"{name}\" a été mis à jour.", - "xpack.securitySolution.trustedapps.creationSuccess.title": "Cette action a réussi !", - "xpack.securitySolution.trustedapps.deletionDialog.calloutMessage": "La suppression de cette entrée entraînera son retrait dans {count} {count, plural, one {politique associée} other {politiques associées}}.", - "xpack.securitySolution.trustedapps.deletionDialog.calloutTitle": "Avertissement", - "xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "Annuler", - "xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "Supprimer", - "xpack.securitySolution.trustedapps.deletionDialog.subMessage": "Cette action ne peut pas être annulée. Voulez-vous vraiment continuer ?", - "xpack.securitySolution.trustedapps.deletionDialog.title": "Supprimer \"{name}\"", - "xpack.securitySolution.trustedapps.deletionError.text": "Impossible de retirer \"{name}\" de la liste d'applications de confiance. Raison : {message}", - "xpack.securitySolution.trustedapps.deletionError.title": "Échec du retrait", - "xpack.securitySolution.trustedapps.deletionSuccess.text": "\"{name}\" a été retiré de la liste d'applications de confiance.", - "xpack.securitySolution.trustedapps.deletionSuccess.title": "Retrait effectué avec succès", - "xpack.securitySolution.trustedApps.detailsSectionTitle": "Détails", - "xpack.securitySolution.trustedapps.docsLink": "Documentation relative aux applications de confiance.", - "xpack.securitySolution.trustedapps.grid.cardAction.delete": "Supprimer une application de confiance", - "xpack.securitySolution.trustedapps.grid.cardAction.edit": "Modifier une application de confiance", - "xpack.securitySolution.trustedapps.grid.policyDetailsLinkBackLabel": "Retour aux applications de confiance", "xpack.securitySolution.trustedapps.list.addButton": "Ajouter une application de confiance", - "xpack.securitySolution.trustedapps.list.pageTitle": "Applications de confiance", - "xpack.securitySolution.trustedapps.list.search.placeholder": "Rechercher sur les champs ci-dessous : nom, description, valeur", - "xpack.securitySolution.trustedapps.list.totalCount": "Affichage de {totalItemsCount, plural, one {# application de confiance} other {# applications de confiance}}", - "xpack.securitySolution.trustedapps.listEmptyState.message": "Ajoutez une application de confiance pour améliorer les performances ou réduire les conflits avec d'autres applications en cours d'exécution sur vos hôtes.", + "xpack.securitySolution.trustedapps.listEmptyState.message": "Aucune application de confiance ne figure actuellement dans votre point de terminaison.", "xpack.securitySolution.trustedapps.listEmptyState.title": "Ajouter votre première application de confiance", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.hash": "md5, sha1 ou sha256", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.path": "Chemin complet de l'application", @@ -26863,14 +26830,9 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "Retirer l'entrée", "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "Aucune condition définie", - "xpack.securitySolution.trustedapps.middleware.editIdMissing": "Aucun ID fourni", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "Champ", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "Opérateur", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "Valeur", - "xpack.securitySolution.trustedapps.updateSuccess.title": "Cette action a réussi !", - "xpack.securitySolution.trustedapps.view.toggle.grid": "Vue Grille", - "xpack.securitySolution.trustedapps.view.toggle.list": "Vue Liste", - "xpack.securitySolution.trustedapps.viewTypeToggle.controlLegend": "Type de vue", "xpack.securitySolution.trustedAppsTab": "Applications de confiance", "xpack.securitySolution.uiSettings.defaultAnomalyScoreDescription": "

Valeur au-dessus de laquelle les anomalies de tâche de Machine Learning sont affichées dans l'application Security.

Valeurs valides : 0 à 100.

", "xpack.securitySolution.uiSettings.defaultAnomalyScoreLabel": "Seuil d'anomalie", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e79baef772910..cd6ac0cdfc14d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26983,8 +26983,6 @@ "xpack.securitySolution.trustedApps.assignmentSectionDescription": "すべてのポリシーでグローバルにこの信頼できるアプリケーションを割り当てるか、特定のポリシーに割り当てます。", "xpack.securitySolution.trustedapps.card.operator.is": "is", "xpack.securitySolution.trustedapps.card.operator.matches": "一致", - "xpack.securitySolution.trustedApps.conditionsSectionDescription": "オペレーティングシステムを選択して、条件を追加します。条件の可用性は選択したOSによって異なる場合があります。", - "xpack.securitySolution.trustedApps.conditionsSectionTitle": "条件", "xpack.securitySolution.trustedapps.create.conditionFieldDegradedPerformanceMsg": "[{row}] ファイル名のワイルドカードはエンドポイントのパフォーマンスに影響します", "xpack.securitySolution.trustedapps.create.conditionFieldDuplicatedMsg": "{field}を複数回追加できません", "xpack.securitySolution.trustedapps.create.conditionFieldInvalidHashMsg": "[{row}] 無効なハッシュ値", @@ -26992,39 +26990,9 @@ "xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg": "[{row}] フィールドエントリには値が必要です", "xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "1つ以上のフィールド定義が必要です", "xpack.securitySolution.trustedapps.create.description": "説明", - "xpack.securitySolution.trustedapps.create.name": "信頼できるアプリケーションに名前を付ける", "xpack.securitySolution.trustedapps.create.nameRequiredMsg": "名前が必要です", - "xpack.securitySolution.trustedapps.create.os": "オペレーティングシステムを選択", "xpack.securitySolution.trustedapps.create.osRequiredMsg": "オペレーティングシステムは必須です", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.cancelButton": "キャンセル", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.createSaveButton": "信頼できるアプリケーションを追加", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.createTitle": "信頼できるアプリケーションを追加", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.editSaveButton": "保存", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.editTitle": "信頼できるアプリケーションを編集", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.expiredLicenseMessage": "Kibanaライセンスがダウングレードされました。今後のポリシー構成はグローバルにすべてのポリシーに割り当てられます。詳細はご覧ください。 ", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.expiredLicenseTitle": "失効したライセンス", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.notFoundToastMessage": "信頼できるアプリケーションを編集できません({apiMsg})", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle": "\"{name}\"は信頼できるアプリケーションリストに追加されました。", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.updateSuccessToastTitle": "\"{name}\"が更新されました。", - "xpack.securitySolution.trustedapps.creationSuccess.title": "成功!", - "xpack.securitySolution.trustedapps.deletionDialog.calloutMessage": "このエントリを削除すると、{count}個の関連付けられた{count, plural, other {ポリシー}}から削除されます。", - "xpack.securitySolution.trustedapps.deletionDialog.calloutTitle": "警告", - "xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "キャンセル", - "xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "削除", - "xpack.securitySolution.trustedapps.deletionDialog.subMessage": "この操作は元に戻すことができません。続行していいですか?", - "xpack.securitySolution.trustedapps.deletionDialog.title": "\"{name}\"を削除", - "xpack.securitySolution.trustedapps.deletionError.text": "信頼できるアプリケーションリストから\"{name}\"を削除できません。理由:{message}", - "xpack.securitySolution.trustedapps.deletionError.title": "削除失敗", - "xpack.securitySolution.trustedapps.deletionSuccess.text": "\"{name}\"は信頼できるアプリケーションリストから削除されました。", - "xpack.securitySolution.trustedapps.deletionSuccess.title": "正常に削除されました", - "xpack.securitySolution.trustedApps.detailsSectionTitle": "詳細", - "xpack.securitySolution.trustedapps.docsLink": "信頼できるアプリケーションドキュメント。", - "xpack.securitySolution.trustedapps.grid.cardAction.delete": "信頼できるアプリケーションを削除", - "xpack.securitySolution.trustedapps.grid.cardAction.edit": "信頼できるアプリケーションを編集", - "xpack.securitySolution.trustedapps.grid.policyDetailsLinkBackLabel": "信頼できるアプリケーションに戻る", "xpack.securitySolution.trustedapps.list.addButton": "信頼できるアプリケーションを追加", - "xpack.securitySolution.trustedapps.list.pageTitle": "信頼できるアプリケーション", - "xpack.securitySolution.trustedapps.list.search.placeholder": "次のフィールドで検索:名前、説明、値", "xpack.securitySolution.trustedapps.listEmptyState.message": "パフォーマンスを改善したり、ホストで実行されている他のアプリケーションとの競合を解消したりするには、信頼できるアプリケーションを追加します。", "xpack.securitySolution.trustedapps.listEmptyState.title": "最初の信頼できるアプリケーションを追加", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.hash": "md5、sha1、sha256", @@ -27036,14 +27004,9 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "エントリを削除", "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", - "xpack.securitySolution.trustedapps.middleware.editIdMissing": "IDが指定されていません", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "フィールド", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "演算子", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "値", - "xpack.securitySolution.trustedapps.updateSuccess.title": "成功!", - "xpack.securitySolution.trustedapps.view.toggle.grid": "グリッドビュー", - "xpack.securitySolution.trustedapps.view.toggle.list": "リストビュー", - "xpack.securitySolution.trustedapps.viewTypeToggle.controlLegend": "ビュータイプ", "xpack.securitySolution.trustedAppsTab": "信頼できるアプリケーション", "xpack.securitySolution.uiSettings.defaultAnomalyScoreDescription": "

機械学習ジョブの異常がこの値を超えると、セキュリティアプリに表示されます。

有効な値:0 ~ 100。

", "xpack.securitySolution.uiSettings.defaultAnomalyScoreLabel": "デフォルトの異常しきい値", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 19e216f111a62..5fe8340d34682 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27017,8 +27017,6 @@ "xpack.securitySolution.trustedApps.assignmentSectionDescription": "跨所有策略全局分配此受信任的应用程序,或将其分配给特定策略。", "xpack.securitySolution.trustedapps.card.operator.is": "是", "xpack.securitySolution.trustedapps.card.operator.matches": "匹配", - "xpack.securitySolution.trustedApps.conditionsSectionDescription": "选择操作系统,然后添加条件。条件的可用性可能取决于您选定的 OS。", - "xpack.securitySolution.trustedApps.conditionsSectionTitle": "条件", "xpack.securitySolution.trustedapps.create.conditionFieldDegradedPerformanceMsg": "[{row}] 文件名中存在通配符将影响终端性能", "xpack.securitySolution.trustedapps.create.conditionFieldDuplicatedMsg": "不能多次添加 {field}", "xpack.securitySolution.trustedapps.create.conditionFieldInvalidHashMsg": "[{row}] 无效的哈希值", @@ -27026,40 +27024,9 @@ "xpack.securitySolution.trustedapps.create.conditionFieldValueRequiredMsg": "[{row}] 字段条目必须包含值", "xpack.securitySolution.trustedapps.create.conditionRequiredMsg": "至少需要一个字段定义", "xpack.securitySolution.trustedapps.create.description": "描述", - "xpack.securitySolution.trustedapps.create.name": "命名受信任的应用程序", "xpack.securitySolution.trustedapps.create.nameRequiredMsg": "“名称”必填", - "xpack.securitySolution.trustedapps.create.os": "选择操作系统", "xpack.securitySolution.trustedapps.create.osRequiredMsg": "“操作系统”必填", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.cancelButton": "取消", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.createSaveButton": "添加受信任的应用程序", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.createTitle": "添加受信任的应用程序", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.editSaveButton": "保存", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.editTitle": "编辑受信任的应用程序", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.expiredLicenseMessage": "您的 Kibana 许可证已降级。现在会将未来的策略配置全局分配给所有策略。有关更多信息,请参见 ", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.expiredLicenseTitle": "已过期许可证", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.notFoundToastMessage": "无法编辑受信任的应用程序 ({apiMsg})", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle": "“{name}”已添加到受信任的应用程序列表。", - "xpack.securitySolution.trustedapps.createTrustedAppFlyout.updateSuccessToastTitle": "“{name}”已更新。", - "xpack.securitySolution.trustedapps.creationSuccess.title": "成功!", - "xpack.securitySolution.trustedapps.deletionDialog.calloutMessage": "删除此条目会将其从 {count} 个关联{count, plural, other {策略}}中移除。", - "xpack.securitySolution.trustedapps.deletionDialog.calloutTitle": "警告", - "xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "取消", - "xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "删除", - "xpack.securitySolution.trustedapps.deletionDialog.subMessage": "此操作无法撤消。是否确定要继续?", - "xpack.securitySolution.trustedapps.deletionDialog.title": "删除“{name}”", - "xpack.securitySolution.trustedapps.deletionError.text": "无法从受信任的应用程序列表中移除“{name}”。原因:{message}", - "xpack.securitySolution.trustedapps.deletionError.title": "移除失败", - "xpack.securitySolution.trustedapps.deletionSuccess.text": "“{name}”已从受信任的应用程序列表中移除。", - "xpack.securitySolution.trustedapps.deletionSuccess.title": "已成功移除", - "xpack.securitySolution.trustedApps.detailsSectionTitle": "详情", - "xpack.securitySolution.trustedapps.docsLink": "受信任的应用程序文档。", - "xpack.securitySolution.trustedapps.grid.cardAction.delete": "删除受信任的应用程序", - "xpack.securitySolution.trustedapps.grid.cardAction.edit": "编辑受信任的应用程序", - "xpack.securitySolution.trustedapps.grid.policyDetailsLinkBackLabel": "返回到受信任的应用程序", "xpack.securitySolution.trustedapps.list.addButton": "添加受信任的应用程序", - "xpack.securitySolution.trustedapps.list.pageTitle": "受信任的应用程序", - "xpack.securitySolution.trustedapps.list.search.placeholder": "搜索下面的字段:name、description、value", - "xpack.securitySolution.trustedapps.list.totalCount": "正在显示 {totalItemsCount, plural, other {# 个受信任的应用程序}}", "xpack.securitySolution.trustedapps.listEmptyState.message": "添加受信任的应用程序,以提高性能或缓解与主机上运行的其他应用程序的冲突。", "xpack.securitySolution.trustedapps.listEmptyState.title": "添加您的首个受信任应用程序", "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.hash": "md5、sha1 或 sha256", @@ -27071,14 +27038,9 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "移除条目", "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", - "xpack.securitySolution.trustedapps.middleware.editIdMissing": "未提供 ID", "xpack.securitySolution.trustedapps.trustedapp.entry.field": "字段", "xpack.securitySolution.trustedapps.trustedapp.entry.operator": "运算符", "xpack.securitySolution.trustedapps.trustedapp.entry.value": "值", - "xpack.securitySolution.trustedapps.updateSuccess.title": "成功!", - "xpack.securitySolution.trustedapps.view.toggle.grid": "网格视图", - "xpack.securitySolution.trustedapps.view.toggle.list": "列表视图", - "xpack.securitySolution.trustedapps.viewTypeToggle.controlLegend": "视图类型", "xpack.securitySolution.trustedAppsTab": "受信任的应用程序", "xpack.securitySolution.uiSettings.defaultAnomalyScoreDescription": "

要在 Security 应用中显示的 Machine Learning 作业异常所需超过的值。

有效值:0 到 100。

", "xpack.securitySolution.uiSettings.defaultAnomalyScoreLabel": "异常阈值", diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/fleet_integrations.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/fleet_integrations.ts index 4f9b7ad7c0401..ea5746060dedc 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/fleet_integrations.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/fleet_integrations.ts @@ -48,7 +48,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should show the Trusted Apps page when link is clicked', async () => { await (await fleetIntegrations.findIntegrationDetailCustomTab()).click(); await (await testSubjects.find('trustedApps-artifactsLink')).click(); - await trustedApps.ensureIsOnTrustedAppsListPage(); + await trustedApps.ensureIsOnTrustedAppsEmptyPage(); }); }); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 7f2357cebb2c6..4643b91303be0 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts @@ -37,18 +37,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const SHA256 = 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476'; // Add it - await testSubjects.click('trustedAppsListAddButton'); - await testSubjects.setValue( - 'addTrustedAppFlyout-createForm-nameTextField', - 'Windows Defender' - ); - await testSubjects.setValue( - 'addTrustedAppFlyout-createForm-conditionsBuilder-group1-entry0-value', - SHA256 - ); - await testSubjects.click('addTrustedAppFlyout-createButton'); + await testSubjects.click('trustedAppsListPage-emptyState-addButton'); + await testSubjects.setValue('trustedApps-form-nameTextField', 'Windows Defender'); + await testSubjects.setValue('trustedApps-form-conditionsBuilder-group1-entry0-value', SHA256); + await testSubjects.click('trustedAppsListPage-flyout-submitButton'); expect( - await testSubjects.getVisibleText('trustedAppCard-criteriaConditions-condition') + await testSubjects.getVisibleText('trustedAppsListPage-card-criteriaConditions-condition') ).to.equal( 'AND process.hash.*IS a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476' ); @@ -61,11 +55,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Remove it await pageObjects.trustedApps.clickCardActionMenu(); - await testSubjects.click('deleteTrustedAppAction'); - await testSubjects.click('trustedAppDeletionConfirm'); - await testSubjects.waitForDeleted('trustedAppDeletionConfirm'); + await testSubjects.click('trustedAppsListPage-card-cardDeleteAction'); + await testSubjects.click('trustedAppsListPage-deleteModal-submitButton'); + await testSubjects.waitForDeleted('trustedAppsListPage-deleteModal-submitButton'); // We only expect one trusted app to have been visible - await testSubjects.missingOrFail('trustedAppCard'); + await testSubjects.missingOrFail('trustedAppsListPage-card'); // Header has gone because there is no trusted app await testSubjects.missingOrFail('header-page-title'); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/trusted_apps_page.ts b/x-pack/test/security_solution_endpoint/page_objects/trusted_apps_page.ts index 1678358acc11e..7dc2e84358f54 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/trusted_apps_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/trusted_apps_page.ts @@ -24,7 +24,11 @@ export function TrustedAppsPageProvider({ getService, getPageObjects }: FtrProvi * ensures that the Policy Page is the currently display view */ async ensureIsOnTrustedAppsListPage() { - await testSubjects.existOrFail('trustedAppsListPage'); + await testSubjects.existOrFail('trustedAppsListPage-list'); + }, + + async ensureIsOnTrustedAppsEmptyPage() { + await testSubjects.existOrFail('trustedAppsListPage-emptyState'); }, /** @@ -41,7 +45,7 @@ export function TrustedAppsPageProvider({ getService, getPageObjects }: FtrProvi */ async clickCardActionMenu() { await this.ensureIsOnTrustedAppsListPage(); - await testSubjects.click('trustedAppCard-header-actions-button'); + await testSubjects.click('trustedAppsListPage-card-header-actions-button'); }, }; } From ee1e2bb0dd85527168a76d51d76e0990c6de678f Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 21 Apr 2022 14:32:11 -0400 Subject: [PATCH 15/24] [Fleet] Fix aligment logstash copy API key tooltip (#130815) --- .../components/logstash_instructions/index.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/index.tsx index c3d43f85038c0..752038e953b1c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/logstash_instructions/index.tsx @@ -111,10 +111,11 @@ const LogstashInstructionSteps = () => {
API Key
{logstashApiKey.apiKey} - - {(copy) => ( -
-
+ +
+
+ + {(copy) => ( { } )} /> -
-
- )} - + )} + +
+
) : ( Date: Thu, 21 Apr 2022 12:33:51 -0600 Subject: [PATCH 16/24] [Security Solution] [Investigations] [Tech Debt] removes deepEqual checks in column headers and data providers (#130740) ## [Security Solution] [Investigations] [Tech Debt] removes `deepEqual` checks in column headers and data providers This tech debt PR is another entry in a series to remove `React.memo` `deepEqual` checks, per the details in - It removes `deepEqual` checks in Timeline's column headers and data providers - Files made redundant by the `timelines` plugin adopting `EuiDataGrid` are deleted ### Methodology The following techniques were used to ensure that removing the `deepEqual` checks did NOT result in unexpected re-renders: - To understand why components re-rendered, Timeline was profiled with the `Record why each component rendered wile profiling` setting in the React dev tools Profiler enabled, shown in the (illustrative) screenshot below: ![record_why_each_component_rendered](https://user-images.githubusercontent.com/4459398/158903740-8122e2d3-11a6-4927-916a-f895717835ae.png) - Components were temporarily instrumented with counters that incremented every time the component was rendered. Log statements prefixed with `[pre]` were observed before making changes, per the screenshot below: ![pre_change](https://user-images.githubusercontent.com/4459398/164310611-3837bd09-0b31-434e-8ef7-94434d35be48.png) - After removing the `deepEqual` checks, the log prefix was updated to `[POST]`, and the log entries were observed again, per the screenshot below: ![post_change](https://user-images.githubusercontent.com/4459398/164310656-f5c82443-2ff4-4e62-8c7b-8fa9dbce5dfd.png) The `[pre]` and `[POST]` counters were compared to verify removing the `deepEqual` checks did NOT introduce unexpected re-renders. --- .../body/column_headers/column_header.tsx | 13 +- .../timeline/body/column_headers/index.tsx | 19 +- .../timeline/data_providers/providers.tsx | 10 +- .../__snapshots__/index.test.tsx.snap | 658 ------------------ .../body/column_headers/column_header.tsx | 309 -------- .../t_grid/body/column_headers/index.test.tsx | 319 --------- .../t_grid/body/column_headers/index.tsx | 294 -------- 7 files changed, 4 insertions(+), 1618 deletions(-) delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/column_header.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx index 74593e40ddf4c..67fcff7de1332 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/column_header.tsx @@ -9,7 +9,6 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover } fr import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { Resizable, ResizeCallback } from 're-resizable'; -import deepEqual from 'fast-deep-equal'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid'; @@ -298,14 +297,4 @@ const ColumnHeaderComponent: React.FC = ({ ); }; -export const ColumnHeader = React.memo( - ColumnHeaderComponent, - (prevProps, nextProps) => - prevProps.draggableIndex === nextProps.draggableIndex && - prevProps.tabType === nextProps.tabType && - prevProps.timelineId === nextProps.timelineId && - prevProps.isDragging === nextProps.isDragging && - prevProps.onFilterChange === nextProps.onFilterChange && - deepEqual(prevProps.sort, nextProps.sort) && - deepEqual(prevProps.header, nextProps.header) -); +export const ColumnHeader = React.memo(ColumnHeaderComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index 56984452964de..2bd5eda49bd98 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import deepEqual from 'fast-deep-equal'; + import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; @@ -312,19 +312,4 @@ export const ColumnHeadersComponent = ({ ); }; -export const ColumnHeaders = React.memo( - ColumnHeadersComponent, - (prevProps, nextProps) => - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.onSelectAll === nextProps.onSelectAll && - prevProps.show === nextProps.show && - prevProps.showEventsSelect === nextProps.showEventsSelect && - prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && - deepEqual(prevProps.sort, nextProps.sort) && - prevProps.timelineId === nextProps.timelineId && - deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && - prevProps.tabType === nextProps.tabType && - deepEqual(prevProps.browserFields, nextProps.browserFields) -); +export const ColumnHeaders = React.memo(ColumnHeadersComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx index 3db938f5efe07..7e52267cd6f1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.tsx @@ -11,7 +11,6 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Draggable, DraggingStyle, Droppable, NotDraggingStyle } from 'react-beautiful-dnd'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, @@ -357,14 +356,7 @@ export const DataProvidersGroupItem = React.memo(
); - }, - (prevProps, nextProps) => - prevProps.groupIndex === nextProps.groupIndex && - prevProps.index === nextProps.index && - prevProps.timelineId === nextProps.timelineId && - deepEqual(prevProps.browserFields, nextProps.browserFields) && - deepEqual(prevProps.group, nextProps.group) && - deepEqual(prevProps.dataProvider, nextProps.dataProvider) + } ); DataProvidersGroupItem.displayName = 'DataProvidersGroupItem'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 233e1c921cd50..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,658 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` - -`; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/column_header.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/column_header.tsx deleted file mode 100644 index 4e6db10cc8bce..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/column_header.tsx +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover } from '@elastic/eui'; -import { - DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, - getDraggableFieldId, -} from '@kbn/securitysolution-t-grid'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { Draggable } from 'react-beautiful-dnd'; -import { Resizable, ResizeCallback } from 're-resizable'; -import deepEqual from 'fast-deep-equal'; -import { useDispatch } from 'react-redux'; -import styled from 'styled-components'; - -import { DEFAULT_COLUMN_MIN_WIDTH } from '../constants'; - -import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers'; -import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; -import { Sort } from '../sort'; - -import { Header } from './header'; - -import * as i18n from './translations'; -import { tGridActions } from '../../../../store/t_grid'; -import { TimelineTabs } from '../../../../../common/types/timeline'; -import type { ColumnHeaderOptions } from '../../../../../common/types/timeline'; - -import { Direction } from '../../../../../common/search_strategy'; -import { useDraggableKeyboardWrapper } from '../../../drag_and_drop'; - -const ContextMenu = styled(EuiContextMenu)` - width: 115px; - - & .euiContextMenuItem { - font-size: 12px; - padding: 4px 8px; - width: 115px; - } -`; - -const PopoverContainer = styled.div<{ $width: number }>` - & .euiPopover__anchor { - padding-right: 8px; - width: ${({ $width }) => $width}px; - } -`; - -const RESIZABLE_ENABLE = { right: true }; - -interface ColumneHeaderProps { - draggableIndex: number; - header: ColumnHeaderOptions; - isDragging: boolean; - sort: Sort[]; - tabType: TimelineTabs; - timelineId: string; -} - -const ColumnHeaderComponent: React.FC = ({ - draggableIndex, - header, - timelineId, - isDragging, - sort, - tabType, -}) => { - const keyboardHandlerRef = useRef(null); - const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); - const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []); - - const dispatch = useDispatch(); - const resizableSize = useMemo( - () => ({ - width: header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH, - height: 'auto', - }), - [header.initialWidth] - ); - const resizableStyle: { - position: 'absolute' | 'relative'; - } = useMemo( - () => ({ - position: isDragging ? 'absolute' : 'relative', - }), - [isDragging] - ); - const resizableHandleComponent = useMemo( - () => ({ - right: , - }), - [] - ); - const handleResizeStop: ResizeCallback = useCallback( - (e, direction, ref, delta) => { - dispatch( - tGridActions.applyDeltaToColumnWidth({ - columnId: header.id, - delta: delta.width, - id: timelineId, - }) - ); - }, - [dispatch, header.id, timelineId] - ); - const draggableId = useMemo( - () => - getDraggableFieldId({ - contextId: `timeline-column-headers-${tabType}-${timelineId}`, - fieldId: header.id, - }), - [tabType, timelineId, header.id] - ); - - const onColumnSort = useCallback( - (sortDirection: Direction) => { - const columnId = header.id; - const headerIndex = sort.findIndex((col) => col.columnId === columnId); - const newSort = - headerIndex === -1 - ? [ - ...sort, - { - columnId, - columnType: `${header.type}`, - sortDirection, - }, - ] - : [ - ...sort.slice(0, headerIndex), - { - columnId, - columnType: `${header.type}`, - sortDirection, - }, - ...sort.slice(headerIndex + 1), - ]; - - dispatch( - tGridActions.updateSort({ - id: timelineId, - sort: newSort, - }) - ); - }, - [dispatch, header, sort, timelineId] - ); - - const handleClosePopOverTrigger = useCallback(() => { - setHoverActionsOwnFocus(false); - restoreFocus(); - }, [restoreFocus]); - - const panels: EuiContextMenuPanelDescriptor[] = useMemo( - () => [ - { - id: 0, - items: [ - { - icon: , - name: i18n.REMOVE_COLUMN, - onClick: () => { - dispatch(tGridActions.removeColumn({ id: timelineId, columnId: header.id })); - handleClosePopOverTrigger(); - }, - }, - ...(tabType !== TimelineTabs.eql - ? [ - { - disabled: !header.aggregatable, - icon: , - name: i18n.SORT_AZ, - onClick: () => { - onColumnSort(Direction.asc); - handleClosePopOverTrigger(); - }, - }, - { - disabled: !header.aggregatable, - icon: , - name: i18n.SORT_ZA, - onClick: () => { - onColumnSort(Direction.desc); - handleClosePopOverTrigger(); - }, - }, - ] - : []), - ], - }, - ], - [ - dispatch, - handleClosePopOverTrigger, - header.aggregatable, - header.id, - onColumnSort, - tabType, - timelineId, - ] - ); - - const headerButton = useMemo( - () =>
, - [header, sort, timelineId] - ); - - const DraggableContent = useCallback( - (dragProvided) => ( - - - - - - - - - - ), - [handleClosePopOverTrigger, headerButton, header.initialWidth, hoverActionsOwnFocus, panels] - ); - - const onFocus = useCallback(() => { - keyboardHandlerRef.current?.focus(); - }, []); - - const openPopover = useCallback(() => { - setHoverActionsOwnFocus(true); - }, []); - - const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({ - closePopover: handleClosePopOverTrigger, - draggableId, - fieldName: header.id, - keyboardHandlerRef, - openPopover, - }); - - const keyDownHandler = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - if (!hoverActionsOwnFocus) { - onKeyDown(keyboardEvent); - } - }, - [hoverActionsOwnFocus, onKeyDown] - ); - - return ( - -
- - {DraggableContent} - -
-
- ); -}; - -export const ColumnHeader = React.memo( - ColumnHeaderComponent, - (prevProps, nextProps) => - prevProps.draggableIndex === nextProps.draggableIndex && - prevProps.tabType === nextProps.tabType && - prevProps.timelineId === nextProps.timelineId && - prevProps.isDragging === nextProps.isDragging && - deepEqual(prevProps.sort, nextProps.sort) && - deepEqual(prevProps.header, nextProps.header) -); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx deleted file mode 100644 index 084cd31d76e61..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.test.tsx +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; - -import { getActionsColumnWidth } from './helpers'; - -import { defaultHeaders } from './default_headers'; -import { Sort } from '../sort'; - -import { ColumnHeadersComponent } from '.'; -import { cloneDeep } from 'lodash/fp'; -import { useMountAppended } from '../../../utils/use_mount_appended'; -import { mockBrowserFields } from '../../../../mock/browser_fields'; -import { Direction } from '../../../../../common/search_strategy'; -import { TimelineTabs } from '../../../../../common/types/timeline'; -import { tGridActions } from '../../../../store/t_grid'; -import { testTrailingControlColumns } from '../../../../mock/mock_timeline_control_columns'; -import { TestProviders } from '../../../../mock'; -import { mockGlobalState } from '../../../../mock/global_state'; - -const mockDispatch = jest.fn(); -jest.mock('../../../../hooks/use_selector', () => ({ - useShallowEqualSelector: () => mockGlobalState.timelineById.test, - useDeepEqualSelector: () => mockGlobalState.timelineById.test, -})); - -window.matchMedia = jest.fn().mockImplementation((query) => { - return { - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - }; -}); - -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); -const timelineId = 'test'; - -describe('ColumnHeaders', () => { - const mount = useMountAppended(); - const ACTION_BUTTON_COUNT = 4; - const actionsColumnWidth = getActionsColumnWidth(ACTION_BUTTON_COUNT); - - describe('rendering', () => { - const sort: Sort[] = [ - { - columnId: '@timestamp', - columnType: 'number', - sortDirection: Direction.desc, - }, - ]; - - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - - - ); - expect(wrapper.find('ColumnHeadersComponent')).toMatchSnapshot(); - }); - - // TODO BrowserField When we bring back browser fields unskip - test.skip('it renders the field browser', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="field-browser"]').first().exists()).toEqual(true); - }); - - test('it renders every column header', () => { - const wrapper = mount( - - - - ); - - defaultHeaders.forEach((h) => { - expect(wrapper.find('[data-test-subj="headers-group"]').first().text()).toContain(h.id); - }); - }); - }); - - describe('#onColumnsSorted', () => { - let mockSort: Sort[] = [ - { - columnId: '@timestamp', - columnType: 'number', - sortDirection: Direction.desc, - }, - { - columnId: 'host.name', - columnType: 'text', - sortDirection: Direction.asc, - }, - ]; - let mockDefaultHeaders = cloneDeep( - defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) - ); - - beforeEach(() => { - mockDefaultHeaders = cloneDeep( - defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true })) - ); - mockSort = [ - { - columnId: '@timestamp', - columnType: 'number', - sortDirection: Direction.desc, - }, - { - columnId: 'host.name', - columnType: 'text', - sortDirection: Direction.asc, - }, - ]; - }); - - test('Add column `event.category` as desc sorting', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="header-event.category"] [data-test-subj="header-sort-button"]') - .first() - .simulate('click'); - expect(mockDispatch).toHaveBeenCalledWith( - tGridActions.updateSort({ - id: timelineId, - sort: [ - { - columnId: '@timestamp', - columnType: 'number', - sortDirection: Direction.desc, - }, - { - columnId: 'host.name', - columnType: 'text', - sortDirection: Direction.asc, - }, - { columnId: 'event.category', columnType: 'text', sortDirection: Direction.desc }, - ], - }) - ); - }); - - test('Change order of column `@timestamp` from desc to asc without changing index position', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="header-@timestamp"] [data-test-subj="header-sort-button"]') - .first() - .simulate('click'); - expect(mockDispatch).toHaveBeenCalledWith( - tGridActions.updateSort({ - id: timelineId, - sort: [ - { - columnId: '@timestamp', - columnType: 'number', - sortDirection: Direction.asc, - }, - { columnId: 'host.name', columnType: 'text', sortDirection: Direction.asc }, - ], - }) - ); - }); - - test('Change order of column `host.name` from asc to desc without changing index position', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="header-host.name"] [data-test-subj="header-sort-button"]') - .first() - .simulate('click'); - expect(mockDispatch).toHaveBeenCalledWith( - tGridActions.updateSort({ - id: timelineId, - sort: [ - { - columnId: '@timestamp', - columnType: 'number', - sortDirection: Direction.desc, - }, - { columnId: 'host.name', columnType: 'text', sortDirection: Direction.desc }, - ], - }) - ); - }); - test('Does not render the default leading action column header and renders a custom trailing header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.exists('[data-test-subj="field-browser"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="test-header-action-cell"]')).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.tsx deleted file mode 100644 index 6a8568dcc6179..0000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/index.tsx +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix } from '@kbn/securitysolution-t-grid'; -import deepEqual from 'fast-deep-equal'; -import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; - -import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; -import type { - ControlColumnProps, - ColumnHeaderOptions, - HeaderActionProps, -} from '../../../../../common/types/timeline'; - -import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; - -import type { OnSelectAll } from '../../types'; -import { - EventsTh, - EventsThead, - EventsThGroupData, - EventsTrHeader, - EventsThGroupActions, -} from '../../styles'; -import { Sort } from '../sort'; -import { ColumnHeader } from './column_header'; -import { DraggableFieldBadge } from '../../../draggables'; - -interface Props { - actionsColumnWidth: number; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - isEventViewer?: boolean; - isSelectAllChecked: boolean; - onSelectAll: OnSelectAll; - showEventsSelect: boolean; - showSelectAllCheckbox: boolean; - sort: Sort[]; - tabType: TimelineTabs; - timelineId: string; - leadingControlColumns: ControlColumnProps[]; - trailingControlColumns: ControlColumnProps[]; -} - -interface DraggableContainerProps { - children: React.ReactNode; - onMount: () => void; - onUnmount: () => void; -} - -export const DraggableContainer = React.memo( - ({ children, onMount, onUnmount }) => { - useEffect(() => { - onMount(); - - return () => onUnmount(); - }, [onMount, onUnmount]); - - return <>{children}; - } -); - -DraggableContainer.displayName = 'DraggableContainer'; - -export const isFullScreen = ({ - globalFullScreen, - timelineId, - timelineFullScreen, -}: { - globalFullScreen: boolean; - timelineId: string; - timelineFullScreen: boolean; -}) => - (timelineId === TimelineId.active && timelineFullScreen) || - (timelineId !== TimelineId.active && globalFullScreen); - -/** Renders the timeline header columns */ -export const ColumnHeadersComponent = ({ - actionsColumnWidth, - browserFields, - columnHeaders, - isEventViewer = false, - isSelectAllChecked, - onSelectAll, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - leadingControlColumns, - trailingControlColumns, -}: Props) => { - const [draggingIndex, setDraggingIndex] = useState(null); - - const renderClone: DraggableChildrenFn = useCallback( - (dragProvided, _dragSnapshot, rubric) => { - const index = rubric.source.index; - const header = columnHeaders[index]; - - const onMount = () => setDraggingIndex(index); - const onUnmount = () => setDraggingIndex(null); - - return ( - - - - - - ); - }, - [columnHeaders, setDraggingIndex] - ); - - const ColumnHeaderList = useMemo( - () => - columnHeaders.map((header, draggableIndex) => ( - - )), - [columnHeaders, timelineId, draggingIndex, sort, tabType] - ); - - const DroppableContent = useCallback( - (dropProvided, snapshot) => ( - <> - - {ColumnHeaderList} - - - ), - [ColumnHeaderList] - ); - - const leadingHeaderCells = useMemo( - () => - leadingControlColumns ? leadingControlColumns.map((column) => column.headerCellRender) : [], - [leadingControlColumns] - ); - - const trailingHeaderCells = useMemo( - () => - trailingControlColumns ? trailingControlColumns.map((column) => column.headerCellRender) : [], - [trailingControlColumns] - ); - - const LeadingHeaderActions = useMemo(() => { - return leadingHeaderCells.map( - (Header: React.ComponentType | React.ComponentType | undefined, index) => { - const passedWidth = leadingControlColumns[index] && leadingControlColumns[index].width; - const width = passedWidth ? passedWidth : actionsColumnWidth; - return ( - - {Header && ( -
- )} - - ); - } - ); - }, [ - leadingHeaderCells, - leadingControlColumns, - actionsColumnWidth, - browserFields, - columnHeaders, - isEventViewer, - isSelectAllChecked, - onSelectAll, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - ]); - - const TrailingHeaderActions = useMemo(() => { - return trailingHeaderCells.map( - (Header: React.ComponentType | React.ComponentType | undefined, index) => { - const passedWidth = trailingControlColumns[index] && trailingControlColumns[index].width; - const width = passedWidth ? passedWidth : actionsColumnWidth; - return ( - - {Header && ( -
- )} - - ); - } - ); - }, [ - trailingHeaderCells, - trailingControlColumns, - actionsColumnWidth, - browserFields, - columnHeaders, - isEventViewer, - isSelectAllChecked, - onSelectAll, - showEventsSelect, - showSelectAllCheckbox, - sort, - tabType, - timelineId, - ]); - return ( - - - {LeadingHeaderActions} - - {DroppableContent} - - {TrailingHeaderActions} - - - ); -}; - -export const ColumnHeaders = React.memo( - ColumnHeadersComponent, - (prevProps, nextProps) => - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.onSelectAll === nextProps.onSelectAll && - prevProps.showEventsSelect === nextProps.showEventsSelect && - prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && - deepEqual(prevProps.sort, nextProps.sort) && - prevProps.timelineId === nextProps.timelineId && - deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && - prevProps.tabType === nextProps.tabType && - deepEqual(prevProps.browserFields, nextProps.browserFields) -); From 90c32024f7f50de4f68984582ac7cf7a141ff86f Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 21 Apr 2022 20:34:46 +0200 Subject: [PATCH 17/24] update code onwer (#130818) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 89cf1fdf38553..869160bfa0fd1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -133,7 +133,7 @@ #CC# /x-pack/plugins/observability/ @elastic/apm-ui # Uptime -/x-pack/plugins/uptime @elastic/uptime +/x-pack/plugins/synthetics @elastic/uptime /x-pack/plugins/ux @elastic/uptime /x-pack/test/functional_with_es_ssl/apps/uptime @elastic/uptime /x-pack/test/functional/apps/uptime @elastic/uptime From 4d78a770a1b0b0b652432f1341fac26c0e27de4a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 14:16:57 -0500 Subject: [PATCH 18/24] Update typescript (main) (#130764) Co-authored-by: Renovate Bot Co-authored-by: Jonathan Budzenski --- package.json | 8 ++-- yarn.lock | 104 +++++++++++++++++++++++++-------------------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index 8a4b8a3e11d4d..245bfc5435af8 100644 --- a/package.json +++ b/package.json @@ -772,9 +772,9 @@ "@types/yargs": "^15.0.0", "@types/yauzl": "^2.9.1", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^5.17.0", - "@typescript-eslint/parser": "^5.17.0", - "@typescript-eslint/typescript-estree": "^5.17.0", + "@typescript-eslint/eslint-plugin": "^5.20.0", + "@typescript-eslint/parser": "^5.20.0", + "@typescript-eslint/typescript-estree": "^5.20.0", "@yarnpkg/lockfile": "^1.1.0", "abab": "^2.0.4", "aggregate-error": "^3.1.0", @@ -912,7 +912,7 @@ "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "postcss-prefix-selector": "^1.7.2", - "prettier": "^2.6.1", + "prettier": "^2.6.2", "pretty-format": "^27.5.1", "q": "^1.5.1", "react-test-renderer": "^16.12.0", diff --git a/yarn.lock b/yarn.lock index ed9e850e8c6c4..bfa0ddcc26a20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7298,14 +7298,14 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.17.0.tgz#704eb4e75039000531255672bf1c85ee85cf1d67" - integrity sha512-qVstvQilEd89HJk3qcbKt/zZrfBZ+9h2ynpAGlWjWiizA7m/MtLT9RoX6gjtpE500vfIg8jogAkDzdCxbsFASQ== - dependencies: - "@typescript-eslint/scope-manager" "5.17.0" - "@typescript-eslint/type-utils" "5.17.0" - "@typescript-eslint/utils" "5.17.0" +"@typescript-eslint/eslint-plugin@^5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz#022531a639640ff3faafaf251d1ce00a2ef000a1" + integrity sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q== + dependencies: + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/type-utils" "5.20.0" + "@typescript-eslint/utils" "5.20.0" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -7325,14 +7325,14 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.17.0.tgz#7def77d5bcd8458d12d52909118cf3f0a45f89d5" - integrity sha512-aRzW9Jg5Rlj2t2/crzhA2f23SIYFlF9mchGudyP0uiD6SenIxzKoLjwzHbafgHn39dNV/TV7xwQkLfFTZlJ4ig== +"@typescript-eslint/parser@^5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.20.0.tgz#4991c4ee0344315c2afc2a62f156565f689c8d0b" + integrity sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w== dependencies: - "@typescript-eslint/scope-manager" "5.17.0" - "@typescript-eslint/types" "5.17.0" - "@typescript-eslint/typescript-estree" "5.17.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/typescript-estree" "5.20.0" debug "^4.3.2" "@typescript-eslint/scope-manager@4.31.2": @@ -7343,20 +7343,20 @@ "@typescript-eslint/types" "4.31.2" "@typescript-eslint/visitor-keys" "4.31.2" -"@typescript-eslint/scope-manager@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.17.0.tgz#4cea7d0e0bc0e79eb60cad431c89120987c3f952" - integrity sha512-062iCYQF/doQ9T2WWfJohQKKN1zmmXVfAcS3xaiialiw8ZUGy05Em6QVNYJGO34/sU1a7a+90U3dUNfqUDHr3w== +"@typescript-eslint/scope-manager@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz#79c7fb8598d2942e45b3c881ced95319818c7980" + integrity sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg== dependencies: - "@typescript-eslint/types" "5.17.0" - "@typescript-eslint/visitor-keys" "5.17.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/visitor-keys" "5.20.0" -"@typescript-eslint/type-utils@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.17.0.tgz#1c4549d68c89877662224aabb29fbbebf5fc9672" - integrity sha512-3hU0RynUIlEuqMJA7dragb0/75gZmwNwFf/QJokWzPehTZousP/MNifVSgjxNcDCkM5HI2K22TjQWUmmHUINSg== +"@typescript-eslint/type-utils@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz#151c21cbe9a378a34685735036e5ddfc00223be3" + integrity sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw== dependencies: - "@typescript-eslint/utils" "5.17.0" + "@typescript-eslint/utils" "5.20.0" debug "^4.3.2" tsutils "^3.21.0" @@ -7365,10 +7365,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.31.2.tgz#2aea7177d6d744521a168ed4668eddbd912dfadf" integrity sha512-kWiTTBCTKEdBGrZKwFvOlGNcAsKGJSBc8xLvSjSppFO88AqGxGNYtF36EuEYG6XZ9vT0xX8RNiHbQUKglbSi1w== -"@typescript-eslint/types@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.17.0.tgz#861ec9e669ffa2aa9b873dd4d28d9b1ce26d216f" - integrity sha512-AgQ4rWzmCxOZLioFEjlzOI3Ch8giDWx8aUDxyNw9iOeCvD3GEYAB7dxWGQy4T/rPVe8iPmu73jPHuaSqcjKvxw== +"@typescript-eslint/types@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.20.0.tgz#fa39c3c2aa786568302318f1cb51fcf64258c20c" + integrity sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg== "@typescript-eslint/typescript-estree@4.31.2": version "4.31.2" @@ -7383,28 +7383,28 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.17.0", "@typescript-eslint/typescript-estree@^5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.17.0.tgz#a7cba7dfc8f9cc2ac78c18584e684507df4f2488" - integrity sha512-X1gtjEcmM7Je+qJRhq7ZAAaNXYhTgqMkR10euC4Si6PIjb+kwEQHSxGazXUQXFyqfEXdkGf6JijUu5R0uceQzg== +"@typescript-eslint/typescript-estree@5.20.0", "@typescript-eslint/typescript-estree@^5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz#ab73686ab18c8781bbf249c9459a55dc9417d6b0" + integrity sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w== dependencies: - "@typescript-eslint/types" "5.17.0" - "@typescript-eslint/visitor-keys" "5.17.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/visitor-keys" "5.20.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.17.0.tgz#549a9e1d491c6ccd3624bc3c1b098f5cfb45f306" - integrity sha512-DVvndq1QoxQH+hFv+MUQHrrWZ7gQ5KcJzyjhzcqB1Y2Xes1UQQkTRPUfRpqhS8mhTWsSb2+iyvDW1Lef5DD7vA== +"@typescript-eslint/utils@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.20.0.tgz#b8e959ed11eca1b2d5414e12417fd94cae3517a5" + integrity sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.17.0" - "@typescript-eslint/types" "5.17.0" - "@typescript-eslint/typescript-estree" "5.17.0" + "@typescript-eslint/scope-manager" "5.20.0" + "@typescript-eslint/types" "5.20.0" + "@typescript-eslint/typescript-estree" "5.20.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -7416,12 +7416,12 @@ "@typescript-eslint/types" "4.31.2" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.17.0.tgz#52daae45c61b0211b4c81b53a71841911e479128" - integrity sha512-6K/zlc4OfCagUu7Am/BD5k8PSWQOgh34Nrv9Rxe2tBzlJ7uOeJ/h7ugCGDCeEZHT6k2CJBhbk9IsbkPI0uvUkA== +"@typescript-eslint/visitor-keys@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz#70236b5c6b67fbaf8b2f58bf3414b76c1e826c2a" + integrity sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg== dependencies: - "@typescript-eslint/types" "5.17.0" + "@typescript-eslint/types" "5.20.0" eslint-visitor-keys "^3.0.0" "@ungap/promise-all-settled@1.1.2": @@ -23040,10 +23040,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.1.tgz#d472797e0d7461605c1609808e27b80c0f9cfe17" - integrity sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A== +prettier@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032" + integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== prettier@~2.2.1: version "2.2.1" From 5a864210036a33c9d426fb6fa713cd68ffb21058 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 21 Apr 2022 13:39:09 -0600 Subject: [PATCH 19/24] [Controls] Clear range/time slider selections when field changes (#129824) * Reset selections on save of existing control * Allow reset to force render for range/time sliders * Reset selections only when field name or data view changes * Make generic DataControlInput interface * Fix infinite useEffect + imports + types * Simpler solution without resetSelections() * Add functional tests * Apply Devon's changes --- .../control_types/options_list/types.ts | 7 +- .../control_types/range_slider/types.ts | 6 +- .../common/control_types/time_slider/types.ts | 6 +- src/plugins/controls/common/types.ts | 5 ++ .../range_slider/range_slider.component.tsx | 11 +-- .../range_slider/range_slider_embeddable.tsx | 2 +- .../range_slider_embeddable_factory.tsx | 4 +- .../control_types/time_slider/time_slider.tsx | 11 +-- .../time_slider_embeddable_factory.tsx | 4 +- src/plugins/controls/public/types.ts | 2 +- .../controls/options_list.ts | 30 ++++++++ .../controls/range_slider.ts | 76 +++++++++++-------- 12 files changed, 98 insertions(+), 66 deletions(-) diff --git a/src/plugins/controls/common/control_types/options_list/types.ts b/src/plugins/controls/common/control_types/options_list/types.ts index 9c051e4ca6235..0f889bed7bacb 100644 --- a/src/plugins/controls/common/control_types/options_list/types.ts +++ b/src/plugins/controls/common/control_types/options_list/types.ts @@ -8,14 +8,11 @@ import { BoolQuery } from '@kbn/es-query'; import { FieldSpec } from '@kbn/data-views-plugin/common'; -import { ControlInput } from '../../types'; +import { DataControlInput } from '../../types'; export const OPTIONS_LIST_CONTROL = 'optionsListControl'; -export interface OptionsListEmbeddableInput extends ControlInput { - fieldName: string; - dataViewId: string; - +export interface OptionsListEmbeddableInput extends DataControlInput { selectedOptions?: string[]; singleSelect?: boolean; loading?: boolean; diff --git a/src/plugins/controls/common/control_types/range_slider/types.ts b/src/plugins/controls/common/control_types/range_slider/types.ts index e63ec0337a57e..a975fdd27ac31 100644 --- a/src/plugins/controls/common/control_types/range_slider/types.ts +++ b/src/plugins/controls/common/control_types/range_slider/types.ts @@ -6,14 +6,12 @@ * Side Public License, v 1. */ -import { ControlInput } from '../../types'; +import { DataControlInput } from '../../types'; export const RANGE_SLIDER_CONTROL = 'rangeSliderControl'; export type RangeValue = [string, string]; -export interface RangeSliderEmbeddableInput extends ControlInput { - fieldName: string; - dataViewId: string; +export interface RangeSliderEmbeddableInput extends DataControlInput { value: RangeValue; } diff --git a/src/plugins/controls/common/control_types/time_slider/types.ts b/src/plugins/controls/common/control_types/time_slider/types.ts index 73d364da80caa..31272380becde 100644 --- a/src/plugins/controls/common/control_types/time_slider/types.ts +++ b/src/plugins/controls/common/control_types/time_slider/types.ts @@ -6,12 +6,10 @@ * Side Public License, v 1. */ -import { ControlInput } from '../../types'; +import { DataControlInput } from '../../types'; export const TIME_SLIDER_CONTROL = 'timeSlider'; -export interface TimeSliderControlEmbeddableInput extends ControlInput { - fieldName: string; - dataViewId: string; +export interface TimeSliderControlEmbeddableInput extends DataControlInput { value?: [number | null, number | null]; } diff --git a/src/plugins/controls/common/types.ts b/src/plugins/controls/common/types.ts index 3715198d7cfe5..abb24299e8180 100644 --- a/src/plugins/controls/common/types.ts +++ b/src/plugins/controls/common/types.ts @@ -27,3 +27,8 @@ export type ControlInput = EmbeddableInput & { controlStyle?: ControlStyle; ignoreParentSettings?: ParentIgnoreSettings; }; + +export type DataControlInput = ControlInput & { + fieldName: string; + dataViewId: string; +}; diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider.component.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider.component.tsx index f1002290eab3d..259b6bd7f66a1 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider.component.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider.component.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useCallback, useState } from 'react'; +import React, { FC, useCallback } from 'react'; import { BehaviorSubject } from 'rxjs'; import { DataViewField } from '@kbn/data-views-plugin/public'; @@ -45,16 +45,13 @@ export const RangeSliderComponent: FC = ({ componentStateSubject }) => { componentStateSubject.getValue() ); - const { value = ['', ''], id, title } = useEmbeddableSelector((state) => state); - - const [selectedValue, setSelectedValue] = useState(value || ['', '']); + const { value, id, title } = useEmbeddableSelector((state) => state); const onChangeComplete = useCallback( (range: RangeValue) => { dispatch(selectRange(range)); - setSelectedValue(range); }, - [selectRange, setSelectedValue, dispatch] + [selectRange, dispatch] ); return ( @@ -64,7 +61,7 @@ export const RangeSliderComponent: FC = ({ componentStateSubject }) => { min={min} max={max} title={title} - value={selectedValue} + value={value ?? ['', '']} onChange={onChangeComplete} fieldFormatter={fieldFormatter} /> diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx index 2777f45d026bd..1ad34fd361ac6 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable.tsx @@ -311,7 +311,7 @@ export class RangeSliderEmbeddable extends Embeddable { + public reload = () => { this.fetchMinMax(); }; diff --git a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable_factory.tsx b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable_factory.tsx index 74bb2d23dbd88..bd8b8a394988b 100644 --- a/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable_factory.tsx +++ b/src/plugins/controls/public/control_types/range_slider/range_slider_embeddable_factory.tsx @@ -37,8 +37,8 @@ export class RangeSliderEmbeddableFactory ) => { if ( embeddable && - (!deepEqual(newInput.fieldName, embeddable.getInput().fieldName) || - !deepEqual(newInput.dataViewId, embeddable.getInput().dataViewId)) + ((newInput.fieldName && !deepEqual(newInput.fieldName, embeddable.getInput().fieldName)) || + (newInput.dataViewId && !deepEqual(newInput.dataViewId, embeddable.getInput().dataViewId))) ) { // if the field name or data view id has changed in this editing session, selected values are invalid, so reset them. newInput.value = ['', '']; diff --git a/src/plugins/controls/public/control_types/time_slider/time_slider.tsx b/src/plugins/controls/public/control_types/time_slider/time_slider.tsx index 61a47d8779a1d..d25d4f145a99d 100644 --- a/src/plugins/controls/public/control_types/time_slider/time_slider.tsx +++ b/src/plugins/controls/public/control_types/time_slider/time_slider.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { FC, useCallback, useState, useMemo } from 'react'; +import React, { FC, useCallback, useMemo } from 'react'; import { BehaviorSubject } from 'rxjs'; import { debounce } from 'lodash'; import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; @@ -59,10 +59,6 @@ export const TimeSlider: FC = ({ const { value } = useEmbeddableSelector((state) => state); - const [selectedValue, setSelectedValue] = useState<[number | null, number | null]>( - value || [null, null] - ); - const dispatchChange = useCallback( (range: [number | null, number | null]) => { dispatch(selectRange(range)); @@ -75,15 +71,14 @@ export const TimeSlider: FC = ({ const onChangeComplete = useCallback( (range: [number | null, number | null]) => { debouncedDispatchChange(range); - setSelectedValue(range); }, - [setSelectedValue, debouncedDispatchChange] + [debouncedDispatchChange] ); return ( { if ( embeddable && - (!deepEqual(newInput.fieldName, embeddable.getInput().fieldName) || - !deepEqual(newInput.dataViewId, embeddable.getInput().dataViewId)) + ((newInput.fieldName && !deepEqual(newInput.fieldName, embeddable.getInput().fieldName)) || + (newInput.dataViewId && !deepEqual(newInput.dataViewId, embeddable.getInput().dataViewId))) ) { // if the field name or data view id has changed in this editing session, selected options are invalid, so reset them. newInput.value = undefined; diff --git a/src/plugins/controls/public/types.ts b/src/plugins/controls/public/types.ts index e5915262223e4..dbe83abf2bf97 100644 --- a/src/plugins/controls/public/types.ts +++ b/src/plugins/controls/public/types.ts @@ -75,4 +75,4 @@ export interface ControlsPluginStartDeps { } // re-export from common -export type { ControlWidth, ControlInput, ControlStyle } from '../common/types'; +export type { ControlWidth, ControlInput, DataControlInput, ControlStyle } from '../common/types'; diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index 39522c43fc847..a4da1c217f92b 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -116,6 +116,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); + it('editing field clears selections', async () => { + const secondId = (await dashboardControls.getAllControlIds())[1]; + await dashboardControls.optionsListOpenPopover(secondId); + await dashboardControls.optionsListPopoverSelectOption('hiss'); + await dashboardControls.optionsListEnsurePopoverIsClosed(secondId); + + await dashboardControls.editExistingControl(secondId); + await dashboardControls.controlsEditorSetfield('animal.keyword'); + await dashboardControls.controlEditorSave(); + + const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId); + expect(selectionString).to.be('Select...'); + }); + + it('editing other control settings keeps selections', async () => { + const secondId = (await dashboardControls.getAllControlIds())[1]; + await dashboardControls.optionsListOpenPopover(secondId); + await dashboardControls.optionsListPopoverSelectOption('dog'); + await dashboardControls.optionsListPopoverSelectOption('cat'); + await dashboardControls.optionsListEnsurePopoverIsClosed(secondId); + + await dashboardControls.editExistingControl(secondId); + await dashboardControls.controlEditorSetTitle('Animal'); + await dashboardControls.controlEditorSetWidth('large'); + await dashboardControls.controlEditorSave(); + + const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId); + expect(selectionString).to.be('dog, cat'); + }); + it('deletes an existing control', async () => { const firstId = (await dashboardControls.getAllControlIds())[0]; diff --git a/test/functional/apps/dashboard_elements/controls/range_slider.ts b/test/functional/apps/dashboard_elements/controls/range_slider.ts index 27f1244120a7a..1ce8f05cae190 100644 --- a/test/functional/apps/dashboard_elements/controls/range_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/range_slider.ts @@ -25,6 +25,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'header', ]); + const validateRange = async ( + compare: 'value' | 'placeholder', // if 'value', compare actual selections; otherwise, compare the default range + controlId: string, + expectedLowerBound: string, + expectedUpperBound: string + ) => { + expect(await dashboardControls.rangeSliderGetLowerBoundAttribute(controlId, compare)).to.be( + expectedLowerBound + ); + expect(await dashboardControls.rangeSliderGetUpperBoundAttribute(controlId, compare)).to.be( + expectedUpperBound + ); + }; + describe('Range Slider Control', async () => { before(async () => { await security.testUser.setRoles([ @@ -82,12 +96,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await dashboardControls.getControlsCount()).to.be(2); const secondId = (await dashboardControls.getAllControlIds())[1]; - expect( - await dashboardControls.rangeSliderGetLowerBoundAttribute(secondId, 'placeholder') - ).to.be('100'); - expect( - await dashboardControls.rangeSliderGetUpperBoundAttribute(secondId, 'placeholder') - ).to.be('1200'); + validateRange('placeholder', secondId, '100', '1200'); // data views should be properly propagated from the control group to the dashboard expect(await filterBar.getIndexPatterns()).to.be('logstash-*,kibana_sample_data_flights'); }); @@ -112,12 +121,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.controlsEditorSetfield('dayOfWeek'); await dashboardControls.controlEditorSave(); await dashboardControls.rangeSliderWaitForLoading(); - expect( - await dashboardControls.rangeSliderGetLowerBoundAttribute(firstId, 'placeholder') - ).to.be('0'); - expect( - await dashboardControls.rangeSliderGetUpperBoundAttribute(firstId, 'placeholder') - ).to.be('6'); + validateRange('placeholder', firstId, '0', '6'); + // when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view await retry.try(async () => { await testSubjects.click('addFilter'); @@ -150,31 +155,38 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('applies filter from the first control on the second control', async () => { await dashboardControls.rangeSliderWaitForLoading(); const secondId = (await dashboardControls.getAllControlIds())[1]; - const availableMin = await dashboardControls.rangeSliderGetLowerBoundAttribute( - secondId, - 'placeholder' - ); - expect(availableMin).to.be('100'); - const availabeMax = await dashboardControls.rangeSliderGetUpperBoundAttribute( - secondId, - 'placeholder' - ); - expect(availabeMax).to.be('1000'); + validateRange('placeholder', secondId, '100', '1000'); + }); + + it('editing field clears selections', async () => { + const secondId = (await dashboardControls.getAllControlIds())[1]; + await dashboardControls.editExistingControl(secondId); + await dashboardControls.controlsEditorSetfield('FlightDelayMin'); + await dashboardControls.controlEditorSave(); + + await dashboardControls.rangeSliderWaitForLoading(); + validateRange('value', secondId, '', ''); + }); + + it('editing other control settings keeps selections', async () => { + const secondId = (await dashboardControls.getAllControlIds())[1]; + await dashboardControls.rangeSliderSetLowerBound(secondId, '50'); + await dashboardControls.rangeSliderSetUpperBound(secondId, '100'); + await dashboardControls.rangeSliderWaitForLoading(); + + await dashboardControls.editExistingControl(secondId); + await dashboardControls.controlEditorSetTitle('Minimum Flight Delay'); + await dashboardControls.controlEditorSetWidth('large'); + await dashboardControls.controlEditorSave(); + + await dashboardControls.rangeSliderWaitForLoading(); + validateRange('value', secondId, '50', '100'); }); it('can clear out selections by clicking the reset button', async () => { const firstId = (await dashboardControls.getAllControlIds())[0]; await dashboardControls.rangeSliderClearSelection(firstId); - const lowerBoundSelection = await dashboardControls.rangeSliderGetLowerBoundAttribute( - firstId, - 'value' - ); - expect(lowerBoundSelection.length).to.be(0); - const upperBoundSelection = await dashboardControls.rangeSliderGetUpperBoundAttribute( - firstId, - 'value' - ); - expect(upperBoundSelection.length).to.be(0); + validateRange('value', firstId, '', ''); }); it('deletes an existing control', async () => { From e3d221ed39d0a902932cf5338f45ce44696d561b Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Thu, 21 Apr 2022 16:38:59 -0400 Subject: [PATCH 20/24] [CI] Bump kibana-buildkite-library for getPrChanges fix (#130824) --- .buildkite/yarn.lock | 228 +++++++++++++++++++++---------------------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/.buildkite/yarn.lock b/.buildkite/yarn.lock index c2d6928d30c5a..c5a4e404ba970 100644 --- a/.buildkite/yarn.lock +++ b/.buildkite/yarn.lock @@ -3,93 +3,93 @@ "@octokit/auth-token@^2.4.4": - "integrity" "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==" - "resolved" "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz" - "version" "2.5.0" + version "2.5.0" + resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz" + integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== dependencies: "@octokit/types" "^6.0.3" -"@octokit/core@^3.5.1", "@octokit/core@>=2", "@octokit/core@>=3": - "integrity" "sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==" - "resolved" "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz" - "version" "3.5.1" +"@octokit/core@^3.5.1": + version "3.5.1" + resolved "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz" + integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== dependencies: "@octokit/auth-token" "^2.4.4" "@octokit/graphql" "^4.5.8" "@octokit/request" "^5.6.0" "@octokit/request-error" "^2.0.5" "@octokit/types" "^6.0.3" - "before-after-hook" "^2.2.0" - "universal-user-agent" "^6.0.0" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" "@octokit/endpoint@^6.0.1": - "integrity" "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==" - "resolved" "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz" - "version" "6.0.12" + version "6.0.12" + resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz" + integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== dependencies: "@octokit/types" "^6.0.3" - "is-plain-object" "^5.0.0" - "universal-user-agent" "^6.0.0" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" "@octokit/graphql@^4.5.8": - "integrity" "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==" - "resolved" "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz" - "version" "4.8.0" + version "4.8.0" + resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz" + integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg== dependencies: "@octokit/request" "^5.6.0" "@octokit/types" "^6.0.3" - "universal-user-agent" "^6.0.0" + universal-user-agent "^6.0.0" "@octokit/openapi-types@^11.2.0": - "integrity" "sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==" - "resolved" "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz" - "version" "11.2.0" + version "11.2.0" + resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz" + integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== "@octokit/plugin-paginate-rest@^2.16.8": - "integrity" "sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==" - "resolved" "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz" - "version" "2.17.0" + version "2.17.0" + resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz" + integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw== dependencies: "@octokit/types" "^6.34.0" "@octokit/plugin-request-log@^1.0.4": - "integrity" "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==" - "resolved" "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz" - "version" "1.0.4" + version "1.0.4" + resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== "@octokit/plugin-rest-endpoint-methods@^5.12.0": - "integrity" "sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==" - "resolved" "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz" - "version" "5.13.0" + version "5.13.0" + resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz" + integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA== dependencies: "@octokit/types" "^6.34.0" - "deprecation" "^2.3.1" + deprecation "^2.3.1" "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": - "integrity" "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==" - "resolved" "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz" - "version" "2.1.0" + version "2.1.0" + resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz" + integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== dependencies: "@octokit/types" "^6.0.3" - "deprecation" "^2.0.0" - "once" "^1.4.0" + deprecation "^2.0.0" + once "^1.4.0" "@octokit/request@^5.6.0": - "integrity" "sha512-je66CvSEVf0jCpRISxkUcCa0UkxmFs6eGDRSbfJtAVwbLH5ceqF+YEyC8lj8ystKyZTy8adWr0qmkY52EfOeLA==" - "resolved" "https://registry.npmjs.org/@octokit/request/-/request-5.6.2.tgz" - "version" "5.6.2" + version "5.6.2" + resolved "https://registry.npmjs.org/@octokit/request/-/request-5.6.2.tgz" + integrity sha512-je66CvSEVf0jCpRISxkUcCa0UkxmFs6eGDRSbfJtAVwbLH5ceqF+YEyC8lj8ystKyZTy8adWr0qmkY52EfOeLA== dependencies: "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.1.0" "@octokit/types" "^6.16.1" - "is-plain-object" "^5.0.0" - "node-fetch" "^2.6.1" - "universal-user-agent" "^6.0.0" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + universal-user-agent "^6.0.0" "@octokit/rest@^18.10.0": - "integrity" "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==" - "resolved" "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz" - "version" "18.12.0" + version "18.12.0" + resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz" + integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q== dependencies: "@octokit/core" "^3.5.1" "@octokit/plugin-paginate-rest" "^2.16.8" @@ -97,84 +97,84 @@ "@octokit/plugin-rest-endpoint-methods" "^5.12.0" "@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0": - "integrity" "sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==" - "resolved" "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz" - "version" "6.34.0" + version "6.34.0" + resolved "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz" + integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== dependencies: "@octokit/openapi-types" "^11.2.0" -"axios@^0.21.4": - "integrity" "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==" - "resolved" "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" - "version" "0.21.4" +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== dependencies: - "follow-redirects" "^1.14.0" - -"before-after-hook@^2.2.0": - "integrity" "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" - "resolved" "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz" - "version" "2.2.2" - -"deprecation@^2.0.0", "deprecation@^2.3.1": - "integrity" "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - "resolved" "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz" - "version" "2.3.1" - -"follow-redirects@^1.14.0": - "integrity" "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==" - "resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz" - "version" "1.14.5" - -"is-plain-object@^5.0.0": - "integrity" "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" - "resolved" "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" - "version" "5.0.0" - -"kibana-buildkite-library@github:elastic/kibana-buildkite-library": - "resolved" "git+ssh://git@github.com/elastic/kibana-buildkite-library.git#ccf5b824c4294d1fdf3569d32218d3bdb0958121" - "version" "1.0.0" + follow-redirects "^1.14.0" + +before-after-hook@^2.2.0: + version "2.2.2" + resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz" + integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +follow-redirects@^1.14.0: + version "1.14.5" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz" + integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +kibana-buildkite-library@elastic/kibana-buildkite-library: + version "1.0.0" + resolved "https://codeload.github.com/elastic/kibana-buildkite-library/tar.gz/bd0bec4c7af5f64a12c781d03cedb9fb2386bfbd" dependencies: "@octokit/rest" "^18.10.0" - "axios" "^0.21.4" + axios "^0.21.4" -"node-fetch@^2.6.1": - "integrity" "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==" - "resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz" - "version" "2.6.6" +node-fetch@^2.6.1: + version "2.6.6" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz" + integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== dependencies: - "whatwg-url" "^5.0.0" + whatwg-url "^5.0.0" -"once@^1.4.0": - "integrity" "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" - "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - "version" "1.4.0" +once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: - "wrappy" "1" - -"tr46@~0.0.3": - "integrity" "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - "resolved" "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - "version" "0.0.3" - -"universal-user-agent@^6.0.0": - "integrity" "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" - "resolved" "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz" - "version" "6.0.0" - -"webidl-conversions@^3.0.0": - "integrity" "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - "resolved" "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - "version" "3.0.1" - -"whatwg-url@^5.0.0": - "integrity" "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=" - "resolved" "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - "version" "5.0.0" + wrappy "1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= dependencies: - "tr46" "~0.0.3" - "webidl-conversions" "^3.0.0" + tr46 "~0.0.3" + webidl-conversions "^3.0.0" -"wrappy@1": - "integrity" "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - "version" "1.0.2" +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= From 5fe9437cafc2a19b36d7971832fca176691258c3 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 21 Apr 2022 22:02:57 +0100 Subject: [PATCH 21/24] [ML] Fixing management app docs links (#130776) * [ML] Fixing management app docs links * changing translation id --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + .../application/management/breadcrumbs.ts | 4 +- .../jobs_list_page/jobs_list_page.tsx | 60 +++++++++++-------- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index a4e788d7639ec..a661707bfa568 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -387,6 +387,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { regressionEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-regression.html#ml-dfanalytics-regression-evaluation`, classificationAucRoc: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfa-classification.html#ml-dfanalytics-class-aucroc`, setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`, + trainedModels: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-trained-models.html`, }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, diff --git a/x-pack/plugins/ml/public/application/management/breadcrumbs.ts b/x-pack/plugins/ml/public/application/management/breadcrumbs.ts index 1043a9a008734..514941fda2c45 100644 --- a/x-pack/plugins/ml/public/application/management/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/management/breadcrumbs.ts @@ -11,8 +11,8 @@ import { JOBS_LIST_PATH } from './management_urls'; export function getJobsListBreadcrumbs() { return [ { - text: i18n.translate('xpack.ml.jobsList.breadcrumb', { - defaultMessage: 'Jobs', + text: i18n.translate('xpack.ml.management.breadcrumb', { + defaultMessage: 'Machine Learning', }), href: `#${JOBS_LIST_PATH}`, }, diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index b3837ceedeb60..15ea5852ca4b5 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -37,7 +37,6 @@ import { PLUGIN_ID } from '../../../../../../common/constants/app'; import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; -import { getDocLinks } from '../../../../util/dependency_cache'; // @ts-ignore undeclared module import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view'; import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; @@ -54,6 +53,7 @@ import { ListingPageUrlState } from '../../../../../../common/types/common'; import { getDefaultDFAListState } from '../../../../data_frame_analytics/pages/analytics_management/page'; import { ExportJobsFlyout, ImportJobsFlyout } from '../../../../components/import_export_jobs'; import type { JobType, MlSavedObjectType } from '../../../../../../common/types/saved_objects'; +import { useMlKibana } from '../../../../contexts/kibana'; interface Tab extends EuiTabbedContentTab { 'data-test-subj': string; @@ -201,30 +201,6 @@ export const JobsListPage: FC<{ return null; } - const anomalyDetectionJobsUrl = getDocLinks().links.ml.anomalyDetectionJobs; - const dataFrameAnalyticsUrl = getDocLinks().links.ml.dataFrameAnalytics; - - const anomalyDetectionDocsLabel = i18n.translate( - 'xpack.ml.management.jobsList.anomalyDetectionDocsLabel', - { - defaultMessage: 'Anomaly detection jobs docs', - } - ); - const analyticsDocsLabel = i18n.translate('xpack.ml.management.jobsList.analyticsDocsLabel', { - defaultMessage: 'Analytics jobs docs', - }); - - const docsLink = ( - - {currentTabId === 'anomaly-detector' ? anomalyDetectionDocsLabel : analyticsDocsLabel} - - ); - function renderTabs() { return ( } - rightSideItems={[docsLink]} + rightSideItems={[]} bottomBorder /> @@ -329,3 +305,35 @@ export const JobsListPage: FC<{ ); }; + +const DocsLink: FC<{ currentTabId: MlSavedObjectType }> = ({ currentTabId }) => { + const { + services: { + docLinks: { + links: { ml }, + }, + }, + } = useMlKibana(); + + let href = ml.anomalyDetectionJobs; + let linkLabel = i18n.translate('xpack.ml.management.jobsList.anomalyDetectionDocsLabel', { + defaultMessage: 'Anomaly detection jobs docs', + }); + + if (currentTabId === 'data-frame-analytics') { + href = ml.dataFrameAnalytics; + linkLabel = i18n.translate('xpack.ml.management.jobsList.analyticsDocsLabel', { + defaultMessage: 'Analytics jobs docs', + }); + } else if (currentTabId === 'trained-model') { + href = ml.trainedModels; + linkLabel = i18n.translate('xpack.ml.management.jobsList.trainedModelsDocsLabel', { + defaultMessage: 'Trained models docs', + }); + } + return ( + + {linkLabel} + + ); +}; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 123ce3c2a15be..43b888dc0378b 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -18504,7 +18504,6 @@ "xpack.ml.jobsList.alertingRules.tooltipContent": "La tâche a {rulesCount} {rulesCount, plural, one { règle d'alerte associée} other { règles d'alerte associées}}", "xpack.ml.jobsList.analyticsSpacesLabel": "Espaces", "xpack.ml.jobsList.auditMessageColumn.screenReaderDescription": "Cette colonne affiche des icônes lorsque des erreurs ou des avertissements pour la tâche ont été signalé(e)s au cours des dernières 24 heures", - "xpack.ml.jobsList.breadcrumb": "Tâches", "xpack.ml.jobsList.cannotSelectRowForJobMessage": "Impossible de sélectionner l'ID de tâche {jobId}", "xpack.ml.jobsList.cloneJobErrorMessage": "Impossible de cloner {jobId}. La tâche est introuvable", "xpack.ml.jobsList.closeActionStatusText": "fermer", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index cd6ac0cdfc14d..2b33d03617c24 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18652,7 +18652,6 @@ "xpack.ml.jobsList.alertingRules.tooltipContent": "ジョブ{rulesCount}はアラート{rulesCount, plural, other { ルール}}に関連付けられています", "xpack.ml.jobsList.analyticsSpacesLabel": "スペース", "xpack.ml.jobsList.auditMessageColumn.screenReaderDescription": "この列は、過去24時間にエラーまたは警告があった場合にアイコンを表示します", - "xpack.ml.jobsList.breadcrumb": "ジョブ", "xpack.ml.jobsList.cannotSelectRowForJobMessage": "ジョブID {jobId}を選択できません", "xpack.ml.jobsList.cloneJobErrorMessage": "{jobId} のクローンを作成できませんでした。ジョブが見つかりませんでした", "xpack.ml.jobsList.closeActionStatusText": "閉じる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5fe8340d34682..670c4e375647b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18679,7 +18679,6 @@ "xpack.ml.jobsList.alertingRules.tooltipContent": "作业具有 {rulesCount} 个关联的告警{rulesCount, plural, other {规则}}", "xpack.ml.jobsList.analyticsSpacesLabel": "工作区", "xpack.ml.jobsList.auditMessageColumn.screenReaderDescription": "过去 24 小时里该作业有错误或警告时,此列显示图标", - "xpack.ml.jobsList.breadcrumb": "作业", "xpack.ml.jobsList.cannotSelectRowForJobMessage": "无法选择作业 ID {jobId}", "xpack.ml.jobsList.cloneJobErrorMessage": "无法克隆 {jobId}。找不到作业", "xpack.ml.jobsList.closeActionStatusText": "关闭", From 47918ae0beeaadae7b2b848dfc7367f4d26f8dca Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 21 Apr 2022 17:30:14 -0400 Subject: [PATCH 22/24] [Console] Add Kibana APIs Support (#130816) * Revert "Revert "[Console] Add Kibana APIs Support (#128562)"" This reverts commit f41dc1fcc9bc3017315da08debcf754ca420bef1. * fix functional test --- src/plugins/console/common/constants/api.ts | 1 + src/plugins/console/common/constants/index.ts | 2 +- .../console_editor/editor.test.mock.tsx | 4 +- .../legacy/console_editor/editor.test.tsx | 8 +-- .../editor/legacy/console_editor/editor.tsx | 10 +-- .../console_editor/keyboard_shortcuts.ts | 10 +-- .../console/public/application/hooks/index.ts | 2 +- .../index.ts | 2 +- .../send_request.test.ts} | 49 ++++++------- .../send_request.ts} | 72 +++++++++---------- .../track.ts | 0 .../use_send_current_request.test.tsx} | 28 ++++---- .../use_send_current_request.ts} | 6 +- .../models/sense_editor/sense_editor.test.js | 6 +- .../models/sense_editor/sense_editor.ts | 21 +++--- .../public/application/stores/request.ts | 10 +-- src/plugins/console/public/lib/es/es.ts | 49 ++++++++++--- src/plugins/console/public/lib/es/index.ts | 2 +- test/functional/apps/console/_autocomplete.ts | 3 + test/functional/apps/console/_console.ts | 16 +++++ test/functional/page_objects/console_page.ts | 1 - 21 files changed, 173 insertions(+), 129 deletions(-) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es => use_send_current_request}/index.ts (81%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es/send_request_to_es.test.ts => use_send_current_request/send_request.test.ts} (66%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es/send_request_to_es.ts => use_send_current_request/send_request.ts} (69%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es => use_send_current_request}/track.ts (100%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es/use_send_current_request_to_es.test.tsx => use_send_current_request/use_send_current_request.test.tsx} (80%) rename src/plugins/console/public/application/hooks/{use_send_current_request_to_es/use_send_current_request_to_es.ts => use_send_current_request/use_send_current_request.ts} (96%) diff --git a/src/plugins/console/common/constants/api.ts b/src/plugins/console/common/constants/api.ts index aa0fad1fe4424..3c5aa11519350 100644 --- a/src/plugins/console/common/constants/api.ts +++ b/src/plugins/console/common/constants/api.ts @@ -7,3 +7,4 @@ */ export const API_BASE_PATH = '/api/console'; +export const KIBANA_API_PREFIX = 'kbn:'; diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts index d8768af8fc8d8..756a79883cbdb 100644 --- a/src/plugins/console/common/constants/index.ts +++ b/src/plugins/console/common/constants/index.ts @@ -7,4 +7,4 @@ */ export { MAJOR_VERSION } from './plugin'; -export { API_BASE_PATH } from './api'; +export { API_BASE_PATH, KIBANA_API_PREFIX } from './api'; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx index dfed86a643627..b410e240151d7 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx @@ -39,8 +39,8 @@ jest.mock('../../../../models/sense_editor', () => { }; }); -jest.mock('../../../../hooks/use_send_current_request_to_es/send_request_to_es', () => ({ - sendRequestToES: jest.fn(), +jest.mock('../../../../hooks/use_send_current_request/send_request', () => ({ + sendRequest: jest.fn(), })); jest.mock('../../../../../lib/autocomplete/get_endpoint_from_position', () => ({ getEndpointFromPosition: jest.fn(), diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx index b942a6d830217..ba5f1e78d5f01 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx @@ -25,7 +25,7 @@ import { } from '../../../../contexts'; // Mocked functions -import { sendRequestToES } from '../../../../hooks/use_send_current_request_to_es/send_request_to_es'; +import { sendRequest } from '../../../../hooks/use_send_current_request/send_request'; import { getEndpointFromPosition } from '../../../../../lib/autocomplete/get_endpoint_from_position'; import type { DevToolsSettings } from '../../../../../services'; import * as consoleMenuActions from '../console_menu_actions'; @@ -58,15 +58,15 @@ describe('Legacy (Ace) Console Editor Component Smoke Test', () => { sandbox.restore(); }); - it('calls send current request to ES', async () => { + it('calls send current request', async () => { (getEndpointFromPosition as jest.Mock).mockReturnValue({ patterns: [] }); - (sendRequestToES as jest.Mock).mockRejectedValue({}); + (sendRequest as jest.Mock).mockRejectedValue({}); const editor = doMount(); act(() => { editor.find('button[data-test-subj~="sendRequestButton"]').simulate('click'); }); await nextTick(); - expect(sendRequestToES).toBeCalledTimes(1); + expect(sendRequest).toBeCalledTimes(1); }); it('opens docs', () => { diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index bafe9ee6ca156..d01a40bdd44b3 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -26,7 +26,7 @@ import { ConsoleMenu } from '../../../../components'; import { useEditorReadContext, useServicesContext } from '../../../../contexts'; import { useSaveCurrentTextObject, - useSendCurrentRequestToES, + useSendCurrentRequest, useSetInputEditor, } from '../../../../hooks'; import * as senseEditor from '../../../../models/sense_editor'; @@ -72,7 +72,7 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { const { settings } = useEditorReadContext(); const setInputEditor = useSetInputEditor(); - const sendCurrentRequestToES = useSendCurrentRequestToES(); + const sendCurrentRequest = useSendCurrentRequest(); const saveCurrentTextObject = useSaveCurrentTextObject(); const editorRef = useRef(null); @@ -231,11 +231,11 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { if (!isKeyboardShortcutsDisabled) { registerCommands({ senseEditor: editorInstanceRef.current!, - sendCurrentRequestToES, + sendCurrentRequest, openDocumentation, }); } - }, [sendCurrentRequestToES, openDocumentation, settings]); + }, [openDocumentation, settings, sendCurrentRequest]); useEffect(() => { const { current: editor } = editorInstanceRef; @@ -262,7 +262,7 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { > void; + sendCurrentRequest: () => void; openDocumentation: () => void; } @@ -24,11 +24,7 @@ const COMMANDS = { GO_TO_LINE: 'gotoline', }; -export function registerCommands({ - senseEditor, - sendCurrentRequestToES, - openDocumentation, -}: Actions) { +export function registerCommands({ senseEditor, sendCurrentRequest, openDocumentation }: Actions) { const throttledAutoIndent = throttle(() => senseEditor.autoIndent(), 500, { leading: true, trailing: true, @@ -39,7 +35,7 @@ export function registerCommands({ keys: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, name: COMMANDS.SEND_TO_ELASTICSEARCH, fn: () => { - sendCurrentRequestToES(); + sendCurrentRequest(); }, }); diff --git a/src/plugins/console/public/application/hooks/index.ts b/src/plugins/console/public/application/hooks/index.ts index 1a9b4e5c472bf..1996330bef66b 100644 --- a/src/plugins/console/public/application/hooks/index.ts +++ b/src/plugins/console/public/application/hooks/index.ts @@ -8,6 +8,6 @@ export { useSetInputEditor } from './use_set_input_editor'; export { useRestoreRequestFromHistory } from './use_restore_request_from_history'; -export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; +export { useSendCurrentRequest } from './use_send_current_request'; export { useSaveCurrentTextObject } from './use_save_current_text_object'; export { useDataInit } from './use_data_init'; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts similarity index 81% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/index.ts index df2431f1f6f43..33bdbef87f2ef 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/index.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { useSendCurrentRequestToES } from './use_send_current_request_to_es'; +export { useSendCurrentRequest } from './use_send_current_request'; diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts similarity index 66% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts index 8578e271f37b3..60ced085c6891 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.test.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.test.ts @@ -8,14 +8,14 @@ import type { ContextValue } from '../../contexts'; -jest.mock('./send_request_to_es', () => ({ sendRequestToES: jest.fn(() => Promise.resolve()) })); +jest.mock('./send_request', () => ({ sendRequest: jest.fn(() => Promise.resolve()) })); -import { sendRequestToES } from './send_request_to_es'; +import { sendRequest } from './send_request'; import { serviceContextMock } from '../../contexts/services_context.mock'; -const mockedSendRequestToES = sendRequestToES as jest.Mock; +const mockedSendRequest = sendRequest as jest.Mock; -describe('sendRequestToES', () => { +describe('sendRequest', () => { let mockContextValue: ContextValue; beforeEach(() => { @@ -26,8 +26,8 @@ describe('sendRequestToES', () => { jest.resetAllMocks(); }); - it('should send request to ES', async () => { - mockedSendRequestToES.mockResolvedValue([ + it('should send request', async () => { + mockedSendRequest.mockResolvedValue([ { response: { statusCode: 200, @@ -40,17 +40,17 @@ describe('sendRequestToES', () => { http: mockContextValue.services.http, requests: [{ method: 'PUT', url: 'test', data: [] }], }; - const results = await sendRequestToES(args); + const results = await sendRequest(args); const [request] = results; expect(request.response.statusCode).toEqual(200); expect(request.response.value).toContain('"acknowledged": true'); - expect(mockedSendRequestToES).toHaveBeenCalledWith(args); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledWith(args); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); }); - it('should send multiple requests to ES', async () => { - mockedSendRequestToES.mockResolvedValue([ + it('should send multiple requests', async () => { + mockedSendRequest.mockResolvedValue([ { response: { statusCode: 200, @@ -70,17 +70,17 @@ describe('sendRequestToES', () => { { method: 'GET', url: 'test-2', data: [] }, ], }; - const results = await sendRequestToES(args); + const results = await sendRequest(args); const [firstRequest, secondRequest] = results; expect(firstRequest.response.statusCode).toEqual(200); expect(secondRequest.response.statusCode).toEqual(200); - expect(mockedSendRequestToES).toHaveBeenCalledWith(args); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledWith(args); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); }); it('should handle errors', async () => { - mockedSendRequestToES.mockRejectedValue({ + mockedSendRequest.mockRejectedValue({ response: { statusCode: 500, statusText: 'error', @@ -88,45 +88,46 @@ describe('sendRequestToES', () => { }); try { - await sendRequestToES({ + await sendRequest({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test', data: [] }], }); } catch (error) { expect(error.response.statusCode).toEqual(500); expect(error.response.statusText).toEqual('error'); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); } }); + describe('successful response value', () => { describe('with text', () => { it('should return value with lines separated', async () => { - mockedSendRequestToES.mockResolvedValue('\ntest_index-1 [] \ntest_index-2 []\n'); - const response = await sendRequestToES({ + mockedSendRequest.mockResolvedValue('\ntest_index-1 []\ntest_index-2 []\n'); + const response = await sendRequest({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test-1', data: [] }], }); expect(response).toMatchInlineSnapshot(` " - test_index-1 [] + test_index-1 [] test_index-2 [] " `); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); }); }); describe('with parsed json', () => { it('should stringify value', async () => { - mockedSendRequestToES.mockResolvedValue(JSON.stringify({ test: 'some value' })); - const response = await sendRequestToES({ + mockedSendRequest.mockResolvedValue(JSON.stringify({ test: 'some value' })); + const response = await sendRequest({ http: mockContextValue.services.http, requests: [{ method: 'GET', url: 'test-2', data: [] }], }); expect(typeof response).toBe('string'); - expect(mockedSendRequestToES).toHaveBeenCalledTimes(1); + expect(mockedSendRequest).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts similarity index 69% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts index 451198aaf2d2b..1247f3f78aa68 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/send_request.ts @@ -15,12 +15,12 @@ import { BaseResponseType } from '../../../types'; const { collapseLiteralStrings } = XJson; -export interface EsRequestArgs { +export interface RequestArgs { http: HttpSetup; requests: Array<{ url: string; method: string; data: string[] }>; } -export interface ESResponseObject { +export interface ResponseObject { statusCode: number; statusText: string; timeMs: number; @@ -28,17 +28,17 @@ export interface ESResponseObject { value: V; } -export interface ESRequestResult { +export interface RequestResult { request: { data: string; method: string; path: string }; - response: ESResponseObject; + response: ResponseObject; } let CURRENT_REQ_ID = 0; -export function sendRequestToES(args: EsRequestArgs): Promise { +export function sendRequest(args: RequestArgs): Promise { const requests = args.requests.slice(); return new Promise((resolve, reject) => { const reqId = ++CURRENT_REQ_ID; - const results: ESRequestResult[] = []; + const results: RequestResult[] = []; if (reqId !== CURRENT_REQ_ID) { return; } @@ -59,11 +59,11 @@ export function sendRequestToES(args: EsRequestArgs): Promise return; } const req = requests.shift()!; - const esPath = req.url; - const esMethod = req.method; - let esData = collapseLiteralStrings(req.data.join('\n')); - if (esData) { - esData += '\n'; + const path = req.url; + const method = req.method; + let data = collapseLiteralStrings(req.data.join('\n')); + if (data) { + data += '\n'; } // append a new line for bulk requests. const startTime = Date.now(); @@ -71,9 +71,9 @@ export function sendRequestToES(args: EsRequestArgs): Promise try { const { response, body } = await es.send({ http: args.http, - method: esMethod, - path: esPath, - data: esData, + method, + path, + data, asResponse: true, }); @@ -115,9 +115,9 @@ export function sendRequestToES(args: EsRequestArgs): Promise value, }, request: { - data: esData, - method: esMethod, - path: esPath, + data, + method, + path, }, }); @@ -127,25 +127,19 @@ export function sendRequestToES(args: EsRequestArgs): Promise } } catch (error) { let value; - let contentType: string | null = ''; + const { response, body } = error as IHttpFetchError; + const contentType = response?.headers.get('Content-Type') ?? ''; + const statusCode = response?.status ?? 500; + const statusText = error?.response?.statusText ?? 'error'; - const { response, body = {} } = error as IHttpFetchError; - if (response) { - const { status, headers } = response; - if (body) { - value = JSON.stringify(body, null, 2); // ES error should be shown - contentType = headers.get('Content-Type'); - } else { - value = 'Request failed to get to the server (status code: ' + status + ')'; - contentType = headers.get('Content-Type'); - } - - if (isMultiRequest) { - value = '# ' + req.method + ' ' + req.url + '\n' + value; - } + if (body) { + value = JSON.stringify(body, null, 2); } else { - value = - "\n\nFailed to connect to Console's backend.\nPlease check the Kibana server is up and running"; + value = 'Request failed to get to the server (status code: ' + statusCode + ')'; + } + + if (isMultiRequest) { + value = '# ' + req.method + ' ' + req.url + '\n' + value; } reject({ @@ -153,13 +147,13 @@ export function sendRequestToES(args: EsRequestArgs): Promise value, contentType, timeMs: Date.now() - startTime, - statusCode: error?.response?.status ?? 500, - statusText: error?.response?.statusText ?? 'error', + statusCode, + statusText, }, request: { - data: esData, - method: esMethod, - path: esPath, + data, + method, + path, }, }); } diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts b/src/plugins/console/public/application/hooks/use_send_current_request/track.ts similarity index 100% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/track.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/track.ts diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx similarity index 80% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx rename to src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx index e0131dc116a34..d16dc3f832d3a 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.test.tsx +++ b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -jest.mock('./send_request_to_es', () => ({ sendRequestToES: jest.fn() })); +jest.mock('./send_request', () => ({ sendRequest: jest.fn() })); jest.mock('../../contexts/editor_context/editor_registry', () => ({ instance: { getInputEditor: jest.fn() }, })); @@ -21,10 +21,10 @@ import { serviceContextMock } from '../../contexts/services_context.mock'; import { useRequestActionContext } from '../../contexts/request_context'; import { instance as editorRegistry } from '../../contexts/editor_context/editor_registry'; -import { sendRequestToES } from './send_request_to_es'; -import { useSendCurrentRequestToES } from './use_send_current_request_to_es'; +import { sendRequest } from './send_request'; +import { useSendCurrentRequest } from './use_send_current_request'; -describe('useSendCurrentRequestToES', () => { +describe('useSendCurrentRequest', () => { let mockContextValue: ContextValue; let dispatch: (...args: unknown[]) => void; const contexts = ({ children }: { children: JSX.Element }) => ( @@ -41,18 +41,18 @@ describe('useSendCurrentRequestToES', () => { jest.resetAllMocks(); }); - it('calls send request to ES', async () => { + it('calls send request', async () => { // Set up mocks (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); // This request should succeed - (sendRequestToES as jest.Mock).mockResolvedValue([]); + (sendRequest as jest.Mock).mockResolvedValue([]); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); await act(() => result.current()); - expect(sendRequestToES).toHaveBeenCalledWith({ + expect(sendRequest).toHaveBeenCalledWith({ http: mockContextValue.services.http, requests: ['test'], }); @@ -64,12 +64,12 @@ describe('useSendCurrentRequestToES', () => { it('handles known errors', async () => { // Set up mocks - (sendRequestToES as jest.Mock).mockRejectedValue({ response: 'nada' }); + (sendRequest as jest.Mock).mockRejectedValue({ response: 'nada' }); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); await act(() => result.current()); // Second call should be the request failure const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls; @@ -80,12 +80,12 @@ describe('useSendCurrentRequestToES', () => { it('handles unknown errors', async () => { // Set up mocks - (sendRequestToES as jest.Mock).mockRejectedValue(NaN /* unexpected error value */); + (sendRequest as jest.Mock).mockRejectedValue(NaN /* unexpected error value */); (editorRegistry.getInputEditor as jest.Mock).mockImplementation(() => ({ getRequestsInRange: () => ['test'], })); - const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); await act(() => result.current()); // Second call should be the request failure const [, [requestFailedCall]] = (dispatch as jest.Mock).mock.calls; @@ -100,7 +100,7 @@ describe('useSendCurrentRequestToES', () => { it('notifies the user about save to history errors once only', async () => { // Set up mocks - (sendRequestToES as jest.Mock).mockReturnValue( + (sendRequest as jest.Mock).mockReturnValue( [{ request: {} }, { request: {} }] /* two responses to save history */ ); (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({}); @@ -112,7 +112,7 @@ describe('useSendCurrentRequestToES', () => { getRequestsInRange: () => ['test', 'test'], })); - const { result } = renderHook(() => useSendCurrentRequestToES(), { wrapper: contexts }); + const { result } = renderHook(() => useSendCurrentRequest(), { wrapper: contexts }); await act(() => result.current()); expect(dispatch).toHaveBeenCalledTimes(2); diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts similarity index 96% rename from src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts rename to src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts index e7c436c9806b3..ed08304d8d660 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/use_send_current_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts @@ -16,10 +16,10 @@ import { retrieveAutoCompleteInfo } from '../../../lib/mappings/mappings'; import { instance as registry } from '../../contexts/editor_context/editor_registry'; import { useRequestActionContext, useServicesContext } from '../../contexts'; import { StorageQuotaError } from '../../components/storage_quota_error'; -import { sendRequestToES } from './send_request_to_es'; +import { sendRequest } from './send_request'; import { track } from './track'; -export const useSendCurrentRequestToES = () => { +export const useSendCurrentRequest = () => { const { services: { history, settings, notifications, trackUiMetric, http }, theme$, @@ -46,7 +46,7 @@ export const useSendCurrentRequestToES = () => { // Fire and forget setTimeout(() => track(requests, editor, trackUiMetric), 0); - const results = await sendRequestToES({ http, requests }); + const results = await sendRequest({ http, requests }); let saveToHistoryError: undefined | Error; const { isHistoryDisabled } = settings.toJSON(); diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js index 0889b98c69388..ff9d245f61275 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.test.js @@ -455,11 +455,11 @@ describe('Editor', () => { editorInput1, { start: { lineNumber: 7 }, end: { lineNumber: 14 } }, ` -curl -XGET "http://localhost:9200/_stats?level=shards" +curl -XGET "http://localhost:9200/_stats?level=shards" -H "kbn-xsrf: reporting" #in between comment -curl -XPUT "http://localhost:9200/index_1/type1/1" -H 'Content-Type: application/json' -d' +curl -XPUT "http://localhost:9200/index_1/type1/1" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d' { "f": 1 }'`.trim() @@ -470,7 +470,7 @@ curl -XPUT "http://localhost:9200/index_1/type1/1" -H 'Content-Type: application editorInput1, { start: { lineNumber: 29 }, end: { lineNumber: 33 } }, ` -curl -XPOST "http://localhost:9200/_sql?format=txt" -H 'Content-Type: application/json' -d' +curl -XPOST "http://localhost:9200/_sql?format=txt" -H "kbn-xsrf: reporting" -H "Content-Type: application/json" -d' { "query": "SELECT prenom FROM claude_index WHERE prenom = '\\''claude'\\'' ", "fetch_size": 1 diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index 5e8ca35f287b7..ac65afce2c18a 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -14,7 +14,7 @@ import RowParser from '../../../lib/row_parser'; import * as utils from '../../../lib/utils'; // @ts-ignore -import * as es from '../../../lib/es/es'; +import { constructUrl } from '../../../lib/es/es'; import { CoreEditor, Position, Range } from '../../../types'; import { createTokenIterator } from '../../factories'; @@ -467,21 +467,22 @@ export class SenseEditor { return req; } - const esPath = req.url; - const esMethod = req.method; - const esData = req.data; + const path = req.url; + const method = req.method; + const data = req.data; // this is the first url defined in elasticsearch.hosts - const url = es.constructESUrl(elasticsearchBaseUrl, esPath); + const url = constructUrl(elasticsearchBaseUrl, path); - let ret = 'curl -X' + esMethod + ' "' + url + '"'; - if (esData && esData.length) { - ret += " -H 'Content-Type: application/json' -d'\n"; - const dataAsString = collapseLiteralStrings(esData.join('\n')); + // Append 'kbn-xsrf' header to bypass (XSRF/CSRF) protections + let ret = `curl -X${method.toUpperCase()} "${url}" -H "kbn-xsrf: reporting"`; + if (data && data.length) { + ret += ` -H "Content-Type: application/json" -d'\n`; + const dataAsString = collapseLiteralStrings(data.join('\n')); // We escape single quoted strings that that are wrapped in single quoted strings ret += dataAsString.replace(/'/g, "'\\''"); - if (esData.length > 1) { + if (data.length > 1) { ret += '\n'; } // end with a new line ret += "'"; diff --git a/src/plugins/console/public/application/stores/request.ts b/src/plugins/console/public/application/stores/request.ts index 099ab24326d31..8056ab5a7987f 100644 --- a/src/plugins/console/public/application/stores/request.ts +++ b/src/plugins/console/public/application/stores/request.ts @@ -10,18 +10,18 @@ import { Reducer } from 'react'; import { produce } from 'immer'; import { identity } from 'fp-ts/lib/function'; import { BaseResponseType } from '../../types/common'; -import { ESRequestResult } from '../hooks/use_send_current_request_to_es/send_request_to_es'; +import { RequestResult } from '../hooks/use_send_current_request/send_request'; export type Actions = | { type: 'sendRequest'; payload: undefined } - | { type: 'requestSuccess'; payload: { data: ESRequestResult[] } } - | { type: 'requestFail'; payload: ESRequestResult | undefined }; + | { type: 'requestSuccess'; payload: { data: RequestResult[] } } + | { type: 'requestFail'; payload: RequestResult | undefined }; export interface Store { requestInFlight: boolean; lastResult: { - data: ESRequestResult[] | null; - error?: ESRequestResult; + data: RequestResult[] | null; + error?: RequestResult; }; } diff --git a/src/plugins/console/public/lib/es/es.ts b/src/plugins/console/public/lib/es/es.ts index 2a4059d664e6c..10d0ad95b0496 100644 --- a/src/plugins/console/public/lib/es/es.ts +++ b/src/plugins/console/public/lib/es/es.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import type { HttpFetchOptions, HttpResponse, HttpSetup } from '@kbn/core/public'; -import { API_BASE_PATH } from '../../../common/constants'; +import type { HttpResponse, HttpSetup } from '@kbn/core/public'; +import { trimStart } from 'lodash'; +import { API_BASE_PATH, KIBANA_API_PREFIX } from '../../../common/constants'; const esVersion: string[] = []; @@ -20,7 +21,7 @@ export function getContentType(body: unknown) { return 'application/json'; } -interface SendProps { +interface SendConfig { http: HttpSetup; method: string; path: string; @@ -30,6 +31,8 @@ interface SendProps { asResponse?: boolean; } +type Method = 'get' | 'post' | 'delete' | 'put' | 'patch' | 'head'; + export async function send({ http, method, @@ -38,18 +41,48 @@ export async function send({ asSystemRequest = false, withProductOrigin = false, asResponse = false, -}: SendProps) { - const options: HttpFetchOptions = { +}: SendConfig) { + const kibanaRequestUrl = getKibanaRequestUrl(path); + + if (kibanaRequestUrl) { + const httpMethod = method.toLowerCase() as Method; + const url = new URL(kibanaRequestUrl); + const { pathname, searchParams } = url; + const query = Object.fromEntries(searchParams.entries()); + const body = ['post', 'put', 'patch'].includes(httpMethod) ? data : null; + + return await http[httpMethod](pathname, { + body, + query, + asResponse, + asSystemRequest, + }); + } + + return await http.post(`${API_BASE_PATH}/proxy`, { query: { path, method, ...(withProductOrigin && { withProductOrigin }) }, body: data, asResponse, asSystemRequest, - }; + }); +} + +function getKibanaRequestUrl(path: string) { + const isKibanaApiRequest = path.startsWith(KIBANA_API_PREFIX); + const kibanaBasePath = window.location.origin; - return await http.post(`${API_BASE_PATH}/proxy`, options); + if (isKibanaApiRequest) { + // window.location.origin is used as a Kibana public base path for sending requests in cURL commands. E.g. "Copy as cURL". + return `${kibanaBasePath}/${trimStart(path.replace(KIBANA_API_PREFIX, ''), '/')}`; + } } -export function constructESUrl(baseUri: string, path: string) { +export function constructUrl(baseUri: string, path: string) { + const kibanaRequestUrl = getKibanaRequestUrl(path); + + if (kibanaRequestUrl) { + return kibanaRequestUrl; + } baseUri = baseUri.replace(/\/+$/, ''); path = path.replace(/^\/+/, ''); return baseUri + '/' + path; diff --git a/src/plugins/console/public/lib/es/index.ts b/src/plugins/console/public/lib/es/index.ts index 61d34ba96ec05..f83893e93713e 100644 --- a/src/plugins/console/public/lib/es/index.ts +++ b/src/plugins/console/public/lib/es/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { send, constructESUrl, getContentType, getVersion } from './es'; +export { send, constructUrl, getContentType, getVersion } from './es'; diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index 57c59793f69f6..7bf872373c6c7 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should provide basic auto-complete functionality', async () => { await PageObjects.console.enterRequest(); + await PageObjects.console.pressEnter(); await PageObjects.console.enterText(`{\n\t"query": {`); await PageObjects.console.pressEnter(); await PageObjects.console.promptAutocomplete(); @@ -39,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.console.clearTextArea(); await PageObjects.console.enterRequest(); + await PageObjects.console.pressEnter(); }); it('should add a comma after previous non empty line', async () => { await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"match": {}`); @@ -85,6 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.console.clearTextArea(); await PageObjects.console.enterRequest('\n POST _snapshot/test_repo'); + await PageObjects.console.pressEnter(); }); await asyncForEach(CONDITIONAL_TEMPLATES, async ({ type, template }) => { diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 367f1ccb56256..52218b88be60d 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -106,5 +106,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); }); + + describe('with kbn: prefix in request', () => { + before(async () => { + await PageObjects.console.clearTextArea(); + }); + it('it should send successful request to Kibana API', async () => { + const expectedResponseContains = 'default space'; + await PageObjects.console.enterRequest('\n GET kbn:/api/spaces/space'); + await PageObjects.console.clickPlay(); + await retry.try(async () => { + const actualResponse = await PageObjects.console.getResponse(); + log.debug(actualResponse); + expect(actualResponse).to.contain(expectedResponseContains); + }); + }); + }); }); } diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 281c49a789acf..7aaf842f28d14 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -102,7 +102,6 @@ export class ConsolePageObject extends FtrService { public async enterRequest(request: string = '\nGET _search') { const textArea = await this.getEditorTextArea(); await textArea.pressKeys(request); - await textArea.pressKeys(Key.ENTER); } public async enterText(text: string) { From fa7323769d42ccf0688085caf203c29b9f0526b4 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 21 Apr 2022 16:37:06 -0500 Subject: [PATCH 23/24] [jest] try running unit tests in parallel (#130823) --- .buildkite/scripts/steps/test/jest_parallel.sh | 9 ++++++++- packages/kbn-cli-dev-mode/src/dev_server.test.ts | 2 +- packages/kbn-cli-dev-mode/src/dev_server.ts | 10 +++++++++- .../kbn-cli-dev-mode/src/using_server_process.ts | 13 +++++++++---- src/cli_encryption_keys/encryption_config.test.js | 2 +- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/.buildkite/scripts/steps/test/jest_parallel.sh b/.buildkite/scripts/steps/test/jest_parallel.sh index 0530fe0f84161..27eb9e9216365 100755 --- a/.buildkite/scripts/steps/test/jest_parallel.sh +++ b/.buildkite/scripts/steps/test/jest_parallel.sh @@ -10,10 +10,17 @@ JOB_COUNT=$BUILDKITE_PARALLEL_JOB_COUNT i=0 exitCode=0 +# run unit tests in parallel +if [[ "$1" == 'jest.config.js' ]]; then + parallelism="-w2" +else + parallelism="--runInBand" +fi + while read -r config; do if [ "$((i % JOB_COUNT))" -eq "$JOB" ]; then echo "--- $ node scripts/jest --config $config" - node --max-old-space-size=14336 ./scripts/jest --config="$config" --runInBand --coverage=false --passWithNoTests + node --max-old-space-size=14336 ./scripts/jest --config="$config" "$parallelism" --coverage=false --passWithNoTests lastCode=$? if [ $lastCode -ne 0 ]; then diff --git a/packages/kbn-cli-dev-mode/src/dev_server.test.ts b/packages/kbn-cli-dev-mode/src/dev_server.test.ts index 772ba097fc31a..959a47ac80803 100644 --- a/packages/kbn-cli-dev-mode/src/dev_server.test.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.test.ts @@ -72,6 +72,7 @@ const defaultOptions: Options = { processExit$, sigint$, sigterm$, + forceColor: true, }; expect.addSnapshotSerializer(extendedEnvSerializer); @@ -80,7 +81,6 @@ beforeEach(() => { jest.clearAllMocks(); log.messages.length = 0; process.execArgv = ['--inheritted', '--exec', '--argv']; - process.env.FORCE_COLOR = process.env.FORCE_COLOR || '1'; currentProc = undefined; }); diff --git a/packages/kbn-cli-dev-mode/src/dev_server.ts b/packages/kbn-cli-dev-mode/src/dev_server.ts index bed1afe126c15..cf89d2c00d16f 100644 --- a/packages/kbn-cli-dev-mode/src/dev_server.ts +++ b/packages/kbn-cli-dev-mode/src/dev_server.ts @@ -34,6 +34,7 @@ export interface Options { sigint$?: Rx.Observable; sigterm$?: Rx.Observable; mapLogLine?: DevServer['mapLogLine']; + forceColor?: boolean; } export class DevServer { @@ -50,6 +51,7 @@ export class DevServer { private readonly argv: string[]; private readonly gracefulTimeout: number; private readonly mapLogLine?: (line: string) => string | null; + private readonly forceColor: boolean; constructor(options: Options) { this.log = options.log; @@ -62,6 +64,7 @@ export class DevServer { this.sigint$ = options.sigint$ ?? Rx.fromEvent(process, 'SIGINT'); this.sigterm$ = options.sigterm$ ?? Rx.fromEvent(process, 'SIGTERM'); this.mapLogLine = options.mapLogLine; + this.forceColor = options.forceColor ?? !!process.stdout.isTTY; } isReady$() { @@ -141,8 +144,13 @@ export class DevServer { }) ); + const serverOptions = { + script: this.script, + argv: this.argv, + forceColor: this.forceColor, + }; const runServer = () => - usingServerProcess(this.script, this.argv, (proc) => { + usingServerProcess(serverOptions, (proc) => { this.phase$.next('starting'); this.ready$.next(false); diff --git a/packages/kbn-cli-dev-mode/src/using_server_process.ts b/packages/kbn-cli-dev-mode/src/using_server_process.ts index eb997295035d8..690ba250cbcbc 100644 --- a/packages/kbn-cli-dev-mode/src/using_server_process.ts +++ b/packages/kbn-cli-dev-mode/src/using_server_process.ts @@ -18,14 +18,19 @@ interface ProcResource extends Rx.Unsubscribable { unsubscribe(): void; } +interface Options { + script: string; + argv: string[]; + forceColor: boolean; +} + export function usingServerProcess( - script: string, - argv: string[], + options: Options, fn: (proc: execa.ExecaChildProcess) => Rx.Observable ) { return Rx.using( (): ProcResource => { - const proc = execa.node(script, argv, { + const proc = execa.node(options.script, options.argv, { stdio: 'pipe', nodeOptions: [ ...process.execArgv, @@ -36,7 +41,7 @@ export function usingServerProcess( NODE_OPTIONS: process.env.NODE_OPTIONS, isDevCliChild: 'true', ELASTIC_APM_SERVICE_NAME: 'kibana', - ...(process.stdout.isTTY ? { FORCE_COLOR: 'true' } : {}), + ...(options.forceColor ? { FORCE_COLOR: 'true' } : {}), }, }); diff --git a/src/cli_encryption_keys/encryption_config.test.js b/src/cli_encryption_keys/encryption_config.test.js index 9629cfcafe78a..dba1967f4f6b1 100644 --- a/src/cli_encryption_keys/encryption_config.test.js +++ b/src/cli_encryption_keys/encryption_config.test.js @@ -14,7 +14,7 @@ describe('encryption key configuration', () => { let encryptionConfig = null; beforeEach(() => { - jest.spyOn(fs, 'readFileSync').mockReturnValue('xpack.security.encryptionKey: foo'); + jest.spyOn(fs, 'readFileSync').mockReturnValueOnce('xpack.security.encryptionKey: foo'); jest.spyOn(crypto, 'randomBytes').mockReturnValue('random-key'); encryptionConfig = new EncryptionConfig(); }); From 7c01257055b9521def55a77bbc5e881910ed7273 Mon Sep 17 00:00:00 2001 From: Nodir Latipov Date: Fri, 22 Apr 2022 11:02:56 +0500 Subject: [PATCH 24/24] [Unified Search] Move autocomplete logic to unified search plugin (#129977) * feat: move autocomplete logic from data plugin to unified search * minor fix after comments * updated Documentation: data.autocomplete -> unifiedSearch.autocomplete * changed renameFromRoot order for autocomplete * removed extra renameFromRoot in config deprecations, updated test * added configPath for unified search plugin * Update kibana.json * updated path to autocomplete * fix conflict * fix conflict * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * fix Linting * fix functional_with_es_ssl test --- config/kibana.yml | 4 +- docs/setup/settings.asciidoc | 4 +- packages/kbn-optimizer/limits.yml | 2 +- .../src/field_value_match/index.tsx | 2 +- .../src/field_value_match_any/index.tsx | 2 +- .../src/field_value_wildcard/index.tsx | 2 +- .../use_field_value_autocomplete/index.ts | 2 +- .../resources/base/bin/kibana-docker | 5 + src/plugins/controls/kibana.json | 3 +- .../public/__stories__/controls.stories.tsx | 2 +- src/plugins/controls/public/services/data.ts | 1 - src/plugins/controls/public/services/index.ts | 2 + .../controls/public/services/kibana/data.ts | 3 +- .../controls/public/services/kibana/index.ts | 2 + .../public/services/kibana/unified_search.ts | 26 +++ .../public/services/storybook/data.ts | 11 +- .../public/services/storybook/index.ts | 2 + .../services/storybook/unified_search.ts | 25 +++ .../controls/public/services/stub/index.ts | 3 +- .../public/services/unified_search.ts | 13 ++ src/plugins/controls/public/types.ts | 2 + .../options_list_suggestions_route.ts | 4 +- src/plugins/controls/server/plugin.ts | 6 +- src/plugins/controls/tsconfig.json | 3 +- src/plugins/data/config.ts | 22 --- src/plugins/data/kibana.json | 2 +- src/plugins/data/public/index.ts | 19 +-- src/plugins/data/public/mocks.ts | 14 -- src/plugins/data/public/plugin.ts | 9 -- src/plugins/data/public/types.ts | 7 - src/plugins/data/server/index.ts | 5 +- src/plugins/data/server/mocks.ts | 6 - src/plugins/data/server/plugin.ts | 6 - .../input_control_vis/public/plugin.ts | 14 +- src/plugins/unified_search/config.ts | 25 ++- src/plugins/unified_search/kibana.json | 6 +- .../autocomplete/autocomplete_service.ts | 6 +- .../collectors/create_usage_collector.ts | 0 .../public/autocomplete/collectors/index.ts | 0 .../public/autocomplete/collectors/types.ts | 0 .../public/autocomplete/index.ts | 0 .../providers/kql_query_suggestion/README.md | 0 .../__fixtures__/index_pattern_response.json | 0 .../kql_query_suggestion/conjunction.test.ts | 5 +- .../kql_query_suggestion/conjunction.tsx | 18 +-- .../kql_query_suggestion/field.test.ts | 3 +- .../providers/kql_query_suggestion/field.tsx | 8 +- .../providers/kql_query_suggestion/index.ts | 6 +- .../lib/escape_kuery.test.ts | 0 .../kql_query_suggestion/lib/escape_kuery.ts | 0 .../kql_query_suggestion/operator.test.ts | 3 +- .../kql_query_suggestion/operator.tsx | 50 +++--- .../sort_prefix_first.test.ts | 0 .../kql_query_suggestion/sort_prefix_first.ts | 0 .../providers/kql_query_suggestion/types.ts | 7 +- .../kql_query_suggestion/value.test.ts | 4 +- .../providers/kql_query_suggestion/value.ts | 12 +- .../providers/query_suggestion_provider.ts | 4 +- .../value_suggestion_provider.test.ts | 8 +- .../providers/value_suggestion_provider.ts | 11 +- .../filter_editor/phrase_suggestor.tsx | 8 +- src/plugins/unified_search/public/index.ts | 18 ++- src/plugins/unified_search/public/mocks.ts | 20 +++ src/plugins/unified_search/public/plugin.ts | 32 +++- .../query_bar_top_row.test.tsx | 8 + .../query_string_input.test.tsx | 8 + .../query_string_input/query_string_input.tsx | 15 +- src/plugins/unified_search/public/services.ts | 4 + .../typeahead/suggestion_component.test.tsx | 2 +- .../public/typeahead/suggestion_component.tsx | 2 +- .../typeahead/suggestions_component.test.tsx | 2 +- .../typeahead/suggestions_component.tsx | 2 +- .../unified_search/public/typeahead/types.ts | 2 +- src/plugins/unified_search/public/types.ts | 16 +- .../autocomplete/autocomplete_service.ts | 0 .../server/autocomplete/index.ts | 0 .../server/autocomplete/routes.ts | 0 .../server/autocomplete/terms_agg.test.ts | 0 .../server/autocomplete/terms_agg.ts | 2 +- .../server/autocomplete/terms_enum.test.ts | 0 .../server/autocomplete/terms_enum.ts | 4 +- .../autocomplete/value_suggestions_route.ts | 2 +- .../server/config_deprecations.test.ts | 137 ++++++++++++++++ .../server/config_deprecations.ts | 59 +++++++ .../unified_search/server/data_views/index.ts | 9 ++ src/plugins/unified_search/server/index.ts | 40 +++++ src/plugins/unified_search/server/mocks.ts | 23 +++ src/plugins/unified_search/server/plugin.ts | 50 ++++++ src/plugins/unified_search/tsconfig.json | 4 +- .../test_suites/core_plugins/rendering.ts | 10 +- x-pack/plugins/apm/kibana.json | 3 +- .../components/shared/kuery_bar/index.tsx | 7 +- x-pack/plugins/apm/public/plugin.ts | 5 +- .../fleet/.storybook/context/stubs.tsx | 2 + x-pack/plugins/fleet/kibana.json | 2 +- .../components/agent_logs/filter_dataset.tsx | 6 +- .../agent_logs/filter_log_level.tsx | 6 +- .../fleet/public/mock/plugin_dependencies.ts | 2 + x-pack/plugins/fleet/public/plugin.ts | 3 + .../public/components/search_bar.test.tsx | 2 + .../autocomplete_field/autocomplete_field.tsx | 2 +- .../autocomplete_field/suggestion_item.tsx | 6 +- .../containers/with_kuery_autocompletion.tsx | 10 +- .../metrics_explorer/components/kuery_bar.tsx | 2 +- x-pack/plugins/lens/kibana.json | 3 +- .../dimension_panel/dimension_editor.tsx | 1 + .../dimension_panel/dimension_panel.test.tsx | 2 + .../dimension_panel/dimension_panel.tsx | 2 + .../droppable/droppable.test.ts | 2 + .../dimension_panel/reference_editor.test.tsx | 2 + .../dimension_panel/reference_editor.tsx | 2 + .../public/indexpattern_datasource/index.ts | 11 +- .../indexpattern.test.ts | 18 ++- .../indexpattern_datasource/indexpattern.tsx | 4 + .../definitions/date_histogram.test.tsx | 3 + .../definitions/filters/filters.test.tsx | 2 + .../formula/editor/formula_editor.tsx | 8 +- .../formula/editor/math_completion.test.ts | 20 +-- .../formula/editor/math_completion.ts | 19 ++- .../operations/definitions/index.ts | 2 + .../definitions/last_value.test.tsx | 2 + .../definitions/percentile.test.tsx | 10 +- .../definitions/ranges/ranges.test.tsx | 5 +- .../definitions/static_value.test.tsx | 8 +- .../definitions/terms/terms.test.tsx | 2 + x-pack/plugins/lens/public/plugin.ts | 9 +- .../components/builder/builder.stories.tsx | 2 +- .../builder/entry_renderer.stories.tsx | 2 +- .../builder/entry_renderer.test.tsx | 10 +- .../components/builder/entry_renderer.tsx | 2 +- .../builder/exception_item_renderer.test.tsx | 4 +- .../builder/exception_item_renderer.tsx | 2 +- .../builder/exception_items_renderer.test.tsx | 4 +- .../builder/exception_items_renderer.tsx | 2 +- x-pack/plugins/lists/tsconfig.json | 1 + x-pack/plugins/maps/public/kibana_services.ts | 2 +- x-pack/plugins/ml/kibana.json | 3 +- x-pack/plugins/ml/public/application/app.tsx | 2 +- .../application/util/dependency_cache.ts | 4 +- x-pack/plugins/ml/public/plugin.ts | 3 + x-pack/plugins/monitoring/kibana.json | 2 +- .../kuery_bar/autocomplete_field.tsx | 2 +- .../public/components/kuery_bar/index.tsx | 2 +- .../components/kuery_bar/suggestion_item.tsx | 2 +- .../kuery_bar/with_kuery_autocompletion.tsx | 10 +- x-pack/plugins/security_solution/kibana.json | 3 +- .../exceptions/add_exception_flyout/index.tsx | 4 +- .../edit_exception_flyout/index.tsx | 4 +- .../components/query_bar/index.test.tsx | 7 + .../components/threat_match/index.test.tsx | 2 +- .../threat_match/list_item.test.tsx | 2 +- .../common/lib/kibana/kibana_react.mock.ts | 3 + .../mock/endpoint/app_context_render.tsx | 2 +- .../rules/severity_mapping/index.tsx | 2 +- .../view/components/form/index.test.tsx | 1 + .../view/components/form/index.tsx | 6 +- .../timeline/query_bar/index.test.tsx | 7 + x-pack/plugins/synthetics/kibana.json | 3 +- .../plugins/synthetics/public/apps/plugin.ts | 2 + .../lazy_wrapper/monitor_status.tsx | 2 +- .../translations/translations/fr-FR.json | 149 +++++++++++++++-- .../translations/translations/ja-JP.json | 150 ++++++++++++++++-- .../translations/translations/zh-CN.json | 150 ++++++++++++++++-- 163 files changed, 1253 insertions(+), 392 deletions(-) create mode 100644 src/plugins/controls/public/services/kibana/unified_search.ts create mode 100644 src/plugins/controls/public/services/storybook/unified_search.ts create mode 100644 src/plugins/controls/public/services/unified_search.ts rename src/plugins/{data => unified_search}/public/autocomplete/autocomplete_service.ts (93%) rename src/plugins/{data => unified_search}/public/autocomplete/collectors/create_usage_collector.ts (100%) rename src/plugins/{data => unified_search}/public/autocomplete/collectors/index.ts (100%) rename src/plugins/{data => unified_search}/public/autocomplete/collectors/types.ts (100%) rename src/plugins/{data => unified_search}/public/autocomplete/index.ts (100%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/README.md (100%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json (100%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts (95%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx (69%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/field.test.ts (97%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/field.tsx (93%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/index.ts (93%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts (100%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts (100%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/operator.test.ts (97%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/operator.tsx (65%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts (100%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts (100%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/types.ts (73%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/value.test.ts (97%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/kql_query_suggestion/value.ts (89%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/query_suggestion_provider.ts (90%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/value_suggestion_provider.test.ts (97%) rename src/plugins/{data => unified_search}/public/autocomplete/providers/value_suggestion_provider.ts (95%) rename src/plugins/{data => unified_search}/server/autocomplete/autocomplete_service.ts (100%) rename src/plugins/{data => unified_search}/server/autocomplete/index.ts (100%) rename src/plugins/{data => unified_search}/server/autocomplete/routes.ts (100%) rename src/plugins/{data => unified_search}/server/autocomplete/terms_agg.test.ts (100%) rename src/plugins/{data => unified_search}/server/autocomplete/terms_agg.ts (97%) rename src/plugins/{data => unified_search}/server/autocomplete/terms_enum.test.ts (100%) rename src/plugins/{data => unified_search}/server/autocomplete/terms_enum.ts (95%) rename src/plugins/{data => unified_search}/server/autocomplete/value_suggestions_route.ts (97%) create mode 100644 src/plugins/unified_search/server/config_deprecations.test.ts create mode 100644 src/plugins/unified_search/server/config_deprecations.ts create mode 100644 src/plugins/unified_search/server/data_views/index.ts create mode 100644 src/plugins/unified_search/server/index.ts create mode 100644 src/plugins/unified_search/server/mocks.ts create mode 100644 src/plugins/unified_search/server/plugin.ts diff --git a/config/kibana.yml b/config/kibana.yml index 50ddad9a4b32a..4233bf2882a29 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -159,8 +159,8 @@ # =================== Search Autocomplete =================== # Time in milliseconds to wait for autocomplete suggestions from Elasticsearch. # This value must be a whole number greater than zero. Defaults to 1000ms -#data.autocomplete.valueSuggestions.timeout: 1000 +#unifiedSearch.autocomplete.valueSuggestions.timeout: 1000 # Maximum number of documents loaded by each shard to generate autocomplete suggestions. # This value must be a whole number greater than zero. Defaults to 100_000 -#data.autocomplete.valueSuggestions.terminateAfter: 100000 +#unifiedSearch.autocomplete.valueSuggestions.terminateAfter: 100000 diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index f5dca28a7da1c..2d52abc1f0138 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -289,11 +289,11 @@ is an alternative to `elasticsearch.username` and `elasticsearch.password`. | `interpreter.enableInVisualize` | Enables use of interpreter in Visualize. *Default: `true`* -| `data.autocomplete.valueSuggestions.timeout:` {ess-icon} +| `unifiedSearch.autocomplete.valueSuggestions.timeout:` {ess-icon} | Time in milliseconds to wait for autocomplete suggestions from {es}. This value must be a whole number greater than zero. *Default: `"1000"`* -| `data.autocomplete.valueSuggestions.terminateAfter:` {ess-icon} +| `unifiedSearch.autocomplete.valueSuggestions.terminateAfter:` {ess-icon} | Maximum number of documents loaded by each shard to generate autocomplete suggestions. This value must be a whole number greater than zero. *Default: `"100000"`* diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 2540976c106f5..98089f1f1c5fb 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -122,7 +122,7 @@ pageLoadAssetSize: sessionView: 77750 cloudSecurityPosture: 19109 visTypeGauge: 24113 - unifiedSearch: 49195 + unifiedSearch: 104869 data: 454087 expressionXY: 26500 eventAnnotation: 19334 diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx index 096058fcc0aa3..bfede3a8fcc4d 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx @@ -21,7 +21,7 @@ import { uniq } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; // TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 -// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +// import { AutocompleteStart } from '../../../../../../../src/plugins/unified_search/public'; type AutocompleteStart = any; import * as i18n from '../translations'; diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx index 5a36e155c548f..8f5afbbc86629 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match_any/index.tsx @@ -13,7 +13,7 @@ import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution- import { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; // TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 -// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +// import { AutocompleteStart } from '../../../../../../../src/plugins/unified_search/public'; type AutocompleteStart = any; import * as i18n from '../translations'; diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.tsx index 82b9cb029d8dc..82d36aef83728 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_wildcard/index.tsx @@ -15,7 +15,7 @@ import { uniq } from 'lodash'; import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; // TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 -// import { AutocompleteStart } from '../../../../../../../src/plugins/data/public'; +// import { AutocompleteStart } from '../../../../../../../src/plugins/unified_search/public'; type AutocompleteStart = any; import * as i18n from '../translations'; diff --git a/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts index ca0868e5056a6..1b2de4567fe2a 100644 --- a/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts +++ b/packages/kbn-securitysolution-autocomplete/src/hooks/use_field_value_autocomplete/index.ts @@ -12,7 +12,7 @@ import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution- import { DataViewBase, DataViewFieldBase, getDataViewFieldSubtypeNested } from '@kbn/es-query'; // TODO: I have to use any here for now, but once this is available below, we should use the correct types, https://github.com/elastic/kibana/issues/100715 -// import { AutocompleteStart } from '../../../../../../../../src/plugins/data/public'; +// import { AutocompleteStart } from '../../../../../../../../src/plugins/unified_search/public'; type AutocompleteStart = any; interface FuncArgs { diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index fbf6d64170ee8..ca81383ba639f 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -40,6 +40,11 @@ kibana_vars=( csp.report_to data.autocomplete.valueSuggestions.terminateAfter data.autocomplete.valueSuggestions.timeout + unifiedSearch.autocomplete.valueSuggestions.terminateAfter + unifiedSearch.autocomplete.valueSuggestions.timeout + unifiedSearch.autocomplete.querySuggestions.enabled + unifiedSearch.autocomplete.valueSuggestions.enabled + unifiedSearch.autocomplete.valueSuggestions.tiers elasticsearch.customHeaders elasticsearch.hosts elasticsearch.logQueries diff --git a/src/plugins/controls/kibana.json b/src/plugins/controls/kibana.json index 20afd63505a73..e87af3f517af2 100644 --- a/src/plugins/controls/kibana.json +++ b/src/plugins/controls/kibana.json @@ -17,7 +17,8 @@ "expressions", "embeddable", "dataViews", - "data" + "data", + "unifiedSearch" ], "optionalPlugins": [] } diff --git a/src/plugins/controls/public/__stories__/controls.stories.tsx b/src/plugins/controls/public/__stories__/controls.stories.tsx index 74d0d3e9de6a9..481016af72a36 100644 --- a/src/plugins/controls/public/__stories__/controls.stories.tsx +++ b/src/plugins/controls/public/__stories__/controls.stories.tsx @@ -31,7 +31,7 @@ import { decorators } from './decorators'; import { ControlsPanels } from '../control_group/types'; import { ControlGroupContainer } from '../control_group'; import { pluginServices, registry } from '../services/storybook'; -import { replaceValueSuggestionMethod } from '../services/storybook/data'; +import { replaceValueSuggestionMethod } from '../services/storybook/unified_search'; import { injectStorybookDataView } from '../services/storybook/data_views'; import { populateStorybookControlFactories } from './storybook_control_factories'; import { OptionsListRequest } from '../services/options_list'; diff --git a/src/plugins/controls/public/services/data.ts b/src/plugins/controls/public/services/data.ts index 74c30a1f6d70e..4e9db3b68e904 100644 --- a/src/plugins/controls/public/services/data.ts +++ b/src/plugins/controls/public/services/data.ts @@ -24,7 +24,6 @@ export interface ControlsDataService { ) => Observable<{ min?: number; max?: number }>; getDataView: DataPublicPluginStart['dataViews']['get']; getDataView$: (id: string) => Observable; - autocomplete: DataPublicPluginStart['autocomplete']; query: DataPublicPluginStart['query']; searchSource: DataPublicPluginStart['search']['searchSource']; timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; diff --git a/src/plugins/controls/public/services/index.ts b/src/plugins/controls/public/services/index.ts index cf358dac8b48b..ac2e8374ad06b 100644 --- a/src/plugins/controls/public/services/index.ts +++ b/src/plugins/controls/public/services/index.ts @@ -12,6 +12,7 @@ import { ControlsOverlaysService } from './overlays'; import { registry as stubRegistry } from './stub'; import { ControlsPluginStart } from '../types'; import { ControlsDataService } from './data'; +import { ControlsUnifiedSearchService } from './unified_search'; import { ControlsService } from './controls'; import { ControlsHTTPService } from './http'; import { ControlsOptionsListService } from './options_list'; @@ -22,6 +23,7 @@ export interface ControlsServices { dataViews: ControlsDataViewsService; overlays: ControlsOverlaysService; data: ControlsDataService; + unifiedSearch: ControlsUnifiedSearchService; http: ControlsHTTPService; settings: ControlsSettingsService; diff --git a/src/plugins/controls/public/services/kibana/data.ts b/src/plugins/controls/public/services/kibana/data.ts index 3a36b929c7cb0..29a96a98c7e76 100644 --- a/src/plugins/controls/public/services/kibana/data.ts +++ b/src/plugins/controls/public/services/kibana/data.ts @@ -43,7 +43,7 @@ const minMaxAgg = (field?: DataViewField) => { export const dataServiceFactory: DataServiceFactory = ({ startPlugins }) => { const { - data: { query: queryPlugin, search, autocomplete }, + data: { query: queryPlugin, search }, } = startPlugins; const { data } = startPlugins; @@ -95,7 +95,6 @@ export const dataServiceFactory: DataServiceFactory = ({ startPlugins }) => { from(fetchFieldRange(dataView, fieldName, input)), getDataView: data.dataViews.get, getDataView$: (id: string) => from(data.dataViews.get(id)), - autocomplete, query: queryPlugin, searchSource: search.searchSource, timefilter: queryPlugin.timefilter.timefilter, diff --git a/src/plugins/controls/public/services/kibana/index.ts b/src/plugins/controls/public/services/kibana/index.ts index bed97091bed55..1bb8f600ecf81 100644 --- a/src/plugins/controls/public/services/kibana/index.ts +++ b/src/plugins/controls/public/services/kibana/index.ts @@ -22,6 +22,7 @@ import { dataServiceFactory } from './data'; import { httpServiceFactory } from './http'; import { optionsListServiceFactory } from './options_list'; import { settingsServiceFactory } from './settings'; +import { unifiedSearchServiceFactory } from './unified_search'; export const providers: PluginServiceProviders< ControlsServices, @@ -29,6 +30,7 @@ export const providers: PluginServiceProviders< > = { http: new PluginServiceProvider(httpServiceFactory), data: new PluginServiceProvider(dataServiceFactory), + unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), overlays: new PluginServiceProvider(overlaysServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), settings: new PluginServiceProvider(settingsServiceFactory), diff --git a/src/plugins/controls/public/services/kibana/unified_search.ts b/src/plugins/controls/public/services/kibana/unified_search.ts new file mode 100644 index 0000000000000..15cb3a02ca45e --- /dev/null +++ b/src/plugins/controls/public/services/kibana/unified_search.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { ControlsUnifiedSearchService } from '../unified_search'; +import { ControlsPluginStartDeps } from '../../types'; + +export type UnifiedSearchServiceFactory = KibanaPluginServiceFactory< + ControlsUnifiedSearchService, + ControlsPluginStartDeps +>; + +export const unifiedSearchServiceFactory: UnifiedSearchServiceFactory = ({ startPlugins }) => { + const { + unifiedSearch: { autocomplete }, + } = startPlugins; + + return { + autocomplete, + }; +}; diff --git a/src/plugins/controls/public/services/storybook/data.ts b/src/plugins/controls/public/services/storybook/data.ts index 38a8d01d8c7c8..5d8cacad9b7b3 100644 --- a/src/plugins/controls/public/services/storybook/data.ts +++ b/src/plugins/controls/public/services/storybook/data.ts @@ -9,20 +9,11 @@ import { of, Observable } from 'rxjs'; import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DataViewField, DataView } from '@kbn/data-views-plugin/common'; +import { DataView } from '@kbn/data-views-plugin/common'; import { ControlsDataService } from '../data'; -let valueSuggestionMethod = ({ field, query }: { field: DataViewField; query: string }) => - Promise.resolve(['storybook', 'default', 'values']); -export const replaceValueSuggestionMethod = ( - newMethod: ({ field, query }: { field: DataViewField; query: string }) => Promise -) => (valueSuggestionMethod = newMethod); - export type DataServiceFactory = PluginServiceFactory; export const dataServiceFactory: DataServiceFactory = () => ({ - autocomplete: { - getValueSuggestions: valueSuggestionMethod, - } as unknown as DataPublicPluginStart['autocomplete'], query: {} as unknown as DataPublicPluginStart['query'], searchSource: { create: () => ({ diff --git a/src/plugins/controls/public/services/storybook/index.ts b/src/plugins/controls/public/services/storybook/index.ts index f586063e5a339..751aaeefee9c2 100644 --- a/src/plugins/controls/public/services/storybook/index.ts +++ b/src/plugins/controls/public/services/storybook/index.ts @@ -14,6 +14,7 @@ import { } from '@kbn/presentation-util-plugin/public'; import { ControlsServices } from '..'; import { dataServiceFactory } from './data'; +import { unifiedSearchServiceFactory } from './unified_search'; import { overlaysServiceFactory } from './overlays'; import { dataViewsServiceFactory } from './data_views'; import { httpServiceFactory } from '../stub/http'; @@ -28,6 +29,7 @@ export const providers: PluginServiceProviders = { dataViews: new PluginServiceProvider(dataViewsServiceFactory), http: new PluginServiceProvider(httpServiceFactory), data: new PluginServiceProvider(dataServiceFactory), + unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), overlays: new PluginServiceProvider(overlaysServiceFactory), settings: new PluginServiceProvider(settingsServiceFactory), diff --git a/src/plugins/controls/public/services/storybook/unified_search.ts b/src/plugins/controls/public/services/storybook/unified_search.ts new file mode 100644 index 0000000000000..cb7df1bd1e483 --- /dev/null +++ b/src/plugins/controls/public/services/storybook/unified_search.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { DataViewField } from '@kbn/data-views-plugin/common'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { ControlsUnifiedSearchService } from '../unified_search'; + +let valueSuggestionMethod = ({ field, query }: { field: DataViewField; query: string }) => + Promise.resolve(['storybook', 'default', 'values']); +export const replaceValueSuggestionMethod = ( + newMethod: ({ field, query }: { field: DataViewField; query: string }) => Promise +) => (valueSuggestionMethod = newMethod); + +export type UnifiedSearchServiceFactory = PluginServiceFactory; +export const unifiedSearchServiceFactory: UnifiedSearchServiceFactory = () => ({ + autocomplete: { + getValueSuggestions: valueSuggestionMethod, + } as unknown as UnifiedSearchPublicPluginStart['autocomplete'], +}); diff --git a/src/plugins/controls/public/services/stub/index.ts b/src/plugins/controls/public/services/stub/index.ts index 9b767496fa985..4cd138689452f 100644 --- a/src/plugins/controls/public/services/stub/index.ts +++ b/src/plugins/controls/public/services/stub/index.ts @@ -20,6 +20,7 @@ import { dataServiceFactory } from '../storybook/data'; import { dataViewsServiceFactory } from '../storybook/data_views'; import { optionsListServiceFactory } from '../storybook/options_list'; import { settingsServiceFactory } from '../storybook/settings'; +import { unifiedSearchServiceFactory } from '../storybook/unified_search'; export const providers: PluginServiceProviders = { http: new PluginServiceProvider(httpServiceFactory), @@ -27,7 +28,7 @@ export const providers: PluginServiceProviders = { overlays: new PluginServiceProvider(overlaysServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), settings: new PluginServiceProvider(settingsServiceFactory), - + unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), controls: new PluginServiceProvider(controlsServiceFactory), optionsList: new PluginServiceProvider(optionsListServiceFactory), }; diff --git a/src/plugins/controls/public/services/unified_search.ts b/src/plugins/controls/public/services/unified_search.ts new file mode 100644 index 0000000000000..2c69537b62d65 --- /dev/null +++ b/src/plugins/controls/public/services/unified_search.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + +export interface ControlsUnifiedSearchService { + autocomplete: UnifiedSearchPublicPluginStart['autocomplete']; +} diff --git a/src/plugins/controls/public/types.ts b/src/plugins/controls/public/types.ts index dbe83abf2bf97..d7766eb8d478b 100644 --- a/src/plugins/controls/public/types.ts +++ b/src/plugins/controls/public/types.ts @@ -17,6 +17,7 @@ import { } from '@kbn/embeddable-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { ControlInput } from '../common/types'; import { ControlsService } from './services/controls'; @@ -70,6 +71,7 @@ export interface ControlsPluginSetupDeps { } export interface ControlsPluginStartDeps { data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; embeddable: EmbeddableStart; dataViews: DataViewsPublicPluginStart; } diff --git a/src/plugins/controls/server/control_types/options_list/options_list_suggestions_route.ts b/src/plugins/controls/server/control_types/options_list/options_list_suggestions_route.ts index 70c1db510c539..7ed1ace5dbde9 100644 --- a/src/plugins/controls/server/control_types/options_list/options_list_suggestions_route.ts +++ b/src/plugins/controls/server/control_types/options_list/options_list_suggestions_route.ts @@ -14,8 +14,8 @@ import { Observable } from 'rxjs'; import { CoreSetup, ElasticsearchClient } from '@kbn/core/server'; import { getKbnServerError, reportServerError } from '@kbn/kibana-utils-plugin/server'; -import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; import { FieldSpec, getFieldSubtypeNested } from '@kbn/data-views-plugin/common'; +import { PluginSetup as UnifiedSearchPluginSetup } from '@kbn/unified-search-plugin/server'; import { OptionsListRequestBody, OptionsListResponse, @@ -23,7 +23,7 @@ import { export const setupOptionsListSuggestionsRoute = ( { http }: CoreSetup, - getAutocompleteSettings: DataPluginSetup['autocomplete']['getAutocompleteSettings'] + getAutocompleteSettings: UnifiedSearchPluginSetup['autocomplete']['getAutocompleteSettings'] ) => { const router = http.createRouter(); diff --git a/src/plugins/controls/server/plugin.ts b/src/plugins/controls/server/plugin.ts index 8e391e5f1ac87..cbe9d39234361 100644 --- a/src/plugins/controls/server/plugin.ts +++ b/src/plugins/controls/server/plugin.ts @@ -10,6 +10,7 @@ import { CoreSetup, Plugin } from '@kbn/core/server'; import { EmbeddableSetup } from '@kbn/embeddable-plugin/server'; import { PluginSetup as DataSetup } from '@kbn/data-plugin/server'; +import { PluginSetup as UnifiedSearchSetup } from '@kbn/unified-search-plugin/server'; 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'; @@ -18,10 +19,11 @@ import { timeSliderPersistableStateServiceFactory } from './control_types/time_s interface SetupDeps { embeddable: EmbeddableSetup; data: DataSetup; + unifiedSearch: UnifiedSearchSetup; } export class ControlsPlugin implements Plugin { - public setup(core: CoreSetup, { embeddable, data }: SetupDeps) { + public setup(core: CoreSetup, { embeddable, unifiedSearch }: SetupDeps) { embeddable.registerEmbeddableFactory(optionsListPersistableStateServiceFactory()); embeddable.registerEmbeddableFactory(timeSliderPersistableStateServiceFactory()); @@ -29,7 +31,7 @@ export class ControlsPlugin implements Plugin { controlGroupContainerPersistableStateServiceFactory(embeddable) ); - setupOptionsListSuggestionsRoute(core, data.autocomplete.getAutocompleteSettings); + setupOptionsListSuggestionsRoute(core, unifiedSearch.autocomplete.getAutocompleteSettings); return {}; } diff --git a/src/plugins/controls/tsconfig.json b/src/plugins/controls/tsconfig.json index 10ec84fbd32f6..5a17afc931340 100644 --- a/src/plugins/controls/tsconfig.json +++ b/src/plugins/controls/tsconfig.json @@ -23,6 +23,7 @@ { "path": "../embeddable/tsconfig.json" }, { "path": "../presentation_util/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, - { "path": "../data/tsconfig.json" } + { "path": "../data/tsconfig.json" }, + { "path": "../unified_search/tsconfig.json" } ] } diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index 2512846554106..c419683de9868 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -9,28 +9,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ - autocomplete: schema.object({ - querySuggestions: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - }), - valueSuggestions: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - tiers: schema.arrayOf( - schema.oneOf([ - schema.literal('data_content'), - schema.literal('data_hot'), - schema.literal('data_warm'), - schema.literal('data_cold'), - schema.literal('data_frozen'), - ]), - { - defaultValue: ['data_hot', 'data_warm', 'data_content', 'data_cold'], - } - ), - terminateAfter: schema.duration({ defaultValue: 100000 }), - timeout: schema.duration({ defaultValue: 1000 }), - }), - }), search: schema.object({ aggs: schema.object({ shardDelay: schema.object({ diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index e3369c2d571a6..fa7453d4c5bda 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -4,7 +4,7 @@ "server": true, "ui": true, "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews"], - "serviceFolders": ["search", "query", "autocomplete", "ui"], + "serviceFolders": ["search", "query", "ui"], "optionalPlugins": ["usageCollection", "taskManager", "security"], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaUtils", "kibanaReact", "inspector"], diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 1f2b00313ddb9..9f1bf02ec2cd4 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -95,20 +95,6 @@ export { DuplicateDataViewError, } from '../common'; -/* - * Autocomplete query suggestions: - */ - -export type { - QuerySuggestion, - QuerySuggestionGetFn, - QuerySuggestionGetFnArgs, - QuerySuggestionBasic, - QuerySuggestionField, - AutocompleteStart, -} from './autocomplete'; - -export { QuerySuggestionTypes } from './autocomplete'; /* * Search: */ @@ -273,8 +259,12 @@ export { TimeHistory, getQueryLog, mapAndFlattenFilters, + QueryService, } from './query'; +export { NowProvider } from './now_provider'; +export type { NowProviderInternalContract } from './now_provider'; + export type { QueryState, SavedQuery, @@ -288,6 +278,7 @@ export type { PersistedLog, QueryStringContract, QuerySetup, + TimefilterSetup, } from './query'; export type { AggsStart } from './search/aggs'; diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index af6fdedc3ed9f..27e365ce0cb37 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -11,27 +11,14 @@ import { createDatatableUtilitiesMock } from '../common/mocks'; import { DataPlugin, DataViewsContract } from '.'; import { searchServiceMock } from './search/mocks'; import { queryServiceMock } from './query/mocks'; -import { AutocompleteStart, AutocompleteSetup } from './autocomplete'; import { createNowProviderMock } from './now_provider/mocks'; export type Setup = jest.Mocked>; export type Start = jest.Mocked>; -const autocompleteSetupMock: jest.Mocked = { - getQuerySuggestions: jest.fn(), - getAutocompleteSettings: jest.fn(), -}; - -const autocompleteStartMock: jest.Mocked = { - getValueSuggestions: jest.fn(), - getQuerySuggestions: jest.fn(), - hasQuerySuggestions: jest.fn(), -}; - const createSetupContract = (): Setup => { const querySetupMock = queryServiceMock.createSetupContract(); return { - autocomplete: autocompleteSetupMock, search: searchServiceMock.createSetupContract(), query: querySetupMock, }; @@ -58,7 +45,6 @@ const createStartContract = (): Start => { createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), createFiltersFromRangeSelectAction: jest.fn(), }, - autocomplete: autocompleteStartMock, datatableUtilities: createDatatableUtilitiesMock(), search: searchServiceMock.createStartContract(), fieldFormats: fieldFormatsServiceMock.createStartContract(), diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index ed9ba5c00e493..175d05870b0c6 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -21,7 +21,6 @@ import type { DataSetupDependencies, DataStartDependencies, } from './types'; -import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; import { QueryService } from './query'; import { @@ -52,7 +51,6 @@ export class DataPublicPlugin DataStartDependencies > { - private readonly autocomplete: AutocompleteService; private readonly searchService: SearchService; private readonly queryService: QueryService; private readonly storage: IStorageWrapper; @@ -62,7 +60,6 @@ export class DataPublicPlugin this.searchService = new SearchService(initializerContext); this.queryService = new QueryService(); - this.autocomplete = new AutocompleteService(initializerContext); this.storage = new Storage(window.localStorage); this.nowProvider = new NowProvider(); } @@ -113,10 +110,6 @@ export class DataPublicPlugin ); return { - autocomplete: this.autocomplete.setup(core, { - timefilter: queryService.timefilter, - usageCollection, - }), search: searchService, query: queryService, }; @@ -161,7 +154,6 @@ export class DataPublicPlugin createFiltersFromValueClickAction, createFiltersFromRangeSelectAction, }, - autocomplete: this.autocomplete.start(), datatableUtilities, fieldFormats, indexPatterns: dataViews, @@ -175,7 +167,6 @@ export class DataPublicPlugin } public stop() { - this.autocomplete.clearProviders(); this.queryService.stop(); this.searchService.stop(); } diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 4f571559df8de..b596e02e7f2ac 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -16,7 +16,6 @@ import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/ import { UsageCollectionSetup, UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; import { Setup as InspectorSetup } from '@kbn/inspector-plugin/public'; import { DatatableUtilitiesService } from '../common'; -import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions'; import type { ISearchSetup, ISearchStart } from './search'; import { QuerySetup, QueryStart } from './query'; @@ -42,7 +41,6 @@ export interface DataStartDependencies { * Data plugin public Setup contract */ export interface DataPublicPluginSetup { - autocomplete: AutocompleteSetup; search: ISearchSetup; query: QuerySetup; } @@ -64,11 +62,6 @@ export interface DataPublicPluginStart { * {@link DataPublicPluginStartActions} */ actions: DataPublicPluginStartActions; - /** - * autocomplete service - * {@link AutocompleteStart} - */ - autocomplete: AutocompleteStart; /** * data views service * {@link DataViewsContract} diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 83fcd58ba9a45..b970221c43391 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -16,6 +16,8 @@ import { DataServerPlugin, DataPluginSetup, DataPluginStart } from './plugin'; export * from './deprecated'; export { getEsQueryConfig, DEFAULT_QUERY_LANGUAGE } from '../common'; +export { getRequestAbortedSignal } from './lib'; + /** * Exporters (CSV) */ @@ -68,7 +70,6 @@ import { // tabify calcAutoIntervalLessThan, } from '../common'; -import { autocompleteConfigDeprecationProvider } from './config_deprecations'; export type { ParsedInterval, @@ -121,9 +122,7 @@ export type { DataPluginSetup as PluginSetup, DataPluginStart as PluginStart }; export { DataServerPlugin as Plugin }; export const config: PluginConfigDescriptor = { - deprecations: autocompleteConfigDeprecationProvider, exposeToBrowser: { - autocomplete: true, search: true, }, schema: configSchema, diff --git a/src/plugins/data/server/mocks.ts b/src/plugins/data/server/mocks.ts index 477ccf3120323..62e1798c714a1 100644 --- a/src/plugins/data/server/mocks.ts +++ b/src/plugins/data/server/mocks.ts @@ -18,16 +18,10 @@ import { import { createIndexPatternsStartMock } from './data_views/mocks'; import { createDatatableUtilitiesMock } from './datatable_utilities/mock'; import { DataRequestHandlerContext } from './search'; -import { AutocompleteSetup } from './autocomplete'; - -const autocompleteSetupMock: jest.Mocked = { - getAutocompleteSettings: jest.fn(), -}; function createSetupContract() { return { search: createSearchSetupMock(), - autocomplete: autocompleteSetupMock, /** * @deprecated - use directly from "fieldFormats" plugin instead */ diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index dcf0baa188eaa..d1beb335dd35c 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -19,17 +19,14 @@ import { SearchService } from './search/search_service'; import { QueryService } from './query/query_service'; import { ScriptsService } from './scripts'; import { KqlTelemetryService } from './kql_telemetry'; -import { AutocompleteService } from './autocomplete'; import { getUiSettings } from './ui_settings'; import { QuerySetup } from './query'; -import { AutocompleteSetup } from './autocomplete/autocomplete_service'; interface DataEnhancements { search: SearchEnhancements; } export interface DataPluginSetup { - autocomplete: AutocompleteSetup; search: ISearchSetup; query: QuerySetup; /** @@ -81,7 +78,6 @@ export class DataServerPlugin private readonly searchService: SearchService; private readonly scriptsService: ScriptsService; private readonly kqlTelemetryService: KqlTelemetryService; - private readonly autocompleteService: AutocompleteService; private readonly queryService = new QueryService(); private readonly logger: Logger; @@ -90,7 +86,6 @@ export class DataServerPlugin this.searchService = new SearchService(initializerContext, this.logger); this.scriptsService = new ScriptsService(); this.kqlTelemetryService = new KqlTelemetryService(initializerContext); - this.autocompleteService = new AutocompleteService(initializerContext); } public setup( @@ -110,7 +105,6 @@ export class DataServerPlugin }); return { - autocomplete: this.autocompleteService.setup(core), __enhance: (enhancements: DataEnhancements) => { searchSetup.__enhance(enhancements.search); }, diff --git a/src/plugins/input_control_vis/public/plugin.ts b/src/plugins/input_control_vis/public/plugin.ts index 73158e17882f6..f1f3cf609d728 100644 --- a/src/plugins/input_control_vis/public/plugin.ts +++ b/src/plugins/input_control_vis/public/plugin.ts @@ -9,7 +9,10 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { + UnifiedSearchPublicPluginStart, + UnifiedSearchPluginSetup, +} from '@kbn/unified-search-plugin/public'; import { Plugin as ExpressionsPublicPlugin } from '@kbn/expressions-plugin/public'; import { VisualizationsSetup, VisualizationsStart } from '@kbn/visualizations-plugin/public'; import { createInputControlVisFn } from './input_control_fn'; @@ -26,6 +29,7 @@ export interface InputControlSettings { export interface InputControlVisDependencies { core: InputControlVisCoreSetup; data: DataPublicPluginSetup; + unifiedSearch: UnifiedSearchPluginSetup; getSettings: () => Promise; } @@ -34,6 +38,7 @@ export interface InputControlVisPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; data: DataPublicPluginSetup; + unifiedSearch: UnifiedSearchPluginSetup; } /** @internal */ @@ -50,15 +55,16 @@ export class InputControlVisPlugin implements Plugin { public setup( core: InputControlVisCoreSetup, - { expressions, visualizations, data }: InputControlVisPluginSetupDependencies + { expressions, visualizations, unifiedSearch, data }: InputControlVisPluginSetupDependencies ) { const visualizationDependencies: Readonly = { core, - data, + unifiedSearch, getSettings: async () => { - const { timeout, terminateAfter } = data.autocomplete.getAutocompleteSettings(); + const { timeout, terminateAfter } = unifiedSearch.autocomplete.getAutocompleteSettings(); return { autocompleteTimeout: timeout, autocompleteTerminateAfter: terminateAfter }; }, + data, }; expressions.registerFunction(createInputControlVisFn); diff --git a/src/plugins/unified_search/config.ts b/src/plugins/unified_search/config.ts index c0067771b6803..4814dddea4052 100644 --- a/src/plugins/unified_search/config.ts +++ b/src/plugins/unified_search/config.ts @@ -8,6 +8,29 @@ import { schema, TypeOf } from '@kbn/config-schema'; -export const configSchema = schema.object({}); +export const configSchema = schema.object({ + autocomplete: schema.object({ + querySuggestions: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + valueSuggestions: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + tiers: schema.arrayOf( + schema.oneOf([ + schema.literal('data_content'), + schema.literal('data_hot'), + schema.literal('data_warm'), + schema.literal('data_cold'), + schema.literal('data_frozen'), + ]), + { + defaultValue: ['data_hot', 'data_warm', 'data_content', 'data_cold'], + } + ), + terminateAfter: schema.duration({ defaultValue: 100000 }), + timeout: schema.duration({ defaultValue: 1000 }), + }), + }), +}); export type ConfigSchema = TypeOf; diff --git a/src/plugins/unified_search/kibana.json b/src/plugins/unified_search/kibana.json index 8673c6ec783bb..b947141a0c68a 100755 --- a/src/plugins/unified_search/kibana.json +++ b/src/plugins/unified_search/kibana.json @@ -7,8 +7,10 @@ "name": "Unified Search", "githubTeam": "kibana-app-services" }, - "server": false, + "server": true, "ui": true, "requiredPlugins": ["dataViews", "data", "uiActions"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "data"] + "requiredBundles": ["kibanaUtils", "kibanaReact", "data"], + "serviceFolders": ["autocomplete"], + "configPath": ["unifiedSearch"] } diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/unified_search/public/autocomplete/autocomplete_service.ts similarity index 93% rename from src/plugins/data/public/autocomplete/autocomplete_service.ts rename to src/plugins/unified_search/public/autocomplete/autocomplete_service.ts index 95db8e31eb4d2..97d128df5a782 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/unified_search/public/autocomplete/autocomplete_service.ts @@ -8,8 +8,8 @@ import { CoreSetup, PluginInitializerContext } from '@kbn/core/public'; import moment from 'moment'; +import type { TimefilterSetup } from '@kbn/data-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import type { TimefilterSetup } from '../query'; import { QuerySuggestionGetFn } from './providers/query_suggestion_provider'; import { getEmptyValueSuggestions, @@ -23,7 +23,7 @@ import { KUERY_LANGUAGE_NAME, setupKqlQuerySuggestionProvider, } from './providers/kql_query_suggestion'; -import { DataPublicPluginStart, DataStartDependencies } from '../types'; +import { UnifiedSearchPublicPluginStart, UnifiedSearchStartDependencies } from '../types'; export class AutocompleteService { autocompleteConfig: ConfigSchema['autocomplete']; @@ -50,7 +50,7 @@ export class AutocompleteService { /** @public **/ public setup( - core: CoreSetup, + core: CoreSetup, { timefilter, usageCollection, diff --git a/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts b/src/plugins/unified_search/public/autocomplete/collectors/create_usage_collector.ts similarity index 100% rename from src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts rename to src/plugins/unified_search/public/autocomplete/collectors/create_usage_collector.ts diff --git a/src/plugins/data/public/autocomplete/collectors/index.ts b/src/plugins/unified_search/public/autocomplete/collectors/index.ts similarity index 100% rename from src/plugins/data/public/autocomplete/collectors/index.ts rename to src/plugins/unified_search/public/autocomplete/collectors/index.ts diff --git a/src/plugins/data/public/autocomplete/collectors/types.ts b/src/plugins/unified_search/public/autocomplete/collectors/types.ts similarity index 100% rename from src/plugins/data/public/autocomplete/collectors/types.ts rename to src/plugins/unified_search/public/autocomplete/collectors/types.ts diff --git a/src/plugins/data/public/autocomplete/index.ts b/src/plugins/unified_search/public/autocomplete/index.ts similarity index 100% rename from src/plugins/data/public/autocomplete/index.ts rename to src/plugins/unified_search/public/autocomplete/index.ts diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/README.md b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/README.md similarity index 100% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/README.md rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/README.md diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json similarity index 100% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts similarity index 95% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts index 68b0d123f0a92..24a27bcb99fbe 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { setupGetConjunctionSuggestions } from './conjunction'; -import { QuerySuggestionGetFnArgs, KueryNode } from '../../..'; import { coreMock } from '@kbn/core/public/mocks'; +import { KueryNode } from '@kbn/data-plugin/public'; +import { setupGetConjunctionSuggestions } from './conjunction'; +import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; const mockKueryNode = (kueryNode: Partial) => kueryNode as unknown as KueryNode; diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx similarity index 69% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx index c11c54c2a6f38..ad757f5afbf9c 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx @@ -10,21 +10,21 @@ import React from 'react'; import { $Keys } from 'utility-types'; import { FormattedMessage } from '@kbn/i18n-react'; import { KqlQuerySuggestionProvider } from './types'; -import { QuerySuggestion, QuerySuggestionTypes } from '../../..'; +import { QuerySuggestion, QuerySuggestionTypes } from '../query_suggestion_provider'; const bothArgumentsText = ( ); const oneOrMoreArgumentsText = ( ); @@ -32,20 +32,20 @@ const conjunctions: Record = { and: (

{bothArgumentsText}, }} description="Full text: ' Requires both arguments to be true'. See - 'data.kueryAutocomplete.andOperatorDescription.bothArgumentsText' for 'both arguments' part." + 'unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText' for 'both arguments' part." />

), or: (

= { ), }} description="Full text: 'Requires one or more arguments to be true'. See - 'data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText' for 'one or more arguments' part." + 'unifiedSearch.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText' for 'one or more arguments' part." />

), diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts similarity index 97% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.test.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts index 7e1633a2cd5e0..4446fcf685bde 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.test.ts @@ -8,8 +8,9 @@ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; +import { indexPatterns as indexPatternsUtils, KueryNode } from '@kbn/data-plugin/public'; import { setupGetFieldSuggestions } from './field'; -import { indexPatterns as indexPatternsUtils, QuerySuggestionGetFnArgs, KueryNode } from '../../..'; +import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; import { coreMock } from '@kbn/core/public/mocks'; const mockKueryNode = (kueryNode: Partial) => kueryNode as unknown as KueryNode; diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.tsx b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx similarity index 93% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.tsx rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx index 9664353515c6b..139405f6af9f4 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/field.tsx +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/field.tsx @@ -6,15 +6,11 @@ * Side Public License, v 1. */ +import { IFieldType, indexPatterns as indexPatternsUtils } from '@kbn/data-plugin/public'; import { flatten } from 'lodash'; import { escapeKuery } from './lib/escape_kuery'; import { sortPrefixFirst } from './sort_prefix_first'; -import { - IFieldType, - indexPatterns as indexPatternsUtils, - QuerySuggestionField, - QuerySuggestionTypes, -} from '../../..'; +import { QuerySuggestionField, QuerySuggestionTypes } from '../query_suggestion_provider'; import { KqlQuerySuggestionProvider } from './types'; const keywordComparator = (first: IFieldType, second: IFieldType) => { diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/index.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/index.ts similarity index 93% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/index.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/index.ts index 6ddf518933af9..1002863fec7f4 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/index.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/index.ts @@ -14,12 +14,12 @@ import { setupGetFieldSuggestions } from './field'; import { setupGetValueSuggestions } from './value'; import { setupGetOperatorSuggestions } from './operator'; import { setupGetConjunctionSuggestions } from './conjunction'; +import { UnifiedSearchPublicPluginStart } from '../../../types'; import { QuerySuggestion, QuerySuggestionGetFnArgs, QuerySuggestionGetFn, - DataPublicPluginStart, -} from '../../..'; +} from '../query_suggestion_provider'; const cursorSymbol = '@kuery-cursor@'; @@ -29,7 +29,7 @@ const dedup = (suggestions: QuerySuggestion[]): QuerySuggestion[] => export const KUERY_LANGUAGE_NAME = 'kuery'; export const setupKqlQuerySuggestionProvider = ( - core: CoreSetup + core: CoreSetup ): QuerySuggestionGetFn => { const providers = { field: setupGetFieldSuggestions(core), diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts similarity index 100% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts similarity index 100% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts similarity index 97% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.test.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts index 6a2411cc5ccbe..a40678ad4ac16 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.test.ts @@ -9,7 +9,8 @@ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; import { setupGetOperatorSuggestions } from './operator'; -import { QuerySuggestionGetFnArgs, KueryNode } from '../../..'; +import { KueryNode } from '@kbn/data-plugin/public'; +import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; import { coreMock } from '@kbn/core/public/mocks'; const mockKueryNode = (kueryNode: Partial) => kueryNode as unknown as KueryNode; diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.tsx b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.tsx similarity index 65% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.tsx rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.tsx index f6517f67218d7..ee53d5cb3d391 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/operator.tsx +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/operator.tsx @@ -12,48 +12,48 @@ import { $Keys } from 'utility-types'; import { flatten } from 'lodash'; import { KqlQuerySuggestionProvider } from './types'; -import { QuerySuggestionTypes } from '../../..'; +import { QuerySuggestionTypes } from '../query_suggestion_provider'; const equalsText = ( ); const lessThanOrEqualToText = ( ); const greaterThanOrEqualToText = ( ); const lessThanText = ( ); const greaterThanText = ( ); const existsText = ( ); @@ -61,11 +61,11 @@ const operators = { ':': { description: ( {equalsText} }} description="Full text: 'equals some value'. See - 'data.kueryAutocomplete.equalOperatorDescription.equalsText' for 'equals' part." + 'unifiedSearch.kueryAutocomplete.equalOperatorDescription.equalsText' for 'equals' part." /> ), fieldTypes: [ @@ -84,7 +84,7 @@ const operators = { '<=': { description: ( ), fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'], @@ -100,7 +100,7 @@ const operators = { '>=': { description: ( ), fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'], @@ -116,11 +116,11 @@ const operators = { '<': { description: ( {lessThanText} }} description="Full text: 'is less than some value'. See - 'data.kueryAutocomplete.lessThanOperatorDescription.lessThanText' for 'less than' part." + 'unifiedSearch.kueryAutocomplete.lessThanOperatorDescription.lessThanText' for 'less than' part." /> ), fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'], @@ -128,13 +128,13 @@ const operators = { '>': { description: ( {greaterThanText}, }} description="Full text: 'is greater than some value'. See - 'data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText' for 'greater than' part." + 'unifiedSearch.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText' for 'greater than' part." /> ), fieldTypes: ['number', 'number_range', 'date', 'date_range', 'ip', 'ip_range'], @@ -142,11 +142,11 @@ const operators = { ': *': { description: ( {existsText} }} description="Full text: 'exists in any form'. See - 'data.kueryAutocomplete.existOperatorDescription.existsText' for 'exists' part." + 'unifiedSearch.kueryAutocomplete.existOperatorDescription.existsText' for 'exists' part." /> ), fieldTypes: undefined, diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts similarity index 100% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts similarity index 100% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/types.ts similarity index 73% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/types.ts index b39f942b1f76c..e9ca34e546f0b 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/types.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/types.ts @@ -6,10 +6,11 @@ * Side Public License, v 1. */ -import { KueryNode } from '@kbn/es-query'; import { CoreSetup } from '@kbn/core/public'; -import { DataPublicPluginStart, QuerySuggestionBasic, QuerySuggestionGetFnArgs } from '../../..'; +import { KueryNode } from '@kbn/es-query'; +import type { UnifiedSearchPublicPluginStart } from '../../../types'; +import { QuerySuggestionBasic, QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; export type KqlQuerySuggestionProvider = ( - core: CoreSetup + core: CoreSetup ) => (querySuggestionsGetFnArgs: QuerySuggestionGetFnArgs, kueryNode: KueryNode) => Promise; diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.test.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts similarity index 97% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.test.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts index 224865449a5a6..3405d26824a26 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.test.ts @@ -8,8 +8,10 @@ import { setupGetValueSuggestions } from './value'; import indexPatternResponse from './__fixtures__/index_pattern_response.json'; + import { coreMock } from '@kbn/core/public/mocks'; -import { QuerySuggestionGetFnArgs, KueryNode } from '../../..'; +import { KueryNode } from '@kbn/data-plugin/public'; +import { QuerySuggestionGetFnArgs } from '../query_suggestion_provider'; const mockKueryNode = (kueryNode: Partial) => kueryNode as unknown as KueryNode; diff --git a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.ts b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts similarity index 89% rename from src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.ts rename to src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts index b75cf9d09d27b..06b0fc9639a3c 100644 --- a/src/plugins/data/public/autocomplete/providers/kql_query_suggestion/value.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/kql_query_suggestion/value.ts @@ -8,15 +8,11 @@ import { flatten } from 'lodash'; import { CoreSetup } from '@kbn/core/public'; +import { IFieldType, IIndexPattern } from '@kbn/data-plugin/public'; import { escapeQuotes } from './lib/escape_kuery'; import { KqlQuerySuggestionProvider } from './types'; -import { - DataPublicPluginStart, - IFieldType, - IIndexPattern, - QuerySuggestion, - QuerySuggestionTypes, -} from '../../..'; +import type { UnifiedSearchPublicPluginStart } from '../../../types'; +import { QuerySuggestion, QuerySuggestionTypes } from '../query_suggestion_provider'; const wrapAsSuggestions = (start: number, end: number, query: string, values: string[]) => values @@ -29,7 +25,7 @@ const wrapAsSuggestions = (start: number, end: number, query: string, values: st })); export const setupGetValueSuggestions: KqlQuerySuggestionProvider = ( - core: CoreSetup + core: CoreSetup ) => { const autoCompleteServicePromise = core .getStartServices() diff --git a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts b/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts similarity index 90% rename from src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts rename to src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts index 0cb229c9b41bc..056fcb716054a 100644 --- a/src/plugins/data/public/autocomplete/providers/query_suggestion_provider.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/query_suggestion_provider.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { ValueSuggestionsMethod } from '../../../common'; -import { IFieldType, IIndexPattern } from '../../../common'; +import { ValueSuggestionsMethod } from '@kbn/data-plugin/common'; +import { IFieldType, IIndexPattern } from '@kbn/data-plugin/common'; export enum QuerySuggestionTypes { Field = 'field', diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts similarity index 97% rename from src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts rename to src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts index bf80438da08a5..a17172a2b6072 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { stubIndexPattern, stubFields } from '../../stubs'; -import type { TimefilterSetup } from '../../query'; +import { IUiSettingsClient, CoreSetup } from '@kbn/core/public'; +import { stubIndexPattern, stubFields } from '@kbn/data-plugin/public/stubs'; +import type { TimefilterSetup } from '@kbn/data-plugin/public'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { setupValueSuggestionProvider } from './value_suggestion_provider'; import type { ValueSuggestionsGetFn } from './value_suggestion_provider'; -import { IUiSettingsClient, CoreSetup } from '@kbn/core/public'; -import { UI_SETTINGS } from '../../../common'; describe('FieldSuggestions', () => { let getValueSuggestions: ValueSuggestionsGetFn; diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts similarity index 95% rename from src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts rename to src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts index 422c4e02d8e49..054a243064329 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/unified_search/public/autocomplete/providers/value_suggestion_provider.ts @@ -6,12 +6,17 @@ * Side Public License, v 1. */ +import { CoreSetup } from '@kbn/core/public'; import dateMath from '@kbn/datemath'; import { buildQueryFromFilters } from '@kbn/es-query'; import { memoize } from 'lodash'; -import { CoreSetup } from '@kbn/core/public'; -import { IIndexPattern, IFieldType, UI_SETTINGS, ValueSuggestionsMethod } from '../../../common'; -import type { TimefilterSetup } from '../../query'; +import { + IIndexPattern, + IFieldType, + UI_SETTINGS, + ValueSuggestionsMethod, +} from '@kbn/data-plugin/common'; +import type { TimefilterSetup } from '@kbn/data-plugin/public'; import { AutocompleteUsageCollector } from '../collectors'; export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise; diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx index 057a730e5cc40..50acadea2a990 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_suggestor.tsx @@ -7,12 +7,13 @@ */ import React from 'react'; -import { debounce } from 'lodash'; - import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; import { IFieldType, UI_SETTINGS } from '@kbn/data-plugin/common'; import { DataView } from '@kbn/data-views-plugin/common'; import { IDataPluginServices } from '@kbn/data-plugin/public'; +import { debounce } from 'lodash'; + +import { getAutocomplete } from '../../services'; export interface PhraseSuggestorProps { kibana: KibanaReactContextValue; @@ -79,8 +80,7 @@ export class PhraseSuggestorUI extends React.Com return; } this.setState({ isLoading: true }); - - const suggestions = await this.services.data.autocomplete.getValueSuggestions({ + const suggestions = await getAutocomplete().getValueSuggestions({ indexPattern, field, query, diff --git a/src/plugins/unified_search/public/index.ts b/src/plugins/unified_search/public/index.ts index ff020ef1e8f94..1200234a793a4 100755 --- a/src/plugins/unified_search/public/index.ts +++ b/src/plugins/unified_search/public/index.ts @@ -7,19 +7,31 @@ */ import { PluginInitializerContext } from '@kbn/core/public'; import { ConfigSchema } from '../config'; -import { UnifiedSearchPublicPlugin } from './plugin'; - export type { IndexPatternSelectProps } from './index_pattern_select'; export type { QueryStringInputProps } from './query_string_input'; export { QueryStringInput } from './query_string_input'; export type { StatefulSearchBarProps, SearchBarProps } from './search_bar'; -export type { UnifiedSearchPublicPluginStart } from './types'; +export type { UnifiedSearchPublicPluginStart, UnifiedSearchPluginSetup } from './types'; export { SearchBar } from './search_bar'; export { FilterLabel, FilterItem } from './filter_bar'; export type { ApplyGlobalFilterActionContext } from './actions'; export { ACTION_GLOBAL_APPLY_FILTER } from './actions'; +/* + * Autocomplete query suggestions: + */ +export type { + QuerySuggestion, + QuerySuggestionGetFn, + QuerySuggestionGetFnArgs, + AutocompleteStart, +} from './autocomplete'; + +export { QuerySuggestionTypes } from './autocomplete'; + +import { UnifiedSearchPublicPlugin } from './plugin'; + // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/unified_search/public/mocks.ts b/src/plugins/unified_search/public/mocks.ts index eacbb72cc55ac..e119c58e89f3f 100644 --- a/src/plugins/unified_search/public/mocks.ts +++ b/src/plugins/unified_search/public/mocks.ts @@ -7,12 +7,31 @@ */ import { UnifiedSearchPublicPlugin } from './plugin'; +import { AutocompleteStart, AutocompleteSetup } from './autocomplete'; export type Setup = jest.Mocked>; export type Start = jest.Mocked>; +const autocompleteSetupMock: jest.Mocked = { + getQuerySuggestions: jest.fn(), + getAutocompleteSettings: jest.fn(), +}; + +const autocompleteStartMock: jest.Mocked = { + getValueSuggestions: jest.fn(), + getQuerySuggestions: jest.fn(), + hasQuerySuggestions: jest.fn(), +}; + +const createSetupContract = (): Setup => { + return { + autocomplete: autocompleteSetupMock, + }; +}; + const createStartContract = (): Start => { return { + autocomplete: autocompleteStartMock, ui: { IndexPatternSelect: jest.fn(), SearchBar: jest.fn().mockReturnValue(null), @@ -22,4 +41,5 @@ const createStartContract = (): Start => { export const unifiedSearchPluginMock = { createStartContract, + createSetupContract, }; diff --git a/src/plugins/unified_search/public/plugin.ts b/src/plugins/unified_search/public/plugin.ts index 6064d98ba554d..93f1aaf19fae8 100755 --- a/src/plugins/unified_search/public/plugin.ts +++ b/src/plugins/unified_search/public/plugin.ts @@ -13,11 +13,16 @@ import { Storage, IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; import { ConfigSchema } from '../config'; -import { setIndexPatterns, setTheme, setOverlays } from './services'; +import { setIndexPatterns, setTheme, setOverlays, setAutocomplete } from './services'; +import { AutocompleteService } from './autocomplete'; import { createSearchBar } from './search_bar'; import { createIndexPatternSelect } from './index_pattern_select'; -import { UnifiedSearchPluginSetup, UnifiedSearchPublicPluginStart } from './types'; -import type { UnifiedSearchStartDependencies, UnifiedSearchSetupDependencies } from './types'; +import type { + UnifiedSearchStartDependencies, + UnifiedSearchSetupDependencies, + UnifiedSearchPluginSetup, + UnifiedSearchPublicPluginStart, +} from './types'; import { createFilterAction } from './actions/apply_filter_action'; import { ACTION_GLOBAL_APPLY_FILTER } from './actions'; @@ -25,22 +30,30 @@ export class UnifiedSearchPublicPlugin implements Plugin { private readonly storage: IStorageWrapper; + private readonly autocomplete: AutocompleteService; private usageCollection: UsageCollectionSetup | undefined; constructor(initializerContext: PluginInitializerContext) { this.storage = new Storage(window.localStorage); + + this.autocomplete = new AutocompleteService(initializerContext); } public setup( - core: CoreSetup, - { uiActions, data }: UnifiedSearchSetupDependencies + core: CoreSetup, + { uiActions, data, usageCollection }: UnifiedSearchSetupDependencies ): UnifiedSearchPluginSetup { const { query } = data; uiActions.registerAction( createFilterAction(query.filterManager, query.timefilter.timefilter, core.theme) ); - return {}; + return { + autocomplete: this.autocomplete.setup(core, { + timefilter: query.timefilter, + usageCollection, + }), + }; } public start( @@ -50,6 +63,8 @@ export class UnifiedSearchPublicPlugin setTheme(core.theme); setOverlays(core.overlays); setIndexPatterns(dataViews); + const autocompleteStart = this.autocomplete.start(); + setAutocomplete(autocompleteStart); const SearchBar = createSearchBar({ core, @@ -68,8 +83,11 @@ export class UnifiedSearchPublicPlugin IndexPatternSelect: createIndexPatternSelect(dataViews), SearchBar, }, + autocomplete: autocompleteStart, }; } - public stop() {} + public stop() { + this.autocomplete.clearProviders(); + } } diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx index 373c8a55b9b6b..189f12765ad15 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx @@ -20,6 +20,9 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { I18nProvider } from '@kbn/i18n-react'; import { stubIndexPattern } from '@kbn/data-plugin/public/stubs'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import { setAutocomplete } from '../services'; +import { unifiedSearchPluginMock } from '../mocks'; + const startMock = coreMock.createStart(); const mockTimeHistory = { @@ -112,6 +115,11 @@ describe('QueryBarTopRowTopRow', () => { jest.clearAllMocks(); }); + beforeEach(() => { + const autocompleteStart = unifiedSearchPluginMock.createStartContract(); + setAutocomplete(autocompleteStart.autocomplete); + }); + it('Should render query and time picker', () => { const { getByText, getByTestId } = render( wrapQueryBarTopRowInContext({ diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx index 39d63a5c888cd..b4eed13da7f58 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.test.tsx @@ -28,6 +28,9 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { stubIndexPattern } from '@kbn/data-plugin/public/stubs'; import { KibanaContextProvider, withKibana } from '@kbn/kibana-react-plugin/public'; +import { setAutocomplete } from '../services'; +import { unifiedSearchPluginMock } from '../mocks'; + jest.useFakeTimers(); const startMock = coreMock.createStart(); @@ -92,6 +95,11 @@ describe('QueryStringInput', () => { jest.clearAllMocks(); }); + beforeEach(() => { + const autocompleteStart = unifiedSearchPluginMock.createStartContract(); + setAutocomplete(autocompleteStart.autocomplete); + }); + it('Should render the given query', async () => { const { getByText } = render( wrapQueryStringInputInContext({ diff --git a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx index f4b99b8e86fb2..a9f4127809ab7 100644 --- a/src/plugins/unified_search/public/query_string_input/query_string_input.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_string_input.tsx @@ -28,13 +28,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { compact, debounce, isEqual, isFunction } from 'lodash'; import { Toast } from '@kbn/core/public'; -import { - IDataPluginServices, - Query, - QuerySuggestion, - QuerySuggestionTypes, - getQueryLog, -} from '@kbn/data-plugin/public'; +import { IDataPluginServices, Query, getQueryLog } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; import type { PersistedLog } from '@kbn/data-plugin/public'; import { getFieldSubtypeNested, KIBANA_USER_QUERY_LANGUAGE_KEY } from '@kbn/data-plugin/common'; @@ -47,7 +41,8 @@ import { QueryLanguageSwitcher } from './language_switcher'; import type { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '../typeahead'; import { onRaf } from '../utils'; -import { getTheme } from '../services'; +import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete'; +import { getTheme, getAutocomplete } from '../services'; export interface QueryStringInputProps { indexPatterns: Array; @@ -201,7 +196,7 @@ export default class QueryStringInputUI extends PureComponent { const queryString = this.getQueryString(); const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString); - const hasQuerySuggestions = this.services.data.autocomplete.hasQuerySuggestions(language); + const hasQuerySuggestions = getAutocomplete().hasQuerySuggestions(language); if ( !hasQuerySuggestions || @@ -222,7 +217,7 @@ export default class QueryStringInputUI extends PureComponent { if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); const suggestions = - (await this.services.data.autocomplete.getQuerySuggestions({ + (await getAutocomplete().getQuerySuggestions({ language, indexPatterns, query: queryString, diff --git a/src/plugins/unified_search/public/services.ts b/src/plugins/unified_search/public/services.ts index 4f8937baf8fdf..f67801dd37730 100644 --- a/src/plugins/unified_search/public/services.ts +++ b/src/plugins/unified_search/public/services.ts @@ -9,6 +9,7 @@ import { ThemeServiceStart, OverlayStart } from '@kbn/core/public'; import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { AutocompleteStart } from '.'; export const [getIndexPatterns, setIndexPatterns] = createGetterSetter('IndexPatterns'); @@ -16,3 +17,6 @@ export const [getIndexPatterns, setIndexPatterns] = export const [getTheme, setTheme] = createGetterSetter('Theme'); export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); + +export const [getAutocomplete, setAutocomplete] = + createGetterSetter('Autocomplete'); diff --git a/src/plugins/unified_search/public/typeahead/suggestion_component.test.tsx b/src/plugins/unified_search/public/typeahead/suggestion_component.test.tsx index d32ce1db3bdf6..31c38ad94815a 100644 --- a/src/plugins/unified_search/public/typeahead/suggestion_component.test.tsx +++ b/src/plugins/unified_search/public/typeahead/suggestion_component.test.tsx @@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { QuerySuggestion, QuerySuggestionTypes } from '@kbn/data-plugin/public'; +import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete'; import { SuggestionComponent } from './suggestion_component'; const noop = () => { diff --git a/src/plugins/unified_search/public/typeahead/suggestion_component.tsx b/src/plugins/unified_search/public/typeahead/suggestion_component.tsx index 9fd4f0ddb9652..b8b3f2640c31a 100644 --- a/src/plugins/unified_search/public/typeahead/suggestion_component.tsx +++ b/src/plugins/unified_search/public/typeahead/suggestion_component.tsx @@ -9,7 +9,7 @@ import { EuiIcon } from '@elastic/eui'; import classNames from 'classnames'; import React, { useCallback } from 'react'; -import { QuerySuggestion } from '@kbn/data-plugin/public'; +import { QuerySuggestion } from '../autocomplete'; import { SuggestionOnClick, SuggestionOnMouseEnter } from './types'; function getEuiIconType(type: string) { diff --git a/src/plugins/unified_search/public/typeahead/suggestions_component.test.tsx b/src/plugins/unified_search/public/typeahead/suggestions_component.test.tsx index 7f1715ed2d706..6a21041f69530 100644 --- a/src/plugins/unified_search/public/typeahead/suggestions_component.test.tsx +++ b/src/plugins/unified_search/public/typeahead/suggestions_component.test.tsx @@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { QuerySuggestion, QuerySuggestionTypes } from '@kbn/data-plugin/public'; +import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete'; import { SuggestionComponent } from './suggestion_component'; import SuggestionsComponent from './suggestions_component'; diff --git a/src/plugins/unified_search/public/typeahead/suggestions_component.tsx b/src/plugins/unified_search/public/typeahead/suggestions_component.tsx index e9d5d51050f9d..75e446cf2d6e8 100644 --- a/src/plugins/unified_search/public/typeahead/suggestions_component.tsx +++ b/src/plugins/unified_search/public/typeahead/suggestions_component.tsx @@ -13,7 +13,7 @@ import classNames from 'classnames'; import styled from 'styled-components'; import useRafState from 'react-use/lib/useRafState'; -import { QuerySuggestion } from '@kbn/data-plugin/public'; +import { QuerySuggestion } from '../autocomplete'; import { SuggestionComponent } from './suggestion_component'; import { SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE, diff --git a/src/plugins/unified_search/public/typeahead/types.ts b/src/plugins/unified_search/public/typeahead/types.ts index 0da396ae56d73..1a58c29d028e1 100644 --- a/src/plugins/unified_search/public/typeahead/types.ts +++ b/src/plugins/unified_search/public/typeahead/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { QuerySuggestion } from '@kbn/data-plugin/public'; +import { QuerySuggestion } from '../autocomplete'; export type SuggestionOnClick = (suggestion: QuerySuggestion, index: number) => void; diff --git a/src/plugins/unified_search/public/types.ts b/src/plugins/unified_search/public/types.ts index e113fd32f54ed..29cf59f41a871 100755 --- a/src/plugins/unified_search/public/types.ts +++ b/src/plugins/unified_search/public/types.ts @@ -5,19 +5,24 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { IndexPatternSelectProps, StatefulSearchBarProps } from '.'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; +import type { IndexPatternSelectProps, StatefulSearchBarProps } from '.'; export interface UnifiedSearchSetupDependencies { uiActions: UiActionsSetup; data: DataPublicPluginStart; + usageCollection?: UsageCollectionSetup; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface UnifiedSearchPluginSetup {} +export interface UnifiedSearchPluginSetup { + autocomplete: AutocompleteSetup; +} export interface UnifiedSearchStartDependencies { dataViews: DataViewsPublicPluginStart; @@ -38,6 +43,11 @@ export interface UnifiedSearchPublicPluginStartUi { * Unified search plugin public Start contract */ export interface UnifiedSearchPublicPluginStart { + /** + * autocomplete service + * {@link AutocompleteStart} + */ + autocomplete: AutocompleteStart; /** * prewired UI components * {@link DataPublicPluginStartUi} diff --git a/src/plugins/data/server/autocomplete/autocomplete_service.ts b/src/plugins/unified_search/server/autocomplete/autocomplete_service.ts similarity index 100% rename from src/plugins/data/server/autocomplete/autocomplete_service.ts rename to src/plugins/unified_search/server/autocomplete/autocomplete_service.ts diff --git a/src/plugins/data/server/autocomplete/index.ts b/src/plugins/unified_search/server/autocomplete/index.ts similarity index 100% rename from src/plugins/data/server/autocomplete/index.ts rename to src/plugins/unified_search/server/autocomplete/index.ts diff --git a/src/plugins/data/server/autocomplete/routes.ts b/src/plugins/unified_search/server/autocomplete/routes.ts similarity index 100% rename from src/plugins/data/server/autocomplete/routes.ts rename to src/plugins/unified_search/server/autocomplete/routes.ts diff --git a/src/plugins/data/server/autocomplete/terms_agg.test.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.test.ts similarity index 100% rename from src/plugins/data/server/autocomplete/terms_agg.test.ts rename to src/plugins/unified_search/server/autocomplete/terms_agg.test.ts diff --git a/src/plugins/data/server/autocomplete/terms_agg.ts b/src/plugins/unified_search/server/autocomplete/terms_agg.ts similarity index 97% rename from src/plugins/data/server/autocomplete/terms_agg.ts rename to src/plugins/unified_search/server/autocomplete/terms_agg.ts index 7e48bc6ee21c3..ffdaca8caad4b 100644 --- a/src/plugins/data/server/autocomplete/terms_agg.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_agg.ts @@ -9,8 +9,8 @@ import { get, map } from 'lodash'; import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IFieldType, getFieldSubtypeNested } from '@kbn/data-plugin/common'; import { ConfigSchema } from '../../config'; -import { IFieldType, getFieldSubtypeNested } from '../../common'; import { findIndexPatternById, getFieldByName } from '../data_views'; export async function termsAggSuggestions( diff --git a/src/plugins/data/server/autocomplete/terms_enum.test.ts b/src/plugins/unified_search/server/autocomplete/terms_enum.test.ts similarity index 100% rename from src/plugins/data/server/autocomplete/terms_enum.test.ts rename to src/plugins/unified_search/server/autocomplete/terms_enum.test.ts diff --git a/src/plugins/data/server/autocomplete/terms_enum.ts b/src/plugins/unified_search/server/autocomplete/terms_enum.ts similarity index 95% rename from src/plugins/data/server/autocomplete/terms_enum.ts rename to src/plugins/unified_search/server/autocomplete/terms_enum.ts index 77e8887f3fcb5..924b5b3a1671e 100644 --- a/src/plugins/data/server/autocomplete/terms_enum.ts +++ b/src/plugins/unified_search/server/autocomplete/terms_enum.ts @@ -8,7 +8,7 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { FieldSpec } from '../../common'; +import { IFieldType } from '@kbn/data-plugin/common'; import { findIndexPatternById, getFieldByName } from '../data_views'; import { ConfigSchema } from '../../config'; @@ -20,7 +20,7 @@ export async function termsEnumSuggestions( fieldName: string, query: string, filters?: estypes.QueryDslQueryContainer[], - field?: FieldSpec, + field?: IFieldType, abortSignal?: AbortSignal ) { const { tiers } = config.autocomplete.valueSuggestions; diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/unified_search/server/autocomplete/value_suggestions_route.ts similarity index 97% rename from src/plugins/data/server/autocomplete/value_suggestions_route.ts rename to src/plugins/unified_search/server/autocomplete/value_suggestions_route.ts index 9c6c037a86a82..013401ee6d0d7 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/unified_search/server/autocomplete/value_suggestions_route.ts @@ -9,8 +9,8 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from '@kbn/core/server'; import { firstValueFrom, Observable } from 'rxjs'; +import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; import { getKbnServerError, reportServerError } from '@kbn/kibana-utils-plugin/server'; -import { getRequestAbortedSignal } from '../lib'; import type { ConfigSchema } from '../../config'; import { termsEnumSuggestions } from './terms_enum'; import { termsAggSuggestions } from './terms_agg'; diff --git a/src/plugins/unified_search/server/config_deprecations.test.ts b/src/plugins/unified_search/server/config_deprecations.test.ts new file mode 100644 index 0000000000000..8c65acfae6291 --- /dev/null +++ b/src/plugins/unified_search/server/config_deprecations.test.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { cloneDeep } from 'lodash'; + +import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; +import { configDeprecationsMock } from '@kbn/core/server/mocks'; + +import { autocompleteConfigDeprecationProvider } from './config_deprecations'; + +const deprecationContext = configDeprecationsMock.createContext(); + +const applyConfigDeprecations = (settings: Record = {}) => { + const deprecations = autocompleteConfigDeprecationProvider(configDeprecationFactory); + const deprecationMessages: string[] = []; + const migrated = applyDeprecations( + settings, + deprecations.map((deprecation) => ({ + deprecation, + path: '', + context: deprecationContext, + })), + () => + ({ message }) => + deprecationMessages.push(message) + ); + return { + messages: deprecationMessages, + migrated: migrated.config, + }; +}; + +describe('Config Deprecations', () => { + it('does not report deprecations for default configurationc', () => { + const configFirstStep = { data: { autocomplete: { valueSuggestions: {} } } }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(configFirstStep)); + expect(migrated).toEqual(configFirstStep); + expect(messages).toHaveLength(0); + }); + + it('renames kibana.autocompleteTerminateAfter to unifiedSearch.autocomplete.valueSuggestions.terminateAfter', () => { + const config = { + kibana: { + autocompleteTerminateAfter: 123, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.kibana?.autocompleteTerminateAfter).not.toBeDefined(); + expect(migrated.unifiedSearch.autocomplete.valueSuggestions.terminateAfter).toEqual(123); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting \\"kibana.autocompleteTerminateAfter\\" has been replaced by \\"unifiedSearch.autocomplete.valueSuggestions.terminateAfter\\"", + ] + `); + }); + + it('renames kibana.autocompleteTimeout to unifiedSearch.autocomplete.valueSuggestions.timeout', () => { + const config = { + kibana: { + autocompleteTimeout: 123, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.kibana?.autocompleteTimeout).not.toBeDefined(); + expect(migrated.unifiedSearch.autocomplete.valueSuggestions.timeout).toEqual(123); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting \\"kibana.autocompleteTimeout\\" has been replaced by \\"unifiedSearch.autocomplete.valueSuggestions.timeout\\"", + ] + `); + }); + + it('renames data.autocomplete.querySuggestions.enabled to unifiedSearch.autocomplete.querySuggestions.enabled', () => { + const config = { + data: { + autocomplete: { + querySuggestions: { + enabled: false, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.data?.autocomplete.querySuggestions.enabled).not.toBeDefined(); + expect(migrated.unifiedSearch.autocomplete.querySuggestions.enabled).toEqual(false); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting \\"data.autocomplete.querySuggestions.enabled\\" has been replaced by \\"unifiedSearch.autocomplete.querySuggestions.enabled\\"", + ] + `); + }); + + it('renames data.autocomplete.valueSuggestions.enabled to unifiedSearch.autocomplete.valueSuggestions.enabled', () => { + const config = { + data: { + autocomplete: { + valueSuggestions: { + enabled: false, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.data?.autocomplete.valueSuggestions.enabled).not.toBeDefined(); + expect(migrated.unifiedSearch.autocomplete.valueSuggestions.enabled).toEqual(false); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting \\"data.autocomplete.valueSuggestions.enabled\\" has been replaced by \\"unifiedSearch.autocomplete.valueSuggestions.enabled\\"", + ] + `); + }); + + it('renames data.autocomplete.valueSuggestions.tiers to unifiedSearch.autocomplete.valueSuggestions.tiers', () => { + const config = { + data: { + autocomplete: { + valueSuggestions: { + tiers: [], + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.data?.autocomplete.valueSuggestions.tiers).not.toBeDefined(); + expect(migrated.unifiedSearch.autocomplete.valueSuggestions.tiers).toEqual([]); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Setting \\"data.autocomplete.valueSuggestions.tiers\\" has been replaced by \\"unifiedSearch.autocomplete.valueSuggestions.tiers\\"", + ] + `); + }); +}); diff --git a/src/plugins/unified_search/server/config_deprecations.ts b/src/plugins/unified_search/server/config_deprecations.ts new file mode 100644 index 0000000000000..de083caffad09 --- /dev/null +++ b/src/plugins/unified_search/server/config_deprecations.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ConfigDeprecationProvider } from '@kbn/core/server'; + +export const autocompleteConfigDeprecationProvider: ConfigDeprecationProvider = ({ + renameFromRoot, +}) => [ + renameFromRoot( + 'data.autocomplete.valueSuggestions.terminateAfter', + 'unifiedSearch.autocomplete.valueSuggestions.terminateAfter', + { level: 'warning' } + ), + renameFromRoot( + 'kibana.autocompleteTerminateAfter', + 'unifiedSearch.autocomplete.valueSuggestions.terminateAfter', + { level: 'warning' } + ), + renameFromRoot( + 'data.autocomplete.valueSuggestions.timeout', + 'unifiedSearch.autocomplete.valueSuggestions.timeout', + { + level: 'warning', + } + ), + renameFromRoot( + 'kibana.autocompleteTimeout', + 'unifiedSearch.autocomplete.valueSuggestions.timeout', + { + level: 'warning', + } + ), + renameFromRoot( + 'data.autocomplete.querySuggestions.enabled', + 'unifiedSearch.autocomplete.querySuggestions.enabled', + { + level: 'warning', + } + ), + renameFromRoot( + 'data.autocomplete.valueSuggestions.enabled', + 'unifiedSearch.autocomplete.valueSuggestions.enabled', + { + level: 'warning', + } + ), + renameFromRoot( + 'data.autocomplete.valueSuggestions.tiers', + 'unifiedSearch.autocomplete.valueSuggestions.tiers', + { + level: 'warning', + } + ), +]; diff --git a/src/plugins/unified_search/server/data_views/index.ts b/src/plugins/unified_search/server/data_views/index.ts new file mode 100644 index 0000000000000..dfe03378f0278 --- /dev/null +++ b/src/plugins/unified_search/server/data_views/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from '@kbn/data-views-plugin/server'; diff --git a/src/plugins/unified_search/server/index.ts b/src/plugins/unified_search/server/index.ts new file mode 100644 index 0000000000000..d1948f32e2fe9 --- /dev/null +++ b/src/plugins/unified_search/server/index.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; +import { ConfigSchema, configSchema } from '../config'; +import { + UnifiedSearchServerPlugin, + UnifiedSearchServerPluginSetup, + UnifiedSearchServerPluginStart, +} from './plugin'; + +import { autocompleteConfigDeprecationProvider } from './config_deprecations'; + +/** + * Static code to be shared externally + * @public + */ + +export function plugin(initializerContext: PluginInitializerContext) { + return new UnifiedSearchServerPlugin(initializerContext); +} + +export type { + UnifiedSearchServerPluginSetup as PluginSetup, + UnifiedSearchServerPluginStart as PluginStart, +}; +export { UnifiedSearchServerPlugin as Plugin }; + +export const config: PluginConfigDescriptor = { + deprecations: autocompleteConfigDeprecationProvider, + exposeToBrowser: { + autocomplete: true, + }, + schema: configSchema, +}; diff --git a/src/plugins/unified_search/server/mocks.ts b/src/plugins/unified_search/server/mocks.ts new file mode 100644 index 0000000000000..7f63abcdaae0c --- /dev/null +++ b/src/plugins/unified_search/server/mocks.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AutocompleteSetup } from './autocomplete'; + +const autocompleteSetupMock: jest.Mocked = { + getAutocompleteSettings: jest.fn(), +}; + +function createSetupContract() { + return { + autocomplete: autocompleteSetupMock, + }; +} + +export const dataPluginMock = { + createSetupContract, +}; diff --git a/src/plugins/unified_search/server/plugin.ts b/src/plugins/unified_search/server/plugin.ts new file mode 100644 index 0000000000000..6328eeba2f416 --- /dev/null +++ b/src/plugins/unified_search/server/plugin.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import { ConfigSchema } from '../config'; +import { AutocompleteService } from './autocomplete'; +import { AutocompleteSetup } from './autocomplete/autocomplete_service'; + +export interface UnifiedSearchServerPluginSetup { + autocomplete: AutocompleteSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedSearchServerPluginStart {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedSearchServerPluginSetupDependencies {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface UnifiedSearchServerPluginStartDependencies {} + +export class UnifiedSearchServerPlugin implements Plugin { + private readonly autocompleteService: AutocompleteService; + + constructor(initializerContext: PluginInitializerContext) { + this.autocompleteService = new AutocompleteService(initializerContext); + } + + public setup( + core: CoreSetup, + {}: UnifiedSearchServerPluginSetupDependencies + ) { + return { + autocomplete: this.autocompleteService.setup(core), + }; + } + + public start(core: CoreStart, {}: UnifiedSearchServerPluginStartDependencies) { + return {}; + } + + public stop() {} +} + +export { UnifiedSearchServerPlugin as Plugin }; diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json index 3ff1ba630ac27..41dc76f1305be 100644 --- a/src/plugins/unified_search/tsconfig.json +++ b/src/plugins/unified_search/tsconfig.json @@ -9,7 +9,9 @@ "include": [ "public/**/*", "config.ts", - "public/**/*.json" + "public/**/*.json", + "server/**/*", + "config.ts", ], "references": [ { "path": "../../core/tsconfig.json" }, diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index c75724f294905..5f124adc92960 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -85,11 +85,11 @@ export default function ({ getService }: PluginFunctionalProviderContext) { // Ensure that your change does not unintentionally expose any sensitive values! 'console.ui.enabled (boolean)', 'dashboard.allowByValueEmbeddables (boolean)', - 'data.autocomplete.querySuggestions.enabled (boolean)', - 'data.autocomplete.valueSuggestions.enabled (boolean)', - 'data.autocomplete.valueSuggestions.terminateAfter (duration)', - 'data.autocomplete.valueSuggestions.tiers (array)', - 'data.autocomplete.valueSuggestions.timeout (duration)', + 'unifiedSearch.autocomplete.querySuggestions.enabled (boolean)', + 'unifiedSearch.autocomplete.valueSuggestions.enabled (boolean)', + 'unifiedSearch.autocomplete.valueSuggestions.terminateAfter (duration)', + 'unifiedSearch.autocomplete.valueSuggestions.tiers (array)', + 'unifiedSearch.autocomplete.valueSuggestions.timeout (duration)', 'data.search.aggs.shardDelay.enabled (boolean)', 'enterpriseSearch.host (string)', 'home.disableWelcomeScreen (boolean)', diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 3917fd601d75e..9bb1c52b52d7c 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -16,7 +16,8 @@ "licensing", "observability", "ruleRegistry", - "triggersActionsUi" + "triggersActionsUi", + "unifiedSearch" ], "optionalPlugins": [ "actions", diff --git a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx index 93fee4bf06865..753d3b6460ea3 100644 --- a/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/kuery_bar/index.tsx @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { uniqueId } from 'lodash'; import React, { useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; +import { QuerySuggestion } from '@kbn/unified-search-plugin/public'; import { DataView } from '@kbn/data-plugin/common'; -import { esKuery, QuerySuggestion } from '@kbn/data-plugin/public'; +import { esKuery } from '@kbn/data-plugin/public'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; import { useApmParams } from '../../../hooks/use_apm_params'; @@ -55,7 +56,7 @@ export function KueryBar(props: { }); const { urlParams } = useLegacyUrlParams(); const location = useLocation(); - const { data } = useApmPluginContext().plugins; + const { unifiedSearch } = useApmPluginContext().plugins; let currentRequestCheck; @@ -103,7 +104,7 @@ export function KueryBar(props: { try { const suggestions = ( - (await data.autocomplete.getQuerySuggestions({ + (await unifiedSearch.autocomplete.getQuerySuggestions({ language: 'kuery', indexPatterns: [dataView], boolFilter: diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 591b2322af0d5..627196a9a4bdc 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { i18n } from '@kbn/i18n'; import { from } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -17,9 +18,10 @@ import { PluginInitializerContext, } from '@kbn/core/public'; import type { - DataPublicPluginSetup, DataPublicPluginStart, + DataPublicPluginSetup, } from '@kbn/data-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public'; @@ -63,6 +65,7 @@ export type ApmPluginStart = void; export interface ApmPluginSetupDeps { alerting?: AlertingPluginPublicSetup; data: DataPublicPluginSetup; + unifiedSearch: UnifiedSearchPublicPluginStart; features: FeaturesPluginSetup; home?: HomePublicPluginSetup; licensing: LicensingPluginSetup; diff --git a/x-pack/plugins/fleet/.storybook/context/stubs.tsx b/x-pack/plugins/fleet/.storybook/context/stubs.tsx index 0f4f81b58f95d..092ab680b819f 100644 --- a/x-pack/plugins/fleet/.storybook/context/stubs.tsx +++ b/x-pack/plugins/fleet/.storybook/context/stubs.tsx @@ -11,6 +11,7 @@ type Stubs = | 'licensing' | 'storage' | 'data' + | 'unifiedSearch' | 'deprecations' | 'fatalErrors' | 'navigation' @@ -23,6 +24,7 @@ export const stubbedStartServices: StubbedStartServices = { licensing: {} as FleetStartServices['licensing'], storage: {} as FleetStartServices['storage'], data: {} as FleetStartServices['data'], + unifiedSearch: {} as FleetStartServices['unifiedSearch'], deprecations: {} as FleetStartServices['deprecations'], fatalErrors: {} as FleetStartServices['fatalErrors'], navigation: {} as FleetStartServices['navigation'], diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index 66301f6b6fb9c..0a45b03803fc3 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -8,7 +8,7 @@ "server": true, "ui": true, "configPath": ["xpack", "fleet"], - "requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security"], + "requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch"], "optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry"], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaReact", "cloud", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"] diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx index 865c360a47bd9..f49d4910ab9f9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx @@ -17,7 +17,7 @@ export const DatasetFilter: React.FunctionComponent<{ selectedDatasets: string[]; onToggleDataset: (dataset: string) => void; }> = memo(({ selectedDatasets, onToggleDataset }) => { - const { data } = useStartServices(); + const { unifiedSearch } = useStartServices(); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [datasetValues, setDatasetValues] = useState([AGENT_DATASET]); @@ -29,7 +29,7 @@ export const DatasetFilter: React.FunctionComponent<{ const fetchValues = async () => { setIsLoading(true); try { - const values = await data.autocomplete.getValueSuggestions({ + const values = await unifiedSearch.autocomplete.getValueSuggestions({ indexPattern: { title: AGENT_LOG_INDEX_PATTERN, fields: [DATASET_FIELD], @@ -44,7 +44,7 @@ export const DatasetFilter: React.FunctionComponent<{ setIsLoading(false); }; fetchValues(); - }, [data.autocomplete]); + }, [unifiedSearch.autocomplete]); return ( void; }> = memo(({ selectedLevels, onToggleLevel }) => { - const { data } = useStartServices(); + const { unifiedSearch } = useStartServices(); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [levelValues, setLevelValues] = useState([]); @@ -40,7 +40,7 @@ export const LogLevelFilter: React.FunctionComponent<{ const fetchValues = async () => { setIsLoading(true); try { - const values: string[] = await data.autocomplete.getValueSuggestions({ + const values: string[] = await unifiedSearch.autocomplete.getValueSuggestions({ indexPattern: { title: AGENT_LOG_INDEX_PATTERN, fields: [LOG_LEVEL_FIELD], @@ -55,7 +55,7 @@ export const LogLevelFilter: React.FunctionComponent<{ setIsLoading(false); }; fetchValues(); - }, [data.autocomplete]); + }, [unifiedSearch.autocomplete]); const noLogsFound = (
diff --git a/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts b/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts index e86503e0216db..3f5ce4a53d1f8 100644 --- a/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts +++ b/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { licensingMock } from '@kbn/licensing-plugin/public/mocks'; import { cloudMock } from '@kbn/cloud-plugin/public/mocks'; @@ -27,6 +28,7 @@ export const createStartDepsMock = () => { return { licensing: licensingMock.createStart(), data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), navigation: navigationPluginMock.createStartContract(), customIntegrations: customIntegrationsMock.createStart(), share: sharePluginMock.createStartContract(), diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index b84562098fc76..c94f2ce139ab6 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -41,6 +41,8 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { CloudSetup } from '@kbn/cloud-plugin/public'; import type { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, @@ -92,6 +94,7 @@ export interface FleetSetupDeps { export interface FleetStartDeps { licensing: LicensingPluginStart; data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; navigation: NavigationPublicPluginStart; customIntegrations: CustomIntegrationsStart; share: SharePluginStart; diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index 3589c6c25b29a..cbb7896647712 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -59,6 +59,8 @@ function getServiceMocks() { query: { savedQueries: {}, }, + }, + unifiedSearch: { autocomplete: { hasQuerySuggestions: () => false, }, diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index 82f5516f0831e..d54b12511a05c 100644 --- a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -7,7 +7,7 @@ import { EuiFieldSearch, EuiOutsideClickDetector, EuiPanel } from '@elastic/eui'; import React from 'react'; -import { QuerySuggestion } from '@kbn/data-plugin/public'; +import { QuerySuggestion } from '@kbn/unified-search-plugin/public'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { composeStateUpdaters } from '../../utils/typed_react'; import { SuggestionItem } from './suggestion_item'; diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx index 47a5114f79173..a317ad3ecff5e 100644 --- a/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { EuiIcon } from '@elastic/eui'; -import { transparentize } from 'polished'; import React from 'react'; +import { EuiIcon } from '@elastic/eui'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import { QuerySuggestion, QuerySuggestionTypes } from '@kbn/data-plugin/public'; +import { QuerySuggestion, QuerySuggestionTypes } from '@kbn/unified-search-plugin/public'; +import { transparentize } from 'polished'; interface Props { isSelected?: boolean; diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index 5da13a4fed711..96410973f8c0e 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -7,16 +7,18 @@ import React from 'react'; import { DataViewBase } from '@kbn/es-query'; -import { QuerySuggestion, DataPublicPluginStart } from '@kbn/data-plugin/public'; import { withKibana, KibanaReactContextValue, KibanaServices, } from '@kbn/kibana-react-plugin/public'; +import { QuerySuggestion, UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { RendererFunction } from '../utils/typed_react'; interface WithKueryAutocompletionLifecycleProps { - kibana: KibanaReactContextValue<{ data: DataPublicPluginStart } & KibanaServices>; + kibana: KibanaReactContextValue< + { unifiedSearch: UnifiedSearchPublicPluginStart } & KibanaServices + >; children: RendererFunction<{ isLoadingSuggestions: boolean; loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; @@ -63,7 +65,7 @@ class WithKueryAutocompletionComponent extends React.Component< const { indexPattern } = this.props; const language = 'kuery'; const hasQuerySuggestions = - this.props.kibana.services.data?.autocomplete.hasQuerySuggestions(language); + this.props.kibana.services.unifiedSearch?.autocomplete.hasQuerySuggestions(language); if (!hasQuerySuggestions) { return; @@ -78,7 +80,7 @@ class WithKueryAutocompletionComponent extends React.Component< }); const suggestions = - (await this.props.kibana.services.data.autocomplete.getQuerySuggestions({ + (await this.props.kibana.services.unifiedSearch.autocomplete.getQuerySuggestions({ language, query: expression, selectionStart: cursorPosition, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx index ff3cbdceefb3f..0e7d4e6d41e1b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { fromKueryExpression } from '@kbn/es-query'; import React, { useEffect, useState } from 'react'; import { DataViewBase } from '@kbn/es-query'; -import { QuerySuggestion } from '@kbn/data-plugin/public'; +import { QuerySuggestion } from '@kbn/unified-search-plugin/public'; import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../../../../components/autocomplete_field'; diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 237a25ce78a9a..8ed33fb304525 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -22,7 +22,8 @@ "dataViewFieldEditor", "expressionGauge", "expressionHeatmap", - "eventAnnotation" + "eventAnnotation", + "unifiedSearch" ], "optionalPlugins": [ "expressionXY", diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index a9e37e2d53d70..2c03ad902ce81 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -93,6 +93,7 @@ export function DimensionEditor(props: DimensionEditorProps) { savedObjectsClient: props.savedObjectsClient, http: props.http, storage: props.storage, + unifiedSearch: props.unifiedSearch, }; const { fieldByOperation, operationWithoutField } = operationSupportMatrix; 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 f4d27a16f19f0..3ab3633725678 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 @@ -16,6 +16,7 @@ import { EuiSelect, EuiButtonIcon, } from '@elastic/eui'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { IndexPatternDimensionEditorComponent, @@ -208,6 +209,7 @@ describe('IndexPatternDimensionEditorPanel', () => { uiSettings: {} as IUiSettingsClient, savedObjectsClient: {} as SavedObjectsClientContract, http: {} as HttpSetup, + unifiedSearch: unifiedSearchPluginMock.createStartContract(), data: { fieldFormats: { getType: jest.fn().mockReturnValue({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index cdccdb65e70db..4f20db3004e8b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -9,6 +9,7 @@ import React, { memo, useMemo } from 'react'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types'; import { GenericIndexPatternColumn } from '../indexpattern'; import { isColumnInvalid } from '../utils'; @@ -31,6 +32,7 @@ export type IndexPatternDimensionEditorProps = layerId: string; http: HttpSetup; data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; uniqueLabel: string; dateRange: DateRange; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts index c5da32f3a7baa..412f8211844b2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { IndexPatternDimensionEditorProps } from '../dimension_panel'; import { onDrop } from './on_drop_handler'; @@ -322,6 +323,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }), } as unknown as DataPublicPluginStart['fieldFormats'], } as unknown as DataPublicPluginStart, + unifiedSearch: {} as UnifiedSearchPublicPluginStart, core: {} as CoreSetup, dimensionGroups: [], isFullscreen: false, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index 1cfea4b6c32e0..804cbde3d170f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -10,6 +10,7 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { EuiComboBox } from '@elastic/eui'; import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -54,6 +55,7 @@ describe('reference editor', () => { savedObjectsClient: {} as SavedObjectsClientContract, http: {} as HttpSetup, data: {} as DataPublicPluginStart, + unifiedSearch: {} as UnifiedSearchPublicPluginStart, dimensionGroups: [], isFullscreen: false, toggleFullscreen: jest.fn(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index 4e373859d6258..3c16d271401ad 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -15,6 +15,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, } from '@elastic/eui'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -65,6 +66,7 @@ export interface ReferenceEditorProps { savedObjectsClient: SavedObjectsClientContract; http: HttpSetup; data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; paramEditorCustomProps?: ParamEditorCustomProps; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index d5ac84568ff1d..41f2a4df9bcda 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -7,11 +7,12 @@ import type { CoreSetup } from '@kbn/core/public'; import { createStartServicesGetter, Storage } from '@kbn/kibana-utils-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { ExpressionsSetup } from '@kbn/expressions-plugin/public'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { FieldFormatsStart, FieldFormatsSetup } from '@kbn/field-formats-plugin/public'; import type { EditorFrameSetup } from '../types'; @@ -28,6 +29,7 @@ export interface IndexPatternDatasourceSetupPlugins { export interface IndexPatternDatasourceStartPlugins { data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; fieldFormats: FieldFormatsStart; dataViewFieldEditor: IndexPatternFieldEditorStart; dataViews: DataViewsPublicPluginStart; @@ -58,14 +60,17 @@ export class IndexPatternDatasource { fieldFormatsSetup.register([suffixFormatter]); } - const [coreStart, { dataViewFieldEditor, uiActions, data, fieldFormats, dataViews }] = - await core.getStartServices(); + const [ + coreStart, + { dataViewFieldEditor, uiActions, data, fieldFormats, dataViews, unifiedSearch }, + ] = await core.getStartServices(); return getIndexPatternDatasource({ core: coreStart, fieldFormats, storage: new Storage(localStorage), data, + unifiedSearch, dataViews, charts, dataViewFieldEditor, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 6ae1f0b6f7f4a..db10c420b90de 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -6,16 +6,22 @@ */ import React, { ReactElement } from 'react'; +import { SavedObjectReference } from '@kbn/core/public'; import { isFragment } from 'react-is'; -import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; -import { getIndexPatternDatasource, GenericIndexPatternColumn } from './indexpattern'; -import { DatasourcePublicAPI, Datasource, FramePublicAPI, OperationDescriptor } from '../types'; import { coreMock } from '@kbn/core/public/mocks'; +import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { Ast } from '@kbn/interpreter'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; +import { indexPatternFieldEditorPluginMock } from '@kbn/data-view-field-editor-plugin/public/mocks'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; +import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; +import { TinymathAST } from '@kbn/tinymath'; +import { getIndexPatternDatasource, GenericIndexPatternColumn } from './indexpattern'; +import { DatasourcePublicAPI, Datasource, FramePublicAPI, OperationDescriptor } from '../types'; import { getFieldByNameFactory } from './pure_helpers'; import { operationDefinitionMap, @@ -29,11 +35,6 @@ import { FiltersIndexPatternColumn, } from './operations'; import { createMockedFullReference } from './operations/mocks'; -import { indexPatternFieldEditorPluginMock } from '@kbn/data-view-field-editor-plugin/public/mocks'; -import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; -import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; -import { TinymathAST } from '@kbn/tinymath'; -import { SavedObjectReference } from '@kbn/core/public'; import { cloneDeep } from 'lodash'; import { DatatableColumn } from '@kbn/expressions-plugin'; @@ -186,6 +187,7 @@ describe('IndexPattern Data Source', () => { beforeEach(() => { indexPatternDatasource = getIndexPatternDatasource({ + unifiedSearch: unifiedSearchPluginMock.createStartContract(), storage: {} as IStorageWrapper, core: coreMock.createStart(), data: dataPluginMock.createStartContract(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index b5fc2d012c2ec..b72519c2191be 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -20,6 +20,7 @@ import { DataPublicPluginStart, ES_FIELD_TYPES } from '@kbn/data-plugin/public'; import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public'; import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { DatasourceDimensionEditorProps, DatasourceDimensionTriggerProps, @@ -115,6 +116,7 @@ export function getIndexPatternDatasource({ core, storage, data, + unifiedSearch, dataViews, fieldFormats, charts, @@ -124,6 +126,7 @@ export function getIndexPatternDatasource({ core: CoreStart; storage: IStorageWrapper; data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; charts: ChartsPluginSetup; @@ -370,6 +373,7 @@ export function getIndexPatternDatasource({ savedObjectsClient={core.savedObjects.client} http={core.http} data={data} + unifiedSearch={unifiedSearch} uniqueLabel={columnLabelMap[props.columnId]} {...props} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index cc1303a2a4f62..d4491fbba00cf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -10,6 +10,7 @@ import type { DateHistogramIndexPatternColumn } from './date_histogram'; import { dateHistogramOperation } from '.'; import { shallow } from 'enzyme'; import { EuiSwitch } from '@elastic/eui'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { UI_SETTINGS } from '@kbn/data-plugin/public'; @@ -19,6 +20,7 @@ import type { IndexPatternLayer, IndexPattern } from '../../types'; import { getFieldByNameFactory } from '../../pure_helpers'; const dataStart = dataPluginMock.createStartContract(); +const unifiedSearchStart = unifiedSearchPluginMock.createStartContract(); dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpression( (path: string) => { if (path === UI_SETTINGS.HISTOGRAM_MAX_BARS) { @@ -93,6 +95,7 @@ const defaultOptions = { toDate: 'now', }, data: dataStart, + unifiedSearch: unifiedSearchStart, http: {} as HttpSetup, indexPattern: indexPattern1, operationDefinitionMap: {}, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx index 0e35a7e96cd7d..7208965ec080c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -25,6 +26,7 @@ const defaultProps = { savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), operationDefinitionMap: {}, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx index fe579e2e5a5f5..ecc46babcfe71 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/formula_editor.tsx @@ -89,7 +89,7 @@ export function FormulaEditor({ columnId, indexPattern, operationDefinitionMap, - data, + unifiedSearch, toggleFullscreen, isFullscreen, setIsCloseable, @@ -416,7 +416,7 @@ export function FormulaEditor({ context, indexPattern, operationDefinitionMap: visibleOperationsMap, - data, + unifiedSearch, dateHistogramInterval: baseIntervalRef.current, }); } @@ -427,7 +427,7 @@ export function FormulaEditor({ context, indexPattern, operationDefinitionMap: visibleOperationsMap, - data, + unifiedSearch, dateHistogramInterval: baseIntervalRef.current, }); } @@ -444,7 +444,7 @@ export function FormulaEditor({ ), }; }, - [indexPattern, visibleOperationsMap, data, baseIntervalRef] + [indexPattern, visibleOperationsMap, unifiedSearch, baseIntervalRef] ); const provideSignatureHelp = useCallback( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts index 92014b340e412..0039f486933b9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts @@ -7,11 +7,11 @@ import { parse } from '@kbn/tinymath'; import { monaco } from '@kbn/monaco'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { createMockedIndexPattern } from '../../../../mocks'; import { GenericOperationDefinition } from '../..'; import type { IndexPatternField } from '../../../../types'; import type { OperationMetadata } from '../../../../../types'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { tinymathFunctions } from '../util'; import { getSignatureHelp, @@ -217,7 +217,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { @@ -238,7 +238,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toHaveLength(2); ['sum', 'last_value'].forEach((key) => { @@ -256,7 +256,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toEqual(['window']); }); @@ -271,7 +271,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toEqual([]); }); @@ -286,7 +286,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { @@ -307,7 +307,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toHaveLength(4 + Object.keys(tinymathFunctions).length); ['sum', 'moving_average', 'cumulative_sum', 'last_value'].forEach((key) => { @@ -328,7 +328,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toHaveLength(0); }); @@ -343,7 +343,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toEqual(['bytes', 'memory']); }); @@ -358,7 +358,7 @@ describe('math completion', () => { }, indexPattern: createMockedIndexPattern(), operationDefinitionMap, - data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), }); expect(results.list).toEqual(['bytes', 'memory']); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts index 33dc7a343be4d..a1b629be9c134 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.ts @@ -16,7 +16,10 @@ import { TinymathVariable, TinymathNamedArgument, } from '@kbn/tinymath'; -import type { DataPublicPluginStart, QuerySuggestion } from '@kbn/data-plugin/public'; +import type { + UnifiedSearchPublicPluginStart, + QuerySuggestion, +} from '@kbn/unified-search-plugin/public'; import { parseTimeShift } from '@kbn/data-plugin/common'; import { IndexPattern } from '../../../../types'; import { memoizedGetAvailableOperationsByMetadata } from '../../../operations'; @@ -117,7 +120,7 @@ export async function suggest({ context, indexPattern, operationDefinitionMap, - data, + unifiedSearch, dateHistogramInterval, }: { expression: string; @@ -125,7 +128,7 @@ export async function suggest({ context: monaco.languages.CompletionContext; indexPattern: IndexPattern; operationDefinitionMap: Record; - data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; dateHistogramInterval?: number; }): Promise { const text = @@ -145,7 +148,7 @@ export async function suggest({ if (tokenInfo?.parent && (context.triggerCharacter === '=' || isNamedArgument)) { return await getNamedArgumentSuggestions({ ast: tokenAst as TinymathNamedArgument, - data, + unifiedSearch, indexPattern, dateHistogramInterval, }); @@ -328,13 +331,13 @@ function getArgumentSuggestions( export async function getNamedArgumentSuggestions({ ast, - data, + unifiedSearch, indexPattern, dateHistogramInterval, }: { ast: TinymathNamedArgument; indexPattern: IndexPattern; - data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; dateHistogramInterval?: number; }) { if (ast.name === 'shift') { @@ -356,14 +359,14 @@ export async function getNamedArgumentSuggestions({ if (ast.name !== 'kql' && ast.name !== 'lucene') { return { list: [], type: SUGGESTION_TYPE.KQL }; } - if (!data.autocomplete.hasQuerySuggestions(ast.name === 'kql' ? 'kuery' : 'lucene')) { + if (!unifiedSearch.autocomplete.hasQuerySuggestions(ast.name === 'kql' ? 'kuery' : 'lucene')) { return { list: [], type: SUGGESTION_TYPE.KQL }; } const query = ast.value.split(MARKER)[0]; const position = ast.value.indexOf(MARKER) + 1; - const suggestions = await data.autocomplete.getQuerySuggestions({ + const suggestions = await unifiedSearch.autocomplete.getQuerySuggestions({ language: ast.name === 'kql' ? 'kuery' : 'lucene', query, selectionStart: position, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 9ac5d683b74ad..e62bab6bd808b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -14,6 +14,7 @@ import { import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { ExpressionAstFunction } from '@kbn/expressions-plugin/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { termsOperation } from './terms'; import { filtersOperation } from './filters'; import { cardinalityOperation } from './cardinality'; @@ -163,6 +164,7 @@ export interface ParamEditorProps { http: HttpSetup; dateRange: DateRange; data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; activeData?: IndexPatternDimensionEditorProps['activeData']; operationDefinitionMap: Record; paramEditorCustomProps?: ParamEditorCustomProps; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index 556c91d3fa46d..5e3f1f0043664 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; import { EuiComboBox, EuiFormRow } from '@elastic/eui'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -25,6 +26,7 @@ const defaultProps = { uiSettings: uiSettingsMock, savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, + unifiedSearch: unifiedSearchPluginMock.createStartContract(), data: dataPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index a900e4cc29ae7..831bb03c89abd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -6,17 +6,18 @@ */ import React, { ChangeEvent } from 'react'; -import { shallow, mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { EuiRange } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; +import { EuiFormRow } from '@elastic/eui'; +import { shallow, mount } from 'enzyme'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { percentileOperation } from '.'; import { IndexPattern, IndexPatternLayer } from '../../types'; import { PercentileIndexPatternColumn } from './percentile'; -import { EuiRange } from '@elastic/eui'; -import { act } from 'react-dom/test-utils'; -import { EuiFormRow } from '@elastic/eui'; import { TermsIndexPatternColumn } from './terms'; jest.mock('lodash', () => { @@ -36,6 +37,7 @@ const defaultProps = { savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: { ...createMockedIndexPattern(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index 534fcdbaf2d02..5f882a3ec2112 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -11,8 +11,9 @@ import { act } from 'react-dom/test-utils'; import { EuiFieldNumber, EuiRange, EuiButtonEmpty, EuiLink, EuiText } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; -import type { IndexPatternLayer, IndexPattern } from '../../../types'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import type { IndexPatternLayer, IndexPattern } from '../../../types'; import { rangeOperation } from '..'; import { RangeIndexPatternColumn } from './ranges'; import { @@ -51,6 +52,7 @@ jest.mock('lodash', () => { }); const dataPluginMockValue = dataPluginMock.createStartContract(); +const unifiedSearchPluginMockValue = unifiedSearchPluginMock.createStartContract(); // need to overwrite the formatter field first dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ id, params }) => { return { @@ -84,6 +86,7 @@ const defaultOptions = { toDate: 'now', }, data: dataPluginMockValue, + unifiedSearch: unifiedSearchPluginMockValue, http: {} as HttpSetup, indexPattern: { id: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx index 546e25308346a..60a871efd85cf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -6,16 +6,17 @@ */ import React from 'react'; -import { shallow, mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { EuiFieldNumber } from '@elastic/eui'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; +import { shallow, mount } from 'enzyme'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { createMockedIndexPattern } from '../../mocks'; import { staticValueOperation } from '.'; import { IndexPattern, IndexPatternLayer } from '../../types'; import { StaticValueIndexPatternColumn } from './static_value'; -import { EuiFieldNumber } from '@elastic/eui'; -import { act } from 'react-dom/test-utils'; import { TermsIndexPatternColumn } from './terms'; jest.mock('lodash', () => { @@ -35,6 +36,7 @@ const defaultProps = { savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: { ...createMockedIndexPattern(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index e028d81ad4549..8cfeb1b68e5b9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -15,6 +15,7 @@ import type { HttpSetup, CoreStart, } from '@kbn/core/public'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { createMockedIndexPattern } from '../../../mocks'; @@ -58,6 +59,7 @@ const defaultProps = { savedObjectsClient: {} as SavedObjectsClientContract, dateRange: { fromDate: 'now-1d', toDate: 'now' }, data: dataPluginMock.createStartContract(), + unifiedSearch: unifiedSearchPluginMock.createStartContract(), http: {} as HttpSetup, indexPattern: createMockedIndexPattern(), // need to provide the terms operation as some helpers use operation specific features diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index f2d82e96b98cd..b39c14cd82454 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -13,11 +13,8 @@ import type { UsageCollectionStart, } from '@kbn/usage-collection-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { - CONTEXT_MENU_TRIGGER, - EmbeddableSetup, - EmbeddableStart, -} from '@kbn/embeddable-plugin/public'; +import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; @@ -45,6 +42,7 @@ import { import { VISUALIZE_EDITOR_TRIGGER } from '@kbn/visualizations-plugin/public'; import { createStartServicesGetter } from '@kbn/kibana-utils-plugin/public'; import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { EditorFrameService as EditorFrameServiceType } from './editor_frame_service'; import type { IndexPatternDatasource as IndexPatternDatasourceType, @@ -111,6 +109,7 @@ export interface LensPluginSetupDependencies { export interface LensPluginStartDependencies { data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; dataViews: DataViewsPublicPluginStart; fieldFormats: FieldFormatsStart; expressions: ExpressionsStart; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx index ff2efccee90a6..bb345f481112a 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/builder.stories.tsx @@ -8,7 +8,7 @@ import { Story, addDecorator } from '@storybook/react'; import React from 'react'; import { HttpStart } from '@kbn/core/public'; -import type { AutocompleteStart } from '@kbn/data-plugin/public'; +import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { fields, getField } from '@kbn/data-plugin/common/mocks'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx index 6388a9e5b7053..2fbc89e719eb1 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.stories.tsx @@ -13,7 +13,7 @@ import { ListOperatorEnum as OperatorEnum, ListOperatorTypeEnum as OperatorTypeEnum, } from '@kbn/securitysolution-io-ts-list-types'; -import type { AutocompleteStart } from '@kbn/data-plugin/public'; +import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; import { fields } from '@kbn/data-plugin/common/mocks'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx index 4409bd0448ec8..d0d931387f0e4 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.test.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { ReactWrapper, mount } from 'enzyme'; import React from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { waitFor } from '@testing-library/dom'; +import { coreMock } from '@kbn/core/public/mocks'; import { doesNotExistOperator, existsOperator, @@ -24,8 +23,9 @@ import { validateFilePathInput } from '@kbn/securitysolution-utils'; import { useFindLists } from '@kbn/securitysolution-list-hooks'; import type { FieldSpec } from '@kbn/data-plugin/common'; import { fields, getField } from '@kbn/data-plugin/common/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { coreMock } from '@kbn/core/public/mocks'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { waitFor } from '@testing-library/dom'; +import { ReactWrapper, mount } from 'enzyme'; import { getFoundListSchemaMock } from '../../../../common/schemas/response/found_list_schema.mock'; @@ -35,7 +35,7 @@ jest.mock('@kbn/securitysolution-list-hooks'); jest.mock('@kbn/securitysolution-utils'); const mockKibanaHttpService = coreMock.createStart().http; -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); +const { autocomplete: autocompleteStartMock } = unifiedSearchPluginMock.createStartContract(); describe('BuilderEntryItem', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx index 3b65042866c55..a28686595053b 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx @@ -44,7 +44,7 @@ import { validateFilePathInput, } from '@kbn/securitysolution-utils'; import { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; -import type { AutocompleteStart } from '@kbn/data-plugin/public'; +import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; import { HttpStart } from '@kbn/core/public'; import { getEmptyValue } from '../../../common/empty_value'; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx index 65c1e38215984..4041c8516ee27 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { fields } from '@kbn/data-plugin/common/mocks'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { coreMock } from '@kbn/core/public/mocks'; @@ -19,7 +19,7 @@ import { getEntryMatchAnyMock } from '../../../../common/schemas/types/entry_mat import { BuilderExceptionListItemComponent } from './exception_item_renderer'; const mockKibanaHttpService = coreMock.createStart().http; -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); +const { autocomplete: autocompleteStartMock } = unifiedSearchPluginMock.createStartContract(); describe('BuilderExceptionListItemComponent', () => { const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx index a8b6850239757..d55289d016883 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx @@ -8,8 +8,8 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; +import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; import { HttpStart } from '@kbn/core/public'; -import type { AutocompleteStart } from '@kbn/data-plugin/public'; import { ExceptionListType, OsTypeArray } from '@kbn/securitysolution-io-ts-list-types'; import { BuilderEntry, diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.test.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.test.tsx index 5e86979432f60..e30c5e5cd5519 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.test.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { fields, getField } from '@kbn/data-plugin/common/mocks'; @@ -20,7 +20,7 @@ import { getEmptyValue } from '../../../common/empty_value'; import { ExceptionBuilderComponent } from './exception_items_renderer'; const mockKibanaHttpService = coreMock.createStart().http; -const { autocomplete: autocompleteStartMock } = dataPluginMock.createStartContract(); +const { autocomplete: autocompleteStartMock } = unifiedSearchPluginMock.createStartContract(); describe('ExceptionBuilderComponent', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 8a04ebed888dd..69467d7ecea8b 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -32,7 +32,7 @@ import { getNewExceptionItem, } from '@kbn/securitysolution-list-utils'; import { DataViewBase } from '@kbn/es-query'; -import type { AutocompleteStart } from '@kbn/data-plugin/public'; +import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; import { AndOrBadge } from '../and_or_badge'; diff --git a/x-pack/plugins/lists/tsconfig.json b/x-pack/plugins/lists/tsconfig.json index 691c5243d9db8..6cfffbbaa7421 100644 --- a/x-pack/plugins/lists/tsconfig.json +++ b/x-pack/plugins/lists/tsconfig.json @@ -18,5 +18,6 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../security/tsconfig.json"}, + { "path": "../../../src/plugins/unified_search/tsconfig.json" } ] } diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index ebebf4b3ba7fc..22857c623c18a 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -32,7 +32,7 @@ export const getIsCloud = () => isCloudEnabled; export const getIndexNameFormComponent = () => pluginsStart.fileUpload.IndexNameFormComponent; export const getFileUploadComponent = () => pluginsStart.fileUpload.FileUploadComponent; export const getIndexPatternService = () => pluginsStart.data.indexPatterns; -export const getAutocompleteService = () => pluginsStart.data.autocomplete; +export const getAutocompleteService = () => pluginsStart.unifiedSearch.autocomplete; export const getInspector = () => pluginsStart.inspector; export const getFileUpload = () => pluginsStart.fileUpload; export const getUiSettings = () => coreStart.uiSettings; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 4218eea4ca72f..eb00ca117f01a 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -19,7 +19,8 @@ "share", "taskManager", "triggersActionsUi", - "uiActions" + "uiActions", + "unifiedSearch" ], "optionalPlugins": [ "alerting", diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index c4ec667e43592..833a4fade128b 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -119,7 +119,7 @@ export const renderApp = ( setDependencyCache({ timefilter: deps.data.query.timefilter, fieldFormats: deps.fieldFormats, - autocomplete: deps.data.autocomplete, + autocomplete: deps.unifiedSearch.autocomplete, config: coreStart.uiSettings!, chrome: coreStart.chrome!, docLinks: coreStart.docLinks!, diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index e2269524fe6e5..3680f8b63b0c9 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -20,7 +20,7 @@ import type { ChromeRecentlyAccessed, IBasePath, } from '@kbn/core/public'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; @@ -38,7 +38,7 @@ export interface DependencyCache { theme: ThemeServiceStart | null; recentlyAccessed: ChromeRecentlyAccessed | null; fieldFormats: FieldFormatsStart | null; - autocomplete: DataPublicPluginStart['autocomplete'] | null; + autocomplete: UnifiedSearchPublicPluginStart['autocomplete'] | null; basePath: IBasePath | null; savedObjectsClient: SavedObjectsClientContract | null; application: ApplicationStart | null; diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index b45ee5f5d3956..1ef7c73d2189a 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -16,6 +16,7 @@ import type { import { BehaviorSubject } from 'rxjs'; import { take } from 'rxjs/operators'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { ManagementSetup } from '@kbn/management-plugin/public'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -50,6 +51,7 @@ import { PLUGIN_ICON_SOLUTION, PLUGIN_ID } from '../common/constants/app'; export interface MlStartDependencies { data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; share: SharePluginStart; uiActions: UiActionsStart; spaces?: SpacesPluginStart; @@ -109,6 +111,7 @@ export class MlPlugin implements Plugin { { charts: pluginsStart.charts, data: pluginsStart.data, + unifiedSearch: pluginsStart.unifiedSearch, dashboard: pluginsStart.dashboard, share: pluginsStart.share, security: pluginsSetup.security, diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 808d052546bf1..bbb6eb374e91d 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -7,7 +7,7 @@ "githubTeam": "stack-monitoring-ui" }, "configPath": ["monitoring"], - "requiredPlugins": ["licensing", "features", "data", "navigation", "observability", "dataViews"], + "requiredPlugins": ["licensing", "features", "data", "navigation", "observability", "dataViews", "unifiedSearch"], "optionalPlugins": [ "infra", "usageCollection", diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx index f241bdb798f88..ae7129f561148 100644 --- a/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/autocomplete_field.tsx @@ -7,7 +7,7 @@ import { EuiFieldSearch, EuiOutsideClickDetector, EuiPanel } from '@elastic/eui'; import React from 'react'; -import { QuerySuggestion } from '@kbn/data-plugin/public'; +import { QuerySuggestion } from '@kbn/unified-search-plugin/public'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { composeStateUpdaters } from '../../lib/typed_react'; import { SuggestionItem } from './suggestion_item'; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx index 9e5588c94a954..6810f2b2b73b9 100644 --- a/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/index.tsx @@ -9,7 +9,7 @@ import { fromKueryExpression } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { QuerySuggestion } from '@kbn/data-plugin/public'; +import { QuerySuggestion } from '@kbn/unified-search-plugin/public'; import { AutocompleteField } from './autocomplete_field'; import { WithKueryAutocompletion } from './with_kuery_autocompletion'; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx index 47a5114f79173..687ba1ebc5e63 100644 --- a/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/suggestion_item.tsx @@ -8,8 +8,8 @@ import { EuiIcon } from '@elastic/eui'; import { transparentize } from 'polished'; import React from 'react'; +import { QuerySuggestion, QuerySuggestionTypes } from '@kbn/unified-search-plugin/public'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import { QuerySuggestion, QuerySuggestionTypes } from '@kbn/data-plugin/public'; interface Props { isSelected?: boolean; diff --git a/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx b/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx index 6284b5b60e0e2..bef933d1431a0 100644 --- a/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/monitoring/public/components/kuery_bar/with_kuery_autocompletion.tsx @@ -6,8 +6,8 @@ */ import React from 'react'; -import { QuerySuggestion, DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; +import { UnifiedSearchPublicPluginStart, QuerySuggestion } from '@kbn/unified-search-plugin/public'; import { withKibana, KibanaReactContextValue, @@ -16,7 +16,9 @@ import { import { RendererFunction } from '../../lib/typed_react'; interface WithKueryAutocompletionLifecycleProps { - kibana: KibanaReactContextValue<{ data: DataPublicPluginStart } & KibanaServices>; + kibana: KibanaReactContextValue< + { unifiedSearch: UnifiedSearchPublicPluginStart } & KibanaServices + >; children: RendererFunction<{ isLoadingSuggestions: boolean; loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; @@ -63,7 +65,7 @@ class WithKueryAutocompletionComponent extends React.Component< const { indexPattern } = this.props; const language = 'kuery'; const hasQuerySuggestions = - this.props.kibana.services.data?.autocomplete.hasQuerySuggestions(language); + this.props.kibana.services.unifiedSearch?.autocomplete.hasQuerySuggestions(language); if (!hasQuerySuggestions) { return; @@ -78,7 +80,7 @@ class WithKueryAutocompletionComponent extends React.Component< }); const suggestions = - (await this.props.kibana.services.data.autocomplete.getQuerySuggestions({ + (await this.props.kibana.services.unifiedSearch.autocomplete.getQuerySuggestions({ language, query: expression, selectionStart: cursorPosition, diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index faece05732b7e..af0bd12303119 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -54,6 +54,7 @@ "kibanaReact", "usageCollection", "lists", - "ml" + "ml", + "unifiedSearch" ] } diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx index ebe77ccfce554..0e063a05bab0c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx @@ -135,7 +135,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ onRuleChange, alertStatus, }: AddExceptionFlyoutProps) { - const { http, data } = useKibana().services; + const { http, unifiedSearch } = useKibana().services; const [errorsExist, setErrorExists] = useState(false); const [comment, setComment] = useState(''); const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId); @@ -505,7 +505,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ allowLargeValueLists: !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), httpService: http, - autocompleteService: data.autocomplete, + autocompleteService: unifiedSearch.autocomplete, exceptionListItems: initialExceptionItems, listType: exceptionListType, osTypes: osTypesSelection, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx index cfa8b08b901d5..0097f2d02cb3e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx @@ -112,7 +112,7 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({ onConfirm, onRuleChange, }: EditExceptionFlyoutProps) { - const { http, data } = useKibana().services; + const { http, unifiedSearch } = useKibana().services; const [comment, setComment] = useState(''); const [errorsExist, setErrorExists] = useState(false); const { rule: maybeRule, loading: isRuleLoading } = useRuleAsync(ruleId); @@ -360,7 +360,7 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({ allowLargeValueLists: !isEqlRule(maybeRule?.type) && !isThresholdRule(maybeRule?.type), httpService: http, - autocompleteService: data.autocomplete, + autocompleteService: unifiedSearch.autocomplete, exceptionListItems: [exceptionItem], listType: exceptionListType, listId: exceptionItem.list_id, diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index f3f9d8dde9c57..2d38f72b338ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -14,6 +14,8 @@ import { TestProviders, mockIndexPattern } from '../../mock'; import { FilterManager } from '@kbn/data-plugin/public'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import { QueryBar, QueryBarComponentProps } from '.'; +import { setAutocomplete } from '@kbn/unified-search-plugin/public/services'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; @@ -267,6 +269,11 @@ describe('QueryBar ', () => { }); describe('#onSavedQueryUpdated', () => { + beforeEach(() => { + const autocompleteStart = unifiedSearchPluginMock.createStartContract(); + setAutocomplete(autocompleteStart.autocomplete); + }); + test('is only reference that changed when dataProviders props get updated', async () => { const wrapper = await getWrapper( { beforeEach(() => { (useKibana as jest.Mock).mockReturnValue({ services: { - data: { + unifiedSearch: { autocomplete: { getValueSuggestions: getValueSuggestionsMock, }, diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx index b49bb1a7fb893..571207918c44c 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.test.tsx @@ -56,7 +56,7 @@ describe('ListItemComponent', () => { beforeAll(() => { (useKibana as jest.Mock).mockReturnValue({ services: { - data: { + unifiedSearch: { autocomplete: { getValueSuggestions: getValueSuggestionsMock, }, diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 4cab4e2722ee2..0097e31cfee8a 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -10,6 +10,7 @@ import React from 'react'; import { RecursivePartial } from '@elastic/eui/src/components/common'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -95,6 +96,7 @@ export const createStartServicesMock = ( const urlService = new MockUrlService(); const locator = urlService.locators.create(new MlLocatorDefinition()); const fleet = fleetMock.createStartMock(); + const unifiedSearch = unifiedSearchPluginMock.createStartContract(); return { ...core, @@ -105,6 +107,7 @@ export const createStartServicesMock = ( getCreateCase: jest.fn(), getRecentCases: jest.fn(), }, + unifiedSearch, data: { ...data, query: { diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 6fdbffe83efcb..2e533230e0a0f 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -46,7 +46,7 @@ export interface AppContextTestRender { store: Store; history: ReturnType; coreStart: ReturnType; - depsStart: Pick; + depsStart: Pick; startServices: StartServices; middlewareSpy: MiddlewareActionSpyHelper; /** diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 4ea2ece016680..bcb368ba288ac 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -256,7 +256,7 @@ export const SeverityField = ({ { services: { http: {}, data: {}, + unifiedSearch: {}, notifications: {}, }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx index 0bd785a28eff0..11d1af0a5a2e9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form/index.tsx @@ -88,7 +88,7 @@ interface EventFiltersFormProps { } export const EventFiltersForm: React.FC = memo( ({ allowSelectOs = false, policies, arePoliciesLoading }) => { - const { http, data } = useKibana().services; + const { http, unifiedSearch } = useKibana().services; const dispatch = useDispatch>(); const exception = useEventFiltersSelector(getFormEntryStateMutable); @@ -225,7 +225,7 @@ export const EventFiltersForm: React.FC = memo( getExceptionBuilderComponentLazy({ allowLargeValueLists: false, httpService: http, - autocompleteService: data.autocomplete, + autocompleteService: unifiedSearch.autocomplete, exceptionListItems: [exception as ExceptionListItemSchema], listType: EVENT_FILTER_LIST_TYPE, listId: ENDPOINT_EVENT_FILTERS_LIST_ID, @@ -243,7 +243,7 @@ export const EventFiltersForm: React.FC = memo( operatorsList: EVENT_FILTERS_OPERATORS, osTypes: exception?.os_types, }), - [data, handleOnBuilderChange, http, indexPatterns, exception] + [unifiedSearch, handleOnBuilderChange, http, indexPatterns, exception] ); const nameInputMemo = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index cae0dda0b4bc3..b301703f3cae2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -18,6 +18,8 @@ import { FilterStateStore } from '@kbn/es-query'; import { FilterManager } from '@kbn/data-plugin/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { buildGlobalQuery } from '../helpers'; +import { setAutocomplete } from '@kbn/unified-search-plugin/public/services'; +import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { QueryBarTimeline, @@ -184,6 +186,11 @@ describe('Timeline QueryBar ', () => { }); describe('#onSavedQuery', () => { + beforeEach(() => { + const autocompleteStart = unifiedSearchPluginMock.createStartContract(); + setAutocomplete(autocompleteStart.autocomplete); + }); + test('is only reference that changed when dataProviders props get updated', async () => { const Proxy = (props: QueryBarTimelineComponentProps) => ( diff --git a/x-pack/plugins/synthetics/kibana.json b/x-pack/plugins/synthetics/kibana.json index 7eaccbbb0817f..d65a89a16161f 100644 --- a/x-pack/plugins/synthetics/kibana.json +++ b/x-pack/plugins/synthetics/kibana.json @@ -18,7 +18,8 @@ "share", "taskManager", "triggersActionsUi", - "usageCollection" + "usageCollection", + "unifiedSearch" ], "server": true, "ui": true, diff --git a/x-pack/plugins/synthetics/public/apps/plugin.ts b/x-pack/plugins/synthetics/public/apps/plugin.ts index ab2ae07e1c454..e413d52d18865 100644 --- a/x-pack/plugins/synthetics/public/apps/plugin.ts +++ b/x-pack/plugins/synthetics/public/apps/plugin.ts @@ -25,6 +25,7 @@ import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; import { FleetStart } from '@kbn/fleet-plugin/public'; @@ -58,6 +59,7 @@ export interface ClientPluginsSetup { export interface ClientPluginsStart { fleet?: FleetStart; data: DataPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; discover: DiscoverStart; inspector: InspectorPluginStart; embeddable: EmbeddableStart; diff --git a/x-pack/plugins/synthetics/public/lib/alert_types/lazy_wrapper/monitor_status.tsx b/x-pack/plugins/synthetics/public/lib/alert_types/lazy_wrapper/monitor_status.tsx index 3c8c8946fa06d..362263fa006ab 100644 --- a/x-pack/plugins/synthetics/public/lib/alert_types/lazy_wrapper/monitor_status.tsx +++ b/x-pack/plugins/synthetics/public/lib/alert_types/lazy_wrapper/monitor_status.tsx @@ -28,7 +28,7 @@ export default function MonitorStatusAlert({ core, plugins, params }: Props) { - + diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 43b888dc0378b..b92c77300e9d0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -1975,22 +1975,6 @@ "data.inspector.table.tableLabel": "Tableau {index}", "data.inspector.table.tablesDescription": "Il y a {tablesCount, plural, one {# tableau} other {# tableaux} } au total.", "data.inspector.table.tableSelectorLabel": "Sélectionné :", - "data.kueryAutocomplete.andOperatorDescription": "Nécessite que {bothArguments} soient ''vrai''.", - "data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "les deux arguments", - "data.kueryAutocomplete.equalOperatorDescription": "{equals} une certaine valeur", - "data.kueryAutocomplete.equalOperatorDescription.equalsText": "égale", - "data.kueryAutocomplete.existOperatorDescription": "{exists} sous un certain format", - "data.kueryAutocomplete.existOperatorDescription.existsText": "existe", - "data.kueryAutocomplete.greaterThanOperatorDescription": "est {greaterThan} une certaine valeur", - "data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "supérieur à", - "data.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "est {greaterThanOrEqualTo} une certaine valeur", - "data.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "supérieur ou égal à", - "data.kueryAutocomplete.lessThanOperatorDescription": "est {lessThan} une certaine valeur", - "data.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "inférieur à", - "data.kueryAutocomplete.lessThanOrEqualOperatorDescription": "est {lessThanOrEqualTo} une certaine valeur", - "data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "inférieur ou égal à", - "data.kueryAutocomplete.orOperatorDescription": "Nécessite qu’{oneOrMoreArguments} soit ''vrai''.", - "data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "au moins un argument", "data.painlessError.buttonTxt": "Modifier le script", "data.painlessError.painlessScriptedFieldErrorMessage": "Erreur d'exécution du champ d'exécution ou du champ scripté sur le modèle d'indexation {indexPatternName}", "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "Intervalle de calendrier non valide : {interval} ; la valeur doit être 1.", @@ -31048,6 +31032,137 @@ "xpack.watcher.watchActions.webhook.portIsRequiredValidationMessage": "Le port webhook est requis.", "xpack.watcher.watchActions.webhook.usernameIsRequiredIfPasswordValidationMessage": "Le nom d'utilisateur est requis.", "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.", - "xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes." + "xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes.", + "unifiedSearch.noDataPopover.content": "Cette plage temporelle ne contient pas de données. Étendez ou ajustez la plage temporelle pour obtenir plus de champs et pouvoir créer des graphiques.", + "unifiedSearch.noDataPopover.dismissAction": "Ne plus afficher", + "unifiedSearch.noDataPopover.subtitle": "Conseil", + "unifiedSearch.noDataPopover.title": "Ensemble de données vide", + "unifiedSearch.query.queryBar.clearInputLabel": "Effacer l'entrée", + "unifiedSearch.query.queryBar.comboboxAriaLabel": "Rechercher et filtrer la page {pageType}", + "unifiedSearch.query.queryBar.kqlFullLanguageName": "Langage de requête Kibana", + "unifiedSearch.query.queryBar.kqlLanguageName": "KQL", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "documents", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "Ne plus afficher", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoText": "Il semblerait que votre requête porte sur un champ imbriqué. Selon le résultat visé, il existe plusieurs façons de construire une syntaxe KQL pour des requêtes imbriquées. Apprenez-en plus avec notre {link}.", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "Syntaxe de requête imbriquée KQL", + "unifiedSearch.query.queryBar.kqlOffLabel": "Désactivé", + "unifiedSearch.query.queryBar.kqlOnLabel": "Activé", + "unifiedSearch.query.queryBar.languageSwitcher.toText": "Passer au langage de requête Kibana pour la recherche", + "unifiedSearch.query.queryBar.luceneLanguageName": "Lucene", + "unifiedSearch.query.queryBar.searchInputAriaLabel": "Commencer à taper pour rechercher et filtrer la page {pageType}", + "unifiedSearch.query.queryBar.searchInputPlaceholder": "Recherche", + "unifiedSearch.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) offre une syntaxe de requête simplifiée et la prise en charge des champs scriptés. KQL offre également une fonctionnalité de saisie semi-automatique. Si vous désactivez KQL, {nonKqlModeHelpText}.", + "unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText": "Kibana utilise Lucene.", + "unifiedSearch.search.searchBar.savedQueryDescriptionLabelText": "Description", + "unifiedSearch.search.searchBar.savedQueryDescriptionText": "Enregistrez le texte et les filtres de la requête que vous souhaitez réutiliser.", + "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "Ce nom est en conflit avec une requête enregistrée existante.", + "unifiedSearch.search.searchBar.savedQueryFormCancelButtonText": "Annuler", + "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "Enregistrer", + "unifiedSearch.search.searchBar.savedQueryFormTitle": "Enregistrer la requête", + "unifiedSearch.search.searchBar.savedQueryIncludeFiltersLabelText": "Inclure les filtres", + "unifiedSearch.search.searchBar.savedQueryIncludeTimeFilterLabelText": "Inclure le filtre temporel", + "unifiedSearch.search.searchBar.savedQueryNameHelpText": "Un nom est requis. Le nom ne peut pas contenir d'espace vide au début ou à la fin. Le nom doit être unique.", + "unifiedSearch.search.searchBar.savedQueryNameLabelText": "Nom", + "unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText": "Aucune requête enregistrée.", + "unifiedSearch.search.searchBar.savedQueryPopoverButtonText": "Voir les requêtes enregistrées", + "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "Effacer la requête enregistrée en cours", + "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText": "Effacer", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "Annuler", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "Supprimer", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "Supprimer \"{savedQueryName}\" ?", + "unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "Supprimer la requête enregistrée {savedQueryName}", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "Enregistrer en tant que nouvelle requête enregistrée", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "Enregistrer en tant que nouvelle", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "Enregistrer une nouvelle requête enregistrée", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText": "Enregistrer la requête en cours", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "Enregistrer les modifications apportées à {title}", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "Enregistrer les modifications", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "Bouton de requête enregistrée {savedQueryName}", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "Description de {savedQueryName}", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "Bouton de requête enregistrée {savedQueryName} sélectionné. Appuyez pour effacer les modifications.", + "unifiedSearch.search.searchBar.savedQueryPopoverTitleText": "Requêtes enregistrées", + "unifiedSearch.search.unableToGetSavedQueryToastTitle": "Impossible de charger la requête enregistrée {savedQueryId}", + "unifiedSearch.query.queryBar.syntaxOptionsTitle": "Options de syntaxe", + "unifiedSearch.filter.applyFilterActionTitle": "Appliquer le filtre à la vue en cours", + "unifiedSearch.filter.applyFilters.popupHeader": "Sélectionner les filtres à appliquer", + "unifiedSearch.filter.applyFiltersPopup.cancelButtonLabel": "Annuler", + "unifiedSearch.filter.applyFiltersPopup.saveButtonLabel": "Appliquer", + "unifiedSearch.filter.filterBar.addFilterButtonLabel": "Ajouter un filtre", + "unifiedSearch.filter.filterBar.deleteFilterButtonLabel": "Supprimer", + "unifiedSearch.filter.filterBar.disabledFilterPrefix": "Désactivé", + "unifiedSearch.filter.filterBar.disableFilterButtonLabel": "Désactiver temporairement", + "unifiedSearch.filter.filterBar.editFilterButtonLabel": "Modifier le filtre", + "unifiedSearch.filter.filterBar.enableFilterButtonLabel": "Réactiver", + "unifiedSearch.filter.filterBar.excludeFilterButtonLabel": "Exclure les résultats", + "unifiedSearch.filter.filterBar.filterItemBadgeAriaLabel": "Actions de filtrage", + "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "Supprimer {filter}", + "unifiedSearch.filter.filterBar.includeFilterButtonLabel": "Inclure les résultats", + "unifiedSearch.filter.filterBar.indexPatternSelectPlaceholder": "Sélectionner un modèle d'indexation", + "unifiedSearch.filter.filterBar.labelErrorInfo": "Modèle d'indexation {indexPattern} introuvable", + "unifiedSearch.filter.filterBar.labelErrorText": "Erreur", + "unifiedSearch.filter.filterBar.labelWarningInfo": "Le champ {fieldName} n'existe pas dans la vue en cours.", + "unifiedSearch.filter.filterBar.labelWarningText": "Avertissement", + "unifiedSearch.filter.filterBar.moreFilterActionsMessage": "Filtre : {innerText}. Sélectionner pour plus d’actions de filtrage.", + "unifiedSearch.filter.filterBar.negatedFilterPrefix": "NON ", + "unifiedSearch.filter.filterBar.pinFilterButtonLabel": "Épingler dans toutes les applications", + "unifiedSearch.filter.filterBar.pinnedFilterPrefix": "Épinglé", + "unifiedSearch.filter.filterBar.unpinFilterButtonLabel": "Désépingler", + "unifiedSearch.filter.filterEditor.cancelButtonLabel": "Annuler", + "unifiedSearch.filter.filterEditor.createCustomLabelInputLabel": "Étiquette personnalisée", + "unifiedSearch.filter.filterEditor.createCustomLabelSwitchLabel": "Créer une étiquette personnalisée ?", + "unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel": "n'existe pas", + "unifiedSearch.filter.filterEditor.editFilterPopupTitle": "Modifier le filtre", + "unifiedSearch.filter.filterEditor.editFilterValuesButtonLabel": "Modifier les valeurs du filtre", + "unifiedSearch.filter.filterEditor.editQueryDslButtonLabel": "Modifier en tant que Query DSL", + "unifiedSearch.filter.filterEditor.existsOperatorOptionLabel": "existe", + "unifiedSearch.filter.filterEditor.falseOptionLabel": "false", + "unifiedSearch.filter.filterEditor.fieldSelectLabel": "Champ", + "unifiedSearch.filter.filterEditor.fieldSelectPlaceholder": "Sélectionner d'abord un champ", + "unifiedSearch.filter.filterEditor.indexPatternSelectLabel": "Modèle d'indexation", + "unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel": "est entre", + "unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel": "n'est pas entre", + "unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel": "n'est pas l'une des options suivantes", + "unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel": "n'est pas", + "unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel": "est l'une des options suivantes", + "unifiedSearch.filter.filterEditor.isOperatorOptionLabel": "est", + "unifiedSearch.filter.filterEditor.operatorSelectLabel": "Opérateur", + "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderSelect": "Sélectionner", + "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderWaiting": "En attente", + "unifiedSearch.filter.filterEditor.queryDslLabel": "Query DSL d'Elasticsearch", + "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "Fin de la plage", + "unifiedSearch.filter.filterEditor.rangeInputLabel": "Plage", + "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "Début de la plage", + "unifiedSearch.filter.filterEditor.saveButtonLabel": "Enregistrer", + "unifiedSearch.filter.filterEditor.trueOptionLabel": "vrai", + "unifiedSearch.filter.filterEditor.valueInputLabel": "Valeur", + "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "Saisir une valeur", + "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "Sélectionner une valeur", + "unifiedSearch.filter.filterEditor.valuesSelectLabel": "Valeurs", + "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "Sélectionner des valeurs", + "unifiedSearch.filter.options.changeAllFiltersButtonLabel": "Changer tous les filtres", + "unifiedSearch.filter.options.deleteAllFiltersButtonLabel": "Tout supprimer", + "unifiedSearch.filter.options.disableAllFiltersButtonLabel": "Tout désactiver", + "unifiedSearch.filter.options.enableAllFiltersButtonLabel": "Tout activer", + "unifiedSearch.filter.options.invertDisabledFiltersButtonLabel": "Inverser l’activation/désactivation", + "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "Inverser l'inclusion", + "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "Tout épingler", + "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "Tout désépingler", + "unifiedSearch.filter.searchBar.changeAllFiltersTitle": "Changer tous les filtres", + "unifiedSearch.kueryAutocomplete.andOperatorDescription": "Nécessite que {bothArguments} soient ''vrai''.", + "unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "les deux arguments", + "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals} une certaine valeur", + "unifiedSearch.kueryAutocomplete.equalOperatorDescription.equalsText": "égale", + "unifiedSearch.kueryAutocomplete.existOperatorDescription": "{exists} sous un certain format", + "unifiedSearch.kueryAutocomplete.existOperatorDescription.existsText": "existe", + "unifiedSearch.kueryAutocomplete.greaterThanOperatorDescription": "est {greaterThan} une certaine valeur", + "unifiedSearch.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "supérieur à", + "unifiedSearch.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "est {greaterThanOrEqualTo} une certaine valeur", + "unifiedSearch.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "supérieur ou égal à", + "unifiedSearch.kueryAutocomplete.lessThanOperatorDescription": "est {lessThan} une certaine valeur", + "unifiedSearch.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "inférieur à", + "unifiedSearch.kueryAutocomplete.lessThanOrEqualOperatorDescription": "est {lessThanOrEqualTo} une certaine valeur", + "unifiedSearch.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "inférieur ou égal à", + "unifiedSearch.kueryAutocomplete.orOperatorDescription": "Nécessite qu’{oneOrMoreArguments} soit ''vrai''.", + "unifiedSearch.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "au moins un argument" } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2b33d03617c24..361ed7985a276 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2069,22 +2069,6 @@ "data.inspector.table.tableLabel": "テーブル{index}", "data.inspector.table.tablesDescription": "合計で{tablesCount, plural, other {# 個のテーブル} }があります", "data.inspector.table.tableSelectorLabel": "選択済み:", - "data.kueryAutocomplete.andOperatorDescription": "{bothArguments} が true であることを条件とする", - "data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "両方の引数", - "data.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", - "data.kueryAutocomplete.equalOperatorDescription.equalsText": "一致する", - "data.kueryAutocomplete.existOperatorDescription": "いずれかの形式中に{exists}", - "data.kueryAutocomplete.existOperatorDescription.existsText": "存在する", - "data.kueryAutocomplete.greaterThanOperatorDescription": "が一部の値{greaterThan}", - "data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "より大きい", - "data.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "が一部の値{greaterThanOrEqualTo}", - "data.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "よりも大きいまたは等しい", - "data.kueryAutocomplete.lessThanOperatorDescription": "が一部の値{lessThan}", - "data.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "より小さい", - "data.kueryAutocomplete.lessThanOrEqualOperatorDescription": "が一部の値{lessThanOrEqualTo}", - "data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "より小さいまたは等しい", - "data.kueryAutocomplete.orOperatorDescription": "{oneOrMoreArguments} が true であることを条件とする", - "data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "1つ以上の引数", "data.painlessError.buttonTxt": "スクリプトを編集", "data.painlessError.painlessScriptedFieldErrorMessage": "インデックスパターン{indexPatternName}でのランタイムフィールドまたはスクリプトフィールドの実行エラー", "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です", @@ -31264,6 +31248,138 @@ "xpack.watcher.watchActions.webhook.portIsRequiredValidationMessage": "Webフックポートが必要です。", "xpack.watcher.watchActions.webhook.usernameIsRequiredIfPasswordValidationMessage": "ユーザー名が必要です。", "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", - "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" + "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。", + "unifiedSearch.search.searchBar.savedQueryDescriptionLabelText": "説明", + "unifiedSearch.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", + "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "タイトルがすでに保存されているクエリに使用されています", + "unifiedSearch.search.searchBar.savedQueryFormCancelButtonText": "キャンセル", + "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "保存", + "unifiedSearch.search.searchBar.savedQueryFormTitle": "クエリを保存", + "unifiedSearch.search.searchBar.savedQueryIncludeFiltersLabelText": "フィルターを含める", + "unifiedSearch.search.searchBar.savedQueryIncludeTimeFilterLabelText": "時間フィルターを含める", + "unifiedSearch.search.searchBar.savedQueryNameHelpText": "名前は必須です。名前の始めと終わりにはスペースを使用できません。名前は一意でなければなりません。", + "unifiedSearch.search.searchBar.savedQueryNameLabelText": "名前", + "unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText": "保存されたクエリがありません。", + "unifiedSearch.search.searchBar.savedQueryPopoverButtonText": "保存されたクエリを表示", + "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "現在保存されているクエリを消去", + "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText": "クリア", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "キャンセル", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "削除", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "「{savedQueryName}」を削除しますか?", + "unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "保存されたクエリ {savedQueryName} を削除", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリを保存", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "新規保存", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "新規保存クエリを保存", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText": "現在のクエリを保存", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "{title} への変更を保存", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "変更を保存", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "保存クエリボタン {savedQueryName}", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName}の説明", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "選択されたクエリボタン {savedQueryName} を保存しました。変更を破棄するには押してください。", + "unifiedSearch.search.searchBar.savedQueryPopoverTitleText": "保存されたクエリ", + "unifiedSearch.noDataPopover.content": "この時間範囲にはデータが含まれていません。表示するフィールドを増やし、グラフを作成するには、時間範囲を広げるか、調整してください。", + "unifiedSearch.noDataPopover.dismissAction": "今後表示しない", + "unifiedSearch.noDataPopover.subtitle": "ヒント", + "unifiedSearch.noDataPopover.title": "空のデータセット", + "unifiedSearch.query.queryBar.clearInputLabel": "インプットを消去", + "unifiedSearch.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング", + "unifiedSearch.query.queryBar.kqlFullLanguageName": "Kibana クエリ言語", + "unifiedSearch.query.queryBar.kqlLanguageName": "KQL", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "ドキュメント", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "今後表示しない", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoText": "ネストされたフィールドをクエリされているようです。ネストされたクエリに対しては、ご希望の結果により異なる方法で KQL 構文を構築することができます。詳細については、{link}をご覧ください。", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "KQL ネストされたクエリ構文", + "unifiedSearch.query.queryBar.kqlOffLabel": "オフ", + "unifiedSearch.query.queryBar.kqlOnLabel": "オン", + "unifiedSearch.query.queryBar.languageSwitcher.toText": "検索用にKibana Query Languageに切り替える", + "unifiedSearch.query.queryBar.luceneLanguageName": "Lucene", + "unifiedSearch.query.queryBar.searchInputAriaLabel": "{pageType} ページの検索とフィルタリングを行うには入力を開始してください", + "unifiedSearch.query.queryBar.searchInputPlaceholder": "検索", + "unifiedSearch.query.queryBar.syntaxOptionsDescription": "{docsLink}(KQL)は、シンプルなクエリ構文とスクリプトフィールドのサポートを提供します。KQLにはオートコンプリート機能もあります。KQLをオフにする場合は、{nonKqlModeHelpText}", + "unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText": "KibanaはLuceneを使用します。", + "unifiedSearch.search.unableToGetSavedQueryToastTitle": "保存したクエリ {savedQueryId} を読み込めません", + "unifiedSearch.query.queryBar.syntaxOptionsTitle": "構文オプション", + "unifiedSearch.filter.applyFilterActionTitle": "現在のビューにフィルターを適用", + "unifiedSearch.filter.applyFilters.popupHeader": "適用するフィルターの選択", + "unifiedSearch.filter.applyFiltersPopup.cancelButtonLabel": "キャンセル", + "unifiedSearch.filter.applyFiltersPopup.saveButtonLabel": "適用", + "unifiedSearch.filter.filterBar.addFilterButtonLabel": "フィルターを追加します", + "unifiedSearch.filter.filterBar.deleteFilterButtonLabel": "削除", + "unifiedSearch.filter.filterBar.disabledFilterPrefix": "無効", + "unifiedSearch.filter.filterBar.disableFilterButtonLabel": "一時的に無効にする", + "unifiedSearch.filter.filterBar.editFilterButtonLabel": "フィルターを編集", + "unifiedSearch.filter.filterBar.enableFilterButtonLabel": "再度有効にする", + "unifiedSearch.filter.filterBar.excludeFilterButtonLabel": "結果を除外", + "unifiedSearch.filter.filterBar.filterItemBadgeAriaLabel": "フィルターアクション", + "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "{filter}を削除", + "unifiedSearch.filter.filterBar.includeFilterButtonLabel": "結果を含める", + "unifiedSearch.filter.filterBar.indexPatternSelectPlaceholder": "インデックスパターンの選択", + "unifiedSearch.filter.filterBar.labelErrorInfo": "インデックスパターン{indexPattern}が見つかりません", + "unifiedSearch.filter.filterBar.labelErrorText": "エラー", + "unifiedSearch.filter.filterBar.labelWarningInfo": "フィールド{fieldName}は現在のビューに存在しません", + "unifiedSearch.filter.filterBar.labelWarningText": "警告", + "unifiedSearch.filter.filterBar.moreFilterActionsMessage": "フィルター:{innerText}。他のフィルターアクションを使用するには選択してください。", + "unifiedSearch.filter.filterBar.negatedFilterPrefix": "NOT ", + "unifiedSearch.filter.filterBar.pinFilterButtonLabel": "すべてのアプリにピン付け", + "unifiedSearch.filter.filterBar.pinnedFilterPrefix": "ピン付け済み", + "unifiedSearch.filter.filterBar.unpinFilterButtonLabel": "ピンを外す", + "unifiedSearch.filter.filterEditor.cancelButtonLabel": "キャンセル", + "unifiedSearch.filter.filterEditor.createCustomLabelInputLabel": "カスタムラベル", + "unifiedSearch.filter.filterEditor.createCustomLabelSwitchLabel": "カスタムラベルを作成しますか?", + "unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel": "存在しない", + "unifiedSearch.filter.filterEditor.editFilterPopupTitle": "フィルターを編集", + "unifiedSearch.filter.filterEditor.editFilterValuesButtonLabel": "フィルター値を編集", + "unifiedSearch.filter.filterEditor.editQueryDslButtonLabel": "クエリ DSL として編集", + "unifiedSearch.filter.filterEditor.existsOperatorOptionLabel": "存在する", + "unifiedSearch.filter.filterEditor.falseOptionLabel": "false", + "unifiedSearch.filter.filterEditor.fieldSelectLabel": "フィールド", + "unifiedSearch.filter.filterEditor.fieldSelectPlaceholder": "フィールドを選択", + "unifiedSearch.filter.filterEditor.indexPatternSelectLabel": "インデックスパターン", + "unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel": "is between", + "unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel": "is not between", + "unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel": "is not one of", + "unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel": "is not", + "unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel": "is one of", + "unifiedSearch.filter.filterEditor.isOperatorOptionLabel": "is", + "unifiedSearch.filter.filterEditor.operatorSelectLabel": "演算子", + "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderSelect": "選択してください", + "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderWaiting": "待機中", + "unifiedSearch.filter.filterEditor.queryDslAriaLabel": "ElasticsearchクエリDSLエディター", + "unifiedSearch.filter.filterEditor.queryDslLabel": "Elasticsearch クエリ DSL", + "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "範囲の終了値", + "unifiedSearch.filter.filterEditor.rangeInputLabel": "範囲", + "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "範囲の開始値", + "unifiedSearch.filter.filterEditor.saveButtonLabel": "保存", + "unifiedSearch.filter.filterEditor.trueOptionLabel": "true", + "unifiedSearch.filter.filterEditor.valueInputLabel": "値", + "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "値を入力", + "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "値を選択", + "unifiedSearch.filter.filterEditor.valuesSelectLabel": "値", + "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "値を選択", + "unifiedSearch.filter.options.changeAllFiltersButtonLabel": "すべてのフィルターの変更", + "unifiedSearch.filter.options.deleteAllFiltersButtonLabel": "すべて削除", + "unifiedSearch.filter.options.disableAllFiltersButtonLabel": "すべて無効にする", + "unifiedSearch.filter.options.enableAllFiltersButtonLabel": "すべて有効にする", + "unifiedSearch.filter.options.invertDisabledFiltersButtonLabel": "有効・無効を反転", + "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "含める・除外を反転", + "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "すべてピン付け", + "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "すべてのピンを外す", + "unifiedSearch.filter.searchBar.changeAllFiltersTitle": "すべてのフィルターの変更", + "unifiedSearch.kueryAutocomplete.andOperatorDescription": "{bothArguments} が true であることを条件とする", + "unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "両方の引数", + "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", + "unifiedSearch.kueryAutocomplete.equalOperatorDescription.equalsText": "一致する", + "unifiedSearch.kueryAutocomplete.existOperatorDescription": "いずれかの形式中に{exists}", + "unifiedSearch.kueryAutocomplete.existOperatorDescription.existsText": "存在する", + "unifiedSearch.kueryAutocomplete.greaterThanOperatorDescription": "が一部の値{greaterThan}", + "unifiedSearch.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "より大きい", + "unifiedSearch.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "が一部の値{greaterThanOrEqualTo}", + "unifiedSearch.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "よりも大きいまたは等しい", + "unifiedSearch.kueryAutocomplete.lessThanOperatorDescription": "が一部の値{lessThan}", + "unifiedSearch.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "より小さい", + "unifiedSearch.kueryAutocomplete.lessThanOrEqualOperatorDescription": "が一部の値{lessThanOrEqualTo}", + "unifiedSearch.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "より小さいまたは等しい", + "unifiedSearch.kueryAutocomplete.orOperatorDescription": "{oneOrMoreArguments} が true であることを条件とする", + "unifiedSearch.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "1つ以上の引数" } } diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 670c4e375647b..2226836cd7669 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2076,22 +2076,6 @@ "data.inspector.table.tableLabel": "表 {index}", "data.inspector.table.tablesDescription": "总共有 {tablesCount, plural, other {# 个表} }", "data.inspector.table.tableSelectorLabel": "已选定:", - "data.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为 true", - "data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "两个参数都", - "data.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", - "data.kueryAutocomplete.equalOperatorDescription.equalsText": "等于", - "data.kueryAutocomplete.existOperatorDescription": "以任意形式{exists}", - "data.kueryAutocomplete.existOperatorDescription.existsText": "存在", - "data.kueryAutocomplete.greaterThanOperatorDescription": "{greaterThan}某一值", - "data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "大于", - "data.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "{greaterThanOrEqualTo}某一值", - "data.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "大于或等于", - "data.kueryAutocomplete.lessThanOperatorDescription": "{lessThan}某一值", - "data.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "小于", - "data.kueryAutocomplete.lessThanOrEqualOperatorDescription": "{lessThanOrEqualTo}某一值", - "data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "小于或等于", - "data.kueryAutocomplete.orOperatorDescription": "需要{oneOrMoreArguments}为 true", - "data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "一个或多个参数", "data.painlessError.buttonTxt": "编辑脚本", "data.painlessError.painlessScriptedFieldErrorMessage": "在索引模式 {indexPatternName} 上执行运行时字段或脚本字段时出错", "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1", @@ -31299,6 +31283,138 @@ "xpack.watcher.watchActions.webhook.portIsRequiredValidationMessage": "Webhook 端口必填。", "xpack.watcher.watchActions.webhook.usernameIsRequiredIfPasswordValidationMessage": "“用户名”必填。", "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", - "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" + "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。", + "unifiedSearch.search.searchBar.savedQueryDescriptionLabelText": "描述", + "unifiedSearch.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", + "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", + "unifiedSearch.search.searchBar.savedQueryFormCancelButtonText": "取消", + "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "保存", + "unifiedSearch.search.searchBar.savedQueryFormTitle": "保存查询", + "unifiedSearch.search.searchBar.savedQueryIncludeFiltersLabelText": "包括筛选", + "unifiedSearch.search.searchBar.savedQueryIncludeTimeFilterLabelText": "包括时间筛选", + "unifiedSearch.search.searchBar.savedQueryNameHelpText": "名称必填,其中不能包含前导或尾随空格,并且必须唯一。", + "unifiedSearch.search.searchBar.savedQueryNameLabelText": "名称", + "unifiedSearch.search.searchBar.savedQueryNoSavedQueriesText": "没有已保存查询。", + "unifiedSearch.search.searchBar.savedQueryPopoverButtonText": "查看已保存查询", + "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "清除当前已保存查询", + "unifiedSearch.search.searchBar.savedQueryPopoverClearButtonText": "清除", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "取消", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "删除", + "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "删除“{savedQueryName}”?", + "unifiedSearch.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "删除已保存查询 {savedQueryName}", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "另存为新的", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "保存新的已保存查询", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveButtonText": "保存当前查询", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "将更改保存到 {title}", + "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "保存更改", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "已保存查询按钮 {savedQueryName}", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} 描述", + "unifiedSearch.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "已保存查询按钮已选择 {savedQueryName}。按下可清除任何更改。", + "unifiedSearch.search.searchBar.savedQueryPopoverTitleText": "已保存查询", + "unifiedSearch.noDataPopover.content": "此时间范围不包含任何数据。增大或调整时间范围,以查看更多的字段并创建图表。", + "unifiedSearch.noDataPopover.dismissAction": "不再显示", + "unifiedSearch.noDataPopover.subtitle": "提示", + "unifiedSearch.noDataPopover.title": "空数据集", + "unifiedSearch.query.queryBar.clearInputLabel": "清除输入", + "unifiedSearch.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面", + "unifiedSearch.query.queryBar.kqlFullLanguageName": "Kibana 查询语言", + "unifiedSearch.query.queryBar.kqlLanguageName": "KQL", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoDocLinkText": "文档", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoOptOutText": "不再显示", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoText": "似乎您正在查询嵌套字段。您可以使用不同的方式构造嵌套查询的 KQL 语法,具体取决于您想要的结果。详细了解我们的 {link}。", + "unifiedSearch.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "KQL 嵌套查询语法", + "unifiedSearch.query.queryBar.kqlOffLabel": "关闭", + "unifiedSearch.query.queryBar.kqlOnLabel": "开启", + "unifiedSearch.query.queryBar.languageSwitcher.toText": "切换到 Kibana 查询语言以进行搜索", + "unifiedSearch.query.queryBar.luceneLanguageName": "Lucene", + "unifiedSearch.query.queryBar.searchInputAriaLabel": "开始键入内容,以搜索并筛选 {pageType} 页面", + "unifiedSearch.query.queryBar.searchInputPlaceholder": "搜索", + "unifiedSearch.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) 提供简化查询语法并支持脚本字段。KQL 还提供自动完成功能。如果关闭 KQL,{nonKqlModeHelpText}", + "unifiedSearch.query.queryBar.syntaxOptionsDescription.nonKqlModeHelpText": "Kibana 使用 Lucene。", + "unifiedSearch.search.unableToGetSavedQueryToastTitle": "无法加载已保存查询 {savedQueryId}", + "unifiedSearch.query.queryBar.syntaxOptionsTitle": "语法选项", + "unifiedSearch.filter.applyFilterActionTitle": "将筛选应用于当前视图", + "unifiedSearch.filter.applyFilters.popupHeader": "选择要应用的筛选", + "unifiedSearch.filter.applyFiltersPopup.cancelButtonLabel": "取消", + "unifiedSearch.filter.applyFiltersPopup.saveButtonLabel": "应用", + "unifiedSearch.filter.filterBar.addFilterButtonLabel": "添加筛选", + "unifiedSearch.filter.filterBar.deleteFilterButtonLabel": "删除", + "unifiedSearch.filter.filterBar.disabledFilterPrefix": "已禁用", + "unifiedSearch.filter.filterBar.disableFilterButtonLabel": "暂时禁用", + "unifiedSearch.filter.filterBar.editFilterButtonLabel": "编辑筛选", + "unifiedSearch.filter.filterBar.enableFilterButtonLabel": "重新启用", + "unifiedSearch.filter.filterBar.excludeFilterButtonLabel": "排除结果", + "unifiedSearch.filter.filterBar.filterItemBadgeAriaLabel": "筛选操作", + "unifiedSearch.filter.filterBar.filterItemBadgeIconAriaLabel": "删除 {filter}", + "unifiedSearch.filter.filterBar.includeFilterButtonLabel": "包括结果", + "unifiedSearch.filter.filterBar.indexPatternSelectPlaceholder": "选择索引模式", + "unifiedSearch.filter.filterBar.labelErrorInfo": "找不到索引模式 {indexPattern}", + "unifiedSearch.filter.filterBar.labelErrorText": "错误", + "unifiedSearch.filter.filterBar.labelWarningInfo": "当前视图中不存在字段 {fieldName}", + "unifiedSearch.filter.filterBar.labelWarningText": "警告", + "unifiedSearch.filter.filterBar.moreFilterActionsMessage": "筛选:{innerText}。选择以获取更多筛选操作。", + "unifiedSearch.filter.filterBar.negatedFilterPrefix": "非 ", + "unifiedSearch.filter.filterBar.pinFilterButtonLabel": "在所有应用上固定", + "unifiedSearch.filter.filterBar.pinnedFilterPrefix": "已置顶", + "unifiedSearch.filter.filterBar.unpinFilterButtonLabel": "取消固定", + "unifiedSearch.filter.filterEditor.cancelButtonLabel": "取消", + "unifiedSearch.filter.filterEditor.createCustomLabelInputLabel": "定制标签", + "unifiedSearch.filter.filterEditor.createCustomLabelSwitchLabel": "创建定制标签?", + "unifiedSearch.filter.filterEditor.doesNotExistOperatorOptionLabel": "不存在", + "unifiedSearch.filter.filterEditor.editFilterPopupTitle": "编辑筛选", + "unifiedSearch.filter.filterEditor.editFilterValuesButtonLabel": "编辑筛选值", + "unifiedSearch.filter.filterEditor.editQueryDslButtonLabel": "编辑为查询 DSL", + "unifiedSearch.filter.filterEditor.existsOperatorOptionLabel": "存在", + "unifiedSearch.filter.filterEditor.falseOptionLabel": "false", + "unifiedSearch.filter.filterEditor.fieldSelectLabel": "字段", + "unifiedSearch.filter.filterEditor.fieldSelectPlaceholder": "首先选择字段", + "unifiedSearch.filter.filterEditor.indexPatternSelectLabel": "索引模式", + "unifiedSearch.filter.filterEditor.isBetweenOperatorOptionLabel": "介于", + "unifiedSearch.filter.filterEditor.isNotBetweenOperatorOptionLabel": "不介于", + "unifiedSearch.filter.filterEditor.isNotOneOfOperatorOptionLabel": "不属于", + "unifiedSearch.filter.filterEditor.isNotOperatorOptionLabel": "不是", + "unifiedSearch.filter.filterEditor.isOneOfOperatorOptionLabel": "属于", + "unifiedSearch.filter.filterEditor.isOperatorOptionLabel": "是", + "unifiedSearch.filter.filterEditor.operatorSelectLabel": "运算符", + "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderSelect": "选择", + "unifiedSearch.filter.filterEditor.operatorSelectPlaceholderWaiting": "正在等候", + "unifiedSearch.filter.filterEditor.queryDslAriaLabel": "Elasticsearch 查询 DSL 编辑器", + "unifiedSearch.filter.filterEditor.queryDslLabel": "Elasticsearch 查询 DSL", + "unifiedSearch.filter.filterEditor.rangeEndInputPlaceholder": "范围结束", + "unifiedSearch.filter.filterEditor.rangeInputLabel": "范围", + "unifiedSearch.filter.filterEditor.rangeStartInputPlaceholder": "范围开始", + "unifiedSearch.filter.filterEditor.saveButtonLabel": "保存", + "unifiedSearch.filter.filterEditor.trueOptionLabel": "true", + "unifiedSearch.filter.filterEditor.valueInputLabel": "值", + "unifiedSearch.filter.filterEditor.valueInputPlaceholder": "输入值", + "unifiedSearch.filter.filterEditor.valueSelectPlaceholder": "选择值", + "unifiedSearch.filter.filterEditor.valuesSelectLabel": "值", + "unifiedSearch.filter.filterEditor.valuesSelectPlaceholder": "选择值", + "unifiedSearch.filter.options.changeAllFiltersButtonLabel": "更改所有筛选", + "unifiedSearch.filter.options.deleteAllFiltersButtonLabel": "全部删除", + "unifiedSearch.filter.options.disableAllFiltersButtonLabel": "全部禁用", + "unifiedSearch.filter.options.enableAllFiltersButtonLabel": "全部启用", + "unifiedSearch.filter.options.invertDisabledFiltersButtonLabel": "反向已启用/已禁用", + "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "反向包括", + "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "全部固定", + "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "全部取消固定", + "unifiedSearch.filter.searchBar.changeAllFiltersTitle": "更改所有筛选", + "unifiedSearch.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为 true", + "unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "两个参数都", + "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", + "unifiedSearch.kueryAutocomplete.equalOperatorDescription.equalsText": "等于", + "unifiedSearch.kueryAutocomplete.existOperatorDescription": "以任意形式{exists}", + "unifiedSearch.kueryAutocomplete.existOperatorDescription.existsText": "存在", + "unifiedSearch.kueryAutocomplete.greaterThanOperatorDescription": "{greaterThan}某一值", + "unifiedSearch.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "大于", + "unifiedSearch.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "{greaterThanOrEqualTo}某一值", + "unifiedSearch.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "大于或等于", + "unifiedSearch.kueryAutocomplete.lessThanOperatorDescription": "{lessThan}某一值", + "unifiedSearch.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "小于", + "unifiedSearch.kueryAutocomplete.lessThanOrEqualOperatorDescription": "{lessThanOrEqualTo}某一值", + "unifiedSearch.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "小于或等于", + "unifiedSearch.kueryAutocomplete.orOperatorDescription": "需要{oneOrMoreArguments}为 true", + "unifiedSearch.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "一个或多个参数" } }