From f3b74b457cb9b56dc922c29a5c3f496f485b40c3 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 16 Mar 2021 11:41:48 +0100 Subject: [PATCH 01/44] [ML] Transforms: Fixes missing number of transform nodes and error reporting in stats bar. (#93956) - Adds a Kibana API endpoint transforms/_nodes - Adds number of nodes to the stats bar in the transforms list. - Shows a callout when no transform nodes are available. - Disable all actions except delete when no transform nodes are available. - Disables the create button when no transform nodes are available. --- .../public/doc_links/doc_links_service.ts | 1 + .../common/api_schemas/transforms.ts | 5 ++ .../common/api_schemas/type_guards.ts | 9 +++ .../transform/public/app/hooks/use_api.ts | 8 +++ .../app/hooks/use_documentation_links.ts | 1 + .../public/app/hooks/use_get_transforms.ts | 8 ++- .../lib/authorization/components/common.ts | 10 ++- .../step_define/step_define_form.tsx | 3 +- .../action_clone/use_clone_action.tsx | 6 +- .../action_edit/use_edit_action.tsx | 6 +- .../action_start/start_action_name.test.tsx | 1 + .../action_start/start_action_name.tsx | 18 +++-- .../action_start/use_start_action.tsx | 13 ++-- .../create_transform_button.test.tsx | 2 +- .../create_transform_button.tsx | 16 ++++- .../transform_list/transform_list.test.tsx | 3 +- .../transform_list/transform_list.tsx | 36 +++------- .../transform_list/transforms_stats_bar.tsx | 68 ++++++++++++++++-- .../transform_list/use_actions.test.tsx | 2 +- .../components/transform_list/use_actions.tsx | 8 ++- .../transform_list/use_columns.test.tsx | 2 +- .../components/transform_list/use_columns.tsx | 6 +- .../transform_management_section.tsx | 40 ++++++++--- .../transform/server/routes/api/transforms.ts | 5 +- .../routes/api/transforms_nodes.test.ts | 43 +++++++++++ .../server/routes/api/transforms_nodes.ts | 71 +++++++++++++++++++ .../api_integration/apis/transform/index.ts | 1 + .../apis/transform/transforms_nodes.ts | 48 +++++++++++++ 28 files changed, 368 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/transform/server/routes/api/transforms_nodes.test.ts create mode 100644 x-pack/plugins/transform/server/routes/api/transforms_nodes.ts create mode 100644 x-pack/test/api_integration/apis/transform/transforms_nodes.ts diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index ee5f50588ff04..6e52245e16bbf 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -129,6 +129,7 @@ export class DocLinksService { elasticsearch: { indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`, mapping: `${ELASTICSEARCH_DOCS}mapping.html`, + nodeRoles: `${ELASTICSEARCH_DOCS}modules-node.html#node-roles`, remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`, remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`, remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`, diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts index 4d25bd74f4e74..fc5c728311f7d 100644 --- a/x-pack/plugins/transform/common/api_schemas/transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -16,6 +16,11 @@ import type { TransformId, TransformPivotConfig } from '../types/transform'; import { transformStateSchema, runtimeMappingsSchema } from './common'; +// GET transform nodes +export interface GetTransformNodesResponseSchema { + count: number; +} + // GET transforms export const getTransformsRequestSchema = schema.arrayOf( schema.object({ diff --git a/x-pack/plugins/transform/common/api_schemas/type_guards.ts b/x-pack/plugins/transform/common/api_schemas/type_guards.ts index 28eaf9ce2894f..476e2bad853c9 100644 --- a/x-pack/plugins/transform/common/api_schemas/type_guards.ts +++ b/x-pack/plugins/transform/common/api_schemas/type_guards.ts @@ -19,6 +19,7 @@ import type { DeleteTransformsResponseSchema } from './delete_transforms'; import type { StartTransformsResponseSchema } from './start_transforms'; import type { StopTransformsResponseSchema } from './stop_transforms'; import type { + GetTransformNodesResponseSchema, GetTransformsResponseSchema, PostTransformsPreviewResponseSchema, PutTransformsResponseSchema, @@ -35,6 +36,14 @@ const isGenericResponseSchema = (arg: any): arg is T => { ); }; +export const isGetTransformNodesResponseSchema = ( + arg: unknown +): arg is GetTransformNodesResponseSchema => { + return ( + isPopulatedObject(arg) && {}.hasOwnProperty.call(arg, 'count') && typeof arg.count === 'number' + ); +}; + export const isGetTransformsResponseSchema = (arg: unknown): arg is GetTransformsResponseSchema => { return isGenericResponseSchema(arg); }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts index 7afbc5e403b78..f3c90a688453d 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts @@ -29,6 +29,7 @@ import type { StopTransformsResponseSchema, } from '../../../common/api_schemas/stop_transforms'; import type { + GetTransformNodesResponseSchema, GetTransformsResponseSchema, PostTransformsPreviewRequestSchema, PostTransformsPreviewResponseSchema, @@ -66,6 +67,13 @@ export const useApi = () => { return useMemo( () => ({ + async getTransformNodes(): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/_nodes`); + } catch (e) { + return e; + } + }, async getTransform( transformId: TransformId ): Promise { diff --git a/x-pack/plugins/transform/public/app/hooks/use_documentation_links.ts b/x-pack/plugins/transform/public/app/hooks/use_documentation_links.ts index ded14a2c0e69e..030f96315835a 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_documentation_links.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_documentation_links.ts @@ -13,6 +13,7 @@ export const useDocumentationLinks = () => { return { esAggsCompositeMissingBucket: deps.docLinks.links.aggs.composite_missing_bucket, esIndicesCreateIndex: deps.docLinks.links.apis.createIndex, + esNodeRoles: deps.docLinks.links.elasticsearch.nodeRoles, esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`, esQueryDsl: deps.docLinks.links.query.queryDsl, esTransform: deps.docLinks.links.transforms.guide, diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts index dbb268b44cfd2..2d3425dfeedca 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts @@ -8,6 +8,7 @@ import { HttpFetchError } from 'src/core/public'; import { + isGetTransformNodesResponseSchema, isGetTransformsResponseSchema, isGetTransformsStatsResponseSchema, } from '../../../common/api_schemas/type_guards'; @@ -22,6 +23,7 @@ export type GetTransforms = (forceRefresh?: boolean) => void; export const useGetTransforms = ( setTransforms: React.Dispatch>, + setTransformNodes: React.Dispatch>, setErrorMessage: React.Dispatch>, setIsInitialized: React.Dispatch>, blockRefresh: boolean @@ -40,17 +42,20 @@ export const useGetTransforms = ( } const fetchOptions = { asSystemRequest: true }; + const transformNodes = await api.getTransformNodes(); const transformConfigs = await api.getTransforms(fetchOptions); const transformStats = await api.getTransformsStats(fetchOptions); if ( !isGetTransformsResponseSchema(transformConfigs) || - !isGetTransformsStatsResponseSchema(transformStats) + !isGetTransformsStatsResponseSchema(transformStats) || + !isGetTransformNodesResponseSchema(transformNodes) ) { // An error is followed immediately by setting the state to idle. // This way we're able to treat ERROR as a one-time-event like REFRESH. refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.ERROR); refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); + setTransformNodes(0); setTransforms([]); setIsInitialized(true); @@ -86,6 +91,7 @@ export const useGetTransforms = ( return reducedtableRows; }, [] as TransformListRow[]); + setTransformNodes(transformNodes.count); setTransforms(tableRows); setErrorMessage(undefined); setIsInitialized(true); diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts index cf82478d94423..28e9f190a9108 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts @@ -58,7 +58,9 @@ export const hasPrivilegeFactory = (privileges: Privileges | undefined | null) = // create the text for button's tooltips if the user // doesn't have the permission to press that button -export function createCapabilityFailureMessage(capability: keyof Capabilities) { +export function createCapabilityFailureMessage( + capability: keyof Capabilities | 'noTransformNodes' +) { let message = ''; switch (capability) { @@ -80,6 +82,12 @@ export function createCapabilityFailureMessage(capability: keyof Capabilities) { defaultMessage: 'You do not have permission to delete transforms.', }); break; + + case 'noTransformNodes': + message = i18n.translate('xpack.transform.capability.noPermission.noTransformNodesTooltip', { + defaultMessage: 'There are no transform nodes available.', + }); + break; } return i18n.translate('xpack.transform.capability.pleaseContactAdministratorTooltip', { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 1ddb9aa61045b..39593e7da59f8 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -191,8 +191,7 @@ export const StepDefineForm: FC = React.memo((props) => { stepDefineForm.advancedPivotEditor.actions.setAdvancedPivotEditorApplyButtonEnabled(false); }; - const { esQueryDsl } = useDocumentationLinks(); - const { esTransformPivot } = useDocumentationLinks(); + const { esQueryDsl, esTransformPivot } = useDocumentationLinks(); const advancedEditorsSidebarWidth = '220px'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx index 958b329814b88..6249e77ce31dc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx @@ -18,7 +18,7 @@ import { useAppDependencies, useToastNotifications } from '../../../../app_depen import { cloneActionNameText, CloneActionName } from './clone_action_name'; export type CloneAction = ReturnType; -export const useCloneAction = (forceDisable: boolean) => { +export const useCloneAction = (forceDisable: boolean, transformNodes: number) => { const history = useHistory(); const appDeps = useAppDependencies(); const savedObjectsClient = appDeps.savedObjects.client; @@ -72,14 +72,14 @@ export const useCloneAction = (forceDisable: boolean) => { const action: TransformListAction = useMemo( () => ({ name: (item: TransformListRow) => , - enabled: () => canCreateTransform && !forceDisable, + enabled: () => canCreateTransform && !forceDisable && transformNodes > 0, description: cloneActionNameText, icon: 'copy', type: 'icon', onClick: clickHandler, 'data-test-subj': 'transformActionClone', }), - [canCreateTransform, forceDisable, clickHandler] + [canCreateTransform, forceDisable, clickHandler, transformNodes] ); return { action }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx index 353a7660ac582..b84b309c478fd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx @@ -14,7 +14,7 @@ import { AuthorizationContext } from '../../../../lib/authorization'; import { editActionNameText, EditActionName } from './edit_action_name'; -export const useEditAction = (forceDisable: boolean) => { +export const useEditAction = (forceDisable: boolean, transformNodes: number) => { const { canCreateTransform } = useContext(AuthorizationContext).capabilities; const [config, setConfig] = useState(); @@ -28,14 +28,14 @@ export const useEditAction = (forceDisable: boolean) => { const action: TransformListAction = useMemo( () => ({ name: () => , - enabled: () => canCreateTransform || !forceDisable, + enabled: () => canCreateTransform && !forceDisable && transformNodes > 0, description: editActionNameText, icon: 'pencil', type: 'icon', onClick: (item: TransformListRow) => showFlyout(item.config), 'data-test-subj': 'transformActionEdit', }), - [canCreateTransform, forceDisable] + [canCreateTransform, forceDisable, transformNodes] ); return { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx index 5559f7758204f..490651afc7e96 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.test.tsx @@ -23,6 +23,7 @@ describe('Transform: Transform List Actions ', () => { const props: StartActionNameProps = { forceDisable: false, items: [item], + transformNodes: 1, }; const wrapper = shallow(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx index 5cc0ac077c240..32207fc586c82 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx @@ -26,7 +26,8 @@ export const startActionNameText = i18n.translate( export const isStartActionDisabled = ( items: TransformListRow[], - canStartStopTransform: boolean + canStartStopTransform: boolean, + transformNodes: number ) => { // Disable start for batch transforms which have completed. const completedBatchTransform = items.some((i: TransformListRow) => isCompletedBatchTransform(i)); @@ -36,15 +37,24 @@ export const isStartActionDisabled = ( ); return ( - !canStartStopTransform || completedBatchTransform || startedTransform || items.length === 0 + !canStartStopTransform || + completedBatchTransform || + startedTransform || + items.length === 0 || + transformNodes === 0 ); }; export interface StartActionNameProps { items: TransformListRow[]; forceDisable?: boolean; + transformNodes: number; } -export const StartActionName: FC = ({ items, forceDisable }) => { +export const StartActionName: FC = ({ + items, + forceDisable, + transformNodes, +}) => { const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; const isBulkAction = items.length > 1; @@ -89,7 +99,7 @@ export const StartActionName: FC = ({ items, forceDisable ); } - const actionIsDisabled = isStartActionDisabled(items, canStartStopTransform); + const actionIsDisabled = isStartActionDisabled(items, canStartStopTransform, transformNodes); let content: string | undefined; if (actionIsDisabled && items.length > 0) { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx index 02379972ba87c..2c45da45509e5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx @@ -16,7 +16,7 @@ import { useStartTransforms } from '../../../../hooks'; import { isStartActionDisabled, startActionNameText, StartActionName } from './start_action_name'; export type StartAction = ReturnType; -export const useStartAction = (forceDisable: boolean) => { +export const useStartAction = (forceDisable: boolean, transformNodes: number) => { const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; const startTransforms = useStartTransforms(); @@ -43,17 +43,22 @@ export const useStartAction = (forceDisable: boolean) => { const action: TransformListAction = useMemo( () => ({ name: (item: TransformListRow) => ( - + ), available: (item: TransformListRow) => item.stats.state === TRANSFORM_STATE.STOPPED, - enabled: (item: TransformListRow) => !isStartActionDisabled([item], canStartStopTransform), + enabled: (item: TransformListRow) => + !isStartActionDisabled([item], canStartStopTransform, transformNodes), description: startActionNameText, icon: 'play', type: 'icon', onClick: (item: TransformListRow) => openModal([item]), 'data-test-subj': 'transformActionStart', }), - [canStartStopTransform, forceDisable] + [canStartStopTransform, forceDisable, transformNodes] ); return { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx index 275585246d82c..0a7324fd09ffc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.test.tsx @@ -14,7 +14,7 @@ jest.mock('../../../../../shared_imports'); describe('Transform: Transform List ', () => { test('Minimal initialization', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx index 47addec6c0e5e..96b0b51294f08 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/create_transform_button/create_transform_button.tsx @@ -18,15 +18,20 @@ import { interface CreateTransformButtonProps { onClick: MouseEventHandler; + transformNodes: number; } -export const CreateTransformButton: FC = ({ onClick }) => { +export const CreateTransformButton: FC = ({ + onClick, + transformNodes, +}) => { const { capabilities } = useContext(AuthorizationContext); const disabled = !capabilities.canCreateTransform || !capabilities.canPreviewTransform || - !capabilities.canStartStopTransform; + !capabilities.canStartStopTransform || + transformNodes === 0; const createTransformButton = ( = ({ onClick if (disabled) { return ( - + 0 ? 'canCreateTransform' : 'noTransformNodes' + )} + > {createTransformButton} ); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx index 7665063dce2d8..ac00d31a620b9 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx @@ -17,9 +17,8 @@ describe('Transform: Transform List ', () => { test('Minimal initialization', () => { const wrapper = shallow( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index 8668281d0b181..bacf8f9deccae 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -12,7 +12,6 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiButtonIcon, - EuiCallOut, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -62,18 +61,16 @@ function getItemIdToExpandedRowMap( }, {} as ItemIdToExpandedRowMap); } -interface Props { - errorMessage: any; - isInitialized: boolean; +interface TransformListProps { onCreateTransform: MouseEventHandler; + transformNodes: number; transforms: TransformListRow[]; transformsLoading: boolean; } -export const TransformList: FC = ({ - errorMessage, - isInitialized, +export const TransformList: FC = ({ onCreateTransform, + transformNodes, transforms, transformsLoading, }) => { @@ -86,7 +83,7 @@ export const TransformList: FC = ({ const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); const [transformSelection, setTransformSelection] = useState([]); const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); - const bulkStartAction = useStartAction(false); + const bulkStartAction = useStartAction(false, transformNodes); const bulkDeleteAction = useDeleteAction(false); const [searchError, setSearchError] = useState(undefined); @@ -106,6 +103,7 @@ export const TransformList: FC = ({ const { columns, modals: singleActionModals } = useColumns( expandedRowItemIds, setExpandedRowItemIds, + transformNodes, transformSelection ); @@ -131,26 +129,10 @@ export const TransformList: FC = ({ } }; - // Before the transforms have been loaded for the first time, display the loading indicator only. - // Otherwise a user would see 'No transforms found' during the initial loading. - if (!isInitialized) { + if (transforms.length === 0 && transformNodes === 0) { return null; } - if (typeof errorMessage !== 'undefined') { - return ( - -
{JSON.stringify(errorMessage)}
-
- ); - } - if (transforms.length === 0) { return ( = ({ const bulkActionMenuItems = [
bulkStartAction.openModal(transformSelection)}> - +
,
@@ -257,7 +239,7 @@ export const TransformList: FC = ({ - + ); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx index fed9f0d9a8518..16d5cd800b548 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx @@ -6,15 +6,21 @@ */ import React, { FC } from 'react'; + +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants'; import { TransformListRow } from '../../../../common'; +import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; + import { StatsBar, TransformStatsBarStats } from '../stats_bar'; -function createTranformStats(transformsList: TransformListRow[]) { +function createTranformStats(transformNodes: number, transformsList: TransformListRow[]) { const transformStats = { total: { label: i18n.translate('xpack.transform.statsBar.totalTransformsLabel', { @@ -51,6 +57,13 @@ function createTranformStats(transformsList: TransformListRow[]) { value: 0, show: true, }, + nodes: { + label: i18n.translate('xpack.transform.statsBar.transformNodesLabel', { + defaultMessage: 'Nodes', + }), + value: transformNodes, + show: true, + }, }; if (transformsList === undefined) { @@ -87,12 +100,57 @@ function createTranformStats(transformsList: TransformListRow[]) { return transformStats; } -interface Props { +interface TransformStatsBarProps { + transformNodes: number; transformsList: TransformListRow[]; } -export const TransformStatsBar: FC = ({ transformsList }) => { - const transformStats: TransformStatsBarStats = createTranformStats(transformsList); +export const TransformStatsBar: FC = ({ + transformNodes, + transformsList, +}) => { + const { esNodeRoles } = useDocumentationLinks(); + + const transformStats: TransformStatsBarStats = createTranformStats( + transformNodes, + transformsList + ); - return ; + return ( + <> + + {transformNodes === 0 && ( + <> + + + } + color="warning" + iconType="alert" + > +

+ + + + ), + }} + /> +

+
+ + )} + + ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx index 5b3f921a07d67..90487d21610ea 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx @@ -14,7 +14,7 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List Actions', () => { test('useActions()', () => { - const { result } = renderHook(() => useActions({ forceDisable: false })); + const { result } = renderHook(() => useActions({ forceDisable: false, transformNodes: 1 })); const actions = result.current.actions; // Using `any` for the callback. Somehow the EUI types don't pass diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx index b30cbd0aba741..d9b9008490666 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx @@ -20,16 +20,18 @@ import { useStopAction } from '../action_stop'; export const useActions = ({ forceDisable, + transformNodes, }: { forceDisable: boolean; + transformNodes: number; }): { actions: EuiTableActionsColumnType['actions']; modals: JSX.Element; } => { - const cloneAction = useCloneAction(forceDisable); + const cloneAction = useCloneAction(forceDisable, transformNodes); const deleteAction = useDeleteAction(forceDisable); - const editAction = useEditAction(forceDisable); - const startAction = useStartAction(forceDisable); + const editAction = useEditAction(forceDisable, transformNodes); + const startAction = useStartAction(forceDisable, transformNodes); const stopAction = useStopAction(forceDisable); return { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx index ec65781acc4cc..53eed01f1226d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx @@ -14,7 +14,7 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: Job List Columns', () => { test('useColumns()', () => { - const { result } = renderHook(() => useColumns([], () => {}, [])); + const { result } = renderHook(() => useColumns([], () => {}, 1, [])); const columns: ReturnType['columns'] = result.current.columns; expect(columns).toHaveLength(7); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index d792192f58b61..a8f6a9a233c62 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -65,9 +65,13 @@ export const getTaskStateBadge = ( export const useColumns = ( expandedRowItemIds: TransformId[], setExpandedRowItemIds: React.Dispatch>, + transformNodes: number, transformSelection: TransformListRow[] ) => { - const { actions, modals } = useActions({ forceDisable: transformSelection.length > 0 }); + const { actions, modals } = useActions({ + forceDisable: transformSelection.length > 0, + transformNodes, + }); function toggleDetails(item: TransformListRow) { const index = expandedRowItemIds.indexOf(item.config.id); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index fa1a42b94f0b7..cc4c502f21eb5 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -7,12 +7,15 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiLoadingContent, EuiModal, EuiPageContent, EuiPageContentBody, @@ -42,10 +45,12 @@ export const TransformManagement: FC = () => { const [isInitialized, setIsInitialized] = useState(false); const [blockRefresh, setBlockRefresh] = useState(false); const [transforms, setTransforms] = useState([]); + const [transformNodes, setTransformNodes] = useState(0); const [errorMessage, setErrorMessage] = useState(undefined); const getTransforms = useGetTransforms( setTransforms, + setTransformNodes, setErrorMessage, setIsInitialized, blockRefresh @@ -111,15 +116,32 @@ export const TransformManagement: FC = () => { - - - + {!isInitialized && } + {isInitialized && ( + <> + + + {typeof errorMessage !== 'undefined' && ( + +
{JSON.stringify(errorMessage)}
+
+ )} + {typeof errorMessage === 'undefined' && ( + + )} + + )}
{isSearchSelectionVisible && ( diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 20961a64da44b..93f5caf7cf5b0 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -58,6 +58,7 @@ import { addBasePath } from '../index'; import { isRequestTimeout, fillResultsWithTimeouts, wrapError, wrapEsError } from './error_utils'; import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages'; +import { registerTransformNodesRoutes } from './transforms_nodes'; import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; import { isLatestTransform } from '../../../common/types/transform'; @@ -175,7 +176,6 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { } }) ); - registerTransformsAuditMessagesRoutes(routeDependencies); /** * @apiGroup Transforms @@ -389,6 +389,9 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { } }) ); + + registerTransformsAuditMessagesRoutes(routeDependencies); + registerTransformNodesRoutes(routeDependencies); } async function getIndexPatternId( diff --git a/x-pack/plugins/transform/server/routes/api/transforms_nodes.test.ts b/x-pack/plugins/transform/server/routes/api/transforms_nodes.test.ts new file mode 100644 index 0000000000000..462a4688ad455 --- /dev/null +++ b/x-pack/plugins/transform/server/routes/api/transforms_nodes.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isNodes } from './transforms_nodes'; + +describe('Transform: Nodes API endpoint', () => { + test('isNodes()', () => { + expect(isNodes(undefined)).toBe(false); + expect(isNodes({})).toBe(false); + expect(isNodes({ nodeId: {} })).toBe(false); + expect(isNodes({ nodeId: { someAttribute: {} } })).toBe(false); + expect(isNodes({ nodeId: { attributes: {} } })).toBe(false); + expect( + isNodes({ + nodeId1: { attributes: { someAttribute: true } }, + nodeId2: { someAttribute: 'asdf' }, + }) + ).toBe(false); + + // Legacy format based on attributes should return false + expect(isNodes({ nodeId: { attributes: { someAttribute: true } } })).toBe(false); + expect( + isNodes({ + nodeId1: { attributes: { someAttribute: true } }, + nodeId2: { attributes: { 'transform.node': 'true' } }, + }) + ).toBe(false); + + // Current format based on roles should return true + expect(isNodes({ nodeId: { roles: ['master', 'transform'] } })).toBe(true); + expect(isNodes({ nodeId: { roles: ['transform'] } })).toBe(true); + expect( + isNodes({ + nodeId1: { roles: ['master', 'data'] }, + nodeId2: { roles: ['transform'] }, + }) + ).toBe(true); + }); +}); diff --git a/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts b/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts new file mode 100644 index 0000000000000..afdcc93998303 --- /dev/null +++ b/x-pack/plugins/transform/server/routes/api/transforms_nodes.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isPopulatedObject } from '../../../common/utils/object_utils'; + +import { RouteDependencies } from '../../types'; + +import { addBasePath } from '../index'; + +import { wrapError, wrapEsError } from './error_utils'; + +const NODE_ROLES = 'roles'; + +interface NodesAttributes { + roles: string[]; +} +type Nodes = Record; + +export const isNodes = (arg: unknown): arg is Nodes => { + return ( + isPopulatedObject(arg) && + Object.values(arg).every( + (node) => + isPopulatedObject(node) && + {}.hasOwnProperty.call(node, NODE_ROLES) && + Array.isArray(node.roles) + ) + ); +}; + +export function registerTransformNodesRoutes({ router, license }: RouteDependencies) { + /** + * @apiGroup Transform Nodes + * + * @api {get} /api/transforms/_nodes Transform Nodes + * @apiName GetTransformNodes + * @apiDescription Get transform nodes + */ + router.get( + { + path: addBasePath('transforms/_nodes'), + validate: false, + }, + license.guardApiRoute(async (ctx, req, res) => { + try { + const { + body: { nodes }, + } = await ctx.core.elasticsearch.client.asInternalUser.nodes.info({ + filter_path: `nodes.*.${NODE_ROLES}`, + }); + + let count = 0; + if (isNodes(nodes)) { + for (const { roles } of Object.values(nodes)) { + if (roles.includes('transform')) { + count++; + } + } + } + + return res.ok({ body: { count } }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } + }) + ); +} diff --git a/x-pack/test/api_integration/apis/transform/index.ts b/x-pack/test/api_integration/apis/transform/index.ts index efea3d69d2212..d0aa9533c3860 100644 --- a/x-pack/test/api_integration/apis/transform/index.ts +++ b/x-pack/test/api_integration/apis/transform/index.ts @@ -32,6 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./start_transforms')); loadTestFile(require.resolve('./stop_transforms')); loadTestFile(require.resolve('./transforms')); + loadTestFile(require.resolve('./transforms_nodes')); loadTestFile(require.resolve('./transforms_preview')); loadTestFile(require.resolve('./transforms_stats')); loadTestFile(require.resolve('./transforms_update')); diff --git a/x-pack/test/api_integration/apis/transform/transforms_nodes.ts b/x-pack/test/api_integration/apis/transform/transforms_nodes.ts new file mode 100644 index 0000000000000..593a1fb8fb723 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms_nodes.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import type { GetTransformNodesResponseSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; +import { isGetTransformNodesResponseSchema } from '../../../../plugins/transform/common/api_schemas/type_guards'; +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + apiTransformTransformsNodes: { + count: 1, + }, + }; + + function assertTransformsNodesResponseBody(body: GetTransformNodesResponseSchema) { + expect(isGetTransformNodesResponseSchema(body)).to.eql(true); + + expect(body.count).to.eql(expected.apiTransformTransformsNodes.count); + } + + describe('/api/transform/transforms/_nodes', function () { + it('should return the number of available transform nodes', async () => { + const { body } = await supertest + .get('/api/transform/transforms/_nodes') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsNodesResponseBody(body); + }); + }); +}; From 73a73332ebd3debd56148aad194e8d5438df64ec Mon Sep 17 00:00:00 2001 From: ymao1 Date: Tue, 16 Mar 2021 08:05:01 -0400 Subject: [PATCH 02/44] [Alerting][Docs] Updating glossary with new terminology (#94447) * Updating glossary with new terminology * Updating glossary with new terminology Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/glossary.asciidoc | 44 +++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc index f86f15b1f0e67..02751ec57a1cf 100644 --- a/docs/glossary.asciidoc +++ b/docs/glossary.asciidoc @@ -2,7 +2,7 @@ [[glossary]] = Glossary -<> | <> | <> | <> | <> | <> | <> | H | I | J | <> | <> | <> | N | O | <> | <> | R | <> | <> | <> | V | <> | X | Y | Z +<> | <> | <> | <> | <> | <> | <> | H | I | J | <> | <> | <> | N | O | <> | <> | <> | <> | <> | <> | V | <> | X | Y | Z [float] [[a_glos]] @@ -13,10 +13,10 @@ + -- // tag::action-def[] -The alert-specific response that occurs when an alert fires. -An alert can have multiple actions. +The rule-specific response that occurs when an alerting rule fires. +A rule can have multiple actions. See -{kibana-ref}/action-types.html[Action and connector types]. +{kibana-ref}/action-types.html[Connectors and actions]. // end::action-def[] -- @@ -28,20 +28,6 @@ Part of {kib} Stack Management. See {kibana-ref}/advanced-options.html[Advanced Settings]. // end::advanced-settings-def[] -[[glossary-alert]] alert :: -// tag::alert-def[] -A set of <>, schedules, and <> -that enable notifications. -See <>. -// end::alert-def[] - -[[glossary-alerts-and-actions]] Alerts and Actions :: -// tag::alerts-and-actions-def[] -A comprehensive view of all your alerts. Enables you to access and -manage alerts for all {kib} apps from one place. -See {kibana-ref}/alerting-getting-started.html[Alerts and Actions]. -// end::alerts-and-actions-def[] - [[glossary-annotation]] annotation :: // tag::annotation-def[] A way to augment a data display with descriptive domain knowledge. @@ -113,13 +99,13 @@ The cluster location is the weighted centroid for all documents in the grid cell [[glossary-condition]] condition :: // tag::condition-def[] -Specifies the circumstances that must be met to trigger an alert. +Specifies the circumstances that must be met to trigger an alerting rule. // end::condition-def[] [[glossary-connector]] connector :: // tag::connector-def[] A configuration that enables integration with an external system (the destination for an action). -See {kibana-ref}/action-types.html[Action and connector types]. +See {kibana-ref}/action-types.html[Connectors and actions]. // end::connector-def[] [[glossary-console]] Console :: @@ -335,6 +321,24 @@ A tool that enables you to inspect and analyze search queries to diagnose and de See {kibana-ref}/xpack-profiler.html[Query Profiler]. // end::query-profiler-def[] +[float] +[[r_glos]] +== R + +[[glossary-rule]] rule :: +// tag::rule-def[] +A set of <>, schedules, and <> +that enable notifications. +See <>. +// end::rule-def[] + +[[glossary-rules-and-connectors]] Rules and Connectors :: +// tag::rules-and-connectors-def[] +A comprehensive view of all your alerting rules. Enables you to access and +manage rules for all {kib} apps from one place. +See {kibana-ref}/alerting-getting-started.html[Rules and Connectors]. +// end::rules-and-connectors-def[] + [float] [[s_glos]] == S From e830e077d3fe88085b7b488cd0f08c554c8a84ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 16 Mar 2021 13:29:29 +0100 Subject: [PATCH 03/44] [Logs UI] Style improvements for log stream search strategy (#94560) --- .../log_entries/log_entries_search_strategy.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts index bf7e497385f9a..190464ab6d5c1 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts @@ -20,7 +20,6 @@ import type { } from '../../../../../../src/plugins/data/server'; import { LogSourceColumnConfiguration, - LogSourceConfigurationProperties, logSourceFieldColumnConfigurationRT, } from '../../../common/http_api/log_sources'; import { @@ -107,7 +106,10 @@ export const logEntriesSearchStrategyProvider = ({ params.size + 1, configuration.fields.timestamp, configuration.fields.tiebreaker, - getRequiredFields(configuration, messageFormattingRules, params.columns), + getRequiredFields( + params.columns ?? configuration.logColumns, + messageFormattingRules + ), params.query, params.highlightPhrase ), @@ -131,7 +133,7 @@ export const logEntriesSearchStrategyProvider = ({ .slice(0, request.params.size) .map( getLogEntryFromHit( - request.params.columns ? request.params.columns : configuration.logColumns, + request.params.columns ?? configuration.logColumns, messageFormattingRules ) ); @@ -257,12 +259,9 @@ function getResponseCursors(entries: LogEntry[]) { const VIEW_IN_CONTEXT_FIELDS = ['log.file.path', 'host.name', 'container.id']; const getRequiredFields = ( - configuration: LogSourceConfigurationProperties, - messageFormattingRules: CompiledLogMessageFormattingRule, - columnOverrides?: LogSourceColumnConfiguration[] + columns: LogSourceColumnConfiguration[], + messageFormattingRules: CompiledLogMessageFormattingRule ): string[] => { - const columns = columnOverrides ? columnOverrides : configuration.logColumns; - const fieldsFromColumns = columns.reduce((accumulatedFields, logColumn) => { if (logSourceFieldColumnConfigurationRT.is(logColumn)) { return [...accumulatedFields, logColumn.fieldColumn.field]; From 638f166f71e9fbb59bfe286d807aec587696be37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Tue, 16 Mar 2021 13:36:06 +0100 Subject: [PATCH 04/44] [SECURITY_SOLUTION] Add helper text on entry input for trusted applications (#94563) * Added text help for each entry input option * Add new unit test * Fix wrong import on test file * Change entry variable to readonly. Use it.each instead of a for loop * Move function inside useMemo since it is only used there * Remove old commented code * Update failing test --- .../condition_entry_input/index.test.tsx | 141 ++++++++++++++++++ .../condition_entry_input/index.tsx | 20 ++- .../create_trusted_app_form.test.tsx | 6 +- .../pages/trusted_apps/view/translations.ts | 15 ++ 4 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx new file mode 100644 index 0000000000000..4e9ec3a0883a2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx @@ -0,0 +1,141 @@ +/* + * 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, mount } from 'enzyme'; +import React from 'react'; +import { keys } from 'lodash'; +import { + ConditionEntry, + ConditionEntryField, + OperatingSystem, +} from '../../../../../../../common/endpoint/types'; + +import { ConditionEntryInput } from '.'; +import { EuiSuperSelectProps } from '@elastic/eui'; + +let onRemoveMock: jest.Mock; +let onChangeMock: jest.Mock; +let onVisitedMock: jest.Mock; + +const entry: Readonly = { + field: ConditionEntryField.HASH, + type: 'match', + operator: 'included', + value: 'trustedApp', +}; + +describe('Condition entry input', () => { + beforeEach(() => { + onRemoveMock = jest.fn(); + onChangeMock = jest.fn(); + onVisitedMock = jest.fn(); + }); + + const getElement = ( + subject: string, + os: OperatingSystem = OperatingSystem.WINDOWS, + isRemoveDisabled: boolean = false + ) => ( + + ); + + it.each(keys(ConditionEntryField).map((k) => [k]))( + 'should call on change for field input with value %s', + (field) => { + const element = shallow(getElement('testOnChange')); + expect(onChangeMock).toHaveBeenCalledTimes(0); + element + .find('[data-test-subj="testOnChange-field"]') + .first() + .simulate('change', { target: { value: field } }); + expect(onChangeMock).toHaveBeenCalledTimes(1); + expect(onChangeMock).toHaveBeenCalledWith( + { + ...entry, + field: { target: { value: field } }, + }, + entry + ); + } + ); + + it('should call on remove for field input', () => { + const element = mount(getElement('testOnRemove')); + expect(onRemoveMock).toHaveBeenCalledTimes(0); + element.find('[data-test-subj="testOnRemove-remove"]').first().simulate('click'); + expect(onRemoveMock).toHaveBeenCalledTimes(1); + expect(onRemoveMock).toHaveBeenCalledWith(entry); + }); + + it('should not be able to call on remove for field input because disabled', () => { + const element = mount(getElement('testOnRemove', OperatingSystem.WINDOWS, true)); + expect(onRemoveMock).toHaveBeenCalledTimes(0); + element.find('[data-test-subj="testOnRemove-remove"]').first().simulate('click'); + expect(onRemoveMock).toHaveBeenCalledTimes(0); + }); + + it('should call on visited for field input', () => { + const element = shallow(getElement('testOnVisited')); + expect(onVisitedMock).toHaveBeenCalledTimes(0); + element.find('[data-test-subj="testOnVisited-value"]').first().simulate('blur'); + expect(onVisitedMock).toHaveBeenCalledTimes(1); + expect(onVisitedMock).toHaveBeenCalledWith(entry); + }); + + it('should change value for field input', () => { + const element = shallow(getElement('testOnChange')); + expect(onChangeMock).toHaveBeenCalledTimes(0); + element + .find('[data-test-subj="testOnChange-value"]') + .first() + .simulate('change', { target: { value: 'new value' } }); + expect(onChangeMock).toHaveBeenCalledTimes(1); + expect(onChangeMock).toHaveBeenCalledWith( + { + ...entry, + value: 'new value', + }, + entry + ); + }); + + it('should be able to select three options when WINDOWS OS', () => { + const element = mount(getElement('testCheckSignatureOption')); + const superSelectProps = element + .find('[data-test-subj="testCheckSignatureOption-field"]') + .first() + .props() as EuiSuperSelectProps; + expect(superSelectProps.options.length).toBe(3); + }); + + it('should be able to select two options when LINUX OS', () => { + const element = mount(getElement('testCheckSignatureOption', OperatingSystem.LINUX)); + const superSelectProps = element + .find('[data-test-subj="testCheckSignatureOption-field"]') + .first() + .props() as EuiSuperSelectProps; + expect(superSelectProps.options.length).toBe(2); + }); + + it('should be able to select two options when MAC OS', () => { + const element = mount(getElement('testCheckSignatureOption', OperatingSystem.MAC)); + const superSelectProps = element + .find('[data-test-subj="testCheckSignatureOption-field"]') + .first() + .props() as EuiSuperSelectProps; + expect(superSelectProps.options.length).toBe(2); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx index 72467cf28ec56..f85f00810bc72 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx @@ -15,6 +15,7 @@ import { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption, + EuiText, } from '@elastic/eui'; import { @@ -23,7 +24,12 @@ import { OperatingSystem, } from '../../../../../../../common/endpoint/types'; -import { CONDITION_FIELD_TITLE, ENTRY_PROPERTY_TITLES, OPERATOR_TITLE } from '../../translations'; +import { + CONDITION_FIELD_DESCRIPTION, + CONDITION_FIELD_TITLE, + ENTRY_PROPERTY_TITLES, + OPERATOR_TITLE, +} from '../../translations'; const ConditionEntryCell = memo<{ showLabel: boolean; @@ -75,18 +81,30 @@ export const ConditionEntryInput = memo( ]); const fieldOptions = useMemo>>(() => { + const getDropdownDisplay = (field: ConditionEntryField) => ( + <> + {CONDITION_FIELD_TITLE[field]} + + {CONDITION_FIELD_DESCRIPTION[field]} + + + ); + return [ { + dropdownDisplay: getDropdownDisplay(ConditionEntryField.HASH), inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.HASH], value: ConditionEntryField.HASH, }, { + dropdownDisplay: getDropdownDisplay(ConditionEntryField.PATH), inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.PATH], value: ConditionEntryField.PATH, }, ...(os === OperatingSystem.WINDOWS ? [ { + dropdownDisplay: getDropdownDisplay(ConditionEntryField.SIGNER), inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER], value: ConditionEntryField.SIGNER, }, 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/create_trusted_app_form.test.tsx index 441847bd88bb9..7d056ae6999e7 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/create_trusted_app_form.test.tsx @@ -165,7 +165,11 @@ describe('When showing the Trusted App Create Form', () => { '.euiSuperSelect__listbox button.euiSuperSelect__item' ) ).map((button) => button.textContent); - expect(options).toEqual(['Hash', 'Path', 'Signature']); + expect(options).toEqual([ + 'Hashmd5, sha1, or sha256', + 'PathThe full path of the application', + 'SignatureThe signer of the application', + ]); }); it('should show the value field as required', () => { 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 3b9db3f8a1c02..b594c355a6983 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 @@ -37,6 +37,21 @@ export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { ), }; +export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } = { + [ConditionEntryField.HASH]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.hash', + { defaultMessage: 'md5, sha1, or sha256' } + ), + [ConditionEntryField.PATH]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.path', + { defaultMessage: 'The full path of the application' } + ), + [ConditionEntryField.SIGNER]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.signature', + { defaultMessage: 'The signer of the application' } + ), +}; + export const OPERATOR_TITLE: { [K in ConditionEntry['operator']]: string } = { included: i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', { defaultMessage: 'is', From 4a83a024336c70f518b6bd3b838eade73c1064ae Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 16 Mar 2021 13:40:13 +0100 Subject: [PATCH 05/44] [ILM] Hide node allocation notices on Cloud (#94581) * block node allocation notices on cloud * added test to check that notices are not showing Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../features/node_allocation.test.ts | 30 +++++------ .../components/index.ts | 2 - .../components/missing_cloud_tier_callout.tsx | 53 ------------------- .../data_tier_allocation_field.tsx | 27 ++-------- 4 files changed, 16 insertions(+), 96 deletions(-) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts index 832963827663d..e289991780c04 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts @@ -365,9 +365,9 @@ describe(' node allocation', () => { await act(async () => { testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); }); - const { actions, component, exists, find } = testBed; + testBed.component.update(); - component.update(); + const { actions, component, exists, find } = testBed; await actions.warm.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -375,35 +375,29 @@ describe(' node allocation', () => { expect(exists('defaultDataAllocationOption')).toBeTruthy(); expect(exists('customDataAllocationOption')).toBeTruthy(); expect(exists('noneDataAllocationOption')).toBeTruthy(); - // We should not be showing the call-to-action for users to activate data tier in cloud - expect(exists('cloudDataTierCallout')).toBeFalsy(); // Do not show the call-to-action for users to migrate their cluster to use node roles expect(find('cloudDataTierCallout').exists()).toBeFalsy(); }); - - test(`shows cloud notice when cold tier nodes do not exist`, async () => { + test('do not show node allocation specific warnings on cloud', async () => { httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, - nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + nodesByAttributes: { test: ['123'] }, + // No nodes with node roles like "data_hot" or "data_warm" + nodesByRoles: {}, isUsingDeprecatedDataRoleConfig: false, }); await act(async () => { testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); }); - const { actions, component, exists, find } = testBed; + testBed.component.update(); - component.update(); + const { actions, component, exists } = testBed; + await actions.warm.enable(true); await actions.cold.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(exists('cloudMissingTierCallout')).toBeTruthy(); - expect(find('cloudMissingTierCallout').text()).toContain( - `Edit your Elastic Cloud deployment to set up a cold tier` - ); - - // Assert that other notices are not showing - expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); - expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); + expect(exists('cloudDataTierCallout')).toBeFalsy(); + expect(exists('defaultAllocationNotice')).toBeFalsy(); + expect(exists('defaultAllocationWarning')).toBeFalsy(); }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts index dacec1df52e2e..e9c884a42fa93 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts @@ -17,8 +17,6 @@ export { DefaultAllocationWarning } from './default_allocation_warning'; export { NoNodeAttributesWarning } from './no_node_attributes_warning'; -export { MissingCloudTierCallout } from './missing_cloud_tier_callout'; - export { CloudDataTierCallout } from './cloud_data_tier_callout'; export { LoadingError } from './loading_error'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx deleted file mode 100644 index 09d3135cde469..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx +++ /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 { i18n } from '@kbn/i18n'; -import React, { FunctionComponent } from 'react'; -import { EuiCallOut, EuiLink } from '@elastic/eui'; - -const geti18nTexts = (tier: 'cold' | 'frozen') => ({ - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.title', { - defaultMessage: 'Create a {tier} tier', - values: { tier }, - }), - body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.body', { - defaultMessage: 'Edit your Elastic Cloud deployment to set up a {tier} tier.', - values: { tier }, - }), - linkText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.linkToCloudDeploymentDescription', - { defaultMessage: 'View cloud deployment' } - ), -}); - -interface Props { - phase: 'cold' | 'frozen'; - linkToCloudDeployment?: string; -} - -/** - * A call-to-action for users to activate their cold tier slider to provision cold tier nodes. - * This may need to be change when we have autoscaling enabled on a cluster because nodes may not - * yet exist, but will automatically be provisioned. - */ -export const MissingCloudTierCallout: FunctionComponent = ({ - phase, - linkToCloudDeployment, -}) => { - const i18nTexts = geti18nTexts(phase); - - return ( - - {i18nTexts.body}{' '} - {Boolean(linkToCloudDeployment) && ( - - {i18nTexts.linkText} - - )} - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index ef0e82063ce20..ffd4e2758ab86 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -25,7 +25,6 @@ import { DefaultAllocationNotice, DefaultAllocationWarning, NoNodeAttributesWarning, - MissingCloudTierCallout, CloudDataTierCallout, LoadingError, } from './components'; @@ -59,10 +58,6 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr const { nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig } = data!; - const hasDataNodeRoles = Object.keys(nodesByRoles).some((nodeRole) => - // match any of the "data_" roles, including data_content. - nodeRole.trim().startsWith('data_') - ); const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); const isCloudEnabled = cloud?.isCloudEnabled ?? false; const cloudDeploymentUrl = cloud?.cloudDeploymentUrl; @@ -71,26 +66,12 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr switch (allocationType) { case 'node_roles': /** - * We'll drive Cloud users to add a cold or frozen tier to their deployment if there are no nodes with that role. + * On cloud most users should be using autoscaling which will provision tiers as they are needed. We do not surface any + * of the notices below. */ - if ( - isCloudEnabled && - !isUsingDeprecatedDataRoleConfig && - (phase === 'cold' || phase === 'frozen') - ) { - const hasNoNodesWithNodeRole = !nodesByRoles[`data_${phase}` as const]?.length; - - if (hasDataNodeRoles && hasNoNodesWithNodeRole) { - // Tell cloud users they can deploy nodes on cloud. - return ( - <> - - - - ); - } + if (isCloudEnabled) { + return null; } - /** * Node role allocation moves data in a phase to a corresponding tier of the same name. To prevent policy execution from getting * stuck ILM allocation will fall back to a previous tier if possible. We show the WARNING below to inform a user when even From 6c9cfd4893983917a36bc9f82f79e9eb235cf9eb Mon Sep 17 00:00:00 2001 From: James Rodewig <40268737+jrodewig@users.noreply.github.com> Date: Tue, 16 Mar 2021 08:45:32 -0400 Subject: [PATCH 06/44] Use documentation link service for Watcher (#93339) --- .../watcher/public/application/app_context.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/watcher/public/application/app_context.tsx b/x-pack/plugins/watcher/public/application/app_context.tsx index 8b1efbc9a1fe5..81fbfa97845a6 100644 --- a/x-pack/plugins/watcher/public/application/app_context.tsx +++ b/x-pack/plugins/watcher/public/application/app_context.tsx @@ -16,18 +16,14 @@ interface ContextValue extends Omit { const AppContext = createContext(null as any); -// eslint-disable-next-line @typescript-eslint/naming-convention -const generateDocLinks = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocLinksStart) => { - const elasticDocLinkBase = `${ELASTIC_WEBSITE_URL}guide/en/`; - const esBase = `${elasticDocLinkBase}elasticsearch/reference/${DOC_LINK_VERSION}`; - const kibanaBase = `${elasticDocLinkBase}kibana/${DOC_LINK_VERSION}`; - const putWatchApiUrl = `${esBase}/watcher-api-put-watch.html`; - const executeWatchApiUrl = `${esBase}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`; - const watcherGettingStartedUrl = `${kibanaBase}/watcher-ui.html`; +const generateDocLinks = ({ links }: DocLinksStart) => { + const putWatchApiUrl = `${links.apis.putWatch}`; + const executeWatchApiUrl = `${links.apis.executeWatchActionModes}`; + const watcherGettingStartedUrl = `${links.watcher.ui}`; const watchActionsConfigurationMap = { - [ACTION_TYPES.SLACK]: `${esBase}/actions-slack.html#configuring-slack`, - [ACTION_TYPES.PAGERDUTY]: `${esBase}/actions-pagerduty.html#configuring-pagerduty`, - [ACTION_TYPES.JIRA]: `${esBase}/actions-jira.html#configuring-jira`, + [ACTION_TYPES.SLACK]: `${links.watcher.slackAction}`, + [ACTION_TYPES.PAGERDUTY]: `${links.watcher.pagerDutyAction}`, + [ACTION_TYPES.JIRA]: `${links.watcher.jiraAction}`, }; return { From b0fa077e8a8e75d2937a0d53a0905e0a5d26612a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 16 Mar 2021 09:12:50 -0400 Subject: [PATCH 07/44] [APM] Adding comparison to Throughput chart, Error rate chart, and Errors table (#94204) * adding comparison to throuput chart * adding comparison to error rate chart * adding comparison to errors table * fixing/adding api test * addressing pr comments * addressing pr comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../get_column.tsx | 16 +- .../service_overview_errors_table/index.tsx | 97 ++- .../service_overview_throughput_chart.tsx | 73 +- .../get_columns.tsx | 5 +- .../transaction_error_rate_chart/index.tsx | 78 +- .../get_service_map_service_node_info.ts | 3 + ...rvice_error_group_comparison_statistics.ts | 88 ++- .../lib/transaction_groups/get_error_rate.ts | 73 +- x-pack/plugins/apm/server/routes/services.ts | 17 +- .../plugins/apm/server/routes/transactions.ts | 12 +- .../error_groups_comparison_statistics.snap | 72 ++ .../error_groups_comparison_statistics.ts | 94 ++- .../__snapshots__/error_rate.snap | 738 ++++++++++++++++++ .../tests/transactions/error_rate.ts | 175 ++++- 14 files changed, 1432 insertions(+), 109 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx index 94913c1678d21..fd1120808db9e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/get_column.tsx @@ -22,9 +22,11 @@ type ErrorGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serv export function getColumns({ serviceName, errorGroupComparisonStatistics, + comparisonEnabled, }: { serviceName: string; errorGroupComparisonStatistics: ErrorGroupComparisonStatistics; + comparisonEnabled?: boolean; }): Array> { return [ { @@ -71,12 +73,17 @@ export function getColumns({ ), width: px(unit * 12), render: (_, { occurrences, group_id: errorGroupId }) => { - const timeseries = - errorGroupComparisonStatistics?.[errorGroupId]?.timeseries; + const currentPeriodTimeseries = + errorGroupComparisonStatistics?.currentPeriod?.[errorGroupId] + ?.timeseries; + const previousPeriodTimeseries = + errorGroupComparisonStatistics?.previousPeriod?.[errorGroupId] + ?.timeseries; + return ( ); }, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index bbd36a4c8df93..d36bee8d6be73 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -18,14 +18,18 @@ import uuid from 'uuid'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; +import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time_range_comparison'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; import { getColumns } from './get_column'; interface Props { serviceName: string; } +type ErrorGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/primary_statistics'>; +type ErrorGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics'>; type SortDirection = 'asc' | 'desc'; type SortField = 'name' | 'last_seen' | 'occurrences'; @@ -36,14 +40,31 @@ const DEFAULT_SORT = { field: 'occurrences' as const, }; -const INITIAL_STATE = { +const INITIAL_STATE_PRIMARY_STATISTICS: { + items: ErrorGroupPrimaryStatistics['error_groups']; + totalItems: number; + requestId?: string; +} = { items: [], + totalItems: 0, requestId: undefined, }; +const INITIAL_STATE_COMPARISON_STATISTICS: ErrorGroupComparisonStatistics = { + currentPeriod: {}, + previousPeriod: {}, +}; + export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { - urlParams: { environment, kuery, start, end }, + urlParams: { + environment, + kuery, + start, + end, + comparisonType, + comparisonEnabled, + }, } = useUrlParams(); const { transactionType } = useApmServiceContext(); const [tableOptions, setTableOptions] = useState<{ @@ -57,9 +78,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { sort: DEFAULT_SORT, }); + const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonType, + }); + const { pageIndex, sort } = tableOptions; + const { direction, field } = sort; - const { data = INITIAL_STATE, status } = useFetcher( + const { data = INITIAL_STATE_PRIMARY_STATISTICS, status } = useFetcher( (callApmApi) => { if (!start || !end || !transactionType) { return; @@ -78,37 +106,43 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { }, }, }).then((response) => { + const currentPageErrorGroups = orderBy( + response.error_groups, + field, + direction + ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + return { requestId: uuid(), - items: response.error_groups, + items: currentPageErrorGroups, + totalItems: response.error_groups.length, }; }); }, - [environment, kuery, start, end, serviceName, transactionType] + // comparisonType is listed as dependency even thought it is not used. This is needed to trigger the comparison api when it is changed. + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + environment, + kuery, + start, + end, + serviceName, + transactionType, + pageIndex, + direction, + field, + comparisonType, + ] ); - const { requestId, items } = data; - const currentPageErrorGroups = orderBy( - items, - sort.field, - sort.direction - ).slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE); + const { requestId, items, totalItems } = data; - const groupIds = JSON.stringify( - currentPageErrorGroups.map(({ group_id: groupId }) => groupId).sort() - ); const { - data: errorGroupComparisonStatistics, + data: errorGroupComparisonStatistics = INITIAL_STATE_COMPARISON_STATISTICS, status: errorGroupComparisonStatisticsStatus, } = useFetcher( (callApmApi) => { - if ( - requestId && - currentPageErrorGroups.length && - start && - end && - transactionType - ) { + if (requestId && items.length && start && end && transactionType) { return callApmApi({ endpoint: 'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics', @@ -121,21 +155,26 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { end, numBuckets: 20, transactionType, - groupIds, + groupIds: JSON.stringify( + items.map(({ group_id: groupId }) => groupId).sort() + ), + comparisonStart, + comparisonEnd, }, }, }); } }, - // only fetches agg results when requestId or group ids change + // only fetches agg results when requestId changes // eslint-disable-next-line react-hooks/exhaustive-deps - [requestId, groupIds], + [requestId], { preservePreviousData: false } ); const columns = getColumns({ serviceName, - errorGroupComparisonStatistics: errorGroupComparisonStatistics ?? {}, + errorGroupComparisonStatistics, + comparisonEnabled, }); return ( @@ -164,16 +203,16 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { (); const { - urlParams: { environment, kuery, start, end }, + urlParams: { + environment, + kuery, + start, + end, + comparisonEnabled, + comparisonType, + }, } = useUrlParams(); const { transactionType } = useApmServiceContext(); + const comparisonChartTheme = getComparisonChartTheme(theme); + const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonType, + }); const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { @@ -48,14 +65,49 @@ export function ServiceOverviewThroughputChart({ start, end, transactionType, + comparisonStart, + comparisonEnd, }, }, }); } }, - [environment, kuery, serviceName, start, end, transactionType] + [ + environment, + kuery, + serviceName, + start, + end, + transactionType, + comparisonStart, + comparisonEnd, + ] ); + const timeseries = [ + { + data: data.currentPeriod, + type: 'linemark', + color: theme.eui.euiColorVis0, + title: i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', { + defaultMessage: 'Throughput', + }), + }, + ...(comparisonEnabled + ? [ + { + data: data.previousPeriod, + type: 'area', + color: theme.eui.euiColorLightestShade, + title: i18n.translate( + 'xpack.apm.serviceOverview.throughtputChart.previousPeriodLabel', + { defaultMessage: 'Previous period' } + ), + }, + ] + : []), + ]; + return ( @@ -70,18 +122,9 @@ export function ServiceOverviewThroughputChart({ height={height} showAnnotations={false} fetchStatus={status} - timeseries={[ - { - data: data.currentPeriod, - type: 'linemark', - color: theme.eui.euiColorVis0, - title: i18n.translate( - 'xpack.apm.serviceOverview.throughtputChartTitle', - { defaultMessage: 'Throughput' } - ), - }, - ]} + timeseries={timeseries} yLabelFormat={asTransactionRate} + customTheme={comparisonChartTheme} /> ); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx index bff45b5d274c3..d9ca3356d7fd2 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx @@ -159,14 +159,13 @@ export function getColumns({ transactionGroupComparisonStatistics?.currentPeriod?.[name]?.impact ?? 0; const previousImpact = - transactionGroupComparisonStatistics?.previousPeriod?.[name] - ?.impact ?? 0; + transactionGroupComparisonStatistics?.previousPeriod?.[name]?.impact; return ( - {comparisonEnabled && ( + {comparisonEnabled && previousImpact && ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index b3c38651ea178..fd9435db57bfd 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -9,12 +9,17 @@ import { EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asPercent } from '../../../../../common/utils/formatters'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { TimeseriesChart } from '../timeseries_chart'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { + getComparisonChartTheme, + getTimeRangeComparison, +} from '../../time_comparison/get_time_range_comparison'; function yLabelFormat(y?: number | null) { return asPercent(y || 0, 1); @@ -25,6 +30,21 @@ interface Props { showAnnotations?: boolean; } +type ErrorRate = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/error_rate'>; + +const INITIAL_STATE: ErrorRate = { + currentPeriod: { + noHits: true, + transactionErrorRate: [], + average: null, + }, + previousPeriod: { + noHits: true, + transactionErrorRate: [], + average: null, + }, +}; + export function TransactionErrorRateChart({ height, showAnnotations = true, @@ -32,11 +52,25 @@ export function TransactionErrorRateChart({ const theme = useTheme(); const { serviceName } = useParams<{ serviceName?: string }>(); const { - urlParams: { environment, kuery, start, end, transactionName }, + urlParams: { + environment, + kuery, + start, + end, + transactionName, + comparisonEnabled, + comparisonType, + }, } = useUrlParams(); const { transactionType } = useApmServiceContext(); + const comparisonChartThem = getComparisonChartTheme(theme); + const { comparisonStart, comparisonEnd } = getTimeRangeComparison({ + start, + end, + comparisonType, + }); - const { data, status } = useFetcher( + const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { if (transactionType && serviceName && start && end) { return callApmApi({ @@ -53,6 +87,8 @@ export function TransactionErrorRateChart({ end, transactionType, transactionName, + comparisonStart, + comparisonEnd, }, }, }); @@ -66,10 +102,34 @@ export function TransactionErrorRateChart({ end, transactionType, transactionName, + comparisonStart, + comparisonEnd, ] ); - const errorRates = data?.transactionErrorRate || []; + const timeseries = [ + { + data: data.currentPeriod.transactionErrorRate, + type: 'linemark', + color: theme.eui.euiColorVis7, + title: i18n.translate('xpack.apm.errorRate.chart.errorRate', { + defaultMessage: 'Error rate (avg.)', + }), + }, + ...(comparisonEnabled + ? [ + { + data: data.previousPeriod.transactionErrorRate, + type: 'area', + color: theme.eui.euiColorLightestShade, + title: i18n.translate( + 'xpack.apm.errorRate.chart.errorRate.previousPeriodLabel', + { defaultMessage: 'Previous period' } + ), + }, + ] + : []), + ]; return ( @@ -85,18 +145,10 @@ export function TransactionErrorRateChart({ height={height} showAnnotations={showAnnotations} fetchStatus={status} - timeseries={[ - { - data: errorRates, - type: 'linemark', - color: theme.eui.euiColorVis7, - title: i18n.translate('xpack.apm.errorRate.chart.errorRate', { - defaultMessage: 'Error rate (avg.)', - }), - }, - ]} + timeseries={timeseries} yLabelFormat={yLabelFormat} yDomain={{ min: 0, max: 1 }} + customTheme={comparisonChartThem} /> ); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index a6e7832bf697d..7da3ce772ef5f 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -106,11 +106,14 @@ async function getErrorStats({ searchAggregatedTransactions: boolean; }) { return withApmSpan('get_error_rate_for_service_map_node', async () => { + const { start, end } = setup; const { noHits, average } = await getErrorRate({ environment, setup, serviceName, searchAggregatedTransactions, + start, + end, }); return { avgErrorRate: noHits ? null : average }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts index e33044bff8ffa..b559f55bbe78e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_comparison_statistics.ts @@ -5,6 +5,7 @@ * 2.0. */ import { keyBy } from 'lodash'; +import { Coordinate } from '../../../../typings/timeseries'; import { ERROR_GROUP_ID, SERVICE_NAME, @@ -16,6 +17,7 @@ import { rangeQuery, kqlQuery, } from '../../../../server/utils/queries'; +import { offsetPreviousPeriodCoordinates } from '../../../utils/offset_previous_period_coordinate'; import { withApmSpan } from '../../../utils/with_apm_span'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -28,19 +30,23 @@ export async function getServiceErrorGroupComparisonStatistics({ transactionType, groupIds, environment, + start, + end, }: { kuery?: string; serviceName: string; - setup: Setup & SetupTimeRange; + setup: Setup; numBuckets: number; transactionType: string; groupIds: string[]; environment?: string; -}) { + start: number; + end: number; +}): Promise> { return withApmSpan( 'get_service_error_group_comparison_statistics', async () => { - const { apmEventClient, start, end } = setup; + const { apmEventClient } = setup; const { intervalString } = getBucketSize({ start, end, numBuckets }); @@ -87,10 +93,10 @@ export async function getServiceErrorGroupComparisonStatistics({ }); if (!timeseriesResponse.aggregations) { - return {}; + return []; } - const groups = timeseriesResponse.aggregations.error_groups.buckets.map( + return timeseriesResponse.aggregations.error_groups.buckets.map( (bucket) => { const groupId = bucket.key as string; return { @@ -104,8 +110,76 @@ export async function getServiceErrorGroupComparisonStatistics({ }; } ); - - return keyBy(groups, 'groupId'); } ); } + +export async function getServiceErrorGroupPeriods({ + kuery, + serviceName, + setup, + numBuckets, + transactionType, + groupIds, + environment, + comparisonStart, + comparisonEnd, +}: { + kuery?: string; + serviceName: string; + setup: Setup & SetupTimeRange; + numBuckets: number; + transactionType: string; + groupIds: string[]; + environment?: string; + comparisonStart?: number; + comparisonEnd?: number; +}) { + const { start, end } = setup; + + const commonProps = { + environment, + kuery, + serviceName, + setup, + numBuckets, + transactionType, + groupIds, + }; + + const currentPeriodPromise = getServiceErrorGroupComparisonStatistics({ + ...commonProps, + start, + end, + }); + + const previousPeriodPromise = + comparisonStart && comparisonEnd + ? getServiceErrorGroupComparisonStatistics({ + ...commonProps, + start: comparisonStart, + end: comparisonEnd, + }) + : []; + + const [currentPeriod, previousPeriod] = await Promise.all([ + currentPeriodPromise, + previousPeriodPromise, + ]); + + const firtCurrentPeriod = currentPeriod.length ? currentPeriod[0] : undefined; + + return { + currentPeriod: keyBy(currentPeriod, 'groupId'), + previousPeriod: keyBy( + previousPeriod.map((errorRateGroup) => ({ + ...errorRateGroup, + timeseries: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: firtCurrentPeriod?.timeseries, + previousPeriodTimeseries: errorRateGroup.timeseries, + }), + })), + 'groupId' + ), + }; +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 627086df9d681..ec5dd1308cb7e 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -31,6 +31,7 @@ import { getTransactionErrorRateTimeSeries, } from '../helpers/transaction_error_rate'; import { withApmSpan } from '../../utils/with_apm_span'; +import { offsetPreviousPeriodCoordinates } from '../../utils/offset_previous_period_coordinate'; export async function getErrorRate({ environment, @@ -40,21 +41,25 @@ export async function getErrorRate({ transactionName, setup, searchAggregatedTransactions, + start, + end, }: { environment?: string; kuery?: string; serviceName: string; transactionType?: string; transactionName?: string; - setup: Setup & SetupTimeRange; + setup: Setup; searchAggregatedTransactions: boolean; + start: number; + end: number; }): Promise<{ noHits: boolean; transactionErrorRate: Coordinate[]; average: number | null; }> { return withApmSpan('get_transaction_group_error_rate', async () => { - const { start, end, apmEventClient } = setup; + const { apmEventClient } = setup; const transactionNamefilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] @@ -129,3 +134,67 @@ export async function getErrorRate({ return { noHits, transactionErrorRate, average }; }); } + +export async function getErrorRatePeriods({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, + comparisonStart, + comparisonEnd, +}: { + environment?: string; + kuery?: string; + serviceName: string; + transactionType?: string; + transactionName?: string; + setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; + comparisonStart?: number; + comparisonEnd?: number; +}) { + const { start, end } = setup; + const commonProps = { + environment, + kuery, + serviceName, + transactionType, + transactionName, + setup, + searchAggregatedTransactions, + }; + + const currentPeriodPromise = getErrorRate({ ...commonProps, start, end }); + + const previousPeriodPromise = + comparisonStart && comparisonEnd + ? getErrorRate({ + ...commonProps, + start: comparisonStart, + end: comparisonEnd, + }) + : { noHits: true, transactionErrorRate: [], average: null }; + + const [currentPeriod, previousPeriod] = await Promise.all([ + currentPeriodPromise, + previousPeriodPromise, + ]); + + const firtCurrentPeriod = currentPeriod.transactionErrorRate.length + ? currentPeriod.transactionErrorRate + : undefined; + + return { + currentPeriod, + previousPeriod: { + ...previousPeriod, + transactionErrorRate: offsetPreviousPeriodCoordinates({ + currentPeriodTimeseries: firtCurrentPeriod, + previousPeriodTimeseries: previousPeriod.transactionErrorRate, + }), + }, + }; +} diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index a84c8dc274248..bac970416792b 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -17,7 +17,7 @@ import { getServices } from '../lib/services/get_services'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServiceDependencies } from '../lib/services/get_service_dependencies'; import { getServiceErrorGroupPrimaryStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_primary_statistics'; -import { getServiceErrorGroupComparisonStatistics } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics'; +import { getServiceErrorGroupPeriods } from '../lib/services/get_service_error_groups/get_service_error_group_comparison_statistics'; import { getServiceInstances } from '../lib/services/get_service_instances'; import { getServiceMetadataDetails } from '../lib/services/get_service_metadata_details'; import { getServiceMetadataIcons } from '../lib/services/get_service_metadata_icons'; @@ -329,6 +329,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ environmentRt, kueryRt, rangeRt, + comparisonRangeRt, t.type({ numBuckets: toNumberRt, transactionType: t.string, @@ -342,10 +343,18 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ const { path: { serviceName }, - query: { environment, kuery, numBuckets, transactionType, groupIds }, + query: { + environment, + kuery, + numBuckets, + transactionType, + groupIds, + comparisonStart, + comparisonEnd, + }, } = context.params; - return getServiceErrorGroupComparisonStatistics({ + return getServiceErrorGroupPeriods({ environment, kuery, serviceName, @@ -353,6 +362,8 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({ numBuckets, transactionType, groupIds, + comparisonStart, + comparisonEnd, }); }, }); diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 1571efb373cc9..f3424a252e409 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -22,7 +22,7 @@ import { getAnomalySeries } from '../lib/transactions/get_anomaly_data'; import { getLatencyPeriods } from '../lib/transactions/get_latency_charts'; import { getThroughputCharts } from '../lib/transactions/get_throughput_charts'; import { getTransactionGroupList } from '../lib/transaction_groups'; -import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; +import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate'; import { createRoute } from './create_route'; import { comparisonRangeRt, @@ -380,11 +380,9 @@ export const transactionChartsErrorRateRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ - environmentRt, - kueryRt, - rangeRt, t.type({ transactionType: t.string }), t.partial({ transactionName: t.string }), + t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]), ]), }), options: { tags: ['access:apm'] }, @@ -397,13 +395,15 @@ export const transactionChartsErrorRateRoute = createRoute({ kuery, transactionType, transactionName, + comparisonStart, + comparisonEnd, } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); - return getErrorRate({ + return getErrorRatePeriods({ environment, kuery, serviceName, @@ -411,6 +411,8 @@ export const transactionChartsErrorRateRoute = createRoute({ transactionName, setup, searchAggregatedTransactions, + comparisonStart, + comparisonEnd, }); }, }); diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap index a536a6de67ff3..31bc29a2476ca 100644 --- a/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/error_groups_comparison_statistics.snap @@ -131,3 +131,75 @@ Object { ], } `; + +exports[`APM API tests basic apm_8.0.0 Error groups comparison statistics when data is loaded with previous data returns the correct data returns correct timeseries 1`] = ` +Object { + "groupId": "051f95eabf120ebe2f8b0399fe3e54c5", + "timeseries": Array [ + Object { + "x": 1607436720000, + "y": 0, + }, + Object { + "x": 1607436780000, + "y": 0, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436900000, + "y": 0, + }, + Object { + "x": 1607436960000, + "y": 0, + }, + Object { + "x": 1607437020000, + "y": 0, + }, + Object { + "x": 1607437080000, + "y": 0, + }, + Object { + "x": 1607437140000, + "y": 0, + }, + Object { + "x": 1607437200000, + "y": 2, + }, + Object { + "x": 1607437260000, + "y": 0, + }, + Object { + "x": 1607437320000, + "y": 1, + }, + Object { + "x": 1607437380000, + "y": 0, + }, + Object { + "x": 1607437440000, + "y": 0, + }, + Object { + "x": 1607437500000, + "y": 0, + }, + Object { + "x": 1607437560000, + "y": 0, + }, + Object { + "x": 1607437620000, + "y": 0, + }, + ], +} +`; diff --git a/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts index 4a19efac5a809..821d0515aa808 100644 --- a/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts +++ b/x-pack/test/apm_api_integration/tests/services/error_groups_comparison_statistics.ts @@ -7,6 +7,7 @@ import url from 'url'; import expect from '@kbn/expect'; +import moment from 'moment'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; @@ -45,8 +46,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }) ); + expect(response.status).to.be(200); - expect(response.body).to.empty(); + expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); }); } ); @@ -72,20 +74,23 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics; - expect(Object.keys(errorGroupsComparisonStatistics).sort()).to.eql(groupIds.sort()); + expect(Object.keys(errorGroupsComparisonStatistics.currentPeriod).sort()).to.eql( + groupIds.sort() + ); groupIds.forEach((groupId) => { - expect(errorGroupsComparisonStatistics[groupId]).not.to.be.empty(); + expect(errorGroupsComparisonStatistics.currentPeriod[groupId]).not.to.be.empty(); }); - const errorgroupsComparisonStatistics = errorGroupsComparisonStatistics[groupIds[0]]; + const errorgroupsComparisonStatistics = + errorGroupsComparisonStatistics.currentPeriod[groupIds[0]]; expect( - errorgroupsComparisonStatistics.timeseries.map(({ y }) => isFinite(y)).length + errorgroupsComparisonStatistics.timeseries.map(({ y }) => y && isFinite(y)).length ).to.be.greaterThan(0); expectSnapshot(errorgroupsComparisonStatistics).toMatch(); }); - it('returns an empty list when requested groupIds are not available in the given time range', async () => { + it('returns an empty state when requested groupIds are not available in the given time range', async () => { const response = await supertest.get( url.format({ pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, @@ -100,7 +105,82 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.empty(); + expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); + }); + } + ); + + registry.when( + 'Error groups comparison statistics when data is loaded with previous data', + { config: 'basic', archives: [archiveName] }, + () => { + describe('returns the correct data', async () => { + let response: { + status: number; + body: ErrorGroupsComparisonStatistics; + }; + before(async () => { + response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(groupIds), + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + comparisonStart: start, + comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + }, + }) + ); + + expect(response.status).to.be(200); + }); + + it('returns correct timeseries', () => { + const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics; + const errorgroupsComparisonStatistics = + errorGroupsComparisonStatistics.currentPeriod[groupIds[0]]; + expect( + errorgroupsComparisonStatistics.timeseries.map(({ y }) => y && isFinite(y)).length + ).to.be.greaterThan(0); + expectSnapshot(errorgroupsComparisonStatistics).toMatch(); + }); + + it('matches x-axis on current period and previous period', () => { + const errorGroupsComparisonStatistics = response.body as ErrorGroupsComparisonStatistics; + + const currentPeriodItems = Object.values(errorGroupsComparisonStatistics.currentPeriod); + const previousPeriodItems = Object.values(errorGroupsComparisonStatistics.previousPeriod); + + const currentPeriodFirstItem = currentPeriodItems[0]; + const previousPeriodFirstItem = previousPeriodItems[0]; + + expect(currentPeriodFirstItem.timeseries.map(({ x }) => x)).to.be.eql( + previousPeriodFirstItem.timeseries.map(({ x }) => x) + ); + }); + }); + + it('returns an empty state when requested groupIds are not available in the given time range', async () => { + const response = await supertest.get( + url.format({ + pathname: `/api/apm/services/opbeans-java/error_groups/comparison_statistics`, + query: { + numBuckets: 20, + transactionType: 'request', + groupIds: JSON.stringify(['foo']), + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + comparisonStart: start, + comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + }, + }) + ); + + expect(response.status).to.be(200); + expect(response.body).to.be.eql({ currentPeriod: {}, previousPeriod: {} }); }); } ); diff --git a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap index d97d39cda1b8d..7ec68bbc0a9fd 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap +++ b/x-pack/test/apm_api_integration/tests/transactions/__snapshots__/error_rate.snap @@ -248,3 +248,741 @@ Array [ }, ] `; + +exports[`APM API tests basic apm_8.0.0 Error rate when data is loaded returns the transaction error rate with comparison data has the correct error rate 1`] = ` +Array [ + Object { + "x": 1607436770000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436790000, + "y": null, + }, + Object { + "x": 1607436800000, + "y": null, + }, + Object { + "x": 1607436810000, + "y": null, + }, + Object { + "x": 1607436820000, + "y": 0, + }, + Object { + "x": 1607436830000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": null, + }, + Object { + "x": 1607436850000, + "y": null, + }, + Object { + "x": 1607436860000, + "y": 1, + }, + Object { + "x": 1607436870000, + "y": 0, + }, + Object { + "x": 1607436880000, + "y": 0, + }, + Object { + "x": 1607436890000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436910000, + "y": null, + }, + Object { + "x": 1607436920000, + "y": null, + }, + Object { + "x": 1607436930000, + "y": null, + }, + Object { + "x": 1607436940000, + "y": null, + }, + Object { + "x": 1607436950000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607436970000, + "y": null, + }, + Object { + "x": 1607436980000, + "y": 0, + }, + Object { + "x": 1607436990000, + "y": 0, + }, + Object { + "x": 1607437000000, + "y": 0, + }, + Object { + "x": 1607437010000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437030000, + "y": null, + }, + Object { + "x": 1607437040000, + "y": null, + }, + Object { + "x": 1607437050000, + "y": null, + }, + Object { + "x": 1607437060000, + "y": null, + }, + Object { + "x": 1607437070000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437090000, + "y": null, + }, + Object { + "x": 1607437100000, + "y": 0, + }, + Object { + "x": 1607437110000, + "y": 0, + }, + Object { + "x": 1607437120000, + "y": null, + }, + Object { + "x": 1607437130000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437150000, + "y": null, + }, + Object { + "x": 1607437160000, + "y": null, + }, + Object { + "x": 1607437170000, + "y": null, + }, + Object { + "x": 1607437180000, + "y": null, + }, + Object { + "x": 1607437190000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437210000, + "y": null, + }, + Object { + "x": 1607437220000, + "y": 0, + }, + Object { + "x": 1607437230000, + "y": 0.6, + }, + Object { + "x": 1607437240000, + "y": 0, + }, + Object { + "x": 1607437250000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437270000, + "y": 0, + }, + Object { + "x": 1607437280000, + "y": null, + }, + Object { + "x": 1607437290000, + "y": null, + }, + Object { + "x": 1607437300000, + "y": null, + }, + Object { + "x": 1607437310000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437330000, + "y": null, + }, + Object { + "x": 1607437340000, + "y": 0, + }, + Object { + "x": 1607437350000, + "y": null, + }, + Object { + "x": 1607437360000, + "y": 0.5, + }, + Object { + "x": 1607437370000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437390000, + "y": null, + }, + Object { + "x": 1607437400000, + "y": null, + }, + Object { + "x": 1607437410000, + "y": null, + }, + Object { + "x": 1607437420000, + "y": null, + }, + Object { + "x": 1607437430000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437450000, + "y": null, + }, + Object { + "x": 1607437460000, + "y": 1, + }, + Object { + "x": 1607437470000, + "y": 0, + }, + Object { + "x": 1607437480000, + "y": 1, + }, + Object { + "x": 1607437490000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437510000, + "y": null, + }, + Object { + "x": 1607437520000, + "y": null, + }, + Object { + "x": 1607437530000, + "y": null, + }, + Object { + "x": 1607437540000, + "y": null, + }, + Object { + "x": 1607437550000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437570000, + "y": 0, + }, + Object { + "x": 1607437580000, + "y": null, + }, + Object { + "x": 1607437590000, + "y": null, + }, + Object { + "x": 1607437600000, + "y": null, + }, + Object { + "x": 1607437610000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, + Object { + "x": 1607437630000, + "y": null, + }, + Object { + "x": 1607437640000, + "y": null, + }, + Object { + "x": 1607437650000, + "y": null, + }, + Object { + "x": 1607437660000, + "y": null, + }, + Object { + "x": 1607437670000, + "y": null, + }, +] +`; + +exports[`APM API tests basic apm_8.0.0 Error rate when data is loaded returns the transaction error rate with comparison data has the correct error rate 2`] = ` +Array [ + Object { + "x": 1607436770000, + "y": null, + }, + Object { + "x": 1607436780000, + "y": null, + }, + Object { + "x": 1607436790000, + "y": null, + }, + Object { + "x": 1607436800000, + "y": 0, + }, + Object { + "x": 1607436810000, + "y": null, + }, + Object { + "x": 1607436820000, + "y": null, + }, + Object { + "x": 1607436830000, + "y": null, + }, + Object { + "x": 1607436840000, + "y": 0, + }, + Object { + "x": 1607436850000, + "y": null, + }, + Object { + "x": 1607436860000, + "y": null, + }, + Object { + "x": 1607436870000, + "y": null, + }, + Object { + "x": 1607436880000, + "y": null, + }, + Object { + "x": 1607436890000, + "y": null, + }, + Object { + "x": 1607436900000, + "y": null, + }, + Object { + "x": 1607436910000, + "y": 0, + }, + Object { + "x": 1607436920000, + "y": 0, + }, + Object { + "x": 1607436930000, + "y": 0, + }, + Object { + "x": 1607436940000, + "y": null, + }, + Object { + "x": 1607436950000, + "y": null, + }, + Object { + "x": 1607436960000, + "y": null, + }, + Object { + "x": 1607436970000, + "y": null, + }, + Object { + "x": 1607436980000, + "y": null, + }, + Object { + "x": 1607436990000, + "y": null, + }, + Object { + "x": 1607437000000, + "y": null, + }, + Object { + "x": 1607437010000, + "y": null, + }, + Object { + "x": 1607437020000, + "y": null, + }, + Object { + "x": 1607437030000, + "y": 0, + }, + Object { + "x": 1607437040000, + "y": 0, + }, + Object { + "x": 1607437050000, + "y": null, + }, + Object { + "x": 1607437060000, + "y": null, + }, + Object { + "x": 1607437070000, + "y": null, + }, + Object { + "x": 1607437080000, + "y": null, + }, + Object { + "x": 1607437090000, + "y": null, + }, + Object { + "x": 1607437100000, + "y": null, + }, + Object { + "x": 1607437110000, + "y": null, + }, + Object { + "x": 1607437120000, + "y": null, + }, + Object { + "x": 1607437130000, + "y": null, + }, + Object { + "x": 1607437140000, + "y": null, + }, + Object { + "x": 1607437150000, + "y": null, + }, + Object { + "x": 1607437160000, + "y": 0, + }, + Object { + "x": 1607437170000, + "y": null, + }, + Object { + "x": 1607437180000, + "y": null, + }, + Object { + "x": 1607437190000, + "y": null, + }, + Object { + "x": 1607437200000, + "y": null, + }, + Object { + "x": 1607437210000, + "y": null, + }, + Object { + "x": 1607437220000, + "y": null, + }, + Object { + "x": 1607437230000, + "y": null, + }, + Object { + "x": 1607437240000, + "y": 1, + }, + Object { + "x": 1607437250000, + "y": null, + }, + Object { + "x": 1607437260000, + "y": null, + }, + Object { + "x": 1607437270000, + "y": null, + }, + Object { + "x": 1607437280000, + "y": 0, + }, + Object { + "x": 1607437290000, + "y": 0, + }, + Object { + "x": 1607437300000, + "y": null, + }, + Object { + "x": 1607437310000, + "y": null, + }, + Object { + "x": 1607437320000, + "y": null, + }, + Object { + "x": 1607437330000, + "y": null, + }, + Object { + "x": 1607437340000, + "y": null, + }, + Object { + "x": 1607437350000, + "y": null, + }, + Object { + "x": 1607437360000, + "y": null, + }, + Object { + "x": 1607437370000, + "y": null, + }, + Object { + "x": 1607437380000, + "y": null, + }, + Object { + "x": 1607437390000, + "y": null, + }, + Object { + "x": 1607437400000, + "y": 0, + }, + Object { + "x": 1607437410000, + "y": 0, + }, + Object { + "x": 1607437420000, + "y": 0.25, + }, + Object { + "x": 1607437430000, + "y": null, + }, + Object { + "x": 1607437440000, + "y": null, + }, + Object { + "x": 1607437450000, + "y": null, + }, + Object { + "x": 1607437460000, + "y": null, + }, + Object { + "x": 1607437470000, + "y": null, + }, + Object { + "x": 1607437480000, + "y": null, + }, + Object { + "x": 1607437490000, + "y": null, + }, + Object { + "x": 1607437500000, + "y": null, + }, + Object { + "x": 1607437510000, + "y": null, + }, + Object { + "x": 1607437520000, + "y": 0.5, + }, + Object { + "x": 1607437530000, + "y": 0.2, + }, + Object { + "x": 1607437540000, + "y": 0, + }, + Object { + "x": 1607437550000, + "y": null, + }, + Object { + "x": 1607437560000, + "y": null, + }, + Object { + "x": 1607437570000, + "y": null, + }, + Object { + "x": 1607437580000, + "y": null, + }, + Object { + "x": 1607437590000, + "y": null, + }, + Object { + "x": 1607437600000, + "y": null, + }, + Object { + "x": 1607437610000, + "y": null, + }, + Object { + "x": 1607437620000, + "y": null, + }, + Object { + "x": 1607437630000, + "y": null, + }, + Object { + "x": 1607437640000, + "y": null, + }, + Object { + "x": 1607437650000, + "y": 1, + }, + Object { + "x": 1607437660000, + "y": 0, + }, + Object { + "x": 1607437670000, + "y": null, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts index 2b94816466aa7..ce16ad2c96c3b 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/error_rate.ts @@ -8,10 +8,14 @@ import expect from '@kbn/expect'; import { first, last } from 'lodash'; import { format } from 'url'; +import moment from 'moment'; +import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; +type ErrorRate = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/error_rate'>; + export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -21,20 +25,43 @@ export default function ApiTest({ getService }: FtrProviderContext) { const { start, end } = archives_metadata[archiveName]; const transactionType = 'request'; - const url = format({ - pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', - query: { start, end, transactionType }, - }); - registry.when('Error rate when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get(url); + const response = await supertest.get( + format({ + pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', + query: { start, end, transactionType }, + }) + ); expect(response.status).to.be(200); - expect(response.body.noHits).to.be(true); + const body = response.body as ErrorRate; + expect(body).to.be.eql({ + currentPeriod: { noHits: true, transactionErrorRate: [], average: null }, + previousPeriod: { noHits: true, transactionErrorRate: [], average: null }, + }); + }); + + it('handles the empty state with comparison data', async () => { + const response = await supertest.get( + format({ + pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', + query: { + transactionType, + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + comparisonStart: start, + comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + }, + }) + ); + expect(response.status).to.be(200); - expect(response.body.transactionErrorRate.length).to.be(0); - expect(response.body.average).to.be(null); + const body = response.body as ErrorRate; + expect(body).to.be.eql({ + currentPeriod: { noHits: true, transactionErrorRate: [], average: null }, + previousPeriod: { noHits: true, transactionErrorRate: [], average: null }, + }); }); }); @@ -43,22 +70,26 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { describe('returns the transaction error rate', () => { - let errorRateResponse: { - transactionErrorRate: Array<{ x: number; y: number | null }>; - average: number; - }; + let errorRateResponse: ErrorRate; before(async () => { - const response = await supertest.get(url); + const response = await supertest.get( + format({ + pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', + query: { start, end, transactionType }, + }) + ); errorRateResponse = response.body; }); it('returns some data', () => { - expect(errorRateResponse.average).to.be.greaterThan(0); + expect(errorRateResponse.currentPeriod.average).to.be.greaterThan(0); + expect(errorRateResponse.previousPeriod.average).to.be(null); - expect(errorRateResponse.transactionErrorRate.length).to.be.greaterThan(0); + expect(errorRateResponse.currentPeriod.transactionErrorRate.length).to.be.greaterThan(0); + expect(errorRateResponse.previousPeriod.transactionErrorRate).to.empty(); - const nonNullDataPoints = errorRateResponse.transactionErrorRate.filter( + const nonNullDataPoints = errorRateResponse.currentPeriod.transactionErrorRate.filter( ({ y }) => y !== null ); @@ -67,26 +98,126 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has the correct start date', () => { expectSnapshot( - new Date(first(errorRateResponse.transactionErrorRate)?.x ?? NaN).toISOString() + new Date( + first(errorRateResponse.currentPeriod.transactionErrorRate)?.x ?? NaN + ).toISOString() ).toMatchInline(`"2020-12-08T13:57:30.000Z"`); }); it('has the correct end date', () => { expectSnapshot( - new Date(last(errorRateResponse.transactionErrorRate)?.x ?? NaN).toISOString() + new Date( + last(errorRateResponse.currentPeriod.transactionErrorRate)?.x ?? NaN + ).toISOString() ).toMatchInline(`"2020-12-08T14:27:30.000Z"`); }); it('has the correct number of buckets', () => { - expectSnapshot(errorRateResponse.transactionErrorRate.length).toMatchInline(`61`); + expectSnapshot(errorRateResponse.currentPeriod.transactionErrorRate.length).toMatchInline( + `61` + ); + }); + + it('has the correct calculation for average', () => { + expectSnapshot(errorRateResponse.currentPeriod.average).toMatchInline(`0.16`); + }); + + it('has the correct error rate', () => { + expectSnapshot(errorRateResponse.currentPeriod.transactionErrorRate).toMatch(); + }); + }); + + describe('returns the transaction error rate with comparison data', () => { + let errorRateResponse: ErrorRate; + + before(async () => { + const response = await supertest.get( + format({ + pathname: '/api/apm/services/opbeans-java/transactions/charts/error_rate', + query: { + transactionType, + start: moment(end).subtract(15, 'minutes').toISOString(), + end, + comparisonStart: start, + comparisonEnd: moment(start).add(15, 'minutes').toISOString(), + }, + }) + ); + errorRateResponse = response.body; + }); + + it('returns some data', () => { + expect(errorRateResponse.currentPeriod.average).to.be.greaterThan(0); + expect(errorRateResponse.previousPeriod.average).to.be.greaterThan(0); + + expect(errorRateResponse.currentPeriod.transactionErrorRate.length).to.be.greaterThan(0); + expect(errorRateResponse.previousPeriod.transactionErrorRate.length).to.be.greaterThan(0); + + const currentPeriodNonNullDataPoints = errorRateResponse.currentPeriod.transactionErrorRate.filter( + ({ y }) => y !== null + ); + + const previousPeriodNonNullDataPoints = errorRateResponse.previousPeriod.transactionErrorRate.filter( + ({ y }) => y !== null + ); + + expect(currentPeriodNonNullDataPoints.length).to.be.greaterThan(0); + expect(previousPeriodNonNullDataPoints.length).to.be.greaterThan(0); + }); + + it('has the correct start date', () => { + expectSnapshot( + new Date( + first(errorRateResponse.currentPeriod.transactionErrorRate)?.x ?? NaN + ).toISOString() + ).toMatchInline(`"2020-12-08T14:12:50.000Z"`); + expectSnapshot( + new Date( + first(errorRateResponse.previousPeriod.transactionErrorRate)?.x ?? NaN + ).toISOString() + ).toMatchInline(`"2020-12-08T14:12:50.000Z"`); + }); + + it('has the correct end date', () => { + expectSnapshot( + new Date( + last(errorRateResponse.currentPeriod.transactionErrorRate)?.x ?? NaN + ).toISOString() + ).toMatchInline(`"2020-12-08T14:27:50.000Z"`); + expectSnapshot( + new Date( + last(errorRateResponse.previousPeriod.transactionErrorRate)?.x ?? NaN + ).toISOString() + ).toMatchInline(`"2020-12-08T14:27:50.000Z"`); + }); + + it('has the correct number of buckets', () => { + expectSnapshot(errorRateResponse.currentPeriod.transactionErrorRate.length).toMatchInline( + `91` + ); + expectSnapshot( + errorRateResponse.previousPeriod.transactionErrorRate.length + ).toMatchInline(`91`); }); it('has the correct calculation for average', () => { - expectSnapshot(errorRateResponse.average).toMatchInline(`0.16`); + expectSnapshot(errorRateResponse.currentPeriod.average).toMatchInline( + `0.233333333333333` + ); + expectSnapshot(errorRateResponse.previousPeriod.average).toMatchInline( + `0.111111111111111` + ); }); it('has the correct error rate', () => { - expectSnapshot(errorRateResponse.transactionErrorRate).toMatch(); + expectSnapshot(errorRateResponse.currentPeriod.transactionErrorRate).toMatch(); + expectSnapshot(errorRateResponse.previousPeriod.transactionErrorRate).toMatch(); + }); + + it('matches x-axis on current period and previous period', () => { + expect(errorRateResponse.currentPeriod.transactionErrorRate.map(({ x }) => x)).to.be.eql( + errorRateResponse.previousPeriod.transactionErrorRate.map(({ x }) => x) + ); }); }); } From b71c6092c9316135131ae2e66ff175a0651c6f86 Mon Sep 17 00:00:00 2001 From: Gabriel Landau <42078554+gabriellandau@users.noreply.github.com> Date: Tue, 16 Mar 2021 10:00:54 -0400 Subject: [PATCH 08/44] Add bytes_compressed_present to Endpoint Security Telemetry (#94594) --- x-pack/plugins/security_solution/server/lib/telemetry/sender.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index e169c036419c5..114cf5d2d3425 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -407,6 +407,7 @@ const allowlistEventFields: AllowlistFields = { bytes_address: true, bytes_allocation_offset: true, bytes_compressed: true, + bytes_compressed_present: true, mapped_pe: { Ext: { code_signature: { From ee84e0b0b709d01c08ae42daa08b20c197f7b2a7 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 16 Mar 2021 15:13:49 +0100 Subject: [PATCH 09/44] Merge tsconfig and x-pack/tsconfig files (#94519) * merge all the typings at root level * merge x-pack/tsconfig into tsconfig.json * fix tsconfig after changes in master * remove unnecessary typings * update paths to the global typings * update paths to the global elaticsearch typings * fix import * fix path to typings/elasticsearch in fleet plugin * remove file deleted from master * fix lint errors --- src/dev/typescript/projects.ts | 1 - test/tsconfig.json | 5 +- test/typings/rison_node.d.ts | 28 ----- tsconfig.json | 81 +++++++++++- .../cytoscape_dagre.d.ts | 12 +- .../elasticsearch/aggregations.d.ts | 5 +- .../elasticsearch/index.d.ts | 5 +- {x-pack/typings => typings}/global_fetch.d.ts | 5 +- typings/index.d.ts | 2 + .../typings => typings}/js_levenshtein.d.ts | 5 +- {x-pack/typings => typings}/react_vis.d.ts | 5 +- .../examples/alerting_example/tsconfig.json | 2 +- .../embedded_lens_example/tsconfig.json | 2 +- .../tsconfig.json | 2 +- .../shared/KueryBar/get_bool_filter.ts | 2 +- .../plugins/apm/public/utils/testHelpers.tsx | 2 +- .../apm/scripts/shared/get_es_client.ts | 2 +- .../server/lib/alerts/alerting_es_client.ts | 2 +- .../chart_preview/get_transaction_duration.ts | 2 +- .../collect_data_telemetry/index.ts | 2 +- .../collect_data_telemetry/tasks.ts | 2 +- .../index.ts | 4 +- .../get_duration_for_percentile.ts | 2 +- .../get_latency_distribution.ts | 4 +- .../get_max_latency.ts | 2 +- .../index.ts | 4 +- .../process_significant_term_aggs.ts | 2 +- .../lib/errors/distribution/get_buckets.ts | 2 +- .../apm/server/lib/errors/get_error_groups.ts | 2 +- .../add_filter_to_exclude_legacy_data.ts | 2 +- .../create_apm_event_client/index.ts | 2 +- .../unpack_processor_events.ts | 2 +- .../create_internal_es_client/index.ts | 2 +- .../lib/helpers/transaction_error_rate.ts | 2 +- .../metrics/fetch_and_transform_metrics.ts | 2 +- .../lib/metrics/transform_metrics_chart.ts | 2 +- .../rum_client/ui_filters/get_es_filter.ts | 2 +- .../lib/service_map/get_service_anomalies.ts | 2 +- .../get_service_map_service_node_info.ts | 2 +- .../lib/service_map/get_trace_sample_ids.ts | 2 +- .../get_derived_service_annotations.ts | 2 +- .../annotations/get_stored_annotations.ts | 2 +- .../lib/services/annotations/index.test.ts | 2 +- ...et_service_instance_system_metric_stats.ts | 2 +- .../services/get_service_metadata_details.ts | 2 +- .../apm/server/lib/services/get_throughput.ts | 2 +- .../get_service_profiling_statistics.ts | 2 +- .../convert_settings_to_string.ts | 2 +- .../find_exact_configuration.ts | 2 +- .../search_configurations.ts | 2 +- .../server/lib/transaction_groups/fetcher.ts | 2 +- .../get_transaction_group_stats.ts | 2 +- .../transactions/get_anomaly_data/fetcher.ts | 2 +- .../transactions/get_latency_charts/index.ts | 2 +- .../get_throughput_charts/index.ts | 2 +- .../plugins/apm/server/projections/typings.ts | 2 +- .../util/merge_projection/index.ts | 2 +- x-pack/plugins/apm/server/utils/queries.ts | 2 +- .../plugins/apm/server/utils/test_helpers.tsx | 2 +- x-pack/plugins/apm/tsconfig.json | 2 +- .../fleet/server/services/agents/crud.ts | 2 +- .../fleet/server/services/agents/helpers.ts | 2 +- .../services/api_keys/enrollment_api_key.ts | 2 +- .../server/services/artifacts/artifacts.ts | 2 +- .../server/services/artifacts/mappings.ts | 2 +- .../fleet/server/services/artifacts/mocks.ts | 2 +- x-pack/plugins/fleet/tsconfig.json | 2 +- .../global_search_providers/tsconfig.json | 2 +- x-pack/plugins/grokdebugger/tsconfig.json | 2 +- .../index_lifecycle_management/tsconfig.json | 2 +- x-pack/plugins/index_management/tsconfig.json | 2 +- x-pack/plugins/infra/tsconfig.json | 2 +- x-pack/plugins/ingest_pipelines/tsconfig.json | 2 +- .../plugins/lens/server/routes/field_stats.ts | 2 +- x-pack/plugins/lens/server/usage/task.ts | 2 +- x-pack/plugins/lens/tsconfig.json | 2 +- .../threshold/get_threshold_bucket_filters.ts | 2 +- .../server/lib/machine_learning/index.test.ts | 2 +- x-pack/plugins/snapshot_restore/tsconfig.json | 2 +- .../common/build_sorted_events_query.ts | 4 +- .../alert_types/es_query/action_context.ts | 2 +- .../alert_types/es_query/alert_type.test.ts | 2 +- .../server/alert_types/es_query/alert_type.ts | 4 +- .../monitoring/workload_statistics.test.ts | 2 +- .../server/monitoring/workload_statistics.ts | 2 +- .../plugins/task_manager/server/task_store.ts | 2 +- .../plugins/ui_actions_enhanced/tsconfig.json | 2 +- x-pack/plugins/uptime/server/lib/lib.ts | 2 +- .../lib/requests/get_monitor_availability.ts | 2 +- .../lib/requests/get_monitor_details.ts | 2 +- .../lib/requests/get_monitor_locations.ts | 2 +- .../lib/requests/get_snapshot_counts.ts | 2 +- .../lib/requests/search/query_context.ts | 2 +- x-pack/plugins/watcher/tsconfig.json | 2 +- .../trial/tests/annotations.ts | 2 +- x-pack/test/tsconfig.json | 4 +- x-pack/tsconfig.json | 117 ------------------ x-pack/typings/@elastic/eui/index.d.ts | 16 --- x-pack/typings/cytoscape_dagre.d.ts | 8 -- x-pack/typings/index.d.ts | 33 ----- x-pack/typings/rison_node.d.ts | 30 ----- 101 files changed, 188 insertions(+), 352 deletions(-) delete mode 100644 test/typings/rison_node.d.ts rename test/typings/index.d.ts => typings/cytoscape_dagre.d.ts (54%) rename {x-pack/typings => typings}/elasticsearch/aggregations.d.ts (98%) rename {x-pack/typings => typings}/elasticsearch/index.d.ts (95%) rename {x-pack/typings => typings}/global_fetch.d.ts (66%) rename {x-pack/typings => typings}/js_levenshtein.d.ts (59%) rename {x-pack/typings => typings}/react_vis.d.ts (50%) delete mode 100644 x-pack/tsconfig.json delete mode 100644 x-pack/typings/@elastic/eui/index.d.ts delete mode 100644 x-pack/typings/cytoscape_dagre.d.ts delete mode 100644 x-pack/typings/index.d.ts delete mode 100644 x-pack/typings/rison_node.d.ts diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 8b306fc967115..050743114f657 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -14,7 +14,6 @@ import { Project } from './project'; export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'tsconfig.json')), new Project(resolve(REPO_ROOT, 'test/tsconfig.json'), { name: 'kibana/test' }), - new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), new Project(resolve(REPO_ROOT, 'src/core/tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/plugins/drilldowns/url_drilldown/tsconfig.json'), { diff --git a/test/tsconfig.json b/test/tsconfig.json index c3acf94f8c267..c68e15b2419a1 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,12 +2,11 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "incremental": false, - "types": ["node", "flot"] + "types": ["node"] }, "include": [ "**/*", - "../typings/elastic__node_crypto.d.ts", - "typings/**/*", + "../typings/**/*", "../packages/kbn-test/types/ftr_globals/**/*" ], "exclude": ["plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"], diff --git a/test/typings/rison_node.d.ts b/test/typings/rison_node.d.ts deleted file mode 100644 index dacb2524907be..0000000000000 --- a/test/typings/rison_node.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -declare module 'rison-node' { - export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray; - - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface RisonArray extends Array {} - - export interface RisonObject { - [key: string]: RisonValue; - } - - export const decode: (input: string) => RisonValue; - - // eslint-disable-next-line @typescript-eslint/naming-convention - export const decode_object: (input: string) => RisonObject; - - export const encode: (input: Input) => string; - - // eslint-disable-next-line @typescript-eslint/naming-convention - export const encode_object: (input: Input) => string; -} diff --git a/tsconfig.json b/tsconfig.json index f6ce6b92b7e02..18647153acb0a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,11 +3,25 @@ "compilerOptions": { "incremental": false }, - "include": ["kibana.d.ts", "src/**/*", "typings/**/*"], + "include": [ + "kibana.d.ts", + "typings/**/*", + + "src/cli/**/*", + "src/dev/**/*", + "src/fixtures/**/*", + "src/legacy/**/*", + "src/optimize/**/*", + + "x-pack/mocks.ts", + "x-pack/typings/**/*", + "x-pack/tasks/**/*", + "x-pack/plugins/cases/**/*", + "x-pack/plugins/lists/**/*", + "x-pack/plugins/security_solution/**/*", + ], "exclude": [ - "src/**/__fixtures__/**/*", - "src/core/**/*", - "src/plugins/**/*" + "x-pack/plugins/security_solution/cypress/**/*" ], "references": [ { "path": "./src/core/tsconfig.json" }, @@ -64,5 +78,64 @@ { "path": "./src/plugins/visualize/tsconfig.json" }, { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, { "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" }, + + { "path": "./x-pack/plugins/actions/tsconfig.json" }, + { "path": "./x-pack/plugins/alerting/tsconfig.json" }, + { "path": "./x-pack/plugins/apm/tsconfig.json" }, + { "path": "./x-pack/plugins/beats_management/tsconfig.json" }, + { "path": "./x-pack/plugins/canvas/tsconfig.json" }, + { "path": "./x-pack/plugins/cloud/tsconfig.json" }, + { "path": "./x-pack/plugins/console_extensions/tsconfig.json" }, + { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, + { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/drilldowns/url_drilldown/tsconfig.json" }, + { "path": "./x-pack/plugins/embeddable_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/encrypted_saved_objects/tsconfig.json" }, + { "path": "./x-pack/plugins/enterprise_search/tsconfig.json" }, + { "path": "./x-pack/plugins/event_log/tsconfig.json" }, + { "path": "./x-pack/plugins/features/tsconfig.json" }, + { "path": "./x-pack/plugins/file_upload/tsconfig.json" }, + { "path": "./x-pack/plugins/fleet/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search_bar/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search_providers/tsconfig.json" }, + { "path": "./x-pack/plugins/global_search/tsconfig.json" }, + { "path": "./x-pack/plugins/graph/tsconfig.json" }, + { "path": "./x-pack/plugins/grokdebugger/tsconfig.json" }, + { "path": "./x-pack/plugins/infra/tsconfig.json" }, + { "path": "./x-pack/plugins/ingest_pipelines/tsconfig.json" }, + { "path": "./x-pack/plugins/lens/tsconfig.json" }, + { "path": "./x-pack/plugins/license_management/tsconfig.json" }, + { "path": "./x-pack/plugins/licensing/tsconfig.json" }, + { "path": "./x-pack/plugins/logstash/tsconfig.json" }, + { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, + { "path": "./x-pack/plugins/maps/tsconfig.json" }, + { "path": "./x-pack/plugins/ml/tsconfig.json" }, + { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, + { "path": "./x-pack/plugins/observability/tsconfig.json" }, + { "path": "./x-pack/plugins/osquery/tsconfig.json" }, + { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, + { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, + { "path": "./x-pack/plugins/searchprofiler/tsconfig.json" }, + { "path": "./x-pack/plugins/security/tsconfig.json" }, + { "path": "./x-pack/plugins/snapshot_restore/tsconfig.json" }, + { "path": "./x-pack/plugins/spaces/tsconfig.json" }, + { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, + { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, + { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "./x-pack/plugins/transform/tsconfig.json" }, + { "path": "./x-pack/plugins/translations/tsconfig.json" }, + { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, + { "path": "./x-pack/plugins/ui_actions_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/upgrade_assistant/tsconfig.json" }, + { "path": "./x-pack/plugins/runtime_fields/tsconfig.json" }, + { "path": "./x-pack/plugins/index_management/tsconfig.json" }, + { "path": "./x-pack/plugins/watcher/tsconfig.json" }, + { "path": "./x-pack/plugins/rollup/tsconfig.json" }, + { "path": "./x-pack/plugins/remote_clusters/tsconfig.json" }, + { "path": "./x-pack/plugins/cross_cluster_replication/tsconfig.json"}, + { "path": "./x-pack/plugins/index_lifecycle_management/tsconfig.json"}, + { "path": "./x-pack/plugins/uptime/tsconfig.json" }, + { "path": "./x-pack/plugins/xpack_legacy/tsconfig.json" } ] } diff --git a/test/typings/index.d.ts b/typings/cytoscape_dagre.d.ts similarity index 54% rename from test/typings/index.d.ts rename to typings/cytoscape_dagre.d.ts index 8ea94a280996e..0cf7cf8be2fee 100644 --- a/test/typings/index.d.ts +++ b/typings/cytoscape_dagre.d.ts @@ -6,14 +6,4 @@ * Side Public License, v 1. */ -declare module '*.html' { - const template: string; - // eslint-disable-next-line import/no-default-export - export default template; -} - -type MethodKeysOf = { - [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; -}[keyof T]; - -type PublicMethodsOf = Pick>; +declare module 'cytoscape-dagre'; diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/typings/elasticsearch/aggregations.d.ts similarity index 98% rename from x-pack/typings/elasticsearch/aggregations.d.ts rename to typings/elasticsearch/aggregations.d.ts index 077399c596d54..2b501c94889f4 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/typings/elasticsearch/aggregations.d.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { Unionize, UnionToIntersection } from 'utility-types'; diff --git a/x-pack/typings/elasticsearch/index.d.ts b/typings/elasticsearch/index.d.ts similarity index 95% rename from x-pack/typings/elasticsearch/index.d.ts rename to typings/elasticsearch/index.d.ts index 41630e81f13e4..a84d4148f6fe7 100644 --- a/x-pack/typings/elasticsearch/index.d.ts +++ b/typings/elasticsearch/index.d.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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 { ValuesType } from 'utility-types'; diff --git a/x-pack/typings/global_fetch.d.ts b/typings/global_fetch.d.ts similarity index 66% rename from x-pack/typings/global_fetch.d.ts rename to typings/global_fetch.d.ts index a79a76aebe539..597bc7e89497c 100644 --- a/x-pack/typings/global_fetch.d.ts +++ b/typings/global_fetch.d.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ // This type needs to still exist due to apollo-link-http-common hasn't yet updated diff --git a/typings/index.d.ts b/typings/index.d.ts index 7192e70559743..c7186a0e5795b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -24,6 +24,8 @@ declare module '*.svg' { export default content; } +declare module 'axios/lib/adapters/xhr'; + // Storybook references this module. It's @ts-ignored in the codebase but when // built into its dist it strips that out. Add it here to avoid a type checking // error. diff --git a/x-pack/typings/js_levenshtein.d.ts b/typings/js_levenshtein.d.ts similarity index 59% rename from x-pack/typings/js_levenshtein.d.ts rename to typings/js_levenshtein.d.ts index f693e17244db1..7c934333dbc7b 100644 --- a/x-pack/typings/js_levenshtein.d.ts +++ b/typings/js_levenshtein.d.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ declare module 'js-levenshtein' { diff --git a/x-pack/typings/react_vis.d.ts b/typings/react_vis.d.ts similarity index 50% rename from x-pack/typings/react_vis.d.ts rename to typings/react_vis.d.ts index bcfbafd47fbc7..209dd398e86f4 100644 --- a/x-pack/typings/react_vis.d.ts +++ b/typings/react_vis.d.ts @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 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. */ declare module 'react-vis'; diff --git a/x-pack/examples/alerting_example/tsconfig.json b/x-pack/examples/alerting_example/tsconfig.json index 99e0f1f0e7c9e..95d42d40aceb3 100644 --- a/x-pack/examples/alerting_example/tsconfig.json +++ b/x-pack/examples/alerting_example/tsconfig.json @@ -9,7 +9,7 @@ "public/**/*.tsx", "server/**/*.ts", "common/**/*.ts", - "../../typings/**/*", + "../../../typings/**/*", ], "exclude": [], "references": [ diff --git a/x-pack/examples/embedded_lens_example/tsconfig.json b/x-pack/examples/embedded_lens_example/tsconfig.json index 2bf577e87041c..195db6effc5e6 100644 --- a/x-pack/examples/embedded_lens_example/tsconfig.json +++ b/x-pack/examples/embedded_lens_example/tsconfig.json @@ -9,7 +9,7 @@ "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", - "../../typings/**/*" + "../../../typings/**/*" ], "exclude": [], "references": [ diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json index 05e5f39d4d628..567baca039d76 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -9,7 +9,7 @@ "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", - "../../typings/**/*" + "../../../typings/**/*" ], "exclude": [], "references": [ diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index 8c7947201927f..c86cf769d7529 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, PROCESSOR_EVENT, diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index 80df113e18190..d0d09f703ae9f 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -19,7 +19,7 @@ import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common import { ESSearchRequest, ESSearchResponse, -} from '../../../../typings/elasticsearch'; +} from '../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../observability/typings/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMConfig } from '../../server'; diff --git a/x-pack/plugins/apm/scripts/shared/get_es_client.ts b/x-pack/plugins/apm/scripts/shared/get_es_client.ts index f17a55cf4e215..7a8e09423ff15 100644 --- a/x-pack/plugins/apm/scripts/shared/get_es_client.ts +++ b/x-pack/plugins/apm/scripts/shared/get_es_client.ts @@ -10,7 +10,7 @@ import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool'; import { ESSearchResponse, ESSearchRequest, -} from '../../../../typings/elasticsearch'; +} from '../../../../../typings/elasticsearch'; export type ESClient = ReturnType; diff --git a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts index 727b0c1f04cf4..d5706ac9063ed 100644 --- a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts +++ b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts @@ -9,7 +9,7 @@ import { ThresholdMetActionGroupId } from '../../../common/alert_types'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../../typings/elasticsearch'; +} from '../../../../../../typings/elasticsearch'; import { AlertInstanceContext, AlertInstanceState, diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts index 86456114698cb..bea90109725d0 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MetricsAggregationResponsePart } from '../../../../../../typings/elasticsearch/aggregations'; +import { MetricsAggregationResponsePart } from '../../../../../../../typings/elasticsearch/aggregations'; import { PROCESSOR_EVENT, SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts index 50ed02879a4d2..98063e3e1e3fd 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/index.ts @@ -11,7 +11,7 @@ import { RequestParams } from '@elastic/elasticsearch'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; import { tasks } from './tasks'; import { APMDataTelemetry } from '../types'; diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index 36c15366b9b48..e9744c6614641 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -7,7 +7,7 @@ import { ValuesType } from 'utility-types'; import { flatten, merge, sortBy, sum, pickBy } from 'lodash'; -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch/aggregations'; import { ProcessorEvent } from '../../../../common/processor_event'; import { TelemetryTask } from '.'; import { AGENT_NAMES, RUM_AGENT_NAMES } from '../../../../common/agent_name'; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts index e2411d1d17adc..f613a0dbca402 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts @@ -11,8 +11,8 @@ import { processSignificantTermAggs, TopSigTerm, } from '../process_significant_term_aggs'; -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch/aggregations'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { environmentQuery, rangeQuery, diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts index 27f69c3ca7d56..02141f5f9e76f 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_duration_for_percentile.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { withApmSpan } from '../../../utils/with_apm_span'; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts index eab09e814c18d..b800a21ffc341 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts @@ -6,8 +6,8 @@ */ import { isEmpty, dropRightWhile } from 'lodash'; -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch/aggregations'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts index 2777c0944afd1..5f12c86a9c70c 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_max_latency.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { withApmSpan } from '../../../utils/with_apm_span'; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts index 824b290a6ba60..6afca46ec7391 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch/aggregations'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { environmentQuery, rangeQuery, diff --git a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts index 1fe50c869f5bf..2732cd45c342e 100644 --- a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts +++ b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts @@ -9,7 +9,7 @@ import { orderBy } from 'lodash'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../../../typings/elasticsearch/aggregations'; +} from '../../../../../../typings/elasticsearch/aggregations'; export interface TopSigTerm { fieldName: string; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 1e161b0383f0b..462c9bcdc4310 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { ERROR_GROUP_ID, SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 5371d69caaa99..1c262ebf882b2 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SortOptions } from '../../../../../typings/elasticsearch/aggregations'; +import { SortOptions } from '../../../../../../typings/elasticsearch/aggregations'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts index 0f2bff09f99c1..96bc8897e62fd 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/add_filter_to_exclude_legacy_data.ts @@ -10,7 +10,7 @@ import { OBSERVER_VERSION_MAJOR } from '../../../../../common/elasticsearch_fiel import { ESSearchRequest, ESFilter, -} from '../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../typings/elasticsearch'; /* Adds a range query to the ES request to exclude legacy data diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 368c0eb305f21..e04b3a70a7593 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -14,7 +14,7 @@ import { import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../typings/elasticsearch'; import { unwrapEsResponse } from '../../../../../../observability/server'; import { ProcessorEvent } from '../../../../../common/processor_event'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index 38989d172a73f..76e615f42bb64 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -11,7 +11,7 @@ import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESSearchRequest, ESFilter, -} from '../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '.'; import { ApmIndicesConfig, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts index ff1509dc83d15..4faf80d7ca8db 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts @@ -13,7 +13,7 @@ import { APMRequestHandlerContext } from '../../../../routes/typings'; import { ESSearchResponse, ESSearchRequest, -} from '../../../../../../../typings/elasticsearch'; +} from '../../../../../../../../typings/elasticsearch'; import { callAsyncWithDebug, getDebugBody, diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index e22c2514a89a4..9bec5eb4a247c 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -10,7 +10,7 @@ import { EventOutcome } from '../../../common/event_outcome'; import { AggregationOptionsByType, AggregationResultOf, -} from '../../../../../typings/elasticsearch/aggregations'; +} from '../../../../../../typings/elasticsearch/aggregations'; export const getOutcomeAggregation = () => ({ terms: { diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index ef24b531d8046..30234447821ec 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -6,7 +6,7 @@ */ import { Overwrite, Unionize } from 'utility-types'; -import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; import { getMetricsProjection } from '../../projections/metrics'; import { mergeProjection } from '../../projections/util/merge_projection'; import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client'; diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index a7c5fc6628c52..17759f9094a87 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -6,7 +6,7 @@ */ import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { getVizColorForIndex } from '../../../common/viz_colors'; import { GenericMetricsRequest } from './fetch_and_transform_metrics'; import { ChartBase } from './types'; diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts index aed361f13bd7d..43cbb485c4510 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { UIFilters } from '../../../../typings/ui_filters'; import { localUIFilters, localUIFilterNames } from './local_ui_filters/config'; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index f08cc27b2e59c..8c97a3993e8c0 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import { sortBy, uniqBy } from 'lodash'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { MlPluginSetup } from '../../../../ml/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { getSeverity, ML_ERRORS } from '../../../common/anomaly_detection'; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 7da3ce772ef5f..c1dfed377a763 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_CPU_PERCENT, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 2b949863bcb30..8bc1b1f0562f5 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; import { sortBy, take, uniq } from 'lodash'; -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index efe9608edb95d..028c8c042c8dc 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -6,7 +6,7 @@ */ import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 87ee0e9830fce..e0329e5f60e19 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -12,7 +12,7 @@ import { unwrapEsResponse, WrappedElasticsearchClientError, } from '../../../../../observability/server'; -import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { Annotation, AnnotationType } from '../../../../common/annotations'; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts index d86016ed9d505..e2597a4a79cba 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts @@ -8,7 +8,7 @@ import { ESSearchRequest, ESSearchResponse, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { inspectSearchParams, SearchParamsMock, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts index 3e788ca8ddf83..6a72f817b3f69 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../../typings/elasticsearch'; import { environmentQuery, rangeQuery, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts index a064d5b3008c2..a71772d1429cb 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts @@ -6,7 +6,7 @@ */ import { ProcessorEvent } from '../../../common/processor_event'; -import { SortOptions } from '../../../../../typings/elasticsearch'; +import { SortOptions } from '../../../../../../typings/elasticsearch'; import { AGENT, CLOUD, diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index 490eec337840e..5f5008a28c232 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { SERVICE_NAME, TRANSACTION_TYPE, diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index 8b60d39a8de5d..858f36e6e2c13 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -15,7 +15,7 @@ import { getValueTypeConfig, } from '../../../../common/profiling'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { PROFILE_STACK, PROFILE_TOP_ID, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts index b38ca71d93c0a..2d6eff33b5b4e 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/convert_settings_to_string.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESSearchHit } from '../../../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../../typings/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; // needed for backwards compatability diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts index 55d00b70b8c29..972c076d88e76 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESSearchHit } from '../../../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../../typings/elasticsearch'; import { AgentConfiguration } from '../../../../common/agent_configuration/configuration_types'; import { SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 0e7205c309e9f..12ba0939508e3 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESSearchHit } from '../../../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../../typings/elasticsearch'; import { SERVICE_NAME, SERVICE_ENVIRONMENT, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index ce0b6cf2a64fe..6308236000a53 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -8,7 +8,7 @@ import { sortBy, take } from 'lodash'; import moment from 'moment'; import { Unionize } from 'utility-types'; -import { AggregationOptionsByType } from '../../../../../typings/elasticsearch'; +import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 5ee46bf1a5918..5409f919bf895 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -8,7 +8,7 @@ import { merge } from 'lodash'; import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { AggregationInputMap } from '../../../../../typings/elasticsearch'; +import { AggregationInputMap } from '../../../../../../typings/elasticsearch'; import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; import { getTransactionDurationFieldForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { withApmSpan } from '../../utils/with_apm_span'; diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index cfd09f0207536..a35780539a256 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { rangeQuery } from '../../../../server/utils/queries'; import { withApmSpan } from '../../../utils/with_apm_span'; diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index 31b5c6ff64dfd..5bd80f500fd2b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts index 3b7ffafff0d2a..a0225eb47e584 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/projections/typings.ts b/x-pack/plugins/apm/server/projections/typings.ts index 725756b61b209..558f165d43cf5 100644 --- a/x-pack/plugins/apm/server/projections/typings.ts +++ b/x-pack/plugins/apm/server/projections/typings.ts @@ -9,7 +9,7 @@ import { AggregationOptionsByType, AggregationInputMap, ESSearchBody, -} from '../../../../typings/elasticsearch'; +} from '../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '../lib/helpers/create_es_client/create_apm_event_client'; export type Projection = Omit & { diff --git a/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts index 33d8b127137f0..7f08707064862 100644 --- a/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts +++ b/x-pack/plugins/apm/server/projections/util/merge_projection/index.ts @@ -10,7 +10,7 @@ import { DeepPartial } from 'utility-types'; import { AggregationInputMap, ESSearchBody, -} from '../../../../../../typings/elasticsearch'; +} from '../../../../../../../typings/elasticsearch'; import { APMEventESSearchRequest } from '../../../lib/helpers/create_es_client/create_apm_event_client'; import { Projection } from '../../typings'; diff --git a/x-pack/plugins/apm/server/utils/queries.ts b/x-pack/plugins/apm/server/utils/queries.ts index 6eab50d089821..3cbcb0a5b684f 100644 --- a/x-pack/plugins/apm/server/utils/queries.ts +++ b/x-pack/plugins/apm/server/utils/queries.ts @@ -6,7 +6,7 @@ */ import { esKuery } from '../../../../../src/plugins/data/server'; -import { ESFilter } from '../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../typings/elasticsearch'; import { SERVICE_ENVIRONMENT } from '../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index e804183c78867..6252c33c5994d 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -10,7 +10,7 @@ import { PromiseReturnType } from '../../../observability/typings/common'; import { ESSearchRequest, ESSearchResponse, -} from '../../../../typings/elasticsearch'; +} from '../../../../../typings/elasticsearch'; import { UIFilters } from '../../typings/ui_filters'; interface Options { diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index ea1602d916822..ffbf11c23f63a 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -8,7 +8,7 @@ "declarationMap": true }, "include": [ - "../../typings/**/*", + "../../../typings/**/*", "common/**/*", "public/**/*", "scripts/**/*", diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 975ca7fec08b1..06353d831c60f 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -14,7 +14,7 @@ import { appContextService, agentPolicyService } from '../../services'; import type { FleetServerAgent } from '../../../common'; import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; -import type { ESSearchHit } from '../../../../../typings/elasticsearch'; +import type { ESSearchHit } from '../../../../../../typings/elasticsearch'; import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; import type { KueryNode } from '../../../../../../src/plugins/data/server'; import { esKuery } from '../../../../../../src/plugins/data/server'; diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 1dab3b64755fd..3fdb347ed246b 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ESSearchHit } from '../../../../../typings/elasticsearch'; +import type { ESSearchHit } from '../../../../../../typings/elasticsearch'; import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; export function searchHitToAgent(hit: ESSearchHit): Agent { diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index 387d69c5c0ca7..4365c3913f433 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -12,7 +12,7 @@ import type { GetResponse } from 'elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; -import type { ESSearchResponse as SearchResponse } from '../../../../../typings/elasticsearch'; +import type { ESSearchResponse as SearchResponse } from '../../../../../../typings/elasticsearch'; import type { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; import { agentPolicyService } from '../agent_policy'; diff --git a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts index 02a0d5669d967..2a5f39c4e8a26 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/artifacts.ts @@ -16,7 +16,7 @@ import type { ElasticsearchClient } from 'kibana/server'; import type { ListResult } from '../../../common'; import { FLEET_SERVER_ARTIFACTS_INDEX } from '../../../common'; -import type { ESSearchHit, ESSearchResponse } from '../../../../../typings/elasticsearch'; +import type { ESSearchHit, ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { ArtifactsElasticsearchError } from '../../errors'; diff --git a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts index bdc50d444c862..863eff5aac74c 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mappings.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mappings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ESSearchHit } from '../../../../../typings/elasticsearch'; +import type { ESSearchHit } from '../../../../../../typings/elasticsearch'; import type { Artifact, ArtifactElasticsearchProperties } from './types'; import { ARTIFACT_DOWNLOAD_RELATIVE_PATH } from './constants'; diff --git a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts index a89d6913c680c..b1e01208a24ca 100644 --- a/x-pack/plugins/fleet/server/services/artifacts/mocks.ts +++ b/x-pack/plugins/fleet/server/services/artifacts/mocks.ts @@ -10,7 +10,7 @@ import type { ApiResponse } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; -import type { ESSearchHit, ESSearchResponse } from '../../../../../typings/elasticsearch'; +import type { ESSearchHit, ESSearchResponse } from '../../../../../../typings/elasticsearch'; import type { Artifact, ArtifactElasticsearchProperties, ArtifactsClientInterface } from './types'; diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index e6dc206912c4b..a20d82de3c859 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -15,7 +15,7 @@ "server/**/*.json", "scripts/**/*", "package.json", - "../../typings/**/*" + "../../../typings/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/global_search_providers/tsconfig.json b/x-pack/plugins/global_search_providers/tsconfig.json index 381d314b2e530..f2759954a6845 100644 --- a/x-pack/plugins/global_search_providers/tsconfig.json +++ b/x-pack/plugins/global_search_providers/tsconfig.json @@ -10,7 +10,7 @@ "include": [ "public/**/*", "server/**/*", - "../../typings/**/*", + "../../../typings/**/*", ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/grokdebugger/tsconfig.json b/x-pack/plugins/grokdebugger/tsconfig.json index 34cf8d74c0024..51d2d0b6db0ea 100644 --- a/x-pack/plugins/grokdebugger/tsconfig.json +++ b/x-pack/plugins/grokdebugger/tsconfig.json @@ -11,7 +11,7 @@ "common/**/*", "public/**/*", "server/**/*", - "../../typings/**/*", + "../../../typings/**/*", ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json index 73dcc62132cbf..75bd775a36749 100644 --- a/x-pack/plugins/index_lifecycle_management/tsconfig.json +++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json @@ -12,7 +12,7 @@ "common/**/*", "public/**/*", "server/**/*", - "../../typings/**/*", + "../../../typings/**/*", ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/index_management/tsconfig.json b/x-pack/plugins/index_management/tsconfig.json index 87be6cfc2d627..81a96a77cef83 100644 --- a/x-pack/plugins/index_management/tsconfig.json +++ b/x-pack/plugins/index_management/tsconfig.json @@ -13,7 +13,7 @@ "public/**/*", "server/**/*", "test/**/*", - "../../typings/**/*", + "../../../typings/**/*", ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 026da311192d2..765af7974a2f1 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -8,7 +8,7 @@ "declarationMap": true }, "include": [ - "../../typings/**/*", + "../../../typings/**/*", "common/**/*", "public/**/*", "scripts/**/*", diff --git a/x-pack/plugins/ingest_pipelines/tsconfig.json b/x-pack/plugins/ingest_pipelines/tsconfig.json index 5d78992600e81..a248bc9f337fe 100644 --- a/x-pack/plugins/ingest_pipelines/tsconfig.json +++ b/x-pack/plugins/ingest_pipelines/tsconfig.json @@ -12,7 +12,7 @@ "public/**/*", "server/**/*", "__jest__/**/*", - "../../typings/**/*" + "../../../typings/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 3b293f9af7f28..49ea8c2076f7a 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -11,7 +11,7 @@ import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; import { IFieldType } from 'src/plugins/data/common'; import { SavedObjectNotFound } from '../../../../../src/plugins/kibana_utils/common'; -import { ESSearchResponse } from '../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; import { PluginStartContract } from '../plugin'; diff --git a/x-pack/plugins/lens/server/usage/task.ts b/x-pack/plugins/lens/server/usage/task.ts index f9296e0a41ca3..d583e1628cbe8 100644 --- a/x-pack/plugins/lens/server/usage/task.ts +++ b/x-pack/plugins/lens/server/usage/task.ts @@ -16,7 +16,7 @@ import { } from '../../../task_manager/server'; import { getVisualizationCounts } from './visualization_counts'; -import { ESSearchResponse } from '../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; // This task is responsible for running daily and aggregating all the Lens click event objects // into daily rolled-up documents, which will be used in reporting click stats diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 636d2f44b0217..dfddccbf20392 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -13,7 +13,7 @@ "common/**/*", "public/**/*", "server/**/*", - "../../typings/**/*" + "../../../typings/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts index 208727944765c..73068a73a38a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/get_threshold_bucket_filters.ts @@ -6,7 +6,7 @@ */ import { Filter } from 'src/plugins/data/common'; -import { ESFilter } from '../../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../../typings/elasticsearch'; import { ThresholdSignalHistory, ThresholdSignalHistoryRecord } from '../types'; export const getThresholdBucketFilters = async ({ diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts index 1f49ac7bf5019..30dd5adf6123b 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; import { getExceptionListItemSchemaMock } from '../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getAnomalies, AnomaliesSearchParams } from '.'; diff --git a/x-pack/plugins/snapshot_restore/tsconfig.json b/x-pack/plugins/snapshot_restore/tsconfig.json index 5d962c7c17aff..39beda02977e1 100644 --- a/x-pack/plugins/snapshot_restore/tsconfig.json +++ b/x-pack/plugins/snapshot_restore/tsconfig.json @@ -13,7 +13,7 @@ "public/**/*", "server/**/*", "test/**/*", - "../../typings/**/*", + "../../../typings/**/*", ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts index c7ed7df38be0f..add3e1f59e20e 100644 --- a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ESSearchBody, ESSearchRequest } from '../../../typings/elasticsearch'; -import { SortOrder } from '../../../typings/elasticsearch/aggregations'; +import { ESSearchBody, ESSearchRequest } from '../../../../typings/elasticsearch'; +import { SortOrder } from '../../../../typings/elasticsearch/aggregations'; type BuildSortedEventsQueryOpts = Pick & Pick, 'index' | 'size'>; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts index 617da80bcc8d7..f0596a9fcb964 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { AlertExecutorOptions, AlertInstanceContext } from '../../../../alerting/server'; import { EsQueryAlertParams } from './alert_type_params'; -import { ESSearchHit } from '../../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../typings/elasticsearch'; // alert type context provided to actions diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 86e07fa64af66..4adc7c05821f9 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -17,7 +17,7 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { getAlertType, ConditionMetAlertInstanceId, ActionGroupId } from './alert_type'; import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; import { ActionContext } from './action_context'; -import { ESSearchResponse, ESSearchRequest } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse, ESSearchRequest } from '../../../../../../typings/elasticsearch'; describe('alertType', () => { const logger = loggingSystemMock.create().get(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 74af8b0038a3a..7734c59425a16 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { Logger } from 'src/core/server'; -import { ESSearchResponse } from '../../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { AlertType, AlertExecutorOptions } from '../../types'; import { ActionContext, EsQueryAlertActionContext, addMessages } from './action_context'; import { @@ -19,7 +19,7 @@ import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { ComparatorFns, getHumanReadableComparator } from '../lib'; import { parseDuration } from '../../../../alerting/server'; import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; -import { ESSearchHit } from '../../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../../typings/elasticsearch'; export const ES_QUERY_ID = '.es-query'; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index a5a7954204492..46d8478a7ecfa 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -14,7 +14,7 @@ import { estimateRecurringTaskScheduling, } from './workload_statistics'; import { ConcreteTaskInstance } from '../task'; -import { AggregationResultOf, ESSearchResponse } from '../../../../typings/elasticsearch'; +import { AggregationResultOf, ESSearchResponse } from '../../../../../typings/elasticsearch'; import { times } from 'lodash'; import { taskStoreMock } from '../task_store.mock'; import { of, Subject } from 'rxjs'; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index 75331615e3f07..08850c8650519 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -12,7 +12,7 @@ import { JsonObject } from 'src/plugins/kibana_utils/common'; import { keyBy, mapValues } from 'lodash'; import { AggregatedStatProvider } from './runtime_statistics_aggregator'; import { parseIntervalAsSecond, asInterval, parseIntervalAsMillisecond } from '../lib/intervals'; -import { AggregationResultOf } from '../../../../typings/elasticsearch'; +import { AggregationResultOf } from '../../../../../typings/elasticsearch'; import { HealthStatus } from './monitoring_stats_stream'; import { TaskStore } from '../task_store'; diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 0b54f2779065f..083ce1507e6e5 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -31,7 +31,7 @@ import { } from './task'; import { TaskTypeDictionary } from './task_type_dictionary'; -import { ESSearchResponse, ESSearchBody } from '../../../typings/elasticsearch'; +import { ESSearchResponse, ESSearchBody } from '../../../../typings/elasticsearch'; export interface StoreOpts { esClient: ElasticsearchClient; diff --git a/x-pack/plugins/ui_actions_enhanced/tsconfig.json b/x-pack/plugins/ui_actions_enhanced/tsconfig.json index af24c30389b8b..39318770126e5 100644 --- a/x-pack/plugins/ui_actions_enhanced/tsconfig.json +++ b/x-pack/plugins/ui_actions_enhanced/tsconfig.json @@ -11,7 +11,7 @@ "public/**/*", "server/**/*", "common/**/*", - "../../typings/**/*" + "../../../typings/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 5ac56d14c171d..1a7cef504b019 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -16,7 +16,7 @@ import { UMBackendFrameworkAdapter } from './adapters'; import { UMLicenseCheck } from './domains'; import { UptimeRequests } from './requests'; import { savedObjectsAdapter } from './saved_objects'; -import { ESSearchResponse } from '../../../../typings/elasticsearch'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; export interface UMDomainLibs { requests: UptimeRequests; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts index 26b7c84b17c98..f6195db6f6bce 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { GetMonitorAvailabilityParams, Ping } from '../../../common/runtime_types'; import { AfterKey } from './get_monitor_status'; -import { SortOptions } from '../../../../../typings/elasticsearch'; +import { SortOptions } from '../../../../../../typings/elasticsearch'; export interface AvailabilityKey { monitorId: string; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts index 7034fef8a9e3b..e4b8fac3b44dd 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -9,7 +9,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { MonitorDetails, Ping } from '../../../common/runtime_types'; import { formatFilterString } from '../alerts/status_check'; import { UptimeESClient } from '../lib'; -import { ESSearchBody } from '../../../../../typings/elasticsearch'; +import { ESSearchBody } from '../../../../../../typings/elasticsearch'; export interface GetMonitorDetailsParams { monitorId: string; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index 64f62de6397ce..24e7376160b67 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { MonitorLocations, MonitorLocation } from '../../../common/runtime_types'; import { UNNAMED_LOCATION } from '../../../common/constants'; -import { SortOptions } from '../../../../../typings/elasticsearch'; +import { SortOptions } from '../../../../../../typings/elasticsearch'; /** * Fetch data for the monitor page title. diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 2999f9ebca065..0e47f2a3d56c2 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -9,7 +9,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { CONTEXT_DEFAULTS } from '../../../common/constants'; import { Snapshot } from '../../../common/runtime_types'; import { QueryContext } from './search'; -import { ESFilter } from '../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; export interface GetSnapshotCountParams { dateRangeStart: string; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index f377ba74dc8af..d749460ba997c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -10,7 +10,7 @@ import { CursorPagination } from './types'; import { parseRelativeDate } from '../../helper'; import { CursorDirection, SortOrder } from '../../../../common/runtime_types'; import { UptimeESClient } from '../../lib'; -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../../../../typings/elasticsearch'; export class QueryContext { callES: UptimeESClient; diff --git a/x-pack/plugins/watcher/tsconfig.json b/x-pack/plugins/watcher/tsconfig.json index 4680847ba486d..e8dabe8cd40a9 100644 --- a/x-pack/plugins/watcher/tsconfig.json +++ b/x-pack/plugins/watcher/tsconfig.json @@ -13,7 +13,7 @@ "common/**/*", "tests_client_integration/**/*", "__fixtures__/*", - "../../typings/**/*" + "../../../typings/**/*" ], "references": [ { "path": "../../../src/core/tsconfig.json" }, diff --git a/x-pack/test/observability_api_integration/trial/tests/annotations.ts b/x-pack/test/observability_api_integration/trial/tests/annotations.ts index ef4e34a2818de..928d160a5df9e 100644 --- a/x-pack/test/observability_api_integration/trial/tests/annotations.ts +++ b/x-pack/test/observability_api_integration/trial/tests/annotations.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { Annotation } from '../../../../plugins/observability/common/annotations'; -import { ESSearchHit } from '../../../../typings/elasticsearch'; +import { ESSearchHit } from '../../../../../typings/elasticsearch'; import { FtrProviderContext } from '../../common/ftr_provider_context'; const DEFAULT_INDEX_NAME = 'observability-annotations'; diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 8ac8bdad6dfd0..8757b39a0b3ac 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -3,9 +3,9 @@ "compilerOptions": { // overhead is too significant "incremental": false, - "types": ["node", "flot"] + "types": ["node"] }, - "include": ["**/*", "../typings/**/*", "../../packages/kbn-test/types/ftr_globals/**/*"], + "include": ["**/*", "../../typings/**/*", "../../packages/kbn-test/types/ftr_globals/**/*"], "references": [ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/bfetch/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json deleted file mode 100644 index aaf014ea6165c..0000000000000 --- a/x-pack/tsconfig.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "include": [ - "mocks.ts", - "typings/**/*", - "tasks/**/*", - "plugins/cases/**/*", - "plugins/lists/**/*", - "plugins/security_solution/**/*" - ], - "exclude": [ - "test/**/*", - "plugins/apm/e2e/cypress/**/*", - "plugins/apm/ftr_e2e/**/*", - "plugins/apm/scripts/**/*", - "plugins/security_solution/cypress/**/*" - ], - "compilerOptions": { - // overhead is too significant - "incremental": false - }, - "references": [ - { "path": "../src/core/tsconfig.json" }, - { "path": "../src/plugins/bfetch/tsconfig.json" }, - { "path": "../src/plugins/charts/tsconfig.json" }, - { "path": "../src/plugins/console/tsconfig.json" }, - { "path": "../src/plugins/dashboard/tsconfig.json" }, - { "path": "../src/plugins/data/tsconfig.json" }, - { "path": "../src/plugins/dev_tools/tsconfig.json" }, - { "path": "../src/plugins/discover/tsconfig.json" }, - { "path": "../src/plugins/embeddable/tsconfig.json" }, - { "path": "../src/plugins/es_ui_shared/tsconfig.json" }, - { "path": "../src/plugins/expressions/tsconfig.json" }, - { "path": "../src/plugins/home/tsconfig.json" }, - { "path": "../src/plugins/index_pattern_management/tsconfig.json" }, - { "path": "../src/plugins/inspector/tsconfig.json" }, - { "path": "../src/plugins/kibana_legacy/tsconfig.json" }, - { "path": "../src/plugins/kibana_overview/tsconfig.json" }, - { "path": "../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../src/plugins/legacy_export/tsconfig.json" }, - { "path": "../src/plugins/management/tsconfig.json" }, - { "path": "../src/plugins/navigation/tsconfig.json" }, - { "path": "../src/plugins/newsfeed/tsconfig.json" }, - { "path": "../src/plugins/presentation_util/tsconfig.json" }, - { "path": "../src/plugins/saved_objects_management/tsconfig.json" }, - { "path": "../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, - { "path": "../src/plugins/saved_objects/tsconfig.json" }, - { "path": "../src/plugins/security_oss/tsconfig.json" }, - { "path": "../src/plugins/share/tsconfig.json" }, - { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "../src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "../src/plugins/telemetry/tsconfig.json" }, - { "path": "../src/plugins/ui_actions/tsconfig.json" }, - { "path": "../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../src/plugins/usage_collection/tsconfig.json" }, - { "path": "./plugins/actions/tsconfig.json" }, - { "path": "./plugins/alerting/tsconfig.json" }, - { "path": "./plugins/apm/tsconfig.json" }, - { "path": "./plugins/beats_management/tsconfig.json" }, - { "path": "./plugins/canvas/tsconfig.json" }, - { "path": "./plugins/cloud/tsconfig.json" }, - { "path": "./plugins/console_extensions/tsconfig.json" }, - { "path": "./plugins/data_enhanced/tsconfig.json" }, - { "path": "./plugins/dashboard_mode/tsconfig.json" }, - { "path": "./plugins/discover_enhanced/tsconfig.json" }, - { "path": "./plugins/drilldowns/url_drilldown/tsconfig.json" }, - { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, - { "path": "./plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "./plugins/enterprise_search/tsconfig.json" }, - { "path": "./plugins/event_log/tsconfig.json" }, - { "path": "./plugins/features/tsconfig.json" }, - { "path": "./plugins/file_upload/tsconfig.json" }, - { "path": "./plugins/fleet/tsconfig.json" }, - { "path": "./plugins/global_search_bar/tsconfig.json" }, - { "path": "./plugins/global_search_providers/tsconfig.json" }, - { "path": "./plugins/global_search/tsconfig.json" }, - { "path": "./plugins/graph/tsconfig.json" }, - { "path": "./plugins/grokdebugger/tsconfig.json" }, - { "path": "./plugins/infra/tsconfig.json" }, - { "path": "./plugins/ingest_pipelines/tsconfig.json" }, - { "path": "./plugins/lens/tsconfig.json" }, - { "path": "./plugins/license_management/tsconfig.json" }, - { "path": "./plugins/licensing/tsconfig.json" }, - { "path": "./plugins/logstash/tsconfig.json" }, - { "path": "./plugins/maps_legacy_licensing/tsconfig.json" }, - { "path": "./plugins/maps/tsconfig.json" }, - { "path": "./plugins/ml/tsconfig.json" }, - { "path": "./plugins/monitoring/tsconfig.json" }, - { "path": "./plugins/observability/tsconfig.json" }, - { "path": "./plugins/osquery/tsconfig.json" }, - { "path": "./plugins/painless_lab/tsconfig.json" }, - { "path": "./plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./plugins/searchprofiler/tsconfig.json" }, - { "path": "./plugins/security/tsconfig.json" }, - { "path": "./plugins/snapshot_restore/tsconfig.json" }, - { "path": "./plugins/spaces/tsconfig.json" }, - { "path": "./plugins/stack_alerts/tsconfig.json" }, - { "path": "./plugins/task_manager/tsconfig.json" }, - { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "./plugins/transform/tsconfig.json" }, - { "path": "./plugins/translations/tsconfig.json" }, - { "path": "./plugins/triggers_actions_ui/tsconfig.json" }, - { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./plugins/upgrade_assistant/tsconfig.json" }, - { "path": "./plugins/runtime_fields/tsconfig.json" }, - { "path": "./plugins/index_management/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" }, - { "path": "./plugins/rollup/tsconfig.json" }, - { "path": "./plugins/remote_clusters/tsconfig.json" }, - { "path": "./plugins/cross_cluster_replication/tsconfig.json"}, - { "path": "./plugins/index_lifecycle_management/tsconfig.json"}, - { "path": "./plugins/uptime/tsconfig.json" }, - { "path": "./plugins/xpack_legacy/tsconfig.json" } - ] -} diff --git a/x-pack/typings/@elastic/eui/index.d.ts b/x-pack/typings/@elastic/eui/index.d.ts deleted file mode 100644 index 7664eaa20e432..0000000000000 --- a/x-pack/typings/@elastic/eui/index.d.ts +++ /dev/null @@ -1,16 +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. - */ - -// TODO: Remove once typescript definitions are in EUI - -declare module '@elastic/eui/lib/services' { - export const RIGHT_ALIGNMENT: any; -} - -declare module '@elastic/eui/lib/services/format' { - export const dateFormatAliases: any; -} diff --git a/x-pack/typings/cytoscape_dagre.d.ts b/x-pack/typings/cytoscape_dagre.d.ts deleted file mode 100644 index ddc991c9fbd0a..0000000000000 --- a/x-pack/typings/cytoscape_dagre.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -declare module 'cytoscape-dagre'; diff --git a/x-pack/typings/index.d.ts b/x-pack/typings/index.d.ts deleted file mode 100644 index 171171de5561f..0000000000000 --- a/x-pack/typings/index.d.ts +++ /dev/null @@ -1,33 +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. - */ - -declare module '*.html' { - const template: string; - // eslint-disable-next-line import/no-default-export - export default template; -} - -declare module '*.png' { - const content: string; - // eslint-disable-next-line import/no-default-export - export default content; -} - -declare module '*.svg' { - const content: string; - // eslint-disable-next-line import/no-default-export - export default content; -} - -declare module 'axios/lib/adapters/xhr'; - -// Storybook references this module. It's @ts-ignored in the codebase but when -// built into its dist it strips that out. Add it here to avoid a type checking -// error. -// -// See https://github.com/storybookjs/storybook/issues/11684 -declare module 'react-syntax-highlighter/dist/cjs/create-element'; diff --git a/x-pack/typings/rison_node.d.ts b/x-pack/typings/rison_node.d.ts deleted file mode 100644 index b24300c100d8b..0000000000000 --- a/x-pack/typings/rison_node.d.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. - */ - -declare module 'rison-node' { - export type RisonValue = undefined | null | boolean | number | string | RisonObject | RisonArray; - - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface RisonArray extends Array {} - - export interface RisonObject { - [key: string]: RisonValue; - } - - export const decode: (input: string) => RisonValue; - - // eslint-disable-next-line @typescript-eslint/naming-convention - export const decode_object: (input: string) => RisonObject; - - export const encode: (input: Input) => string; - - // eslint-disable-next-line @typescript-eslint/naming-convention - export const encode_object: (input: Input) => string; - - // eslint-disable-next-line @typescript-eslint/naming-convention - export const encode_array: (input: Input) => string; -} From 09f90d86510d9620c254f8bd3ebc7b05931ef73f Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 16 Mar 2021 15:22:09 +0100 Subject: [PATCH 10/44] [Canvas] Cleanup types in lib (#94517) * fix get_legend_config error in canvas/lib/index * convert resolve_dataurl to ts to fix canvas/lib/index failure * convert expression_form_handler to ts to fix canvas/lib/index failure * convert canvas lib/error into ts * canvas: do not compile json file due to effect on performance * remove type. it is not exported and inferred as any implicitly * fix datatable error in lib/index.d.ts file * fix url resolver * case manually to avoid incompatibility error --- .../canvas_plugin_src/functions/common/image.ts | 3 +-- .../functions/common/repeat_image.ts | 1 - .../functions/common/revealImage.ts | 5 ++--- .../functions/server/demodata/index.ts | 2 -- .../common/lib/datatable/{index.js => index.ts} | 2 +- .../common/lib/datatable/{query.js => query.ts} | 12 ++++++------ .../canvas/common/lib/{errors.js => errors.ts} | 14 +++++++++----- ...orm_handlers.js => expression_form_handlers.ts} | 4 +++- .../{get_legend_config.js => get_legend_config.ts} | 13 ++++++++++--- x-pack/plugins/canvas/common/lib/index.ts | 5 ----- .../lib/{resolve_dataurl.js => resolve_dataurl.ts} | 9 ++++++--- x-pack/plugins/canvas/public/functions/pie.ts | 1 - .../plugins/canvas/public/functions/plot/index.ts | 1 - x-pack/plugins/canvas/shareable_runtime/types.ts | 4 +--- x-pack/plugins/canvas/tsconfig.json | 12 ++++-------- 15 files changed, 43 insertions(+), 45 deletions(-) rename x-pack/plugins/canvas/common/lib/datatable/{index.js => index.ts} (85%) rename x-pack/plugins/canvas/common/lib/datatable/{query.js => query.ts} (73%) rename x-pack/plugins/canvas/common/lib/{errors.js => errors.ts} (73%) rename x-pack/plugins/canvas/common/lib/{expression_form_handlers.js => expression_form_handlers.ts} (82%) rename x-pack/plugins/canvas/common/lib/{get_legend_config.js => get_legend_config.ts} (66%) rename x-pack/plugins/canvas/common/lib/{resolve_dataurl.js => resolve_dataurl.ts} (75%) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts index 7c4b973511672..b4d067280cb69 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts @@ -8,7 +8,6 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; -// @ts-expect-error untyped local import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl'; import { elasticLogo } from '../../lib/elastic_logo'; @@ -64,7 +63,7 @@ export function image(): ExpressionFunctionDefinition<'image', null, Arguments, return { type: 'image', mode: modeStyle, - dataurl: resolveWithMissingImage(dataurl, elasticLogo), + dataurl: resolveWithMissingImage(dataurl, elasticLogo) as string, }; }, }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts index c685a7aab84a8..6e62139e4da0d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeat_image.ts @@ -6,7 +6,6 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -// @ts-expect-error untyped local import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl'; import { elasticOutline } from '../../lib/elastic_outline'; import { Render } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts index 1c9f2c7c1e0f9..91d70609ab708 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts @@ -6,7 +6,6 @@ */ import { ExpressionFunctionDefinition, ExpressionValueRender } from 'src/plugins/expressions'; -// @ts-expect-error untyped local import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl'; import { elasticOutline } from '../../lib/elastic_outline'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; @@ -75,8 +74,8 @@ export function revealImage(): ExpressionFunctionDefinition< value: { percent, ...args, - image: resolveWithMissingImage(args.image, elasticOutline), - emptyImage: resolveWithMissingImage(args.emptyImage), + image: resolveWithMissingImage(args.image, elasticOutline) as string, + emptyImage: resolveWithMissingImage(args.emptyImage) as string, }, }; }, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts index bdaeb9cb0929f..5dc1790e67d7d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts @@ -7,7 +7,6 @@ import { sortBy } from 'lodash'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; -// @ts-expect-error unconverted lib file import { queryDatatable } from '../../../../common/lib/datatable/query'; import { DemoRows } from './demo_rows_types'; import { getDemoRows } from './get_demo_rows'; @@ -62,7 +61,6 @@ export function demodata(): ExpressionFunctionDefinition< { id: 'project', name: 'project', meta: { type: 'string' } }, { id: 'percent_uptime', name: 'percent_uptime', meta: { type: 'number' } }, ], - // @ts-expect-error invalid json mock rows: sortBy(demoRows, 'time'), }; } else if (args.type === DemoRows.SHIRTS) { diff --git a/x-pack/plugins/canvas/common/lib/datatable/index.js b/x-pack/plugins/canvas/common/lib/datatable/index.ts similarity index 85% rename from x-pack/plugins/canvas/common/lib/datatable/index.js rename to x-pack/plugins/canvas/common/lib/datatable/index.ts index 66ede766e4741..a1bb7d690ec4c 100644 --- a/x-pack/plugins/canvas/common/lib/datatable/index.js +++ b/x-pack/plugins/canvas/common/lib/datatable/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './query'; +export { queryDatatable } from './query'; diff --git a/x-pack/plugins/canvas/common/lib/datatable/query.js b/x-pack/plugins/canvas/common/lib/datatable/query.ts similarity index 73% rename from x-pack/plugins/canvas/common/lib/datatable/query.js rename to x-pack/plugins/canvas/common/lib/datatable/query.ts index da2219a2e2bcd..748810432bd22 100644 --- a/x-pack/plugins/canvas/common/lib/datatable/query.js +++ b/x-pack/plugins/canvas/common/lib/datatable/query.ts @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -export function queryDatatable(datatable, query) { +import type { Datatable } from '../../../types'; +export function queryDatatable(datatable: Datatable, query: Record) { if (query.size) { datatable = { ...datatable, @@ -14,17 +14,17 @@ export function queryDatatable(datatable, query) { } if (query.and) { - query.and.forEach((filter) => { + query.and.forEach((filter: any) => { // handle exact matches if (filter.filterType === 'exactly') { - datatable.rows = datatable.rows.filter((row) => { + datatable.rows = datatable.rows.filter((row: any) => { return row[filter.column] === filter.value; }); } // handle time filters if (filter.filterType === 'time') { - const columnNames = datatable.columns.map((col) => col.name); + const columnNames = datatable.columns.map((col: any) => col.name); // remove row if no column match if (!columnNames.includes(filter.column)) { @@ -32,7 +32,7 @@ export function queryDatatable(datatable, query) { return; } - datatable.rows = datatable.rows.filter((row) => { + datatable.rows = datatable.rows.filter((row: any) => { const fromTime = new Date(filter.from).getTime(); const toTime = new Date(filter.to).getTime(); const rowTime = new Date(row[filter.column]).getTime(); diff --git a/x-pack/plugins/canvas/common/lib/errors.js b/x-pack/plugins/canvas/common/lib/errors.ts similarity index 73% rename from x-pack/plugins/canvas/common/lib/errors.js rename to x-pack/plugins/canvas/common/lib/errors.ts index 93678c308b5c4..f9039e7aa5ba6 100644 --- a/x-pack/plugins/canvas/common/lib/errors.js +++ b/x-pack/plugins/canvas/common/lib/errors.ts @@ -5,8 +5,10 @@ * 2.0. */ +type NewableError = (...args: any[]) => Error; + // helper to correctly set the prototype of custom error constructor -function setErrorPrototype(CustomError) { +function setErrorPrototype(CustomError: NewableError) { CustomError.prototype = Object.create(Error.prototype, { constructor: { value: Error, @@ -20,15 +22,17 @@ function setErrorPrototype(CustomError) { } // helper to create a custom error by name -function createError(name) { - function CustomError(...args) { +function createError(name: string) { + function CustomError(...args: any[]) { const instance = new Error(...args); - instance.name = this.name = name; + // @ts-expect-error this has not type annotation + const self = this as any; + instance.name = self.name = name; if (Error.captureStackTrace) { Error.captureStackTrace(instance, CustomError); } else { - Object.defineProperty(this, 'stack', { + Object.defineProperty(self, 'stack', { get() { return instance.stack; }, diff --git a/x-pack/plugins/canvas/common/lib/expression_form_handlers.js b/x-pack/plugins/canvas/common/lib/expression_form_handlers.ts similarity index 82% rename from x-pack/plugins/canvas/common/lib/expression_form_handlers.js rename to x-pack/plugins/canvas/common/lib/expression_form_handlers.ts index ac6ef62d3bba8..18e32eb635bb3 100644 --- a/x-pack/plugins/canvas/common/lib/expression_form_handlers.js +++ b/x-pack/plugins/canvas/common/lib/expression_form_handlers.ts @@ -6,12 +6,14 @@ */ export class ExpressionFormHandlers { + public destroy: () => void; + public done: () => void; constructor() { this.destroy = () => {}; this.done = () => {}; } - onDestroy(fn) { + onDestroy(fn: () => void) { this.destroy = fn; } } diff --git a/x-pack/plugins/canvas/common/lib/get_legend_config.js b/x-pack/plugins/canvas/common/lib/get_legend_config.ts similarity index 66% rename from x-pack/plugins/canvas/common/lib/get_legend_config.js rename to x-pack/plugins/canvas/common/lib/get_legend_config.ts index 6f143b26ab783..ae27d1449f140 100644 --- a/x-pack/plugins/canvas/common/lib/get_legend_config.js +++ b/x-pack/plugins/canvas/common/lib/get_legend_config.ts @@ -5,7 +5,15 @@ * 2.0. */ -export const getLegendConfig = (legend, size) => { +import { Legend } from '../../types'; +const acceptedPositions: Legend[] = [ + Legend.NORTH_WEST, + Legend.SOUTH_WEST, + Legend.NORTH_EAST, + Legend.SOUTH_EAST, +]; + +export const getLegendConfig = (legend: boolean | Legend, size: number) => { if (!legend || size < 2) { return { show: false }; } @@ -16,8 +24,7 @@ export const getLegendConfig = (legend, size) => { labelBoxBorderColor: 'transparent', }; - const acceptedPositions = ['nw', 'ne', 'sw', 'se']; - + // @ts-expect-error config.position = !legend || acceptedPositions.includes(legend) ? legend : 'ne'; return config; diff --git a/x-pack/plugins/canvas/common/lib/index.ts b/x-pack/plugins/canvas/common/lib/index.ts index f7b4de235f353..afce09c6d5ee9 100644 --- a/x-pack/plugins/canvas/common/lib/index.ts +++ b/x-pack/plugins/canvas/common/lib/index.ts @@ -5,26 +5,21 @@ * 2.0. */ -// @ts-expect-error missing local definition export * from './datatable'; export * from './autocomplete'; export * from './constants'; export * from './dataurl'; -// @ts-expect-error missing local definition export * from './errors'; -// @ts-expect-error missing local definition export * from './expression_form_handlers'; export * from './fetch'; export * from './fonts'; export * from './get_field_type'; -// @ts-expect-error missing local definition export * from './get_legend_config'; export * from './hex_to_rgb'; export * from './httpurl'; export * from './missing_asset'; export * from './palettes'; export * from './pivot_object_array'; -// @ts-expect-error missing local definition export * from './resolve_dataurl'; export * from './unquote_string'; export * from './url'; diff --git a/x-pack/plugins/canvas/common/lib/resolve_dataurl.js b/x-pack/plugins/canvas/common/lib/resolve_dataurl.ts similarity index 75% rename from x-pack/plugins/canvas/common/lib/resolve_dataurl.js rename to x-pack/plugins/canvas/common/lib/resolve_dataurl.ts index 92bb69ff9c7fb..79e49c0595355 100644 --- a/x-pack/plugins/canvas/common/lib/resolve_dataurl.js +++ b/x-pack/plugins/canvas/common/lib/resolve_dataurl.ts @@ -14,13 +14,16 @@ import { missingImage } from '../../common/lib/missing_asset'; * For example: * [{"type":"expression","chain":[{"type":"function","function":"asset","arguments":{"_":["..."]}}]}] */ -export const resolveFromArgs = (args, defaultDataurl = null) => { +export const resolveFromArgs = (args: any, defaultDataurl: string | null = null): string => { const dataurl = get(args, 'dataurl.0', null); return isValidUrl(dataurl) ? dataurl : defaultDataurl; }; -export const resolveWithMissingImage = (img, alt = null) => { - if (isValidUrl(img)) { +export const resolveWithMissingImage = ( + img: string | null, + alt: string | null = null +): string | null => { + if (img !== null && isValidUrl(img)) { return img; } if (img === null) { diff --git a/x-pack/plugins/canvas/public/functions/pie.ts b/x-pack/plugins/canvas/public/functions/pie.ts index 31da3e074152f..0840667302ebe 100644 --- a/x-pack/plugins/canvas/public/functions/pie.ts +++ b/x-pack/plugins/canvas/public/functions/pie.ts @@ -7,7 +7,6 @@ import { get, keyBy, map, groupBy } from 'lodash'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; -// @ts-expect-error untyped local import { getLegendConfig } from '../../common/lib/get_legend_config'; import { getFunctionHelp } from '../../i18n'; import { diff --git a/x-pack/plugins/canvas/public/functions/plot/index.ts b/x-pack/plugins/canvas/public/functions/plot/index.ts index 6dff62b7d7cd7..47b9212bbc4c0 100644 --- a/x-pack/plugins/canvas/public/functions/plot/index.ts +++ b/x-pack/plugins/canvas/public/functions/plot/index.ts @@ -9,7 +9,6 @@ import { set } from '@elastic/safer-lodash-set'; import { groupBy, get, keyBy, map, sortBy } from 'lodash'; import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; -// @ts-expect-error untyped local import { getLegendConfig } from '../../../common/lib/get_legend_config'; import { getFlotAxisConfig } from './get_flot_axis_config'; import { getFontSpec } from './get_font_spec'; diff --git a/x-pack/plugins/canvas/shareable_runtime/types.ts b/x-pack/plugins/canvas/shareable_runtime/types.ts index 14449ca6d9a93..ac8f140b7f11d 100644 --- a/x-pack/plugins/canvas/shareable_runtime/types.ts +++ b/x-pack/plugins/canvas/shareable_runtime/types.ts @@ -6,8 +6,6 @@ */ import { RefObject } from 'react'; -// @ts-expect-error Unlinked Webpack Type -import ContainerStyle from 'types/interpreter'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; import { ElementPosition, CanvasPage, CanvasWorkpad, RendererSpec } from '../types'; @@ -52,7 +50,7 @@ export interface CanvasRenderable { state: 'ready' | 'error'; value: { as: string; - containerStyle: ContainerStyle; + containerStyle: any; css: string; type: 'render'; value: any; diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 3e3986082e207..487b68ba3542b 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -5,7 +5,10 @@ "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + + // the plugin contains some heavy json files + "resolveJsonModule": false, }, "include": [ "../../../typings/**/*", @@ -19,13 +22,6 @@ "storybook/**/*", "tasks/mocks/*", "types/**/*", - "**/*.json", - ], - "exclude": [ - // these files are too large and upset tsc, so we exclude them - "server/sample_data/*.json", - "canvas_plugin_src/functions/server/demodata/*.json", - "shareable_runtime/test/workpads/*.json", ], "references": [ { "path": "../../../src/core/tsconfig.json" }, From c9d1dbf599669fdd3e548c682d4323842197ad19 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Tue, 16 Mar 2021 11:48:07 -0300 Subject: [PATCH 11/44] [Workplace Search] Misc bugfixes (#94612) * Fix incorrect copy * Fix incorrect copy * Update Box icon to match other icon sizes * Add missing spacer on connector configuration screen * Add missing spacer to Manage Source modal on Group page * Remove shadows on Security page * Align the last column content in tables to the right * Fix colors on save custom source page "Secondary" is greenish in new version is EUI, we need "subdued" * Fix link to personal dashboard * Add missing breadcrumbs to Security and Settings pages * Deduplicate Security tests on Basic and Platinum licenses * Prevent range slider from shifting to left when priority is 10 When priority is 10, the number become wider and it pushes the range slider to the left. This commit is a quick fix for that. We could improve it later by adding a proper input. * Fix i18n duplicate ID * Revert "Fix link to personal dashboard" This reverts commit 5fc3ad2937f3c3236ed140c994daf2edd178a555. --- .../components/shared/assets/source_icons/box.svg | 2 +- .../components/shared/source_row/source_row.scss | 4 ---- .../components/shared/source_row/source_row.tsx | 2 +- .../public/applications/workplace_search/constants.ts | 2 +- .../components/add_source/save_config.tsx | 1 + .../components/add_source/save_custom.tsx | 8 ++++---- .../views/content_sources/components/overview.tsx | 4 ++-- .../views/content_sources/components/source_content.tsx | 6 ++++-- .../workplace_search/views/content_sources/constants.ts | 4 ++-- .../views/groups/components/group_row.tsx | 2 +- .../groups/components/group_source_prioritization.tsx | 2 +- .../views/groups/components/shared_sources_modal.tsx | 2 ++ .../views/security/components/private_sources_table.tsx | 4 ++-- .../workplace_search/views/security/security.test.tsx | 9 +++++---- .../workplace_search/views/security/security.tsx | 5 ++++- .../views/settings/settings_router.test.tsx | 2 ++ .../workplace_search/views/settings/settings_router.tsx | 5 +++++ 17 files changed, 38 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/box.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/box.svg index 827f8cf0a55ec..b1b542eadd59c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/box.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/box.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.scss index a099b974a0d41..fb8a47d134269 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.scss +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.scss @@ -8,10 +8,6 @@ font-weight: 500; } - &__actions { - width: 100px; - } - &__actions a { opacity: 1.0; pointer-events: auto; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx index 6cfc68b45ee3c..a6b2878de6449 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -164,7 +164,7 @@ export const SourceRow: React.FC = ({ /> )} - + {showFix && {fixLink}} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index cdfd07b07de91..ddec0d9d13873 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -574,7 +574,7 @@ export const CUSTOMIZE_HEADER_DESCRIPTION = i18n.translate( export const CUSTOMIZE_NAME_LABEL = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.customize.name.label', { - defaultMessage: 'Personalize general organization settings.', + defaultMessage: 'Organization name', } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx index 956d5143ef2c5..053a3b6b0e6bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx @@ -224,6 +224,7 @@ export const SaveConfig: React.FC = ({ return ( <> {header} +
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index b42bd674109fe..5aae4b352a1fb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -102,7 +102,7 @@ export const SaveCustom: React.FC = ({

{SAVE_CUSTOM_API_KEYS_TITLE}

- +

{SAVE_CUSTOM_API_KEYS_BODY}

@@ -126,7 +126,7 @@ export const SaveCustom: React.FC = ({

{SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE}

- +

= ({

{SAVE_CUSTOM_STYLING_RESULTS_TITLE}

- +

= ({

{SAVE_CUSTOM_DOC_PERMISSIONS_TITLE}

- +

{ {EVENT_HEADER} {!custom && {STATUS_HEADER}} - {TIME_HEADER} + {TIME_HEADER} {activities.map(({ details: activityDetails, event, time, status }, i) => ( @@ -203,7 +203,7 @@ export const Overview: React.FC = () => { )} - + {time} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index 3dd8ad1dc7899..1a6d97bbf75ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -151,7 +151,9 @@ export const SourceContent: React.FC = () => { )} - {moment(updated).format('M/D/YYYY, h:mm:ss A')} + + {moment(updated).format('M/D/YYYY, h:mm:ss A')} + ); }; @@ -164,7 +166,7 @@ export const SourceContent: React.FC = () => { {TITLE_HEADING} {startCase(urlField)} - {LAST_UPDATED_HEADING} + {LAST_UPDATED_HEADING} {contentItems.map(contentItem)} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index 3e1290292704e..aa6d4da99ea40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -300,9 +300,9 @@ export const SOURCE_REMOVE_TITLE = i18n.translate( ); export const SOURCE_REMOVE_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.config.description', + 'xpack.enterpriseSearch.workplaceSearch.sources.remove.description', { - defaultMessage: 'Edit content source connector settings to change.', + defaultMessage: 'This action cannot be undone.', } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 5e89d4491d597..204d8f5655172 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -91,7 +91,7 @@ export const GroupRow: React.FC = ({

)} - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx index 9b131e730b937..df7435bd25461 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx @@ -164,7 +164,7 @@ export const GroupSourcePrioritization: React.FC = () => { } /> - +
{activeSourcePriorities[id]}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.tsx index 2fcd880318a27..631c4f80f36b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { useActions, useValues } from 'kea'; +import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { GroupLogic } from '../group_logic'; @@ -53,6 +54,7 @@ export const SharedSourcesModal: React.FC = () => { values: { groupName: group.name }, })}

+ = ({ const emptyState = ( <> - + {isRemote ? REMOTE_SOURCES_EMPTY_TABLE_TITLE : STANDARD_SOURCES_EMPTY_TABLE_TITLE} @@ -175,7 +175,7 @@ export const PrivateSourcesTable: React.FC = ({ ); return ( - + {sectionHeading} {hasSources && sourcesTable} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx index 51346a69eeec2..346994ac557f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx @@ -14,6 +14,8 @@ import { shallow } from 'enzyme'; import { EuiSwitch, EuiConfirmModal } from '@elastic/eui'; +import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + import { Loading } from '../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../components/shared/view_content_header'; @@ -53,20 +55,19 @@ describe('Security', () => { }); }); - it('renders on Basic license', () => { + it('renders', () => { setMockValues({ ...mockValues, hasPlatinumLicense: false }); const wrapper = shallow(); + expect(wrapper.find(SetPageChrome)).toHaveLength(1); expect(wrapper.find(UnsavedChangesPrompt)).toHaveLength(1); expect(wrapper.find(ViewContentHeader)).toHaveLength(1); expect(wrapper.find(EuiSwitch).prop('disabled')).toEqual(true); }); - it('renders on Platinum license', () => { + it('does not disable switch on Platinum license', () => { const wrapper = shallow(); - expect(wrapper.find(UnsavedChangesPrompt)).toHaveLength(1); - expect(wrapper.find(ViewContentHeader)).toHaveLength(1); expect(wrapper.find(EuiSwitch).prop('disabled')).toEqual(false); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx index a81ac93ab69dd..669015794baef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { LicensingLogic } from '../../../shared/licensing'; import { Loading } from '../../../shared/loading'; import { UnsavedChangesPrompt } from '../../../shared/unsaved_changes_prompt'; @@ -40,6 +41,7 @@ import { PRIVATE_PLATINUM_LICENSE_CALLOUT, CONFIRM_CHANGES_TEXT, PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT, + NAV, } from '../../constants'; import { PrivateSourcesTable } from './components/private_sources_table'; @@ -114,7 +116,7 @@ export const Security: React.FC = () => { ); const allSourcesToggle = ( - + { return ( <> + { const wrapper = shallow(); expect(wrapper.find(FlashMessages)).toHaveLength(1); + expect(wrapper.find(SetPageChrome)).toHaveLength(3); expect(wrapper.find(Switch)).toHaveLength(1); expect(wrapper.find(Route)).toHaveLength(NUM_ROUTES); expect(wrapper.find(Redirect)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx index 34dcc48621a2e..e6264103df6d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx @@ -11,6 +11,8 @@ import { Redirect, Route, Switch } from 'react-router-dom'; import { useActions } from 'kea'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { NAV } from '../../constants'; import { ORG_SETTINGS_PATH, ORG_SETTINGS_CUSTOMIZE_PATH, @@ -38,12 +40,15 @@ export const SettingsRouter: React.FC = () => { + + + {staticSourceData.map(({ editPath }, i) => ( From 310194193aad34fee6852a4bd195562fb494c7ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 16 Mar 2021 15:22:24 +0000 Subject: [PATCH 12/44] [ILM] Use global field to set the snapshot repository (#94602) --- .../features/searchable_snapshots.test.ts | 48 ++++++++++- .../phases/cold_phase/cold_phase.tsx | 4 +- .../phases/delete_phase/delete_phase.tsx | 12 ++- .../components/phases/hot_phase/hot_phase.tsx | 4 +- .../min_age_field/min_age_field.tsx | 4 +- .../repository_combobox_field.tsx | 66 ++++++++++++++ .../searchable_snapshot_field.tsx | 85 +++++-------------- .../phases/warm_phase/warm_phase.tsx | 4 +- .../timeline/timeline.container.tsx | 4 +- .../sections/edit_policy/constants.ts | 6 ++ .../sections/edit_policy/edit_policy.tsx | 49 +++++++---- .../edit_policy/form/components/form.tsx | 25 +++--- ..._context.tsx => configuration_context.tsx} | 33 +++---- .../sections/edit_policy/form/deserializer.ts | 22 ++++- .../form/deserializer_and_serializer.test.ts | 4 +- .../form/global_fields_context.tsx | 54 ++++++++++++ .../sections/edit_policy/form/index.ts | 11 ++- .../form/phase_timings_context.tsx | 25 +++--- .../sections/edit_policy/form/schema.ts | 27 ++++-- .../edit_policy/form/serializer/serializer.ts | 23 ++++- ...absolute_timing_to_relative_timing.test.ts | 4 +- .../edit_policy/lib/get_default_repository.ts | 20 +++++ .../sections/edit_policy/lib/index.ts | 2 + .../application/sections/edit_policy/types.ts | 4 +- .../public/shared_imports.ts | 1 + 25 files changed, 373 insertions(+), 168 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/repository_combobox_field.tsx rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/{configuration_issues_context.tsx => configuration_context.tsx} (66%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/global_fields_context.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/get_default_repository.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts index 44e03564cb89a..a570c817cfe1b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts @@ -67,6 +67,46 @@ describe(' searchable snapshots', () => { expect(actions.hot.searchableSnapshotsExists()).toBeTruthy(); }); + test('should set the repository from previously defined repository', async () => { + const { actions } = testBed; + + const repository = 'myRepo'; + await actions.hot.setSearchableSnapshot(repository); + await actions.cold.enable(true); + await actions.cold.toggleSearchableSnapshot(true); + await actions.frozen.enable(true); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + expect(latestRequest.method).toBe('POST'); + expect(latestRequest.url).toBe('/api/index_lifecycle_management/policies'); + const reqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(reqBody.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe(repository); + expect(reqBody.phases.cold.actions.searchable_snapshot.snapshot_repository).toBe(repository); + expect(reqBody.phases.frozen.actions.searchable_snapshot.snapshot_repository).toBe(repository); + }); + + test('should update the repository in all searchable snapshot actions', async () => { + const { actions } = testBed; + + await actions.hot.setSearchableSnapshot('myRepo'); + await actions.cold.enable(true); + await actions.cold.toggleSearchableSnapshot(true); + await actions.frozen.enable(true); + + // We update the repository in one phase + await actions.frozen.setSearchableSnapshot('changed'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const reqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + // And all phases should be updated + expect(reqBody.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe('changed'); + expect(reqBody.phases.cold.actions.searchable_snapshot.snapshot_repository).toBe('changed'); + expect(reqBody.phases.frozen.actions.searchable_snapshot.snapshot_repository).toBe('changed'); + }); + describe('on cloud', () => { describe('new policy', () => { beforeEach(async () => { @@ -86,6 +126,7 @@ describe(' searchable snapshots', () => { const { component } = testBed; component.update(); }); + test('defaults searchable snapshot to true on cloud', async () => { const { find, actions } = testBed; await actions.cold.enable(true); @@ -112,14 +153,17 @@ describe(' searchable snapshots', () => { const { component } = testBed; component.update(); }); + test('correctly sets snapshot repository default to "found-snapshots"', async () => { const { actions } = testBed; await actions.cold.enable(true); await actions.cold.toggleSearchableSnapshot(true); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; - const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + expect(latestRequest.method).toBe('POST'); + expect(latestRequest.url).toBe('/api/index_lifecycle_management/policies'); + const reqBody = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(reqBody.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( 'found-snapshots' ); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index bc22516e6c996..72651778f403e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { useConfigurationIssues } from '../../../form'; +import { useConfiguration } from '../../../form'; import { DataTierAllocationField, SearchableSnapshotField, @@ -29,7 +29,7 @@ const i18nTexts = { }; export const ColdPhase: FunctionComponent = () => { - const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); + const { isUsingSearchableSnapshotInHotPhase } = useConfiguration(); return ( }> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx index 6c96178c86b5b..7b613757fa474 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx @@ -20,18 +20,16 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { useFormData } from '../../../../../../shared_imports'; - import { i18nTexts } from '../../../i18n_texts'; - -import { usePhaseTimings } from '../../../form'; - -import { MinAgeField, SnapshotPoliciesField } from '../shared_fields'; -import './delete_phase.scss'; +import { usePhaseTimings, globalFields } from '../../../form'; import { PhaseIcon } from '../../phase_icon'; +import { MinAgeField, SnapshotPoliciesField } from '../shared_fields'; import { PhaseErrorIndicator } from '../phase/phase_error_indicator'; +import './delete_phase.scss'; + const formFieldPaths = { - enabled: '_meta.delete.enabled', + enabled: globalFields.deleteEnabled.path, }; export const DeletePhase: FunctionComponent = () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 6d4e2750bb2e8..ea345009b230b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -23,7 +23,7 @@ import { useFormData, SelectField, NumericField } from '../../../../../../shared import { i18nTexts } from '../../../i18n_texts'; -import { ROLLOVER_EMPTY_VALIDATION, useConfigurationIssues, UseField } from '../../../form'; +import { ROLLOVER_EMPTY_VALIDATION, useConfiguration, UseField } from '../../../form'; import { useEditPolicyContext } from '../../../edit_policy_context'; @@ -47,7 +47,7 @@ export const HotPhase: FunctionComponent = () => { const [formData] = useFormData({ watch: isUsingDefaultRolloverPath, }); - const { isUsingRollover } = useConfigurationIssues(); + const { isUsingRollover } = useConfiguration(); const isUsingDefaultRollover: boolean = get(formData, isUsingDefaultRolloverPath); const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx index 04b756dc23559..3fe2f08cb4066 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx @@ -22,7 +22,7 @@ import { import { getFieldValidityAndErrorMessage } from '../../../../../../../shared_imports'; -import { UseField, useConfigurationIssues } from '../../../../form'; +import { UseField, useConfiguration } from '../../../../form'; import { getUnitsAriaLabelForPhase, getTimingLabelForPhase } from './util'; @@ -81,7 +81,7 @@ interface Props { } export const MinAgeField: FunctionComponent = ({ phase }): React.ReactElement => { - const { isUsingRollover } = useConfigurationIssues(); + const { isUsingRollover } = useConfiguration(); return ( {(field) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/repository_combobox_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/repository_combobox_field.tsx new file mode 100644 index 0000000000000..a5a9d8d492682 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/repository_combobox_field.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useEffect, useRef } from 'react'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; + +import { ComboBoxField, FieldHook } from '../../../../../../../shared_imports'; +import { useGlobalFields } from '../../../../form'; + +interface PropsRepositoryCombobox { + field: FieldHook; + isLoading: boolean; + repos: string[]; + noSuggestions: boolean; + globalRepository: string; +} + +export const RepositoryComboBoxField = ({ + field, + isLoading, + repos, + noSuggestions, + globalRepository, +}: PropsRepositoryCombobox) => { + const isMounted = useRef(false); + const { setValue } = field; + const { + searchableSnapshotRepo: { setValue: setSearchableSnapshotRepository }, + } = useGlobalFields(); + + useEffect(() => { + // We keep our phase searchable action field in sync + // with the default repository field declared globally for the policy + if (isMounted.current) { + setValue(Boolean(globalRepository.trim()) ? [globalRepository] : []); + } + isMounted.current = true; + }, [setValue, globalRepository]); + + return ( + ({ label: repo, value: repo })), + singleSelection: { asPlainText: true }, + isLoading, + noSuggestions, + onCreateOption: (newOption: string) => { + setSearchableSnapshotRepository(newOption); + }, + onChange: (options: EuiComboBoxOptionOption[]) => { + if (options.length > 0) { + setSearchableSnapshotRepository(options[0].label); + } else { + setSearchableSnapshotRepository(''); + } + }, + }} + /> + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 816e1aaec31d7..4cef7615a2d8d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -5,24 +5,18 @@ * 2.0. */ +import React, { FunctionComponent, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import React, { FunctionComponent, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiComboBoxOptionOption, - EuiTextColor, - EuiSpacer, - EuiCallOut, - EuiLink, -} from '@elastic/eui'; - -import { ComboBoxField, useKibana, useFormData } from '../../../../../../../shared_imports'; +import { EuiTextColor, EuiSpacer, EuiCallOut, EuiLink } from '@elastic/eui'; +import { useKibana, useFormData } from '../../../../../../../shared_imports'; import { useEditPolicyContext } from '../../../../edit_policy_context'; -import { useConfigurationIssues, UseField, searchableSnapshotFields } from '../../../../form'; +import { useConfiguration, UseField, globalFields } from '../../../../form'; import { FieldLoadingError, DescribedFormRow, LearnMoreLink } from '../../../'; import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provider'; +import { RepositoryComboBoxField } from './repository_combobox_field'; import './_searchable_snapshot_field.scss'; @@ -31,12 +25,6 @@ export interface Props { canBeDisabled?: boolean; } -/** - * This repository is provisioned by Elastic Cloud and will always - * exist as a "managed" repository. - */ -const CLOUD_DEFAULT_REPO = 'found-snapshots'; - const geti18nTexts = (phase: Props['phase']) => ({ title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', { defaultMessage: 'Searchable snapshot', @@ -71,13 +59,15 @@ export const SearchableSnapshotField: FunctionComponent = ({ services: { cloud }, } = useKibana(); const { getUrlForApp, policy, license, isNewPolicy } = useEditPolicyContext(); - const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); + const { isUsingSearchableSnapshotInHotPhase } = useConfiguration(); const searchableSnapshotRepoPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; - const [formData] = useFormData({ watch: searchableSnapshotRepoPath }); - const searchableSnapshotRepo = get(formData, searchableSnapshotRepoPath); + const [formData] = useFormData({ + watch: globalFields.searchableSnapshotRepo.path, + }); + const searchableSnapshotGlobalRepo = get(formData, globalFields.searchableSnapshotRepo.path); const isColdPhase = phase === 'cold'; const isFrozenPhase = phase === 'frozen'; const isColdOrFrozenPhase = isColdPhase || isFrozenPhase; @@ -164,7 +154,10 @@ export const SearchableSnapshotField: FunctionComponent = ({ /> ); - } else if (searchableSnapshotRepo && !repos.includes(searchableSnapshotRepo)) { + } else if ( + searchableSnapshotGlobalRepo && + !repos.includes(searchableSnapshotGlobalRepo) + ) { calloutContent = ( = ({ return (
- - config={{ - ...searchableSnapshotFields.snapshot_repository, - defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined, - }} + - {(field) => { - const singleSelectionArray: [selectedSnapshot?: string] = field.value - ? [field.value] - : []; - - return ( - ({ label: repo, value: repo })), - singleSelection: { asPlainText: true }, - isLoading, - noSuggestions: !!(error || repos.length === 0), - onCreateOption: (newOption: string) => { - field.setValue(newOption); - }, - onChange: (options: EuiComboBoxOptionOption[]) => { - if (options.length > 0) { - field.setValue(options[0].label); - } else { - field.setValue(''); - } - }, - }} - /> - ); + defaultValue={!!searchableSnapshotGlobalRepo ? [searchableSnapshotGlobalRepo] : []} + component={RepositoryComboBoxField} + componentProps={{ + globalRepository: searchableSnapshotGlobalRepo, + isLoading, + repos, + noSuggestions: !!(error || repos.length === 0), }} - + /> {calloutContent && ( <> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx index 577dab6804147..d082489c4b918 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { useConfigurationIssues } from '../../../form'; +import { useConfiguration } from '../../../form'; import { ForcemergeField, @@ -30,7 +30,7 @@ const i18nTexts = { }; export const WarmPhase: FunctionComponent = () => { - const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); + const { isUsingSearchableSnapshotInHotPhase } = useConfiguration(); return ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx index 88d9d2de03d89..d5cbb267c77c3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx @@ -11,7 +11,7 @@ import { useFormData } from '../../../../../shared_imports'; import { formDataToAbsoluteTimings } from '../../lib'; -import { useConfigurationIssues } from '../../form'; +import { useConfiguration } from '../../form'; import { FormInternal } from '../../types'; @@ -20,7 +20,7 @@ import { Timeline as ViewComponent } from './timeline'; export const Timeline: FunctionComponent = () => { const [formData] = useFormData(); const timings = formDataToAbsoluteTimings(formData); - const { isUsingRollover } = useConfigurationIssues(); + const { isUsingRollover } = useConfiguration(); return ( = ({ history }) => { license, } = useEditPolicyContext(); - const serializer = useMemo(() => { - return createSerializer(isNewPolicy ? undefined : currentPolicy); - }, [isNewPolicy, currentPolicy]); + const { + services: { cloud }, + } = useKibana(); const [saveAsNew, setSaveAsNew] = useState(false); const originalPolicyName: string = isNewPolicy ? '' : policyName!; const isAllowedByLicense = license.canUseSearchableSnapshot(); + const isCloudEnabled = Boolean(cloud?.isCloudEnabled); - const { form } = useForm({ - schema, - defaultValue: { + const serializer = useMemo(() => { + return createSerializer(isNewPolicy ? undefined : currentPolicy); + }, [isNewPolicy, currentPolicy]); + + const deserializer = useMemo(() => { + return createDeserializer(isCloudEnabled); + }, [isCloudEnabled]); + + const defaultValue = useMemo( + () => ({ ...currentPolicy, name: originalPolicyName, - }, + }), + [currentPolicy, originalPolicyName] + ); + + const schema = useMemo(() => { + return getSchema(isCloudEnabled); + }, [isCloudEnabled]); + + const { form } = useForm({ + schema, + defaultValue, deserializer, serializer, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx index be8243cab289f..5d1add85bf9f4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx @@ -9,20 +9,25 @@ import React, { FunctionComponent } from 'react'; import { Form as LibForm, FormHook } from '../../../../../shared_imports'; -import { ConfigurationIssuesProvider } from '../configuration_issues_context'; +import { ConfigurationProvider } from '../configuration_context'; import { FormErrorsProvider } from '../form_errors_context'; import { PhaseTimingsProvider } from '../phase_timings_context'; +import { GlobalFieldsProvider } from '../global_fields_context'; interface Props { form: FormHook; } -export const Form: FunctionComponent = ({ form, children }) => ( - - - - {children} - - - -); +export const Form: FunctionComponent = ({ form, children }) => { + return ( + + + + + {children} + + + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_context.tsx similarity index 66% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_context.tsx index c2e55f7aa6e61..97952a3a212c7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_context.tsx @@ -12,7 +12,7 @@ import { useFormData } from '../../../../shared_imports'; import { isUsingDefaultRolloverPath, isUsingCustomRolloverPath } from '../constants'; -export interface ConfigurationIssues { +export interface Configuration { /** * Whether the serialized policy will use rollover. This blocks certain actions in * the form such as hot phase (forcemerge, shrink) and cold phase (searchable snapshot). @@ -28,7 +28,7 @@ export interface ConfigurationIssues { isUsingSearchableSnapshotInColdPhase: boolean; } -const ConfigurationIssuesContext = createContext(null as any); +const ConfigurationContext = createContext(null as any); const pathToHotPhaseSearchableSnapshot = 'phases.hot.actions.searchable_snapshot.snapshot_repository'; @@ -36,7 +36,7 @@ const pathToHotPhaseSearchableSnapshot = const pathToColdPhaseSearchableSnapshot = 'phases.cold.actions.searchable_snapshot.snapshot_repository'; -export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => { +export const ConfigurationProvider: FunctionComponent = ({ children }) => { const [formData] = useFormData({ watch: [ pathToHotPhaseSearchableSnapshot, @@ -49,25 +49,18 @@ export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => // Provide default value, as path may become undefined if removed from the DOM const isUsingCustomRollover = get(formData, isUsingCustomRolloverPath, true); - return ( - - {children} - - ); + const context: Configuration = { + isUsingRollover: isUsingDefaultRollover === false ? isUsingCustomRollover : true, + isUsingSearchableSnapshotInHotPhase: get(formData, pathToHotPhaseSearchableSnapshot) != null, + isUsingSearchableSnapshotInColdPhase: get(formData, pathToColdPhaseSearchableSnapshot) != null, + }; + + return {children}; }; -export const useConfigurationIssues = () => { - const ctx = useContext(ConfigurationIssuesContext); - if (!ctx) - throw new Error('Cannot use configuration issues outside of configuration issues context'); +export const useConfiguration = () => { + const ctx = useContext(ConfigurationContext); + if (!ctx) throw new Error('Cannot use configuration outside of configuration context'); return ctx; }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 227f135ca7b72..d8cffb974dfd1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -8,18 +8,29 @@ import { produce } from 'immer'; import { SerializedPolicy } from '../../../../../common/types'; - import { splitSizeAndUnits } from '../../../lib/policies'; - import { determineDataTierAllocationType, isUsingDefaultRollover } from '../../../lib'; - +import { getDefaultRepository } from '../lib'; import { FormInternal } from '../types'; +import { CLOUD_DEFAULT_REPO } from '../constants'; -export const deserializer = (policy: SerializedPolicy): FormInternal => { +export const createDeserializer = (isCloudEnabled: boolean) => ( + policy: SerializedPolicy +): FormInternal => { const { phases: { hot, warm, cold, frozen, delete: deletePhase }, } = policy; + let defaultRepository = getDefaultRepository([ + hot?.actions.searchable_snapshot, + cold?.actions.searchable_snapshot, + frozen?.actions.searchable_snapshot, + ]); + + if (!defaultRepository && isCloudEnabled) { + defaultRepository = CLOUD_DEFAULT_REPO; + } + const _meta: FormInternal['_meta'] = { hot: { isUsingDefaultRollover: isUsingDefaultRollover(policy), @@ -49,6 +60,9 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { delete: { enabled: Boolean(deletePhase), }, + searchableSnapshot: { + repository: defaultRepository, + }, }; return produce( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts index ab60a631dacc5..bdb915ba62d44 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -9,7 +9,7 @@ import { setAutoFreeze } from 'immer'; import { cloneDeep } from 'lodash'; import { SerializedPolicy } from '../../../../../common/types'; import { defaultRolloverAction } from '../../../constants'; -import { deserializer } from './deserializer'; +import { createDeserializer } from './deserializer'; import { createSerializer } from './serializer'; import { FormInternal } from '../types'; @@ -18,6 +18,8 @@ const isObject = (v: unknown): v is { [key: string]: any } => const unknownValue = { some: 'value' }; +const deserializer = createDeserializer(false); + const populateWithUnknownEntries = (v: unknown) => { if (isObject(v)) { for (const key of Object.keys(v)) { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/global_fields_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/global_fields_context.tsx new file mode 100644 index 0000000000000..30a00390a18cc --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/global_fields_context.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, FunctionComponent, useContext } from 'react'; +import { UseMultiFields, FieldHook, FieldConfig } from '../../../../shared_imports'; + +/** + * Those are the fields that we always want present in our form. + */ +interface GlobalFieldsTypes { + deleteEnabled: boolean; + searchableSnapshotRepo: string; +} + +type GlobalFields = { + [K in keyof GlobalFieldsTypes]: FieldHook; +}; + +const GlobalFieldsContext = createContext(null); + +export const globalFields: Record< + keyof GlobalFields, + { path: string; config?: FieldConfig } +> = { + deleteEnabled: { + path: '_meta.delete.enabled', + }, + searchableSnapshotRepo: { + path: '_meta.searchableSnapshot.repository', + }, +}; + +export const GlobalFieldsProvider: FunctionComponent = ({ children }) => { + return ( + fields={globalFields}> + {(fields) => { + return ( + {children} + ); + }} + + ); +}; + +export const useGlobalFields = () => { + const ctx = useContext(GlobalFieldsContext); + if (!ctx) throw new Error('Cannot use global fields outside of global fields context'); + + return ctx; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts index 6deb4d7fd4711..f31fedfac6681 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts @@ -5,20 +5,17 @@ * 2.0. */ -export { deserializer } from './deserializer'; +export { createDeserializer } from './deserializer'; export { createSerializer } from './serializer'; -export { schema, searchableSnapshotFields } from './schema'; +export { getSchema } from './schema'; export * from './validations'; export { Form, EnhancedUseField as UseField } from './components'; -export { - ConfigurationIssuesProvider, - useConfigurationIssues, -} from './configuration_issues_context'; +export { ConfigurationProvider, useConfiguration } from './configuration_context'; export { FormErrorsProvider, useFormErrorsContext } from './form_errors_context'; @@ -27,3 +24,5 @@ export { usePhaseTimings, PhaseTimingConfiguration, } from './phase_timings_context'; + +export { useGlobalFields, globalFields } from './global_fields_context'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx index 98ffb7e2dd7af..0cbee8832c55b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx @@ -8,7 +8,7 @@ import React, { createContext, FunctionComponent, useContext } from 'react'; import { useFormData } from '../../../../shared_imports'; import { FormInternal } from '../types'; -import { UseField } from './index'; +import { useGlobalFields } from './index'; export interface PhaseTimingConfiguration { /** @@ -48,6 +48,7 @@ export interface PhaseTimings { const PhaseTimingsContext = createContext(null as any); export const PhaseTimingsProvider: FunctionComponent = ({ children }) => { + const { deleteEnabled } = useGlobalFields(); const [formData] = useFormData({ watch: [ '_meta.warm.enabled', @@ -58,21 +59,15 @@ export const PhaseTimingsProvider: FunctionComponent = ({ children }) => { }); return ( - - {(field) => { - return ( - - {children} - - ); + + > + {children} + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 5861c7b320de1..c0e489042586c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -9,12 +9,8 @@ import { i18n } from '@kbn/i18n'; import { FormSchema, fieldValidators } from '../../../../shared_imports'; import { defaultIndexPriority } from '../../../constants'; -import { ROLLOVER_FORM_PATHS } from '../constants'; - -import { FormInternal } from '../types'; - -const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS); - +import { ROLLOVER_FORM_PATHS, CLOUD_DEFAULT_REPO } from '../constants'; +import { i18nTexts } from '../i18n_texts'; import { ifExistsNumberGreaterThanZero, ifExistsNumberNonNegative, @@ -22,7 +18,7 @@ import { minAgeValidator, } from './validations'; -import { i18nTexts } from '../i18n_texts'; +const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS); const { emptyField, numberGreaterThanField } = fieldValidators; @@ -54,6 +50,13 @@ export const searchableSnapshotFields = { validations: [ { validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) }, ], + // TODO: update text copy + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.repositoryHelpText', + { + defaultMessage: 'Each phase uses the same snapshot repository.', + } + ), }, storage: { label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageLabel', { @@ -114,7 +117,7 @@ const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ serializer: serializers.stringToNumber, }); -export const schema: FormSchema = { +export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ _meta: { hot: { isUsingDefaultRollover: { @@ -230,6 +233,11 @@ export const schema: FormSchema = { defaultValue: 'd', }, }, + searchableSnapshot: { + repository: { + defaultValue: isCloudEnabled ? CLOUD_DEFAULT_REPO : '', + }, + }, }, phases: { hot: { @@ -288,6 +296,7 @@ export const schema: FormSchema = { set_priority: { priority: getPriorityField('hot'), }, + searchable_snapshot: searchableSnapshotFields, }, }, warm: { @@ -375,4 +384,4 @@ export const schema: FormSchema = { }, }, }, -}; +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index b21545ce1739c..57112b0e1cb16 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -124,7 +124,12 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( /** * HOT PHASE SEARCHABLE SNAPSHOT */ - if (!updatedPolicy.phases.hot!.actions?.searchable_snapshot) { + if (updatedPolicy.phases.hot!.actions?.searchable_snapshot) { + hotPhaseActions.searchable_snapshot = { + ...hotPhaseActions.searchable_snapshot, + snapshot_repository: _meta.searchableSnapshot.repository, + }; + } else { delete hotPhaseActions.searchable_snapshot; } } @@ -234,7 +239,12 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( /** * COLD PHASE SEARCHABLE SNAPSHOT */ - if (!updatedPolicy.phases.cold?.actions?.searchable_snapshot) { + if (updatedPolicy.phases.cold?.actions?.searchable_snapshot) { + coldPhase.actions.searchable_snapshot = { + ...coldPhase.actions.searchable_snapshot, + snapshot_repository: _meta.searchableSnapshot.repository, + }; + } else { delete coldPhase.actions.searchable_snapshot; } } else { @@ -251,7 +261,12 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( /** * FROZEN PHASE SEARCHABLE SNAPSHOT */ - if (!updatedPolicy.phases.frozen?.actions?.searchable_snapshot) { + if (updatedPolicy.phases.frozen?.actions?.searchable_snapshot) { + frozenPhase.actions.searchable_snapshot = { + ...frozenPhase.actions.searchable_snapshot, + snapshot_repository: _meta.searchableSnapshot.repository, + }; + } else { delete frozenPhase.actions.searchable_snapshot; } } else { @@ -271,7 +286,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( deletePhase.actions.delete = deletePhase.actions.delete ?? {}; /** - * DELETE PHASE SEARCHABLE SNAPSHOT + * DELETE PHASE MIN AGE */ if (updatedPolicy.phases.delete?.min_age) { deletePhase.min_age = `${updatedPolicy.phases.delete!.min_age}${_meta.delete.minAgeUnit}`; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts index 8a9635e2db219..d4a26924385f0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts @@ -6,13 +6,15 @@ */ import { flow } from 'fp-ts/function'; -import { deserializer } from '../form'; +import { createDeserializer } from '../form'; import { formDataToAbsoluteTimings, calculateRelativeFromAbsoluteMilliseconds, } from './absolute_timing_to_relative_timing'; +const deserializer = createDeserializer(false); + export const calculateRelativeTimingMs = flow( formDataToAbsoluteTimings, calculateRelativeFromAbsoluteMilliseconds diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/get_default_repository.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/get_default_repository.ts new file mode 100644 index 0000000000000..43e911333e357 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/get_default_repository.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SearchableSnapshotAction } from '../../../../../common/types'; + +export const getDefaultRepository = ( + configs: Array +): string => { + if (configs.length === 0) { + return ''; + } + if (Boolean(configs[0]?.snapshot_repository)) { + return configs[0]!.snapshot_repository; + } + return getDefaultRepository(configs.slice(1)); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts index af4757a7b7105..19d87532f2bfe 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts @@ -12,3 +12,5 @@ export { PhaseAgeInMilliseconds, RelativePhaseTimingInMs, } from './absolute_timing_to_relative_timing'; + +export { getDefaultRepository } from './get_default_repository'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 4330cde378b6d..977554f12da42 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { SerializedPolicy } from '../../../../common/types'; export type DataTierAllocationType = 'node_roles' | 'node_attrs' | 'none'; @@ -76,5 +75,8 @@ export interface FormInternal extends SerializedPolicy { cold: ColdPhaseMetaFields; frozen: FrozenPhaseMetaFields; delete: DeletePhaseMetaFields; + searchableSnapshot: { + repository: string; + }; }; } diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts index cf2d5d5efc0f8..a8e0182ada77b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -23,6 +23,7 @@ export { useFormContext, FormSchema, ValidationConfig, + UseMultiFields, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; From c937f2648ecbc869042bf3d66f3178db03704089 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 16 Mar 2021 11:23:19 -0400 Subject: [PATCH 13/44] tiny fix for loading state in dashboard top nav (#94643) --- .../dashboard/public/application/top_nav/dashboard_top_nav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 6230a16f10491..a82aa78b815ec 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -249,11 +249,11 @@ export function DashboardTopNav({ useReplace: true, }); } else { - setIsSaveInProgress(false); dashboardStateManager.resetState(); chrome.docTitle.change(dashboardStateManager.savedDashboard.lastSavedTitle); } } + setIsSaveInProgress(false); return { id }; }) .catch((error) => { From badf38b0cd5ae78bff3bb8b730253fd617a3f3be Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 16 Mar 2021 08:30:32 -0700 Subject: [PATCH 14/44] [Curation] Add support for a custom drag handle to the Result component (#94652) * Update Result component to render a custom drag handle * Update Result library with a draggable example - note: this doesn't actually handle reorder logic, it's purely a UI/UX example * Update CurationResult to pass dragHandleProps through to Result --- .../curation/results/curation_result.test.tsx | 9 +++++-- .../curation/results/curation_result.tsx | 5 +++- .../app_search/components/library/library.tsx | 25 +++++++++++++++++++ .../app_search/components/result/result.scss | 17 ++++++++++--- .../components/result/result.test.tsx | 15 +++++++++++ .../app_search/components/result/result.tsx | 8 ++++++ 6 files changed, 72 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.test.tsx index 5c417d308636e..460c0f4dfa44c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.test.tsx @@ -8,6 +8,7 @@ import { setMockValues } from '../../../../../__mocks__'; import React from 'react'; +import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import { shallow, ShallowWrapper } from 'enzyme'; @@ -29,12 +30,15 @@ describe('CurationResult', () => { { title: 'add', iconType: 'plus', onClick: () => {} }, { title: 'remove', iconType: 'minus', onClick: () => {} }, ]; + const mockDragging = {} as DraggableProvidedDragHandleProps; // Passed from EuiDraggable let wrapper: ShallowWrapper; beforeAll(() => { setMockValues(values); - wrapper = shallow(); + wrapper = shallow( + + ); }); it('passes EngineLogic state', () => { @@ -42,8 +46,9 @@ describe('CurationResult', () => { expect(wrapper.find(Result).prop('schemaForTypeHighlights')).toEqual('some mock schema'); }); - it('passes result and actions props', () => { + it('passes result, actions, and dragHandleProps props', () => { expect(wrapper.find(Result).prop('result')).toEqual(mockResult); expect(wrapper.find(Result).prop('actions')).toEqual(mockActions); + expect(wrapper.find(Result).prop('dragHandleProps')).toEqual(mockDragging); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.tsx index 3be11bcd65956..c737d93ce1823 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/results/curation_result.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import { useValues } from 'kea'; @@ -18,9 +19,10 @@ import { Result as ResultType, ResultAction } from '../../../result/types'; interface Props { result: ResultType; actions: ResultAction[]; + dragHandleProps?: DraggableProvidedDragHandleProps; } -export const CurationResult: React.FC = ({ result, actions }) => { +export const CurationResult: React.FC = ({ result, actions, dragHandleProps }) => { const { isMetaEngine, engine: { schema }, @@ -33,6 +35,7 @@ export const CurationResult: React.FC = ({ result, actions }) => { actions={actions} isMetaEngine={isMetaEngine} schemaForTypeHighlights={schema} + dragHandleProps={dragHandleProps} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index 3f72199d12805..594584d9ba101 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -14,6 +14,9 @@ import { EuiTitle, EuiPageContentBody, EuiPageContent, + EuiDragDropContext, + EuiDroppable, + EuiDraggable, } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; @@ -228,6 +231,28 @@ export const Library: React.FC = () => { + + +

With a drag handle

+
+ + {}}> + + {[1, 2, 3].map((_, i) => ( + + {(provided) => } + + ))} + + + +

With field value type highlights

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss index f69acbdaba150..5f1b165f2c362 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss @@ -1,10 +1,10 @@ .appSearchResult { display: grid; - grid-template-columns: 1fr auto; - grid-template-rows: 1fr auto; + grid-template-columns: auto 1fr auto; + grid-template-rows: auto 1fr auto; grid-template-areas: - 'content actions' - 'toggle actions'; + 'drag content actions' + 'drag toggle actions'; overflow: hidden; // Prevents child background-colors from clipping outside of panel border-radius &__content { @@ -52,6 +52,15 @@ background-color: $euiPageBackgroundColor; } } + + &__dragHandle { + grid-area: drag; + display: flex; + justify-content: center; + align-items: center; + width: $euiSizeXL; + border-right: $euiBorderThin; + } } /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index 86b71229f3785..15c9ee2967d3e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import { shallow, ShallowWrapper } from 'enzyme'; @@ -129,6 +130,20 @@ describe('Result', () => { }); }); + describe('dragging', () => { + // In the real world, the drag library sets data attributes, role, tabIndex, etc. + const mockDragHandleProps = ({ + someMockProp: true, + } as unknown) as DraggableProvidedDragHandleProps; + + it('will render a drag handle with the passed props', () => { + const wrapper = shallow(); + + expect(wrapper.find('.appSearchResult__dragHandle')).toHaveLength(1); + expect(wrapper.find('.appSearchResult__dragHandle').prop('someMockProp')).toEqual(true); + }); + }); + it('will render field details with type highlights if schemaForTypeHighlights has been provided', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index 2812b596e87fa..89208a041af35 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -6,6 +6,7 @@ */ import React, { useState, useMemo } from 'react'; +import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import classNames from 'classnames'; @@ -31,6 +32,7 @@ interface Props { shouldLinkToDetailPage?: boolean; schemaForTypeHighlights?: Schema; actions?: ResultAction[]; + dragHandleProps?: DraggableProvidedDragHandleProps; } const RESULT_CUTOFF = 5; @@ -42,6 +44,7 @@ export const Result: React.FC = ({ shouldLinkToDetailPage = false, schemaForTypeHighlights, actions = [], + dragHandleProps, }) => { const [isOpen, setIsOpen] = useState(false); @@ -87,6 +90,11 @@ export const Result: React.FC = ({ values: { id: result[ID].raw }, })} > + {dragHandleProps && ( +
+ +
+ )} {conditionallyLinkedArticle( <> Date: Tue, 16 Mar 2021 16:41:07 +0100 Subject: [PATCH 15/44] [Uptime] Synthetic check steps list view (#90978) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - x-pack/plugins/uptime/common/constants/ui.ts | 2 + .../uptime/common/runtime_types/ping/ping.ts | 2 + .../components/common/step_detail_link.tsx | 12 +- .../columns/ping_timestamp/nav_buttons.tsx | 8 +- .../ping_timestamp/ping_timestamp.test.tsx | 12 +- .../columns/ping_timestamp/ping_timestamp.tsx | 45 ++- .../step_image_caption.test.tsx | 5 +- .../ping_timestamp/step_image_caption.tsx | 33 ++- .../ping_timestamp/step_image_popover.tsx | 3 +- .../monitor/ping_list/expanded_row.test.tsx | 37 ++- .../monitor/ping_list/expanded_row.tsx | 19 -- .../monitor/ping_list/ping_list.tsx | 57 +++- .../synthetics/browser_expanded_row.test.tsx | 203 -------------- .../synthetics/browser_expanded_row.tsx | 65 ----- .../synthetics/executed_journey.test.tsx | 265 ------------------ .../monitor/synthetics/executed_journey.tsx | 88 ------ .../monitor/synthetics/executed_step.tsx | 110 -------- .../monitor/synthetics/status_badge.test.tsx | 42 --- .../step_detail/step_detail_container.tsx | 2 +- .../step_detail/use_monitor_breadcrumb.tsx | 33 ++- .../use_monitor_breadcrumbs.test.tsx | 84 +++++- .../columns/monitor_status_column.tsx | 2 +- .../step_expanded_row/screenshot_link.tsx | 47 ++++ .../step_expanded_row/step_screenshots.tsx | 86 ++++++ .../synthetics/check_steps/step_image.tsx | 28 ++ .../synthetics/check_steps/step_list.test.tsx | 144 ++++++++++ .../synthetics/check_steps/steps_list.tsx | 175 ++++++++++++ .../synthetics/check_steps/use_check_steps.ts | 29 ++ .../check_steps/use_expanded_row.test.tsx | 152 ++++++++++ .../check_steps/use_expanded_row.tsx | 88 ++++++ .../synthetics/code_block_accordion.tsx | 4 +- .../synthetics/console_event.test.tsx | 0 .../synthetics/console_event.tsx | 4 +- .../console_output_event_list.test.tsx | 0 .../synthetics/console_output_event_list.tsx | 4 +- .../synthetics/empty_journey.test.tsx | 0 .../synthetics/empty_journey.tsx | 0 .../synthetics/executed_step.test.tsx | 53 ++-- .../components/synthetics/executed_step.tsx | 118 ++++++++ .../synthetics/status_badge.test.tsx | 33 +++ .../{monitor => }/synthetics/status_badge.tsx | 20 +- .../step_screenshot_display.test.tsx | 2 +- .../synthetics/step_screenshot_display.tsx | 89 ++---- .../components/synthetics/translations.ts | 20 ++ .../uptime/public/hooks/use_telemetry.ts | 1 + x-pack/plugins/uptime/public/pages/index.ts | 2 +- .../pages/synthetics/checks_navigation.tsx | 60 ++++ .../{ => synthetics}/step_detail_page.tsx | 6 +- .../pages/synthetics/synthetics_checks.tsx | 44 +++ x-pack/plugins/uptime/public/routes.tsx | 9 + .../uptime/public/state/api/journey.ts | 17 ++ .../lib/requests/get_journey_details.ts | 6 +- .../lib/requests/get_journey_screenshot.ts | 4 +- .../lib/requests/get_journey_steps.test.ts | 4 +- .../server/lib/requests/get_journey_steps.ts | 2 +- .../lib/requests/get_last_successful_step.ts | 77 +++++ .../uptime/server/lib/requests/index.ts | 2 + .../plugins/uptime/server/rest_api/index.ts | 2 + .../rest_api/pings/journey_screenshots.ts | 5 +- .../uptime/server/rest_api/pings/journeys.ts | 23 +- .../synthetics/last_successful_step.ts | 33 +++ 63 files changed, 1514 insertions(+), 1020 deletions(-) delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.test.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.test.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx delete mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/screenshot_link.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/step_image.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/step_list.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/use_check_steps.ts create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/code_block_accordion.tsx (87%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/console_event.test.tsx (100%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/console_event.tsx (89%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/console_output_event_list.test.tsx (100%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/console_output_event_list.tsx (92%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/empty_journey.test.tsx (100%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/empty_journey.tsx (100%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/executed_step.test.tsx (54%) create mode 100644 x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx create mode 100644 x-pack/plugins/uptime/public/components/synthetics/status_badge.test.tsx rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/status_badge.tsx (66%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/step_screenshot_display.test.tsx (96%) rename x-pack/plugins/uptime/public/components/{monitor => }/synthetics/step_screenshot_display.tsx (52%) create mode 100644 x-pack/plugins/uptime/public/components/synthetics/translations.ts create mode 100644 x-pack/plugins/uptime/public/pages/synthetics/checks_navigation.tsx rename x-pack/plugins/uptime/public/pages/{ => synthetics}/step_detail_page.tsx (75%) create mode 100644 x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx create mode 100644 x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts create mode 100644 x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 32b749d2d7fa7..a558834ab67f6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23257,12 +23257,8 @@ "xpack.uptime.synthetics.emptyJourney.message.footer": "表示する詳細情報はありません。", "xpack.uptime.synthetics.emptyJourney.message.heading": "ステップが含まれていませんでした。", "xpack.uptime.synthetics.emptyJourney.title": "ステップがありません。", - "xpack.uptime.synthetics.executedJourney.heading": "概要情報", "xpack.uptime.synthetics.executedStep.errorHeading": "エラー", - "xpack.uptime.synthetics.executedStep.scriptHeading": "スクリプトのステップ", "xpack.uptime.synthetics.executedStep.stackTrace": "スタックトレース", - "xpack.uptime.synthetics.executedStep.stepName": "{stepNumber}. {stepName}", - "xpack.uptime.synthetics.experimentalCallout.title": "実験的機能", "xpack.uptime.synthetics.imageLoadingSpinner.ariaLabel": "画像を示すアニメーションスピナーを読み込んでいます", "xpack.uptime.synthetics.journey.allFailedMessage": "{total}ステップ - すべて失敗またはスキップされました", "xpack.uptime.synthetics.journey.allSucceededMessage": "{total}ステップ - すべて成功しました", @@ -23273,8 +23269,6 @@ "xpack.uptime.synthetics.screenshot.noImageMessage": "画像がありません", "xpack.uptime.synthetics.screenshotDisplay.altText": "名前「{stepName}」のステップのスクリーンショット", "xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName": "スクリーンショット", - "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltText": "名前「{stepName}」のステップのサムネイルスクリーンショット", - "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltTextWithoutName": "サムネイルスクリーンショット", "xpack.uptime.synthetics.statusBadge.failedMessage": "失敗", "xpack.uptime.synthetics.statusBadge.skippedMessage": "スキップ", "xpack.uptime.synthetics.statusBadge.succeededMessage": "成功", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index db3ca3d56ec5a..9113b44d6ad31 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23614,12 +23614,8 @@ "xpack.uptime.synthetics.emptyJourney.message.footer": "没有更多可显示的信息。", "xpack.uptime.synthetics.emptyJourney.message.heading": "此过程不包含任何步骤。", "xpack.uptime.synthetics.emptyJourney.title": "没有此过程的任何步骤", - "xpack.uptime.synthetics.executedJourney.heading": "摘要信息", "xpack.uptime.synthetics.executedStep.errorHeading": "错误", - "xpack.uptime.synthetics.executedStep.scriptHeading": "步骤脚本", "xpack.uptime.synthetics.executedStep.stackTrace": "堆栈跟踪", - "xpack.uptime.synthetics.executedStep.stepName": "{stepNumber}:{stepName}", - "xpack.uptime.synthetics.experimentalCallout.title": "实验功能", "xpack.uptime.synthetics.imageLoadingSpinner.ariaLabel": "表示图像正在加载的动画旋转图标", "xpack.uptime.synthetics.journey.allFailedMessage": "{total} 个步骤 - 全部失败或跳过", "xpack.uptime.synthetics.journey.allSucceededMessage": "{total} 个步骤 - 全部成功", @@ -23630,8 +23626,6 @@ "xpack.uptime.synthetics.screenshot.noImageMessage": "没有可用图像", "xpack.uptime.synthetics.screenshotDisplay.altText": "名称为“{stepName}”的步骤的屏幕截图", "xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName": "屏幕截图", - "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltText": "名称为“{stepName}”的步骤的缩略屏幕截图", - "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltTextWithoutName": "缩略屏幕截图", "xpack.uptime.synthetics.statusBadge.failedMessage": "失败", "xpack.uptime.synthetics.statusBadge.skippedMessage": "已跳过", "xpack.uptime.synthetics.statusBadge.succeededMessage": "成功", diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 880bc0f92ddf6..dcaf4bb310ad7 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -15,6 +15,8 @@ export const CERTIFICATES_ROUTE = '/certificates'; export const STEP_DETAIL_ROUTE = '/journey/:checkGroupId/step/:stepIndex'; +export const SYNTHETIC_CHECK_STEPS_ROUTE = '/journey/:checkGroupId/steps'; + export enum STATUS { UP = 'up', DOWN = 'down', diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index 8991d52f6a920..77b9473f2912e 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -216,6 +216,7 @@ export const PingType = t.intersection([ type: t.string, url: t.string, end: t.number, + text: t.string, }), }), tags: t.array(t.string), @@ -251,6 +252,7 @@ export const SyntheticsJourneyApiResponseType = t.intersection([ t.intersection([ t.type({ timestamp: t.string, + journey: PingType, }), t.partial({ next: t.type({ diff --git a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx index 313dd18e67c11..fa6d0b4c3f8bb 100644 --- a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx +++ b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx @@ -6,7 +6,7 @@ */ import React, { FC } from 'react'; -import { ReactRouterEuiButton } from './react_router_helpers'; +import { ReactRouterEuiButtonEmpty } from './react_router_helpers'; interface StepDetailLinkProps { /** @@ -23,14 +23,8 @@ export const StepDetailLink: FC = ({ children, checkGroupId const to = `/journey/${checkGroupId}/step/${stepIndex}`; return ( - + {children} - + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/nav_buttons.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/nav_buttons.tsx index 390a133b1819b..3b0aad721be8a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/nav_buttons.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/nav_buttons.tsx @@ -6,7 +6,7 @@ */ import { EuiButtonIcon, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import React from 'react'; +import React, { MouseEvent } from 'react'; import { nextAriaLabel, prevAriaLabel } from './translations'; export interface NavButtonsProps { @@ -34,8 +34,9 @@ export const NavButtons: React.FC = ({ disabled={stepNumber === 1} color="subdued" size="s" - onClick={() => { + onClick={(evt: MouseEvent) => { setStepNumber(stepNumber - 1); + evt.stopPropagation(); }} iconType="arrowLeft" aria-label={prevAriaLabel} @@ -46,8 +47,9 @@ export const NavButtons: React.FC = ({ disabled={stepNumber === maxSteps} color="subdued" size="s" - onClick={() => { + onClick={(evt: MouseEvent) => { setStepNumber(stepNumber + 1); + evt.stopPropagation(); }} iconType="arrowRight" aria-label={nextAriaLabel} diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx index 2a1989cafa434..d628b2d8388f9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.test.tsx @@ -12,6 +12,8 @@ import { mockReduxHooks } from '../../../../../lib/helper/test_helpers'; import { render } from '../../../../../lib/helper/rtl_helpers'; import { Ping } from '../../../../../../common/runtime_types/ping'; import * as observabilityPublic from '../../../../../../../observability/public'; +import { getShortTimeStamp } from '../../../../overview/monitor_list/columns/monitor_status_column'; +import moment from 'moment'; mockReduxHooks(); @@ -68,7 +70,7 @@ describe('Ping Timestamp component', () => { .spyOn(observabilityPublic, 'useFetcher') .mockReturnValue({ status: fetchStatus, data: null, refetch: () => null }); const { getByTestId } = render( - + ); expect(getByTestId('pingTimestampSpinner')).toBeInTheDocument(); } @@ -79,7 +81,7 @@ describe('Ping Timestamp component', () => { .spyOn(observabilityPublic, 'useFetcher') .mockReturnValue({ status: FETCH_STATUS.SUCCESS, data: null, refetch: () => null }); const { getByTestId } = render( - + ); expect(getByTestId('pingTimestampNoImageAvailable')).toBeInTheDocument(); }); @@ -91,7 +93,9 @@ describe('Ping Timestamp component', () => { data: { src }, refetch: () => null, }); - const { container } = render(); + const { container } = render( + + ); expect(container.querySelector('img')?.src).toBe(src); }); @@ -103,7 +107,7 @@ describe('Ping Timestamp component', () => { refetch: () => null, }); const { getByAltText, getAllByText, queryByAltText } = render( - + ); const caption = getAllByText('Nov 26, 2020 10:28:56 AM'); fireEvent.mouseEnter(caption[0]); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx index cfb92dd31190e..16553e9de8604 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx @@ -8,18 +8,15 @@ import React, { useContext, useEffect, useState } from 'react'; import useIntersection from 'react-use/lib/useIntersection'; import styled from 'styled-components'; -import moment from 'moment'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Ping } from '../../../../../../common/runtime_types/ping'; import { useFetcher, FETCH_STATUS } from '../../../../../../../observability/public'; import { getJourneyScreenshot } from '../../../../../state/api/journey'; import { UptimeSettingsContext } from '../../../../../contexts'; -import { NavButtons } from './nav_buttons'; import { NoImageDisplay } from './no_image_display'; import { StepImageCaption } from './step_image_caption'; import { StepImagePopover } from './step_image_popover'; import { formatCaptionContent } from './translations'; -import { getShortTimeStamp } from '../../../../overview/monitor_list/columns/monitor_status_column'; const StepDiv = styled.div` figure.euiImage { @@ -27,25 +24,16 @@ const StepDiv = styled.div` display: none; } } - - position: relative; - div.stepArrows { - display: none; - } - :hover { - div.stepArrows { - display: flex; - } - } `; interface Props { - timestamp: string; + label?: string; ping: Ping; + initialStepNo?: number; } -export const PingTimestamp = ({ timestamp, ping }: Props) => { - const [stepNumber, setStepNumber] = useState(1); +export const PingTimestamp = ({ label, ping, initialStepNo = 1 }: Props) => { + const [stepNumber, setStepNumber] = useState(initialStepNo); const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); const [stepImages, setStepImages] = useState([]); @@ -77,6 +65,8 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { const captionContent = formatCaptionContent(stepNumber, data?.maxSteps); + const [numberOfCaptions, setNumberOfCaptions] = useState(0); + const ImageCaption = ( { maxSteps={data?.maxSteps} setStepNumber={setStepNumber} stepNumber={stepNumber} - timestamp={timestamp} isLoading={status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING} + label={label} + onVisible={(val) => setNumberOfCaptions((prevVal) => (val ? prevVal + 1 : prevVal - 1))} /> ); + useEffect(() => { + // This is a hack to get state if image is in full screen, we should refactor + // it once eui image exposes it's full screen state + // we are checking if number of captions are 2, that means + // image is in full screen mode since caption is also rendered on + // full screen image + // we dont want to change image displayed in thumbnail + if (numberOfCaptions === 1 && stepNumber !== initialStepNo) { + setStepNumber(initialStepNo); + } + }, [numberOfCaptions, initialStepNo, stepNumber]); + return ( @@ -111,16 +114,10 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { isPending={status === FETCH_STATUS.PENDING} /> )} - - {getShortTimeStamp(moment(timestamp))} + {label} ); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx index a33e587093279..5c2c4d3669e79 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.test.tsx @@ -9,6 +9,8 @@ import { fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { render } from '../../../../../lib/helper/rtl_helpers'; import { StepImageCaption, StepImageCaptionProps } from './step_image_caption'; +import { getShortTimeStamp } from '../../../../overview/monitor_list/columns/monitor_status_column'; +import moment from 'moment'; describe('StepImageCaption', () => { let defaultProps: StepImageCaptionProps; @@ -20,7 +22,8 @@ describe('StepImageCaption', () => { maxSteps: 3, setStepNumber: jest.fn(), stepNumber: 2, - timestamp: '2020-11-26T15:28:56.896Z', + label: getShortTimeStamp(moment('2020-11-26T15:28:56.896Z')), + onVisible: jest.fn(), isLoading: false, }; }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx index fe9709a02b684..80d41ccc23dc8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx @@ -5,11 +5,9 @@ * 2.0. */ +import React, { MouseEvent, useEffect } from 'react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React from 'react'; -import moment from 'moment'; import { nextAriaLabel, prevAriaLabel } from './translations'; -import { getShortTimeStamp } from '../../../../overview/monitor_list/columns/monitor_status_column'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; export interface StepImageCaptionProps { @@ -18,7 +16,8 @@ export interface StepImageCaptionProps { maxSteps?: number; setStepNumber: React.Dispatch>; stepNumber: number; - timestamp: string; + label?: string; + onVisible: (val: boolean) => void; isLoading: boolean; } @@ -35,19 +34,34 @@ export const StepImageCaption: React.FC = ({ maxSteps, setStepNumber, stepNumber, - timestamp, isLoading, + label, + onVisible, }) => { + useEffect(() => { + onVisible(true); + return () => { + onVisible(false); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( - + { + // we don't want this to be captured by row click which leads to step list page + evt.stopPropagation(); + }} + >
{imgSrc && ( { + onClick={(evt: MouseEvent) => { setStepNumber(stepNumber - 1); + evt.preventDefault(); }} iconType="arrowLeft" aria-label={prevAriaLabel} @@ -62,8 +76,9 @@ export const StepImageCaption: React.FC = ({ { + onClick={(evt: MouseEvent) => { setStepNumber(stepNumber + 1); + evt.stopPropagation(); }} iconType="arrowRight" iconSide="right" @@ -75,7 +90,7 @@ export const StepImageCaption: React.FC = ({ )} - {getShortTimeStamp(moment(timestamp))} + {label}
); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx index 4fc8db515a5d6..d3dce3a2505b2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp/step_image_popover.tsx @@ -38,7 +38,7 @@ export const StepImagePopover: React.FC = ({ isImagePopoverOpen, }) => ( = ({ /> } isOpen={isImagePopoverOpen} + closePopover={() => {}} > { > - - - + color="primary" + > + + The Title", + "hash": "testhash", + } + } + /> + + + , + "title": "Response Body", + }, + ] + } + /> + `); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx index 2599b8ed9fdca..df0d273d3bc3a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Ping, HttpResponseBody } from '../../../../common/runtime_types'; import { DocLinkForBody } from './doc_link_body'; import { PingRedirects } from './ping_redirects'; -import { BrowserExpandedRow } from '../synthetics/browser_expanded_row'; import { PingHeaders } from './headers'; interface Props { @@ -57,24 +56,6 @@ const BodyExcerpt = ({ content }: { content: string }) => export const PingListExpandedRowComponent = ({ ping }: Props) => { const listItems = []; - if (ping.monitor.type === 'browser') { - return ( - - - - - - - - - ); - } - // Show the error block if (ping.error) { listItems.push({ diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx index 18bc5f5ec3ecb..65644ce493906 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx @@ -7,8 +7,10 @@ import { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect, MouseEvent } from 'react'; import styled from 'styled-components'; +import { useHistory } from 'react-router-dom'; +import moment from 'moment'; import { useDispatch } from 'react-redux'; import { Ping } from '../../../../common/runtime_types'; import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; @@ -27,6 +29,7 @@ import { FailedStep } from './columns/failed_step'; import { usePingsList } from './use_pings'; import { PingListHeader } from './ping_list_header'; import { clearPings } from '../../../state/actions'; +import { getShortTimeStamp } from '../../overview/monitor_list/columns/monitor_status_column'; export const SpanWithMargin = styled.span` margin-right: 16px; @@ -69,6 +72,8 @@ export const PingList = () => { const dispatch = useDispatch(); + const history = useHistory(); + const pruneJourneysCallback = useCallback( (checkGroups: string[]) => dispatch(pruneJourneyState(checkGroups)), [dispatch] @@ -140,7 +145,7 @@ export const PingList = () => { field: 'timestamp', name: TIMESTAMP_LABEL, render: (timestamp: string, item: Ping) => ( - + ), }, ] @@ -197,20 +202,43 @@ export const PingList = () => { }, ] : []), - { - align: 'right', - width: '24px', - isExpander: true, - render: (item: Ping) => ( - - ), - }, + ...(monitorType !== MONITOR_TYPES.BROWSER + ? [ + { + align: 'right', + width: '24px', + isExpander: true, + render: (item: Ping) => ( + + ), + }, + ] + : []), ]; + const getRowProps = (item: Ping) => { + if (monitorType !== MONITOR_TYPES.BROWSER) { + return {}; + } + const { monitor } = item; + return { + height: '85px', + 'data-test-subj': `row-${monitor.check_group}`, + onClick: (evt: MouseEvent) => { + const targetElem = evt.target as HTMLElement; + + // we dont want to capture image click event + if (targetElem.tagName !== 'IMG' && targetElem.tagName !== 'path') { + history.push(`/journey/${monitor.check_group}/steps`); + } + }, + }; + }; + const pagination: Pagination = { initialPageSize: DEFAULT_PAGE_SIZE, pageIndex, @@ -247,6 +275,7 @@ export const PingList = () => { setPageIndex(criteria.page!.index); }} tableLayout={'auto'} + rowProps={getRowProps} /> ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.test.tsx deleted file mode 100644 index 396d51e3002b2..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.test.tsx +++ /dev/null @@ -1,203 +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 { shallowWithIntl } from '@kbn/test/jest'; -import React from 'react'; -import { BrowserExpandedRowComponent } from './browser_expanded_row'; -import { Ping } from '../../../../common/runtime_types'; - -describe('BrowserExpandedRowComponent', () => { - let defStep: Ping; - beforeEach(() => { - defStep = { - docId: 'doc-id', - timestamp: '123', - monitor: { - duration: { - us: 100, - }, - id: 'mon-id', - status: 'up', - type: 'browser', - }, - }; - }); - - it('returns empty step state when no journey', () => { - expect(shallowWithIntl()).toMatchInlineSnapshot( - `` - ); - }); - - it('returns empty step state when journey has no steps', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(``); - }); - - it('displays loading spinner when loading', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(` -
- -
- `); - }); - - it('renders executed journey when step/end is present', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(` - - `); - }); - - it('handles case where synth type is somehow missing', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(`""`); - }); - - it('renders console output step list when only console steps are present', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(` - - `); - }); - - it('renders null when only unsupported steps are present', () => { - expect( - shallowWithIntl( - - ) - ).toMatchInlineSnapshot(`""`); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx deleted file mode 100644 index 2ceaa2d1b68ef..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/browser_expanded_row.tsx +++ /dev/null @@ -1,65 +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 { EuiLoadingSpinner } from '@elastic/eui'; -import React, { useEffect, FC } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Ping } from '../../../../common/runtime_types'; -import { getJourneySteps } from '../../../state/actions/journey'; -import { JourneyState } from '../../../state/reducers/journey'; -import { journeySelector } from '../../../state/selectors'; -import { EmptyJourney } from './empty_journey'; -import { ExecutedJourney } from './executed_journey'; -import { ConsoleOutputEventList } from './console_output_event_list'; - -interface BrowserExpandedRowProps { - checkGroup?: string; -} - -export const BrowserExpandedRow: React.FC = ({ checkGroup }) => { - const dispatch = useDispatch(); - useEffect(() => { - if (checkGroup) { - dispatch(getJourneySteps({ checkGroup })); - } - }, [dispatch, checkGroup]); - - const journeys = useSelector(journeySelector); - const journey = journeys[checkGroup ?? '']; - - return ; -}; - -type ComponentProps = BrowserExpandedRowProps & { - journey?: JourneyState; -}; - -const stepEnd = (step: Ping) => step.synthetics?.type === 'step/end'; -const stepConsole = (step: Ping) => - ['stderr', 'cmd/status'].indexOf(step.synthetics?.type ?? '') !== -1; - -export const BrowserExpandedRowComponent: FC = ({ checkGroup, journey }) => { - if (!!journey && journey.loading) { - return ( -
- -
- ); - } - - if (!journey || journey.steps.length === 0) { - return ; - } - - if (journey.steps.some(stepEnd)) return ; - - if (journey.steps.some(stepConsole)) return ; - - // TODO: should not happen, this means that the journey has no step/end and no console logs, but some other steps; filmstrip, screenshot, etc. - // we should probably create an error prompt letting the user know this step is not supported yet - return null; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.test.tsx deleted file mode 100644 index 2fbc19d245826..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.test.tsx +++ /dev/null @@ -1,265 +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 { shallowWithIntl } from '@kbn/test/jest'; -import React from 'react'; -import { ExecutedJourney } from './executed_journey'; -import { Ping } from '../../../../common/runtime_types'; - -const MONITOR_BOILERPLATE = { - id: 'MON_ID', - duration: { - us: 10, - }, - status: 'down', - type: 'browser', -}; - -describe('ExecutedJourney component', () => { - let steps: Ping[]; - - beforeEach(() => { - steps = [ - { - docId: '1', - timestamp: '123', - monitor: MONITOR_BOILERPLATE, - synthetics: { - payload: { - status: 'failed', - }, - type: 'step/end', - }, - }, - { - docId: '2', - timestamp: '124', - monitor: MONITOR_BOILERPLATE, - synthetics: { - payload: { - status: 'failed', - }, - type: 'step/end', - }, - }, - ]; - }); - - it('creates expected message for all failed', () => { - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - all failed or skipped -

-
- `); - }); - - it('creates expected message for all succeeded', () => { - steps[0].synthetics!.payload!.status = 'succeeded'; - steps[1].synthetics!.payload!.status = 'succeeded'; - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - all succeeded -

-
- `); - }); - - it('creates appropriate message for mixed results', () => { - steps[0].synthetics!.payload!.status = 'succeeded'; - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - 1 succeeded -

-
- `); - }); - - it('tallies skipped steps', () => { - steps[0].synthetics!.payload!.status = 'succeeded'; - steps[1].synthetics!.payload!.status = 'skipped'; - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - 1 succeeded -

-
- `); - }); - - it('uses appropriate count when non-step/end steps are included', () => { - steps[0].synthetics!.payload!.status = 'succeeded'; - steps.push({ - docId: '3', - timestamp: '125', - monitor: MONITOR_BOILERPLATE, - synthetics: { - type: 'stderr', - error: { - message: `there was an error, that's all we know`, - stack: 'your.error.happened.here', - }, - }, - }); - const wrapper = shallowWithIntl( - - ); - expect(wrapper.find('EuiText')).toMatchInlineSnapshot(` - -

- -

-

- 2 Steps - 1 succeeded -

-
- `); - }); - - it('renders a component per step', () => { - expect( - shallowWithIntl( - - ).find('EuiFlexGroup') - ).toMatchInlineSnapshot(` - - - - - - `); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx deleted file mode 100644 index 1ded7f065d8ab..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_journey.tsx +++ /dev/null @@ -1,88 +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 { EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC } from 'react'; -import { Ping } from '../../../../common/runtime_types'; -import { JourneyState } from '../../../state/reducers/journey'; -import { ExecutedStep } from './executed_step'; - -interface StepStatusCount { - failed: number; - skipped: number; - succeeded: number; -} - -function statusMessage(count: StepStatusCount) { - const total = count.succeeded + count.failed + count.skipped; - if (count.failed + count.skipped === total) { - return i18n.translate('xpack.uptime.synthetics.journey.allFailedMessage', { - defaultMessage: '{total} Steps - all failed or skipped', - values: { total }, - }); - } else if (count.succeeded === total) { - return i18n.translate('xpack.uptime.synthetics.journey.allSucceededMessage', { - defaultMessage: '{total} Steps - all succeeded', - values: { total }, - }); - } - return i18n.translate('xpack.uptime.synthetics.journey.partialSuccessMessage', { - defaultMessage: '{total} Steps - {succeeded} succeeded', - values: { succeeded: count.succeeded, total }, - }); -} - -function reduceStepStatus(prev: StepStatusCount, cur: Ping): StepStatusCount { - if (cur.synthetics?.payload?.status === 'succeeded') { - prev.succeeded += 1; - return prev; - } else if (cur.synthetics?.payload?.status === 'skipped') { - prev.skipped += 1; - return prev; - } - prev.failed += 1; - return prev; -} - -function isStepEnd(step: Ping) { - return step.synthetics?.type === 'step/end'; -} - -interface ExecutedJourneyProps { - journey: JourneyState; -} - -export const ExecutedJourney: FC = ({ journey }) => { - return ( -
- -

- -

-

- {statusMessage( - journey.steps - .filter(isStepEnd) - .reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }) - )} -

-
- - - {journey.steps.filter(isStepEnd).map((step, index) => ( - - ))} - - -
- ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx deleted file mode 100644 index 991aa8fefba0a..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ /dev/null @@ -1,110 +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 { EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { CodeBlockAccordion } from './code_block_accordion'; -import { StepScreenshotDisplay } from './step_screenshot_display'; -import { StatusBadge } from './status_badge'; -import { Ping } from '../../../../common/runtime_types'; -import { StepDetailLink } from '../../common/step_detail_link'; -import { VIEW_PERFORMANCE } from './translations'; - -const CODE_BLOCK_OVERFLOW_HEIGHT = 360; - -interface ExecutedStepProps { - step: Ping; - index: number; - checkGroup: string; -} - -export const ExecutedStep: FC = ({ step, index, checkGroup }) => { - return ( - <> -
- - - - - - - - -
- -
-
-
- -
- - - - - - {step.synthetics?.step?.index && ( - - - {VIEW_PERFORMANCE} - - - - )} - - {step.synthetics?.payload?.source} - - - {step.synthetics?.error?.message} - - - {step.synthetics?.error?.stack} - - - -
-
- - ); -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.test.tsx deleted file mode 100644 index 304787e96818f..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.test.tsx +++ /dev/null @@ -1,42 +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 { shallowWithIntl } from '@kbn/test/jest'; -import React from 'react'; -import { StatusBadge } from './status_badge'; - -describe('StatusBadge', () => { - it('displays success message', () => { - expect(shallowWithIntl()).toMatchInlineSnapshot(` - - Succeeded - - `); - }); - - it('displays failed message', () => { - expect(shallowWithIntl()).toMatchInlineSnapshot(` - - Failed - - `); - }); - - it('displays skipped message', () => { - expect(shallowWithIntl()).toMatchInlineSnapshot(` - - Skipped - - `); - }); -}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx index 346af9d31a28b..ef0d001ac905e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail_container.tsx @@ -48,7 +48,7 @@ export const StepDetailContainer: React.FC = ({ checkGroup, stepIndex }) }; }, [stepIndex, journey]); - useMonitorBreadcrumb({ journey, activeStep }); + useMonitorBreadcrumb({ details: journey?.details, activeStep, performanceBreakDownView: true }); const handleNextStep = useCallback(() => { history.push(`/journey/${checkGroup}/step/${stepIndex + 1}`); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumb.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumb.tsx index c51b85f76d605..8b85f05130d0b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumb.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumb.tsx @@ -6,20 +6,25 @@ */ import moment from 'moment'; +import { i18n } from '@kbn/i18n'; import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs'; -import { useKibana, useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { JourneyState } from '../../../../state/reducers/journey'; import { Ping } from '../../../../../common/runtime_types/ping'; import { PLUGIN } from '../../../../../common/constants/plugin'; +import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column'; interface Props { - journey: JourneyState; + details: JourneyState['details']; activeStep?: Ping; + performanceBreakDownView?: boolean; } -export const useMonitorBreadcrumb = ({ journey, activeStep }: Props) => { - const [dateFormat] = useUiSetting$('dateFormat'); - +export const useMonitorBreadcrumb = ({ + details, + activeStep, + performanceBreakDownView = false, +}: Props) => { const kibana = useKibana(); const appPath = kibana.services.application?.getUrlForApp(PLUGIN.ID) ?? ''; @@ -32,8 +37,22 @@ export const useMonitorBreadcrumb = ({ journey, activeStep }: Props) => { }, ] : []), - ...(journey?.details?.timestamp - ? [{ text: moment(journey?.details?.timestamp).format(dateFormat) }] + ...(details?.journey?.monitor?.check_group + ? [ + { + text: getShortTimeStamp(moment(details?.timestamp)), + href: `${appPath}/journey/${details.journey.monitor.check_group}/steps`, + }, + ] + : []), + ...(performanceBreakDownView + ? [ + { + text: i18n.translate('xpack.uptime.synthetics.performanceBreakDown.label', { + defaultMessage: 'Performance breakdown', + }), + }, + ] : []), ]); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumbs.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumbs.test.tsx index ac79d7f4c2a8a..4aed073424788 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumbs.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/use_monitor_breadcrumbs.test.tsx @@ -17,7 +17,7 @@ import { JourneyState } from '../../../../state/reducers/journey'; import { chromeServiceMock, uiSettingsServiceMock } from 'src/core/public/mocks'; describe('useMonitorBreadcrumbs', () => { - it('sets the given breadcrumbs', () => { + it('sets the given breadcrumbs for steps list view', () => { let breadcrumbObj: ChromeBreadcrumb[] = []; const getBreadcrumbs = () => { return breadcrumbObj; @@ -43,8 +43,13 @@ describe('useMonitorBreadcrumbs', () => { const Component = () => { useMonitorBreadcrumb({ - activeStep: { monitor: { id: 'test-monitor' } } as Ping, - journey: { details: { timestamp: '2021-01-04T11:25:19.104Z' } } as JourneyState, + activeStep: { monitor: { id: 'test-monitor', check_group: 'fake-test-group' } } as Ping, + details: { + timestamp: '2021-01-04T11:25:19.104Z', + journey: { + monitor: { id: 'test-monitor', check_group: 'fake-test-group' }, + }, + } as JourneyState['details'], }); return <>Step Water Fall; }; @@ -69,7 +74,78 @@ describe('useMonitorBreadcrumbs', () => { "text": "test-monitor", }, Object { - "text": "Jan 4, 2021 @ 06:25:19.104", + "href": "/app/uptime/journey/fake-test-group/steps", + "onClick": [Function], + "text": "Jan 4, 2021 6:25:19 AM", + }, + ] + `); + }); + + it('sets the given breadcrumbs for performance breakdown page', () => { + let breadcrumbObj: ChromeBreadcrumb[] = []; + const getBreadcrumbs = () => { + return breadcrumbObj; + }; + + const core = { + chrome: { + ...chromeServiceMock.createStartContract(), + setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => { + breadcrumbObj = newBreadcrumbs; + }, + }, + uiSettings: { + ...uiSettingsServiceMock.createSetupContract(), + get(key: string, defaultOverride?: any): any { + return `MMM D, YYYY @ HH:mm:ss.SSS` || defaultOverride; + }, + get$(key: string, defaultOverride?: any): any { + return of(`MMM D, YYYY @ HH:mm:ss.SSS`) || of(defaultOverride); + }, + }, + }; + + const Component = () => { + useMonitorBreadcrumb({ + activeStep: { monitor: { id: 'test-monitor', check_group: 'fake-test-group' } } as Ping, + details: { + timestamp: '2021-01-04T11:25:19.104Z', + journey: { + monitor: { id: 'test-monitor', check_group: 'fake-test-group' }, + }, + } as JourneyState['details'], + performanceBreakDownView: true, + }); + return <>Step Water Fall; + }; + + render( + + + , + { core } + ); + + expect(getBreadcrumbs()).toMatchInlineSnapshot(` + Array [ + Object { + "href": "/app/uptime", + "onClick": [Function], + "text": "Uptime", + }, + Object { + "href": "/app/uptime/monitor/dGVzdC1tb25pdG9y", + "onClick": [Function], + "text": "test-monitor", + }, + Object { + "href": "/app/uptime/journey/fake-test-group/steps", + "onClick": [Function], + "text": "Jan 4, 2021 6:25:19 AM", + }, + Object { + "text": "Performance breakdown", }, ] `); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx index c6476a5bf2e53..f5581f75b3759 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_status_column.tsx @@ -67,7 +67,7 @@ export const getShortTimeStamp = (timeStamp: moment.Moment, relative = false) => moment.locale(prevLocale); return shortTimestamp; } else { - if (moment().diff(timeStamp, 'd') > 1) { + if (moment().diff(timeStamp, 'd') >= 1) { return timeStamp.format('ll LTS'); } return timeStamp.format('LTS'); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/screenshot_link.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/screenshot_link.tsx new file mode 100644 index 0000000000000..16068e0d72b46 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/screenshot_link.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. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ReactRouterEuiLink } from '../../../common/react_router_helpers'; +import { Ping } from '../../../../../common/runtime_types/ping'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; + +const LabelLink = euiStyled.div` + margin-bottom: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; +`; + +interface Props { + lastSuccessfulStep: Ping; +} + +export const ScreenshotLink = ({ lastSuccessfulStep }: Props) => { + return ( + + + + + + + ), + }} + /> + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx new file mode 100644 index 0000000000000..eb7bc95751557 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_expanded_row/step_screenshots.tsx @@ -0,0 +1,86 @@ +/* + * 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 moment from 'moment'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { StepScreenshotDisplay } from '../../step_screenshot_display'; +import { Ping } from '../../../../../common/runtime_types/ping'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { useFetcher } from '../../../../../../observability/public'; +import { fetchLastSuccessfulStep } from '../../../../state/api/journey'; +import { ScreenshotLink } from './screenshot_link'; +import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column'; + +const Label = euiStyled.div` + margin-bottom: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + color: ${({ theme }) => theme.eui.euiColorDarkShade}; +`; + +interface Props { + step: Ping; +} + +export const StepScreenshots = ({ step }: Props) => { + const isSucceeded = step.synthetics?.payload?.status === 'succeeded'; + + const { data: lastSuccessfulStep } = useFetcher(() => { + if (!isSucceeded) { + return fetchLastSuccessfulStep({ + timestamp: step.timestamp, + monitorId: step.monitor.id, + stepIndex: step.synthetics?.step?.index!, + }); + } + }, [step.docId, step.timestamp]); + + return ( + + + + + + + + {!isSucceeded && lastSuccessfulStep?.monitor && ( + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_image.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_image.tsx new file mode 100644 index 0000000000000..69a5ef91a5925 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_image.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { Ping } from '../../../../common/runtime_types/ping'; +import { PingTimestamp } from '../../monitor/ping_list/columns/ping_timestamp'; + +interface Props { + step: Ping; +} + +export const StepImage = ({ step }: Props) => { + return ( + + + + + + {step.synthetics?.step?.name} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_list.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_list.test.tsx new file mode 100644 index 0000000000000..959bf0f644580 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_list.test.tsx @@ -0,0 +1,144 @@ +/* + * 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 { Ping } from '../../../../common/runtime_types/ping'; +import { StepsList } from './steps_list'; +import { render } from '../../../lib/helper/rtl_helpers'; + +describe('StepList component', () => { + let steps: Ping[]; + + beforeEach(() => { + steps = [ + { + docId: '1', + timestamp: '123', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group', + }, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + step: { + name: 'load page', + index: 1, + }, + }, + }, + { + docId: '2', + timestamp: '124', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group-1', + }, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + step: { + name: 'go to login', + index: 2, + }, + }, + }, + ]; + }); + + it('creates expected message for all failed', () => { + const { getByText } = render(); + expect(getByText('2 Steps - all failed or skipped')); + }); + + it('renders a link to the step detail view', () => { + const { getByTitle, getByTestId } = render(); + expect(getByTestId('step-detail-link')).toHaveAttribute('href', '/journey/fake-group/step/1'); + expect(getByTitle(`Failed`)); + }); + + it.each([ + ['succeeded', 'Succeeded'], + ['failed', 'Failed'], + ['skipped', 'Skipped'], + ])('supplies status badge correct status', (status, expectedStatus) => { + const step = steps[0]; + step.synthetics!.payload!.status = status; + const { getByText } = render(); + expect(getByText(expectedStatus)); + }); + + it('creates expected message for all succeeded', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps[1].synthetics!.payload!.status = 'succeeded'; + + const { getByText } = render(); + expect(getByText('2 Steps - all succeeded')); + }); + + it('creates appropriate message for mixed results', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + + const { getByText } = render(); + expect(getByText('2 Steps - 1 succeeded')); + }); + + it('tallies skipped steps', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps[1].synthetics!.payload!.status = 'skipped'; + + const { getByText } = render(); + expect(getByText('2 Steps - 1 succeeded')); + }); + + it('uses appropriate count when non-step/end steps are included', () => { + steps[0].synthetics!.payload!.status = 'succeeded'; + steps.push({ + docId: '3', + timestamp: '125', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group-2', + }, + synthetics: { + type: 'stderr', + error: { + message: `there was an error, that's all we know`, + stack: 'your.error.happened.here', + }, + }, + }); + + const { getByText } = render(); + expect(getByText('2 Steps - 1 succeeded')); + }); + + it('renders a row per step', () => { + const { getByTestId } = render(); + expect(getByTestId('row-fake-group')); + expect(getByTestId('row-fake-group-1')); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx new file mode 100644 index 0000000000000..47bf3ae0a1784 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx @@ -0,0 +1,175 @@ +/* + * 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 { EuiBasicTable, EuiButtonIcon, EuiPanel, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { MouseEvent } from 'react'; +import styled from 'styled-components'; +import { Ping } from '../../../../common/runtime_types'; +import { STATUS_LABEL } from '../../monitor/ping_list/translations'; +import { COLLAPSE_LABEL, EXPAND_LABEL, STEP_NAME_LABEL } from '../translations'; +import { StatusBadge } from '../status_badge'; +import { StepDetailLink } from '../../common/step_detail_link'; +import { VIEW_PERFORMANCE } from '../../monitor/synthetics/translations'; +import { StepImage } from './step_image'; +import { useExpandedRow } from './use_expanded_row'; + +export const SpanWithMargin = styled.span` + margin-right: 16px; +`; + +interface Props { + data: Ping[]; + error?: Error; + loading: boolean; +} + +interface StepStatusCount { + failed: number; + skipped: number; + succeeded: number; +} + +function isStepEnd(step: Ping) { + return step.synthetics?.type === 'step/end'; +} + +function statusMessage(count: StepStatusCount, loading?: boolean) { + if (loading) { + return i18n.translate('xpack.uptime.synthetics.journey.loadingSteps', { + defaultMessage: 'Loading steps ...', + }); + } + const total = count.succeeded + count.failed + count.skipped; + if (count.failed + count.skipped === total) { + return i18n.translate('xpack.uptime.synthetics.journey.allFailedMessage', { + defaultMessage: '{total} Steps - all failed or skipped', + values: { total }, + }); + } else if (count.succeeded === total) { + return i18n.translate('xpack.uptime.synthetics.journey.allSucceededMessage', { + defaultMessage: '{total} Steps - all succeeded', + values: { total }, + }); + } + return i18n.translate('xpack.uptime.synthetics.journey.partialSuccessMessage', { + defaultMessage: '{total} Steps - {succeeded} succeeded', + values: { succeeded: count.succeeded, total }, + }); +} + +function reduceStepStatus(prev: StepStatusCount, cur: Ping): StepStatusCount { + if (cur.synthetics?.payload?.status === 'succeeded') { + prev.succeeded += 1; + return prev; + } else if (cur.synthetics?.payload?.status === 'skipped') { + prev.skipped += 1; + return prev; + } + prev.failed += 1; + return prev; +} + +export const StepsList = ({ data, error, loading }: Props) => { + const steps = data.filter(isStepEnd); + + const { expandedRows, toggleExpand } = useExpandedRow({ steps, allPings: data, loading }); + + const columns: any[] = [ + { + field: 'synthetics.payload.status', + name: STATUS_LABEL, + render: (pingStatus: string, item: Ping) => ( + + ), + }, + { + align: 'left', + field: 'timestamp', + name: STEP_NAME_LABEL, + render: (timestamp: string, item: Ping) => , + }, + { + align: 'left', + field: 'timestamp', + name: '', + render: (val: string, item: Ping) => ( + + {VIEW_PERFORMANCE} + + ), + }, + { + align: 'right', + width: '24px', + isExpander: true, + render: (ping: Ping) => { + return ( + toggleExpand({ ping })} + aria-label={expandedRows[ping.docId] ? COLLAPSE_LABEL : EXPAND_LABEL} + iconType={expandedRows[ping.docId] ? 'arrowUp' : 'arrowDown'} + /> + ); + }, + }, + ]; + + const getRowProps = (item: Ping) => { + const { monitor } = item; + + return { + height: '85px', + 'data-test-subj': `row-${monitor.check_group}`, + onClick: (evt: MouseEvent) => { + const targetElem = evt.target as HTMLElement; + + // we dont want to capture image click event + if (targetElem.tagName !== 'IMG' && targetElem.tagName !== 'BUTTON') { + toggleExpand({ ping: item }); + } + }, + }; + }; + + return ( + + +

+ {statusMessage( + steps.reduce(reduceStepStatus, { failed: 0, skipped: 0, succeeded: 0 }), + loading + )} +

+
+ +
+ ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_check_steps.ts b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_check_steps.ts new file mode 100644 index 0000000000000..da40b900fdcc2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_check_steps.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useParams } from 'react-router-dom'; +import { FETCH_STATUS, useFetcher } from '../../../../../observability/public'; +import { fetchJourneySteps } from '../../../state/api/journey'; +import { JourneyState } from '../../../state/reducers/journey'; + +export const useCheckSteps = (): JourneyState => { + const { checkGroupId } = useParams<{ checkGroupId: string }>(); + + const { data, status, error } = useFetcher(() => { + return fetchJourneySteps({ + checkGroup: checkGroupId, + }); + }, [checkGroupId]); + + return { + error, + checkGroup: checkGroupId, + steps: data?.steps ?? [], + details: data?.details, + loading: status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING, + }; +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx new file mode 100644 index 0000000000000..d94122a7311ca --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx @@ -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 React from 'react'; +import { Route } from 'react-router-dom'; +import { fireEvent, screen } from '@testing-library/dom'; +import { EuiButtonIcon } from '@elastic/eui'; +import { createMemoryHistory } from 'history'; + +import { useExpandedRow } from './use_expanded_row'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { Ping } from '../../../../common/runtime_types/ping'; +import { SYNTHETIC_CHECK_STEPS_ROUTE } from '../../../../common/constants'; +import { COLLAPSE_LABEL, EXPAND_LABEL } from '../translations'; +import { act } from 'react-dom/test-utils'; + +describe('useExpandedROw', () => { + let expandedRowsObj = {}; + const TEST_ID = 'uptimeStepListExpandBtn'; + + const history = createMemoryHistory({ + initialEntries: ['/journey/fake-group/steps'], + }); + const steps: Ping[] = [ + { + docId: '1', + timestamp: '123', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group', + }, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + step: { + name: 'load page', + index: 1, + }, + }, + }, + { + docId: '2', + timestamp: '124', + monitor: { + id: 'MON_ID', + duration: { + us: 10, + }, + status: 'down', + type: 'browser', + check_group: 'fake-group', + }, + synthetics: { + payload: { + status: 'failed', + }, + type: 'step/end', + step: { + name: 'go to login', + index: 2, + }, + }, + }, + ]; + + const Component = () => { + const { expandedRows, toggleExpand } = useExpandedRow({ + steps, + allPings: steps, + loading: false, + }); + + expandedRowsObj = expandedRows; + + return ( + + Step list + {steps.map((ping, index) => ( + toggleExpand({ ping })} + aria-label={expandedRows[ping.docId] ? COLLAPSE_LABEL : EXPAND_LABEL} + iconType={expandedRows[ping.docId] ? 'arrowUp' : 'arrowDown'} + /> + ))} + + ); + }; + + it('it toggles rows on expand click', async () => { + render(, { + history, + }); + + fireEvent.click(await screen.findByTestId(TEST_ID + '1')); + + expect(Object.keys(expandedRowsObj)).toStrictEqual(['1']); + + expect(JSON.stringify(expandedRowsObj)).toContain('fake-group'); + + await act(async () => { + fireEvent.click(await screen.findByTestId(TEST_ID + '1')); + }); + + expect(Object.keys(expandedRowsObj)).toStrictEqual([]); + }); + + it('it can expand both rows at same time', async () => { + render(, { + history, + }); + + // let's expand both rows + fireEvent.click(await screen.findByTestId(TEST_ID + '1')); + fireEvent.click(await screen.findByTestId(TEST_ID + '0')); + + expect(Object.keys(expandedRowsObj)).toStrictEqual(['0', '1']); + }); + + it('it updates already expanded rows on new check group monitor', async () => { + render(, { + history, + }); + + // let's expand both rows + fireEvent.click(await screen.findByTestId(TEST_ID + '1')); + fireEvent.click(await screen.findByTestId(TEST_ID + '0')); + + const newFakeGroup = 'new-fake-group-1'; + + steps[0].monitor.check_group = newFakeGroup; + steps[1].monitor.check_group = newFakeGroup; + + act(() => { + history.push(`/journey/${newFakeGroup}/steps`); + }); + + expect(JSON.stringify(expandedRowsObj)).toContain(newFakeGroup); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx new file mode 100644 index 0000000000000..bb56b237dfbd2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.tsx @@ -0,0 +1,88 @@ +/* + * 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, { useEffect, useState, useCallback } from 'react'; +import { useParams } from 'react-router-dom'; +import { ExecutedStep } from '../executed_step'; +import { Ping } from '../../../../common/runtime_types/ping'; + +interface HookProps { + loading: boolean; + allPings: Ping[]; + steps: Ping[]; +} + +type ExpandRowType = Record; + +export const useExpandedRow = ({ loading, steps, allPings }: HookProps) => { + const [expandedRows, setExpandedRows] = useState({}); + // eui table uses index from 0, synthetics uses 1 + + const { checkGroupId } = useParams<{ checkGroupId: string }>(); + + const getBrowserConsole = useCallback( + (index: number) => { + return allPings.find( + (stepF) => + stepF.synthetics?.type === 'journey/browserconsole' && + stepF.synthetics?.step?.index! === index + )?.synthetics?.payload?.text; + }, + [allPings] + ); + + useEffect(() => { + const expandedRowsN: ExpandRowType = {}; + for (const expandedRowKeyStr in expandedRows) { + if (expandedRows.hasOwnProperty(expandedRowKeyStr)) { + const expandedRowKey = Number(expandedRowKeyStr); + + const step = steps.find((stepF) => stepF.synthetics?.step?.index !== expandedRowKey)!; + + expandedRowsN[expandedRowKey] = ( + + ); + } + } + + setExpandedRows(expandedRowsN); + + // we only want to update when checkGroupId changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [checkGroupId, loading]); + + const toggleExpand = ({ ping }: { ping: Ping }) => { + // eui table uses index from 0, synthetics uses 1 + const stepIndex = ping.synthetics?.step?.index! - 1; + + // If already expanded, collapse + if (expandedRows[stepIndex]) { + delete expandedRows[stepIndex]; + setExpandedRows({ ...expandedRows }); + } else { + // Otherwise expand this row + setExpandedRows({ + ...expandedRows, + [stepIndex]: ( + + ), + }); + } + }; + + return { expandedRows, toggleExpand }; +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/code_block_accordion.tsx b/x-pack/plugins/uptime/public/components/synthetics/code_block_accordion.tsx similarity index 87% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/code_block_accordion.tsx rename to x-pack/plugins/uptime/public/components/synthetics/code_block_accordion.tsx index 18aeb7a236ca8..225ba1041c263 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/code_block_accordion.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/code_block_accordion.tsx @@ -13,6 +13,7 @@ interface Props { id?: string; language: 'html' | 'javascript'; overflowHeight: number; + initialIsOpen?: boolean; } /** @@ -25,9 +26,10 @@ export const CodeBlockAccordion: FC = ({ id, language, overflowHeight, + initialIsOpen = false, }) => { return children && id ? ( - + {children} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/console_event.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/console_event.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.tsx b/x-pack/plugins/uptime/public/components/synthetics/console_event.tsx similarity index 89% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.tsx rename to x-pack/plugins/uptime/public/components/synthetics/console_event.tsx index dc7b6ce9ea123..19672f953607b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_event.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/console_event.tsx @@ -7,8 +7,8 @@ import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import React, { useContext, FC } from 'react'; -import { Ping } from '../../../../common/runtime_types'; -import { UptimeThemeContext } from '../../../contexts'; +import { UptimeThemeContext } from '../../contexts'; +import { Ping } from '../../../common/runtime_types/ping'; interface Props { event: Ping; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx b/x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.tsx similarity index 92% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx rename to x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.tsx index df1f6aeb3623b..df4314e5ccf1c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/console_output_event_list.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/console_output_event_list.tsx @@ -8,9 +8,9 @@ import { EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC } from 'react'; -import { Ping } from '../../../../common/runtime_types'; -import { JourneyState } from '../../../state/reducers/journey'; import { ConsoleEvent } from './console_event'; +import { Ping } from '../../../common/runtime_types/ping'; +import { JourneyState } from '../../state/reducers/journey'; interface Props { journey: JourneyState; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/empty_journey.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/empty_journey.test.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx b/x-pack/plugins/uptime/public/components/synthetics/empty_journey.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/empty_journey.tsx rename to x-pack/plugins/uptime/public/components/synthetics/empty_journey.tsx diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx similarity index 54% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx index 225ccb884ad00..24b52e09adbf9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { ExecutedStep } from './executed_step'; -import { Ping } from '../../../../common/runtime_types'; -import { render } from '../../../lib/helper/rtl_helpers'; +import { render } from '../../lib/helper/rtl_helpers'; +import { Ping } from '../../../common/runtime_types/ping'; describe('ExecutedStep', () => { let step: Ping; @@ -34,33 +34,6 @@ describe('ExecutedStep', () => { }; }); - it('renders correct step heading', () => { - const { getByText } = render(); - - expect(getByText(`${step?.synthetics?.step?.index}. ${step?.synthetics?.step?.name}`)); - }); - - it('renders a link to the step detail view', () => { - const { getByRole, getByText } = render( - - ); - expect(getByRole('link')).toHaveAttribute('href', '/journey/fake-group/step/4'); - expect(getByText('4. STEP_NAME')); - }); - - it.each([ - ['succeeded', 'Succeeded'], - ['failed', 'Failed'], - ['skipped', 'Skipped'], - ['somegarbage', '4.'], - ])('supplies status badge correct status', (status, expectedStatus) => { - step.synthetics = { - payload: { status }, - }; - const { getByText } = render(); - expect(getByText(expectedStatus)); - }); - it('renders accordion for step', () => { step.synthetics = { payload: { @@ -72,10 +45,9 @@ describe('ExecutedStep', () => { }, }; - const { getByText } = render(); + const { getByText } = render(); - expect(getByText('4. STEP_NAME')); - expect(getByText('Step script')); + expect(getByText('Script executed at this step')); expect(getByText(`const someVar = "the var"`)); }); @@ -87,11 +59,22 @@ describe('ExecutedStep', () => { }, }; - const { getByText } = render(); + const { getByText } = render(); - expect(getByText('4.')); - expect(getByText('Error')); + expect(getByText('Error message')); expect(getByText('There was an error executing the step.')); expect(getByText('some.stack.trace.string')); }); + + it('renders accordions for console output', () => { + const browserConsole = + "Refused to execute script from because its MIME type ('image/gif') is not executable"; + + const { getByText } = render( + + ); + + expect(getByText('Console output')); + expect(getByText(browserConsole)); + }); }); diff --git a/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx new file mode 100644 index 0000000000000..a77b3dfe3ba21 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.tsx @@ -0,0 +1,118 @@ +/* + * 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 { EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CodeBlockAccordion } from './code_block_accordion'; +import { Ping } from '../../../common/runtime_types/ping'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; +import { StepScreenshots } from './check_steps/step_expanded_row/step_screenshots'; + +const CODE_BLOCK_OVERFLOW_HEIGHT = 360; + +interface ExecutedStepProps { + step: Ping; + index: number; + loading: boolean; + browserConsole?: string; +} + +const Label = euiStyled.div` + margin-bottom: ${(props) => props.theme.eui.paddingSizes.xs}; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + color: ${({ theme }) => theme.eui.euiColorDarkShade}; +`; + +const Message = euiStyled.div` + font-weight: bold; + font-size:${({ theme }) => theme.eui.euiFontSizeM}; + margin-bottom: ${(props) => props.theme.eui.paddingSizes.m}; +`; + +const ExpandedRow = euiStyled.div` + padding: '8px'; + max-width: 1000px; + width: 100%; +`; + +export const ExecutedStep: FC = ({ + loading, + step, + index, + browserConsole = '', +}) => { + const isSucceeded = step.synthetics?.payload?.status === 'succeeded'; + + return ( + + {loading ? ( + + ) : ( + <> + + {step.synthetics?.error?.message && ( + + + {step.synthetics?.error?.message} + + )} + + + {step.synthetics?.payload?.source} + + + + <> + {browserConsole} + + + + + + + {step.synthetics?.error?.stack} + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/synthetics/status_badge.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/status_badge.test.tsx new file mode 100644 index 0000000000000..500c680b91bf6 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/status_badge.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { StatusBadge } from './status_badge'; +import { render } from '../../lib/helper/rtl_helpers'; + +describe('StatusBadge', () => { + it('displays success message', () => { + const { getByText } = render(); + + expect(getByText('1.')); + expect(getByText('Succeeded')); + }); + + it('displays failed message', () => { + const { getByText } = render(); + + expect(getByText('2.')); + expect(getByText('Failed')); + }); + + it('displays skipped message', () => { + const { getByText } = render(); + + expect(getByText('3.')); + expect(getByText('Skipped')); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.tsx b/x-pack/plugins/uptime/public/components/synthetics/status_badge.tsx similarity index 66% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.tsx rename to x-pack/plugins/uptime/public/components/synthetics/status_badge.tsx index 0cf9e5477d0db..b4c4e310abe6b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/status_badge.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/status_badge.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import { EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, FC } from 'react'; -import { UptimeAppColors } from '../../../apps/uptime_app'; -import { UptimeThemeContext } from '../../../contexts'; +import { UptimeAppColors } from '../../apps/uptime_app'; +import { UptimeThemeContext } from '../../contexts'; interface StatusBadgeProps { status?: string; + stepNo: number; } export function colorFromStatus(color: UptimeAppColors, status?: string) { @@ -45,9 +46,18 @@ export function textFromStatus(status?: string) { } } -export const StatusBadge: FC = ({ status }) => { +export const StatusBadge: FC = ({ status, stepNo }) => { const theme = useContext(UptimeThemeContext); return ( - {textFromStatus(status)} + + + + {stepNo}. + + + + {textFromStatus(status)} + + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx similarity index 96% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx rename to x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx index 29dca39c34bf2..52d2eacaf0e52 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.test.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import { render } from '../../../lib/helper/rtl_helpers'; import React from 'react'; import { StepScreenshotDisplay } from './step_screenshot_display'; +import { render } from '../../lib/helper/rtl_helpers'; jest.mock('react-use/lib/useIntersection', () => () => ({ isIntersecting: true, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.tsx similarity index 52% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx rename to x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.tsx index 654193de72a9c..78c65b7d40803 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/step_screenshot_display.tsx @@ -5,33 +5,32 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiImage, EuiPopover, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiImage, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useContext, useEffect, useRef, useState, FC } from 'react'; import useIntersection from 'react-use/lib/useIntersection'; -import { UptimeSettingsContext, UptimeThemeContext } from '../../../contexts'; +import { UptimeSettingsContext, UptimeThemeContext } from '../../contexts'; interface StepScreenshotDisplayProps { screenshotExists?: boolean; checkGroup?: string; stepIndex?: number; stepName?: string; + lazyLoad?: boolean; } -const THUMBNAIL_WIDTH = 320; -const THUMBNAIL_HEIGHT = 180; -const POPOVER_IMG_WIDTH = 640; -const POPOVER_IMG_HEIGHT = 360; +const IMAGE_WIDTH = 640; +const IMAGE_HEIGHT = 360; const StepImage = styled(EuiImage)` &&& { figcaption { display: none; } - width: ${THUMBNAIL_WIDTH}, - height: ${THUMBNAIL_HEIGHT}, + width: ${IMAGE_WIDTH}, + height: ${IMAGE_HEIGHT}, objectFit: 'cover', objectPosition: 'center top', } @@ -42,6 +41,7 @@ export const StepScreenshotDisplay: FC = ({ screenshotExists, stepIndex, stepName, + lazyLoad = true, }) => { const containerRef = useRef(null); const { @@ -50,8 +50,6 @@ export const StepScreenshotDisplay: FC = ({ const { basePath } = useContext(UptimeSettingsContext); - const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); - const intersection = useIntersection(containerRef, { root: null, rootMargin: '0px', @@ -69,57 +67,26 @@ export const StepScreenshotDisplay: FC = ({ let content: JSX.Element | null = null; const imgSrc = basePath + `/api/uptime/journey/screenshot/${checkGroup}/${stepIndex}`; - if (hasIntersected && screenshotExists) { + if ((hasIntersected || !lazyLoad) && screenshotExists) { content = ( - <> - setIsImagePopoverOpen(true)} - onMouseLeave={() => setIsImagePopoverOpen(false)} - /> - } - closePopover={() => setIsImagePopoverOpen(false)} - isOpen={isImagePopoverOpen} - > - - - + ); } else if (screenshotExists === false) { content = ( @@ -148,7 +115,7 @@ export const StepScreenshotDisplay: FC = ({ return (
{content}
diff --git a/x-pack/plugins/uptime/public/components/synthetics/translations.ts b/x-pack/plugins/uptime/public/components/synthetics/translations.ts new file mode 100644 index 0000000000000..743118574b325 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/translations.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const STEP_NAME_LABEL = i18n.translate('xpack.uptime.stepList.stepName', { + defaultMessage: 'Step name', +}); + +export const COLLAPSE_LABEL = i18n.translate('xpack.uptime.stepList.collapseRow', { + defaultMessage: 'Collapse', +}); + +export const EXPAND_LABEL = i18n.translate('xpack.uptime.stepList.expandRow', { + defaultMessage: 'Expand', +}); diff --git a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts index da0f109747758..b9ec9cc5e5516 100644 --- a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts @@ -16,6 +16,7 @@ export enum UptimePage { Settings = 'Settings', Certificates = 'Certificates', StepDetail = 'StepDetail', + SyntheticCheckStepsPage = 'SyntheticCheckStepsPage', NotFound = '__not-found__', } diff --git a/x-pack/plugins/uptime/public/pages/index.ts b/x-pack/plugins/uptime/public/pages/index.ts index 828942bc1eb1e..5624f61c3abb5 100644 --- a/x-pack/plugins/uptime/public/pages/index.ts +++ b/x-pack/plugins/uptime/public/pages/index.ts @@ -6,6 +6,6 @@ */ export { MonitorPage } from './monitor'; -export { StepDetailPage } from './step_detail_page'; +export { StepDetailPage } from './synthetics/step_detail_page'; export { SettingsPage } from './settings'; export { NotFoundPage } from './not_found'; diff --git a/x-pack/plugins/uptime/public/pages/synthetics/checks_navigation.tsx b/x-pack/plugins/uptime/public/pages/synthetics/checks_navigation.tsx new file mode 100644 index 0000000000000..291019d93c398 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/synthetics/checks_navigation.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; +import moment from 'moment'; +import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types/ping'; +import { getShortTimeStamp } from '../../components/overview/monitor_list/columns/monitor_status_column'; + +interface Props { + timestamp: string; + details: SyntheticsJourneyApiResponse['details']; +} + +export const ChecksNavigation = ({ timestamp, details }: Props) => { + const history = useHistory(); + + return ( + + + { + history.push(`/journey/${details?.previous?.checkGroup}/steps`); + }} + > + + + + + {getShortTimeStamp(moment(timestamp))} + + + { + history.push(`/journey/${details?.next?.checkGroup}/steps`); + }} + > + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/pages/step_detail_page.tsx b/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx similarity index 75% rename from x-pack/plugins/uptime/public/pages/step_detail_page.tsx rename to x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx index aa81ddd0eae3d..de38d2d663523 100644 --- a/x-pack/plugins/uptime/public/pages/step_detail_page.tsx +++ b/x-pack/plugins/uptime/public/pages/synthetics/step_detail_page.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { useParams } from 'react-router-dom'; -import { useTrackPageview } from '../../../observability/public'; -import { useInitApp } from '../hooks/use_init_app'; -import { StepDetailContainer } from '../components/monitor/synthetics/step_detail/step_detail_container'; +import { useTrackPageview } from '../../../../observability/public'; +import { useInitApp } from '../../hooks/use_init_app'; +import { StepDetailContainer } from '../../components/monitor/synthetics/step_detail/step_detail_container'; export const StepDetailPage: React.FC = () => { useInitApp(); diff --git a/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx b/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx new file mode 100644 index 0000000000000..edfd7ae24f91b --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/synthetics/synthetics_checks.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { useTrackPageview } from '../../../../observability/public'; +import { useInitApp } from '../../hooks/use_init_app'; +import { StepsList } from '../../components/synthetics/check_steps/steps_list'; +import { useCheckSteps } from '../../components/synthetics/check_steps/use_check_steps'; +import { ChecksNavigation } from './checks_navigation'; +import { useMonitorBreadcrumb } from '../../components/monitor/synthetics/step_detail/use_monitor_breadcrumb'; +import { EmptyJourney } from '../../components/synthetics/empty_journey'; + +export const SyntheticsCheckSteps: React.FC = () => { + useInitApp(); + useTrackPageview({ app: 'uptime', path: 'syntheticCheckSteps' }); + useTrackPageview({ app: 'uptime', path: 'syntheticCheckSteps', delay: 15000 }); + + const { error, loading, steps, details, checkGroup } = useCheckSteps(); + + useMonitorBreadcrumb({ details, activeStep: details?.journey }); + + return ( + <> + + + +

{details?.journey?.monitor.name || details?.journey?.monitor.id}

+
+
+ + {details && } + +
+ + + {(!steps || steps.length === 0) && !loading && } + + ); +}; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index 82aa09c3293e6..dcfb21955f219 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -15,10 +15,12 @@ import { OVERVIEW_ROUTE, SETTINGS_ROUTE, STEP_DETAIL_ROUTE, + SYNTHETIC_CHECK_STEPS_ROUTE, } from '../common/constants'; import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages'; import { CertificatesPage } from './pages/certificates'; import { UptimePage, useUptimeTelemetry } from './hooks'; +import { SyntheticsCheckSteps } from './pages/synthetics/synthetics_checks'; interface RouteProps { path: string; @@ -71,6 +73,13 @@ const Routes: RouteProps[] = [ dataTestSubj: 'uptimeStepDetailPage', telemetryId: UptimePage.StepDetail, }, + { + title: baseTitle, + path: SYNTHETIC_CHECK_STEPS_ROUTE, + component: SyntheticsCheckSteps, + dataTestSubj: 'uptimeSyntheticCheckStepsPage', + telemetryId: UptimePage.SyntheticCheckStepsPage, + }, { title: baseTitle, path: OVERVIEW_ROUTE, diff --git a/x-pack/plugins/uptime/public/state/api/journey.ts b/x-pack/plugins/uptime/public/state/api/journey.ts index 5c4c7c7149792..63796a66d1c5c 100644 --- a/x-pack/plugins/uptime/public/state/api/journey.ts +++ b/x-pack/plugins/uptime/public/state/api/journey.ts @@ -8,6 +8,7 @@ import { apiService } from './utils'; import { FetchJourneyStepsParams } from '../actions/journey'; import { + Ping, SyntheticsJourneyApiResponse, SyntheticsJourneyApiResponseType, } from '../../../common/runtime_types'; @@ -34,6 +35,22 @@ export async function fetchJourneysFailedSteps({ )) as SyntheticsJourneyApiResponse; } +export async function fetchLastSuccessfulStep({ + monitorId, + timestamp, + stepIndex, +}: { + monitorId: string; + timestamp: string; + stepIndex: number; +}): Promise { + return (await apiService.get(`/api/uptime/synthetics/step/success/`, { + monitorId, + timestamp, + stepIndex, + })) as Ping; +} + export async function getJourneyScreenshot(imgSrc: string) { try { const imgRequest = new Request(imgSrc); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts index e0edcc4576378..de37688b155f5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts @@ -27,13 +27,12 @@ export const getJourneyDetails: UMElasticsearchQueryFn< }, { term: { - 'synthetics.type': 'journey/end', + 'synthetics.type': 'journey/start', }, }, ], }, }, - _source: ['@timestamp', 'monitor.id'], size: 1, }; @@ -53,7 +52,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< }, { term: { - 'synthetics.type': 'journey/end', + 'synthetics.type': 'journey/start', }, }, ], @@ -109,6 +108,7 @@ export const getJourneyDetails: UMElasticsearchQueryFn< nextJourneyResult?.hits?.hits.length > 0 ? nextJourneyResult?.hits?.hits[0] : null; return { timestamp: thisJourneySource['@timestamp'], + journey: thisJourneySource, previous: previousJourney ? { checkGroup: previousJourney._source.monitor.check_group, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts index 9cb5e1eedb6b0..faa260eb9abd4 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts @@ -60,10 +60,10 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn< return null; } - const stepHit = result?.aggregations?.step.image.hits.hits[0]._source as Ping; + const stepHit = result?.aggregations?.step.image.hits.hits[0]?._source as Ping; return { - blob: stepHit.synthetics?.blob ?? null, + blob: stepHit?.synthetics?.blob ?? null, stepName: stepHit?.synthetics?.step?.name ?? '', totalSteps: result?.hits?.total.value, }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts index 1034318257f66..af7752b05997e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.test.ts @@ -14,9 +14,9 @@ describe('getJourneySteps request module', () => { expect(formatSyntheticEvents()).toMatchInlineSnapshot(` Array [ "step/end", - "stderr", "cmd/status", "step/screenshot", + "journey/browserconsole", ] `); }); @@ -121,9 +121,9 @@ describe('getJourneySteps request module', () => { "terms": Object { "synthetics.type": Array [ "step/end", - "stderr", "cmd/status", "step/screenshot", + "journey/browserconsole", ], }, } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index 3055f169fc495..43d17cb938159 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -13,7 +13,7 @@ export interface GetJourneyStepsParams { syntheticEventTypes?: string | string[]; } -const defaultEventTypes = ['step/end', 'stderr', 'cmd/status', 'step/screenshot']; +const defaultEventTypes = ['step/end', 'cmd/status', 'step/screenshot', 'journey/browserconsole']; export const formatSyntheticEvents = (eventTypes?: string | string[]) => { if (!eventTypes) { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts b/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts new file mode 100644 index 0000000000000..82958167341c0 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_last_successful_step.ts @@ -0,0 +1,77 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters/framework'; +import { Ping } from '../../../common/runtime_types/ping'; + +export interface GetStepScreenshotParams { + monitorId: string; + timestamp: string; + stepIndex: number; +} + +export const getStepLastSuccessfulStep: UMElasticsearchQueryFn< + GetStepScreenshotParams, + any +> = async ({ uptimeEsClient, monitorId, stepIndex, timestamp }) => { + const lastSuccessCheckParams = { + size: 1, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + lte: timestamp, + }, + }, + }, + { + term: { + 'monitor.id': monitorId, + }, + }, + { + term: { + 'synthetics.type': 'step/end', + }, + }, + { + term: { + 'synthetics.step.status': 'succeeded', + }, + }, + { + term: { + 'synthetics.step.index': stepIndex, + }, + }, + ], + }, + }, + }; + + const { body: result } = await uptimeEsClient.search({ body: lastSuccessCheckParams }); + + if (result?.hits?.total.value < 1) { + return null; + } + + const step = result?.hits.hits[0]._source as Ping & { '@timestamp': string }; + + return { + ...step, + timestamp: step['@timestamp'], + }; +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts index 9e665fb8bbdb0..24109245c2902 100644 --- a/x-pack/plugins/uptime/server/lib/requests/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/index.ts @@ -24,6 +24,7 @@ import { getJourneyScreenshot } from './get_journey_screenshot'; import { getJourneyDetails } from './get_journey_details'; import { getNetworkEvents } from './get_network_events'; import { getJourneyFailedSteps } from './get_journey_failed_steps'; +import { getStepLastSuccessfulStep } from './get_last_successful_step'; export const requests = { getCerts, @@ -42,6 +43,7 @@ export const requests = { getIndexStatus, getJourneySteps, getJourneyFailedSteps, + getStepLastSuccessfulStep, getJourneyScreenshot, getJourneyDetails, getNetworkEvents, diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index 41556d3c8d513..91b5597321ed0 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -27,6 +27,7 @@ import { createGetMonitorDurationRoute } from './monitors/monitors_durations'; import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state'; import { createNetworkEventsRoute } from './network_events'; import { createJourneyFailedStepsRoute } from './pings/journeys'; +import { createLastSuccessfulStepRoute } from './synthetics/last_successful_step'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; @@ -52,4 +53,5 @@ export const restApiRoutes: UMRestApiRouteFactory[] = [ createJourneyScreenshotRoute, createNetworkEventsRoute, createJourneyFailedStepsRoute, + createLastSuccessfulStepRoute, ]; diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts index cda078da01539..2b056498d7f10 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts @@ -18,6 +18,9 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ stepIndex: schema.number(), _debug: schema.maybe(schema.boolean()), }), + query: schema.object({ + _debug: schema.maybe(schema.boolean()), + }), }, handler: async ({ uptimeEsClient, request, response }) => { const { checkGroup, stepIndex } = request.params; @@ -28,7 +31,7 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ stepIndex, }); - if (result === null) { + if (result === null || !result.blob) { return response.notFound(); } return response.ok({ diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts index def373e88ae16..9b5bffc380c27 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts @@ -15,7 +15,6 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => validate: { params: schema.object({ checkGroup: schema.string(), - _debug: schema.maybe(schema.boolean()), }), query: schema.object({ // provides a filter for the types of synthetic events to include @@ -23,21 +22,24 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => syntheticEventTypes: schema.maybe( schema.oneOf([schema.arrayOf(schema.string()), schema.string()]) ), + _debug: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { const { checkGroup } = request.params; const { syntheticEventTypes } = request.query; - const result = await libs.requests.getJourneySteps({ - uptimeEsClient, - checkGroup, - syntheticEventTypes, - }); - const details = await libs.requests.getJourneyDetails({ - uptimeEsClient, - checkGroup, - }); + const [result, details] = await Promise.all([ + await libs.requests.getJourneySteps({ + uptimeEsClient, + checkGroup, + syntheticEventTypes, + }), + await libs.requests.getJourneyDetails({ + uptimeEsClient, + checkGroup, + }), + ]); return { checkGroup, @@ -53,6 +55,7 @@ export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMSer validate: { query: schema.object({ checkGroups: schema.arrayOf(schema.string()), + _debug: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts b/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts new file mode 100644 index 0000000000000..a1523fae9d4a1 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/synthetics/last_successful_step.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; + +export const createLastSuccessfulStepRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/synthetics/step/success/', + validate: { + query: schema.object({ + monitorId: schema.string(), + stepIndex: schema.number(), + timestamp: schema.string(), + _debug: schema.maybe(schema.boolean()), + }), + }, + handler: async ({ uptimeEsClient, request, response }) => { + const { timestamp, monitorId, stepIndex } = request.query; + + return await libs.requests.getStepLastSuccessfulStep({ + uptimeEsClient, + monitorId, + stepIndex, + timestamp, + }); + }, +}); From f90a4d95a52c7b337f0ebed319efe8a8c9de9014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 16 Mar 2021 11:41:43 -0400 Subject: [PATCH 16/44] [APM] Telemetry: Add UI usage telemetry on the Timeseries comparison feature (#91439) * adding telemetry and removing time comparison * adding telemetry and removing time comparison * adding telemetry Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/app/transaction_overview/index.tsx | 1 - .../components/shared/time_comparison/index.tsx | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 97be35ec6f5b9..0814c6d95b96a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -83,7 +83,6 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { return ( <> - diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index 1769119593c0e..84a2dad278a9b 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React from 'react'; import { useHistory } from 'react-router-dom'; +import { useUiTracker } from '../../../../../observability/public'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { getDateDifference } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -132,6 +133,7 @@ function getSelectOptions({ } export function TimeComparison() { + const trackApmEvent = useUiTracker({ app: 'apm' }); const history = useHistory(); const { isMedium, isLarge } = useBreakPoints(); const { @@ -181,9 +183,17 @@ export function TimeComparison() { })} checked={comparisonEnabled} onChange={() => { + const nextComparisonEnabledValue = !comparisonEnabled; + if (nextComparisonEnabledValue === false) { + trackApmEvent({ + metric: 'time_comparison_disabled', + }); + } urlHelpers.push(history, { query: { - comparisonEnabled: Boolean(!comparisonEnabled).toString(), + comparisonEnabled: Boolean( + nextComparisonEnabledValue + ).toString(), }, }); }} @@ -191,6 +201,9 @@ export function TimeComparison() { } onChange={(e) => { + trackApmEvent({ + metric: `time_comparison_type_change_${e.target.value}`, + }); urlHelpers.push(history, { query: { comparisonType: e.target.value, From bae6021d7f2b184229a2c2dfd628e3760ef3fb49 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 16 Mar 2021 11:55:11 -0400 Subject: [PATCH 17/44] [Save Modal] Quick Fix for Scrollbar (#94640) * quick fixes for no scroll bar on save modals --- .../panel_actions/customize_title/customize_panel_modal.tsx | 2 +- .../__snapshots__/saved_object_save_modal.test.tsx.snap | 4 ++++ .../public/save_modal/saved_object_save_modal.tsx | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx index 411da6c037900..6d5c2224c0635 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx @@ -71,7 +71,7 @@ export class CustomizePanelModal extends Component { return ( -
+

Customize panel

diff --git a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap index 1f05ed6b94405..91f99fd8e87dd 100644 --- a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -7,6 +7,7 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` onClose={[Function]} >
@@ -103,6 +104,7 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali onClose={[Function]} > @@ -199,6 +201,7 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali onClose={[Function]} > @@ -295,6 +298,7 @@ exports[`SavedObjectSaveModal should render matching snapshot when given options onClose={[Function]} > diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index 07f6336dac52c..c9e21d5204b01 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -128,7 +128,7 @@ export class SavedObjectSaveModal extends React.Component className={`kbnSavedObjectSaveModal${hasColumns ? ' kbnSavedObjectsSaveModal--wide' : ''}`} onClose={this.props.onClose} > - + Date: Tue, 16 Mar 2021 17:19:42 +0100 Subject: [PATCH 18/44] [APM] Use top_hits instead of top_metrics (#94694) * [APM] Use top_hits instead of top_metrics for high cardinality fields Closes #94676. * Update snapshots --- .../get_destination_map.ts | 17 +++++++---------- .../distribution/get_buckets/index.ts | 14 ++++++-------- .../tests/transactions/distribution.ts | 8 ++++---- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts index cb9d37d56b867..e41a88649c5ff 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts @@ -71,12 +71,9 @@ export const getDestinationMap = ({ }, aggs: { sample: { - top_metrics: { - metrics: [ - { field: SPAN_TYPE }, - { field: SPAN_SUBTYPE }, - { field: SPAN_ID }, - ] as const, + top_hits: { + size: 1, + _source: [SPAN_TYPE, SPAN_SUBTYPE, SPAN_ID], sort: { '@timestamp': 'desc', }, @@ -91,15 +88,15 @@ export const getDestinationMap = ({ const outgoingConnections = response.aggregations?.connections.buckets.map((bucket) => { - const fieldValues = bucket.sample.top[0].metrics; + const sample = bucket.sample.hits.hits[0]._source; return { [SPAN_DESTINATION_SERVICE_RESOURCE]: String( bucket.key[SPAN_DESTINATION_SERVICE_RESOURCE] ), - [SPAN_ID]: (fieldValues[SPAN_ID] ?? '') as string, - [SPAN_TYPE]: (fieldValues[SPAN_TYPE] ?? '') as string, - [SPAN_SUBTYPE]: (fieldValues[SPAN_SUBTYPE] ?? '') as string, + [SPAN_ID]: sample.span.id, + [SPAN_TYPE]: sample.span.type, + [SPAN_SUBTYPE]: sample.span.subtype, }; }) ?? []; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index fb7544e5fcb8d..1771b5ead68a7 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -118,11 +118,8 @@ export async function getBuckets({ }), aggs: { samples: { - top_metrics: { - metrics: [ - { field: TRANSACTION_ID }, - { field: TRACE_ID }, - ] as const, + top_hits: { + _source: [TRANSACTION_ID, TRACE_ID], size: 10, sort: { _score: 'desc', @@ -138,11 +135,12 @@ export async function getBuckets({ return ( response.aggregations?.distribution.buckets.map((bucket) => { + const samples = bucket.samples.hits.hits; return { key: bucket.key, - samples: bucket.samples.top.map((sample) => ({ - traceId: sample.metrics[TRACE_ID] as string, - transactionId: sample.metrics[TRANSACTION_ID] as string, + samples: samples.map(({ _source: sample }) => ({ + traceId: sample.trace.id, + transactionId: sample.transaction.id, })), }; }) ?? [] diff --git a/x-pack/test/apm_api_integration/tests/transactions/distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/distribution.ts index 770fc90267680..cfedcc9ac2232 100644 --- a/x-pack/test/apm_api_integration/tests/transactions/distribution.ts +++ b/x-pack/test/apm_api_integration/tests/transactions/distribution.ts @@ -82,16 +82,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ Object { - "traceId": "a4eb3781a21dc11d289293076fd1a1b3", - "transactionId": "21892bde4ff1364d", + "traceId": "af0f18dc0841cfc1f567e7e1d55cfda7", + "transactionId": "925f02e5ac122897", }, Object { "traceId": "ccd327537120e857bdfa407434dfb9a4", "transactionId": "c5f923159cc1b8a6", }, Object { - "traceId": "af0f18dc0841cfc1f567e7e1d55cfda7", - "transactionId": "925f02e5ac122897", + "traceId": "a4eb3781a21dc11d289293076fd1a1b3", + "transactionId": "21892bde4ff1364d", }, ] `); From 378073c74a7331ab23fdaaae04b2545c53f877eb Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 16 Mar 2021 11:21:35 -0500 Subject: [PATCH 19/44] [DOCS] Canvas updates for 7.12.0 (#94325) * [DOCS] Canvas updates for 7.12.0 * Fixes Visualize Library title --- docs/canvas/canvas-edit-workpads.asciidoc | 10 +-- docs/canvas/canvas-present-workpad.asciidoc | 4 +- docs/canvas/canvas-share-workpad.asciidoc | 4 +- docs/canvas/canvas-tutorial.asciidoc | 17 ++-- docs/canvas/images/canvas-gs-example.png | Bin 99894 -> 204911 bytes docs/user/canvas.asciidoc | 90 +++++++++++++------- 6 files changed, 75 insertions(+), 50 deletions(-) diff --git a/docs/canvas/canvas-edit-workpads.asciidoc b/docs/canvas/canvas-edit-workpads.asciidoc index 6ad2d89be4a42..9f2808c9ad451 100644 --- a/docs/canvas/canvas-edit-workpads.asciidoc +++ b/docs/canvas/canvas-edit-workpads.asciidoc @@ -22,13 +22,13 @@ each element instead of updating them manually. For example, to change the index pattern for a set of charts: -Specify the variable options. - +. Specify the variable options. ++ [role="screenshot"] image::images/specify_variable_syntax.png[Image describing how to specify the variable syntax] - -Copy the variable, then apply it to each element you want to update in the *Expression editor*. - ++ +. Copy the variable, then apply it to each element you want to update in the *Expression editor*. ++ [role="screenshot"] image::images/copy_variable_syntax.png[Image demonstrating expression editor] diff --git a/docs/canvas/canvas-present-workpad.asciidoc b/docs/canvas/canvas-present-workpad.asciidoc index b1492f57e46f8..438e09b701fa3 100644 --- a/docs/canvas/canvas-present-workpad.asciidoc +++ b/docs/canvas/canvas-present-workpad.asciidoc @@ -20,7 +20,7 @@ image::images/canvas-autoplay-interval.png[Element autoplay interval] [role="screenshot"] image::images/canvas-fullscreen.png[Image showing how to enter fullscreen mode from view dropdown] -. When you are ready to exit fullscreen mode, press the Esc (Escape) key. +. When you are ready to exit fullscreen mode, press Esc. [float] [[zoom-in-out]] @@ -48,4 +48,4 @@ Change how often the data refreshes on your workpad. [role="screenshot"] image::images/canvas-refresh-interval.png[Element data refresh interval] + -To manually refresh the data, click image:canvas/images/canvas-refresh-data.png[]. +To manually refresh the data, click image:canvas/images/canvas-refresh-data.png[Canvas refresh data button]. diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index 68078b74da171..348d15f39ad76 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -23,7 +23,7 @@ Want to export multiple workpads? Go to the *Canvas* home page, select the workp [[add-workpad-website]] === Share the workpad on a website -beta[] Canvas allows you to create _shareables_, which are workpads that you download and securely share on any website. +beta[] *Canvas* allows you to create _shareables_, which are workpads that you download and securely share on any website. To customize the behavior of the workpad on your website, you can choose to autoplay the pages or hide the workpad toolbar. . Click *Share > Share on a website*. @@ -32,7 +32,7 @@ To customize the behavior of the workpad on your website, you can choose to auto . To customize the workpad behavior to autoplay the pages or hide the toolbar, use the inline parameters. + -To make sure that your data remains secure, the data in the JSON file is not connected to {kib}. Canvas does not display elements that manipulate the data on the workpad. +To make sure that your data remains secure, the data in the JSON file is not connected to {kib}. *Canvas* does not display elements that manipulate the data on the workpad. + [role="screenshot"] image::canvas/images/canvas-embed_workpad.gif[Image showing how to share the workpad on a website] diff --git a/docs/canvas/canvas-tutorial.asciidoc b/docs/canvas/canvas-tutorial.asciidoc index 6456ba02bb8a8..89114affb9322 100644 --- a/docs/canvas/canvas-tutorial.asciidoc +++ b/docs/canvas/canvas-tutorial.asciidoc @@ -2,17 +2,16 @@ [[canvas-tutorial]] == Tutorial: Create a workpad for monitoring sales -To get up and running with Canvas, add the Sample eCommerce orders data, then use the data to create a workpad for monitoring sales at an eCommerce store. +To familiarize yourself with *Canvas*, add the Sample eCommerce orders data, then use the data to create a workpad for monitoring sales at an eCommerce store. [float] -=== Before you begin +=== Open and set up Canvas -For this tutorial, you'll need to add the <>. +To create a workpad of the eCommerce store data, add the data set, then create the workpad. -[float] -=== Create your workpad +. On the {kib} *Home* page, click *Try our sample data*. -Your first step to working with Canvas is to create a workpad. +. From *Sample eCommerce orders data*, click *Add data*. . Open the main menu, then click *Canvas*. @@ -59,7 +58,7 @@ The query selects the total price field and sets it to the sum_total_price field .. Change the *Label* to `Total sales`. -. The error is gone, but the element could use some formatting. To format the number, use the Canvas expression language. +. The error is gone, but the element could use some formatting. To format the number, use the *Canvas* expression language. .. Click *Expression editor*. + @@ -118,7 +117,7 @@ Your workpad is complete! [float] === What's next? -Now that you know the Canvas basics, you're ready to explore on your own. +Now that you know the basics, you're ready to explore on your own. Here are some things to try: @@ -126,4 +125,4 @@ Here are some things to try: * Build presentations of your own data with <>. -* Deep dive into the {kibana-ref}/canvas-function-reference.html[expression language and functions] that drive Canvas. +* Deep dive into the {kibana-ref}/canvas-function-reference.html[expression language and functions] that drive *Canvas*. diff --git a/docs/canvas/images/canvas-gs-example.png b/docs/canvas/images/canvas-gs-example.png index a9b960342709f81c14d17b29684deaada3a3be6b..bae32ef96a93fd1a72f79de258fcd7052da00ccb 100644 GIT binary patch literal 204911 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 Z^2W`OnV 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 VLo-e}P_Rilj-rk`a_w<({{V|X1qc8D literal 99894 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 diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 6a3cee020538d..e5ac44a4e5401 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -5,31 +5,52 @@ [partintro] -- -Canvas is a data visualization and presentation tool that sits within {kib}. With Canvas, you can pull live data directly from {es}, and combine the data with colors, images, text, and your imagination to create dynamic, multi-page, pixel-perfect displays. If you are a little bit creative, a little bit technical, and a whole lot curious, then Canvas is for you. +*Canvas* is a data visualization and presentation tool that allows you to pull live data from {es}, +then combine the data with colors, images, text, and your imagination to create dynamic, multi-page, pixel-perfect displays. +If you are a little bit creative, a little bit technical, and a whole lot curious, then *Canvas* is for you. -With Canvas, you can: +With *Canvas*, you can: * Create and personalize your work space with backgrounds, borders, colors, fonts, and more. * Customize your workpad with your own visualizations, such as images and text. -* Pull your data directly from Elasticsearch, then show it off with charts, graphs, progress monitors, and more. +* Pull your data directly from {es}, then show it off with charts, graphs, progress monitors, and more. * Focus the data you want to display with filters. -To begin, open the main menu, then click *Canvas*. - -[role="screenshot"] -image::images/canvas-gs-example.png[Getting started example] - -For a quick overview of Canvas, watch link:https://www.youtube.com/watch?v=ZqvF_5-1xjQ[Stand out with Canvas]. +++++ + +
+++++ [float] [[create-workpads]] == Create workpads -A _workpad_ provides you with a space where you can build presentations of your live data. With Canvas, -you can create a workpad from scratch, start with a preconfigured workpad, import an existing workpad, or use a sample data workpad. +A _workpad_ provides you with a space where you can build presentations of your live data. You can create a workpad from scratch, start with a preconfigured workpad, +import an existing workpad, or use a sample data workpad. + +[float] +[[canvas-minimum-requirements]] +=== Minimum requirements + +To create workpads, you must meet the minimum requirements. + +* If you need to set up {kib}, use https://www.elastic.co/cloud/elasticsearch-service/signup?baymax=docs-body&elektra=docs[our free trial]. + +* Make sure you have {ref}/getting-started-index.html[data indexed into {es}] and an <>. + +* Have an understanding of {ref}/documents-indices.html[{es} documents and indices]. + +* Make sure you have sufficient privileges to create and save workpads. When the read-only indicator appears, you have insufficient privileges, +and the options to create and save workpads are unavailable. For more information, refer to <>. + +To open *Canvas*, open the main menu, then click *Canvas*. [float] [[start-with-a-blank-workpad]] @@ -54,7 +75,7 @@ image::images/canvas-background-color-picker.png[Canvas color picker] [[create-workpads-from-templates]] === Create workpads from templates -If you're unsure about where to start, you can use one of the preconfigured templates that come with Canvas. +If you're unsure about where to start, you can use one of the preconfigured templates that come with *Canvas*. . On the *Canvas workpads* page, select *Templates*. @@ -90,55 +111,60 @@ Create a story about your data by adding elements to your workpad that include i [[create-elements]] === Create elements -Choose the type of element you want to use, then use the preconfigured demo data to familiarize yourself with the element. When you're ready, connect the element to your own data. By default, most of the elements you create use -demo data until you change the data source. The demo data includes a small data set that you can use to experiment with your element. - -To begin, click *Add element*, then select the element you want to use. +Choose the type of element you want to use, then use the preconfigured demo data to familiarize yourself with the element. When you're ready, connect the element to your own data. +By default, most of the elements you create use the demo data until you change the data source. The demo data includes a small data set that you can use to experiment with your element. +. Click *Add element*, then select the element you want to use. ++ [role="screenshot"] image::images/canvas-element-select.gif[Canvas elements] -When you're ready to connect the element to your data, select *Data*, then select one of the following data sources: +. To connect the element to your data, select *Data*, then select one of the following data sources: * *{es} SQL* — Access your data in {es} using {ref}/sql-spec.html[SQL syntax]. -* *{es} documents* — Access your data in {es} without using aggregations. To use, select an index and fields, and optionally enter a query using the <>. Use the *{es} documents* data source when you have low volume datasets, to view raw documents, or to plot exact, non-aggregated values on a chart. - -* *Timelion* — Access your time series data using <> queries. To use Timelion queries, you can enter a query using the <>. +* *{es} documents* — Access your data in {es} without using aggregations. To use, select an index and fields, and optionally enter a query using the <>. +Use the *{es} documents* data source when you have low volume datasets, to view raw documents, or to plot exact, non-aggregated values on a chart. +* *Timelion* — Access your time series data using <> queries. To use *Timelion* queries, you can enter a query using the <>. ++ Each element can display a different data source, and pages and workpads often contain multiple data sources. -When you're ready to save your element, select the element, then click *Edit > Save as new element*. +. To save, use the following options: +* To save a single element, select the element, then click *Edit > Save as new element*. ++ [role="screenshot"] image::images/canvas_save_element.png[] -To save a group of elements, press and hold Shift, select the elements you want to save, then click *Edit > Save as new element*. +* To save a group of elements, press and hold Shift, select the elements you want to save, then click *Edit > Save as new element*. -Elements are saved in *Add element > My elements*. +To access your saved elements, click *Add element > My elements*. [float] -[[add-saved-objects]] -=== Add saved objects +[[add-kibana-visualizations]] +=== Add panels from the Visualize Library -Add <> to your workpad, such as maps and visualizations. +Add a panel that you saved in *Visualize Library* to your workpad. . Click *Add element > Add from {kib}*. -. Select the saved object you want to add. +. Select the panel you want to add. + [role="screenshot"] image::images/canvas-map-embed.gif[] -. To use the customization options, click the panel menu, then select one of the following options: +. To use the customization options, open the panel menu, then select one of the following options: + +* *Edit map* — Opens <> so that you can edit the panel. -* *Edit map* — Opens <> or a visualization builder so that you can edit the original saved object. +* *Edit visualization* — Opens the visualization editor so that you can edit the panel. -* *Edit panel title* — Adds a title to the saved object. +* *Edit panel title* — Allows you to change the panel title. -* *Customize time range* — Exposes a time filter dedicated to the saved object. +* *Customize time range* — Allows you to change the time filter dedicated to the panel. -* *Inspect* — Allows you to drill down into the element data. +* *Inspect* — Allows you to drill down into the panel data. [float] [[add-your-own-images]] From 067543b51ba66f35a80233c1739eb842f34cbce1 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 16 Mar 2021 09:34:03 -0700 Subject: [PATCH 20/44] App Services API updates for CSV Export (#94537) * App Arch API updates for CSV Export * add logging to show error cause from es * revert logging the error in an inconsistent way --- ...erver.iscopedsearchclient.cancelsession.md | 11 ++++ ...erver.iscopedsearchclient.deletesession.md | 11 ++++ ...erver.iscopedsearchclient.extendsession.md | 11 ++++ ...server.iscopedsearchclient.findsessions.md | 11 ++++ ...a-server.iscopedsearchclient.getsession.md | 11 ++++ ...plugins-data-server.iscopedsearchclient.md | 24 ++++++++ ...-server.iscopedsearchclient.savesession.md | 11 ++++ ...erver.iscopedsearchclient.updatesession.md | 11 ++++ .../kibana-plugin-plugins-data-server.md | 1 + src/plugins/data/server/index.ts | 1 + src/plugins/data/server/server.api.md | 55 +++++++++++++------ src/plugins/discover/public/index.ts | 1 + src/plugins/discover/public/shared/index.ts | 14 +++++ 13 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.cancelsession.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.deletesession.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.extendsession.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.findsessions.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.getsession.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.savesession.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.updatesession.md create mode 100644 src/plugins/discover/public/shared/index.ts diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.cancelsession.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.cancelsession.md new file mode 100644 index 0000000000000..3b38e64ecc3da --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.cancelsession.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) > [cancelSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.cancelsession.md) + +## IScopedSearchClient.cancelSession property + +Signature: + +```typescript +cancelSession: IScopedSearchSessionsClient['cancel']; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.deletesession.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.deletesession.md new file mode 100644 index 0000000000000..609c730c2911c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.deletesession.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) > [deleteSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.deletesession.md) + +## IScopedSearchClient.deleteSession property + +Signature: + +```typescript +deleteSession: IScopedSearchSessionsClient['delete']; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.extendsession.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.extendsession.md new file mode 100644 index 0000000000000..33ce8f2a82d0f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.extendsession.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) > [extendSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.extendsession.md) + +## IScopedSearchClient.extendSession property + +Signature: + +```typescript +extendSession: IScopedSearchSessionsClient['extend']; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.findsessions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.findsessions.md new file mode 100644 index 0000000000000..2a78e09841e77 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.findsessions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) > [findSessions](./kibana-plugin-plugins-data-server.iscopedsearchclient.findsessions.md) + +## IScopedSearchClient.findSessions property + +Signature: + +```typescript +findSessions: IScopedSearchSessionsClient['find']; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.getsession.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.getsession.md new file mode 100644 index 0000000000000..4afcf4ad29195 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.getsession.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) > [getSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.getsession.md) + +## IScopedSearchClient.getSession property + +Signature: + +```typescript +getSession: IScopedSearchSessionsClient['get']; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.md new file mode 100644 index 0000000000000..41ac662905b6b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) + +## IScopedSearchClient interface + +Signature: + +```typescript +export interface IScopedSearchClient extends ISearchClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cancelSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.cancelsession.md) | IScopedSearchSessionsClient['cancel'] | | +| [deleteSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.deletesession.md) | IScopedSearchSessionsClient['delete'] | | +| [extendSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.extendsession.md) | IScopedSearchSessionsClient['extend'] | | +| [findSessions](./kibana-plugin-plugins-data-server.iscopedsearchclient.findsessions.md) | IScopedSearchSessionsClient['find'] | | +| [getSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.getsession.md) | IScopedSearchSessionsClient['get'] | | +| [saveSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.savesession.md) | IScopedSearchSessionsClient['save'] | | +| [updateSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.updatesession.md) | IScopedSearchSessionsClient['update'] | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.savesession.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.savesession.md new file mode 100644 index 0000000000000..78cd49c376005 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.savesession.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) > [saveSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.savesession.md) + +## IScopedSearchClient.saveSession property + +Signature: + +```typescript +saveSession: IScopedSearchSessionsClient['save']; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.updatesession.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.updatesession.md new file mode 100644 index 0000000000000..5e010f9168e43 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iscopedsearchclient.updatesession.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) > [updateSession](./kibana-plugin-plugins-data-server.iscopedsearchclient.updatesession.md) + +## IScopedSearchClient.updateSession property + +Signature: + +```typescript +updateSession: IScopedSearchSessionsClient['update']; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index fd9ed1e8f635c..e0734bc017f4f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -52,6 +52,7 @@ | [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) | | | [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | Interface for an index pattern saved object | +| [IScopedSearchClient](./kibana-plugin-plugins-data-server.iscopedsearchclient.md) | | | [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) | | | [ISearchSessionService](./kibana-plugin-plugins-data-server.isearchsessionservice.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 2a21b9e434596..cbf09ef57d96a 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -220,6 +220,7 @@ export { } from '../common'; export { + IScopedSearchClient, ISearchStrategy, ISearchSetup, ISearchStart, diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f755679405de0..0118906c181cc 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -963,6 +963,29 @@ export class IndexPatternsServiceProvider implements Plugin_3 { - // Warning: (ae-forgotten-export) The symbol "IScopedSearchSessionsClient" needs to be exported by the entry point index.d.ts - // // (undocumented) asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSearchSessionsClient; } @@ -1010,8 +1031,6 @@ export interface ISearchStart IScopedSearchClient; getSearchStrategy: (name?: string) => ISearchStrategy; @@ -1481,20 +1500,20 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "HistogramFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:128:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:245:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:261:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:269:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:258:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index de76c65ccdc98..fbe853ec6deb5 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -16,4 +16,5 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; +export { loadSharingDataHelpers } from './shared'; export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; diff --git a/src/plugins/discover/public/shared/index.ts b/src/plugins/discover/public/shared/index.ts new file mode 100644 index 0000000000000..b1e4d9d87000e --- /dev/null +++ b/src/plugins/discover/public/shared/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +/* + * Allows the getSharingData function to be lazy loadable + */ +export async function loadSharingDataHelpers() { + return await import('../application/helpers/get_sharing_data'); +} From d71e1b47af46a4c7141051a8d6f5dac985c4bf60 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 16 Mar 2021 17:34:24 +0100 Subject: [PATCH 21/44] [Discover] Remove redundant execution of onRequestStart when fetching data (#94093) --- .../discover/public/application/angular/discover.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 420e626031b7a..4a761f2fefa65 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -700,7 +700,13 @@ function discoverController($route, $scope, Promise) { async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages - if (!getTimeField() || $scope.state.hideChart) return; + if (!getTimeField() || $scope.state.hideChart) { + if ($scope.volatileSearchSource.getField('aggs')) { + // cleanup aggs field in case it was set before + $scope.volatileSearchSource.removeField('aggs'); + } + return; + } const { interval: histogramInterval } = $scope.state; const visStateAggs = [ @@ -723,11 +729,6 @@ function discoverController($route, $scope, Promise) { visStateAggs ); - $scope.volatileSearchSource.onRequestStart((searchSource, options) => { - if (!$scope.opts.chartAggConfigs) return; - return $scope.opts.chartAggConfigs.onSearchRequestStart(searchSource, options); - }); - $scope.volatileSearchSource.setField('aggs', function () { if (!$scope.opts.chartAggConfigs) return; return $scope.opts.chartAggConfigs.toDsl(); From 4d27af99c49229146e066aadb59dfbfb73443066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 16 Mar 2021 12:47:33 -0400 Subject: [PATCH 22/44] [APM] kuery is not applied to latency chart (#94707) --- .../apm/server/lib/transactions/get_latency_charts/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index 5bd80f500fd2b..468585ddd23cb 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -183,6 +183,7 @@ export async function getLatencyPeriods({ latencyAggregationType, comparisonStart, comparisonEnd, + kuery, }: { serviceName: string; transactionType: string | undefined; @@ -192,6 +193,7 @@ export async function getLatencyPeriods({ latencyAggregationType: LatencyAggregationType; comparisonStart?: number; comparisonEnd?: number; + kuery?: string; }) { const { start, end } = setup; const options = { @@ -200,6 +202,7 @@ export async function getLatencyPeriods({ transactionName, setup, searchAggregatedTransactions, + kuery, }; const currentPeriodPromise = getLatencyTimeseries({ From 9e244062e78e557b4ad3832242ce8ed5521b675a Mon Sep 17 00:00:00 2001 From: Anish Khanna <35355702+anish-khanna@users.noreply.github.com> Date: Tue, 16 Mar 2021 13:11:10 -0400 Subject: [PATCH 23/44] Display correct reset terminology for visualizations (#94539) Co-authored-by: Anish Khanna --- .../config_panel/layer_panel.test.tsx | 9 ++++ .../editor_frame/config_panel/layer_panel.tsx | 1 + .../config_panel/remove_layer_button.tsx | 51 ++++++++++++------- .../public/editor_frame_service/mocks.tsx | 1 + 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 5c27958aa1786..30740bbd6b217 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -119,6 +119,15 @@ describe('LayerPanel', () => { ); }); + it('should show to reset visualization for visualizations only allowing a single layer', () => { + const layerPanelAttributes = getDefaultProps(); + delete layerPanelAttributes.activeVisualization.removeLayer; + const component = mountWithIntl(); + expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + 'Reset visualization' + ); + }); + it('should call the clear callback', () => { const cb = jest.fn(); const component = mountWithIntl(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 14063aea02665..21115285b5ce0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -397,6 +397,7 @@ export function LayerPanel( onRemoveLayer={onRemoveLayer} layerIndex={layerIndex} isOnlyLayer={isOnlyLayer} + activeVisualization={activeVisualization} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx index d842d2af5c777..cca8cc88c6ab1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx @@ -8,33 +8,54 @@ import React from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Visualization } from '../../../types'; export function RemoveLayerButton({ onRemoveLayer, layerIndex, isOnlyLayer, + activeVisualization, }: { onRemoveLayer: () => void; layerIndex: number; isOnlyLayer: boolean; + activeVisualization: Visualization; }) { + let ariaLabel; + let componentText; + + if (!activeVisualization.removeLayer) { + ariaLabel = i18n.translate('xpack.lens.resetVisualizationAriaLabel', { + defaultMessage: 'Reset visualization', + }); + componentText = i18n.translate('xpack.lens.resetVisualization', { + defaultMessage: 'Reset visualization', + }); + } else if (isOnlyLayer) { + ariaLabel = i18n.translate('xpack.lens.resetLayerAriaLabel', { + defaultMessage: 'Reset layer {index}', + values: { index: layerIndex + 1 }, + }); + componentText = i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }); + } else { + ariaLabel = i18n.translate('xpack.lens.deleteLayerAriaLabel', { + defaultMessage: `Delete layer {index}`, + values: { index: layerIndex + 1 }, + }); + componentText = i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: `Delete layer`, + }); + } + return ( { // If we don't blur the remove / clear button, it remains focused // which is a strange UX in this case. e.target.blur doesn't work @@ -49,13 +70,7 @@ export function RemoveLayerButton({ onRemoveLayer(); }} > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: `Delete layer`, - })} + {componentText} ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 7f256dc588c25..f769b20a6a454 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -23,6 +23,7 @@ export function createMockVisualization(): jest.Mocked { return { id: 'TEST_VIS', clearLayer: jest.fn((state, _layerId) => state), + removeLayer: jest.fn(), getLayerIds: jest.fn((_state) => ['layer1']), visualizationTypes: [ { From 0e1a2cd4f78231a68042c98238875f322842e023 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Tue, 16 Mar 2021 10:13:08 -0700 Subject: [PATCH 24/44] docs: [APM] Correlations (#94620) --- docs/apm/correlations.asciidoc | 91 ++++++++++++++++++ docs/apm/how-to-guides.asciidoc | 3 + docs/apm/images/correlations-hover.png | Bin 0 -> 510511 bytes docs/apm/images/correlations.png | Bin 0 -> 517392 bytes docs/apm/images/error-rate-hover.png | Bin 0 -> 524077 bytes .../app/correlations/custom_fields.tsx | 2 +- 6 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 docs/apm/correlations.asciidoc create mode 100644 docs/apm/images/correlations-hover.png create mode 100644 docs/apm/images/correlations.png create mode 100644 docs/apm/images/error-rate-hover.png diff --git a/docs/apm/correlations.asciidoc b/docs/apm/correlations.asciidoc new file mode 100644 index 0000000000000..1776cd72ac584 --- /dev/null +++ b/docs/apm/correlations.asciidoc @@ -0,0 +1,91 @@ +[role="xpack"] +[[correlations]] +=== Find latency and error correlations + +**Correlations** surface attributes of your data that are potentially correlated with high-latency or erroneous transactions. +Surfaced attributes are user-defined, meaning that they are completely customizable to your APM data. +Find something interesting? A quick click of a button will auto-query your data as you work to resolve the underlying issue. + +For example, a site reliability engineer, who is responsible for keeping production systems up and running, +notices an increase in latency in certain transactions. +Analyzing metadata or tags that exist in high-latency transactions but not in lower-latency transactions +can potentially point towards the root cause. +They may find that a particular piece of hardware, like a host or pod, has failed, increasing latency. +Or, perhaps a set of users, based on IP address or region, is physically too far away from the nearest +data center, increasing latency. + +[discrete] +[[view-correlations]] +=== View correlations + +With a service selected, click **View correlations**: + +[role="screenshot"] +image::apm/images/correlations.png[Correlations] + +Queries within the APM app apply to the correlations shown in the correlations fly-out. + +If a correlated field seems noteworthy, use the **Filter** quick links: + +* `+` creates a new query in the APM app for transactions containing the selected value. +* `-` creates a new query in the APM app for transactions without the selected value. + +[discrete] +[[correlations-latency]] +==== Find high-latency correlations + +Correlations help you discover which fields are contributing to increased service latency. + +A latency distribution chart visualizes the overall latency of the selected service's transactions. +Correlated attributes are sorted by _Impact_–a visual representation of the score for the underlying +aggregation that powers correlations. +Attributes with a high impact, or attributes present in a large percentage of slow transactions, +may contribute to increased latency. + +To find high-latency correlations, hover over each potentially correlated attribute to +compare the latency distribution of transactions with and without the selected attribute. + +For example, in the screenshot below, the field `user_agent.name` and value `HeadlessChrome` +exists primarily in higher-latency transactions between 3.7 and 8.7 seconds. + +[role="screenshot"] +image::apm/images/correlations-hover.png[Correlations hover effect] + +Selecting the `+` filter creates a new query in the APM app for transactions with +`user_agent.name: HeadlessChrome`. With the "noise" now filtered out, +you can begin viewing sample traces to continue your investigation. + +[discrete] +[[correlations-error-rate]] +==== Find error rate correlations + +Correlations help you discover which fields are contributing to failed transactions. + +The Error rate over time chart visualizes the change in error rate over the selected time frame. +Correlated attributes are sorted by _Impact_–a visual representation of the score for the underlying +aggregation that powers correlations. +Attributes with a high impact, or attributes present in a large percentage of failed transactions, +may contribute to increased error rates. + +To find error rate correlations, hover over each potentially correlated attribute to +compare the error rate distribution of transactions with and without the selected attribute. + +For example, in the screenshot below, the field `url.original` and value `http://localhost:3100...` +existed in 100% of failed transactions between 6:00 and 10:30. + +[role="screenshot"] +image::apm/images/error-rate-hover.png[Correlations errors hover effect] + +Selecting the `+` filter creates a new query in the APM app for transactions with +`url.original: http://localhost:3100...`. With the "noise" now filtered out, +you can begin viewing sample traces to continue your investigation. + +[discrete] +[[correlations-custom-fields]] +==== Customize fields + +Correlations are only as good as the data they're searching for. +By default, a handful of potentially useful fields are selected, like `lables`, `service.version`, and `host.ip`. +You can remove and add fields to this list under the **Customize fields** dropdown. + +TIP: Want to start over? Select **reset** to clear your customizations. diff --git a/docs/apm/how-to-guides.asciidoc b/docs/apm/how-to-guides.asciidoc index 9a415375f17fd..b4e49a69d5a7e 100644 --- a/docs/apm/how-to-guides.asciidoc +++ b/docs/apm/how-to-guides.asciidoc @@ -9,6 +9,7 @@ Learn how to perform common APM app tasks. * <> * <> * <> +* <> * <> * <> * <> @@ -22,6 +23,8 @@ include::custom-links.asciidoc[] include::filters.asciidoc[] +include::correlations.asciidoc[] + include::machine-learning.asciidoc[] include::advanced-queries.asciidoc[] diff --git a/docs/apm/images/correlations-hover.png b/docs/apm/images/correlations-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..b903a8cdf8de60d1961ca6fc30cfa916290bda8e GIT binary patch literal 510511 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 z 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

PZOmY^FIj-Fhf1HG%tUPTE~}IL}cqohieuR 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%0A(wiW^8E@eD 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�lhRlyXEL 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

7lSuz!lJluan8`6|frk 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

Ch_dkD)-B! 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

7ItYrwrY2J0u5RGzKe9K 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 Du8r@#ULnnn?uHwJP=0(7ML8O?3 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 WeIBg}6~+_bkD{!qO!2LILH`G$q*KZO literal 0 HcmV?d00001 diff --git a/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx b/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx index 550650941d6cf..9a13c44602c2d 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx @@ -122,7 +122,7 @@ export function CustomFields({ {i18n.translate( 'xpack.apm.correlations.customize.fieldHelpTextDocsLink', From a1f15fbe5d1d5948f4148f516da40a729fce5633 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 16 Mar 2021 19:36:48 +0200 Subject: [PATCH 25/44] [XY axis] Integrates legend color picker with the eui palette (#90589) * XY Axis, integrate legend color picker with the eui palette * Fix functional test to work with the eui palette * Order eui colors by group * Add unit test for use color picker * Add useMemo to getColorPicker * Remove the grey background from the first focused circle * Fix bug caused by comparing lowercase with uppercase characters * Fix bug on complimentary palette * Fix CI * fix linter * Use uppercase for hex color * Use eui variable instead * Changes on charts.json * Make the color picker accessible * Fix ci and tests * Allow keyboard navigation * Close the popover on mouse click event * Fix ci Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- api_docs/charts.json | 8 +- .../static/components/color_picker.scss | 12 ++ .../public/static/components/color_picker.tsx | 138 ++++++++++++------ .../vislib/components/legend/legend.test.tsx | 4 +- .../vislib/components/legend/legend.tsx | 1 - .../vislib/components/legend/legend_item.tsx | 3 - .../public/utils/get_color_picker.test.tsx | 99 +++++++++++++ .../public/utils/get_color_picker.tsx | 96 ++++++++++++ .../vis_type_xy/public/utils/index.tsx | 2 +- .../public/utils/use_color_picker.tsx | 76 ---------- .../vis_type_xy/public/vis_component.tsx | 43 ++---- .../apps/dashboard/dashboard_state.ts | 5 +- .../page_objects/visualize_chart_page.ts | 3 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 15 files changed, 326 insertions(+), 166 deletions(-) create mode 100644 src/plugins/vis_type_xy/public/utils/get_color_picker.test.tsx create mode 100644 src/plugins/vis_type_xy/public/utils/get_color_picker.tsx delete mode 100644 src/plugins/vis_type_xy/public/utils/use_color_picker.tsx diff --git a/api_docs/charts.json b/api_docs/charts.json index 181ed29399291..f063a2271aec7 100644 --- a/api_docs/charts.json +++ b/api_docs/charts.json @@ -9,7 +9,7 @@ "children": [ { "type": "Object", - "label": "{ onChange, color: selectedColor, id, label }", + "label": "{\n onChange,\n color: selectedColor,\n label,\n useLegacyColors = true,\n colorIsOverwritten = true,\n}", "isRequired": true, "signature": [ "ColorPickerProps" @@ -17,18 +17,18 @@ "description": [], "source": { "path": "src/plugins/charts/public/static/components/color_picker.tsx", - "lineNumber": 83 + "lineNumber": 108 } } ], "signature": [ - "({ onChange, color: selectedColor, id, label }: ColorPickerProps) => JSX.Element" + "({ onChange, color: selectedColor, label, useLegacyColors, colorIsOverwritten, }: ColorPickerProps) => JSX.Element" ], "description": [], "label": "ColorPicker", "source": { "path": "src/plugins/charts/public/static/components/color_picker.tsx", - "lineNumber": 83 + "lineNumber": 108 }, "tags": [], "returnComment": [], diff --git a/src/plugins/charts/public/static/components/color_picker.scss b/src/plugins/charts/public/static/components/color_picker.scss index 85bfefca41a09..5def2b75a4c50 100644 --- a/src/plugins/charts/public/static/components/color_picker.scss +++ b/src/plugins/charts/public/static/components/color_picker.scss @@ -4,6 +4,18 @@ $visColorPickerWidth: $euiSizeL * 8; // 8 columns width: $visColorPickerWidth; } +.visColorPicker__colorBtn { + position: relative; + + input[type='radio'] { + position: absolute; + top: 50%; + left: 50%; + opacity: 0; + transform: translate(-50%, -50%); + } +} + .visColorPicker__valueDot { cursor: pointer; diff --git a/src/plugins/charts/public/static/components/color_picker.tsx b/src/plugins/charts/public/static/components/color_picker.tsx index 07372e0aec43c..4974400a3767a 100644 --- a/src/plugins/charts/public/static/components/color_picker.tsx +++ b/src/plugins/charts/public/static/components/color_picker.tsx @@ -9,12 +9,19 @@ import classNames from 'classnames'; import React, { BaseSyntheticEvent } from 'react'; -import { EuiButtonEmpty, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiFlexItem, + EuiIcon, + euiPaletteColorBlind, + EuiScreenReaderOnly, + EuiFlexGroup, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import './color_picker.scss'; -export const legendColors: string[] = [ +export const legacyColors: string[] = [ '#3F6833', '#967302', '#2F575E', @@ -74,54 +81,91 @@ export const legendColors: string[] = [ ]; interface ColorPickerProps { - id?: string; + /** + * Label that characterizes the color that is going to change + */ label: string | number | null; + /** + * Callback on the color change + */ onChange: (color: string | null, event: BaseSyntheticEvent) => void; + /** + * Initial color. + */ color: string; + /** + * Defines if the compatibility (legacy) or eui palette is going to be used. Defauls to true. + */ + useLegacyColors?: boolean; + /** + * Defines if the default color is overwritten. Defaults to true. + */ + colorIsOverwritten?: boolean; + /** + * Callback for onKeyPress event + */ + onKeyDown?: (e: React.KeyboardEvent) => void; } +const euiColors = euiPaletteColorBlind({ rotations: 4, order: 'group' }); -export const ColorPicker = ({ onChange, color: selectedColor, id, label }: ColorPickerProps) => ( -

- - - -
- {legendColors.map((color) => ( - onChange(color, e)} - onKeyPress={(e) => onChange(color, e)} - className={classNames('visColorPicker__valueDot', { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'visColorPicker__valueDot-isSelected': color === selectedColor, - })} - style={{ color }} - data-test-subj={`visColorPickerColor-${color}`} - /> - ))} +export const ColorPicker = ({ + onChange, + color: selectedColor, + label, + useLegacyColors = true, + colorIsOverwritten = true, + onKeyDown, +}: ColorPickerProps) => { + const legendColors = useLegacyColors ? legacyColors : euiColors; + + return ( +
+
+ + + + + + + {legendColors.map((color) => ( + + ))} + +
+ {legendColors.some((c) => c === selectedColor) && colorIsOverwritten && ( + + onChange(null, e)}> + + + + )}
- {legendColors.some((c) => c === selectedColor) && ( - - onChange(null, e)} - onKeyPress={(e: any) => onChange(null, e)} - > - - - - )} -
-); + ); +}; diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 901c6091fdea4..34c1772ecdc17 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -246,8 +246,8 @@ describe('VisLegend Component', () => { first.simulate('click'); const popover = wrapper.find('.visColorPicker').first(); - const firstColor = popover.find('.visColorPicker__valueDot').first(); - firstColor.simulate('click'); + const firstColor = popover.find('.visColorPicker__colorBtn input').first(); + firstColor.simulate('change'); const colors = mockState.get('vis.colors'); diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index d99f3953ee105..9ce5a5339c04f 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -233,7 +233,6 @@ export class VisLegend extends PureComponent { canFilter={this.state.filterableLabels.has(item.label)} onFilter={this.filter} onSelect={this.toggleDetails} - legendId={this.legendId} setColor={this.setColor} getColor={this.getColor} onHighlight={this.highlight} diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx index 59f5a4f8a6c64..f4ca3eb5c40ae 100644 --- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx @@ -25,7 +25,6 @@ import { ColorPicker } from '../../../../../charts/public'; interface Props { item: LegendItem; - legendId: string; selected: boolean; canFilter: boolean; anchorPosition: EuiPopoverProps['anchorPosition']; @@ -39,7 +38,6 @@ interface Props { const VisLegendItemComponent = ({ item, - legendId, selected, canFilter, anchorPosition, @@ -150,7 +148,6 @@ const VisLegendItemComponent = ({ {canFilter && renderFilterBar()} setColor(item.label, c, e)} diff --git a/src/plugins/vis_type_xy/public/utils/get_color_picker.test.tsx b/src/plugins/vis_type_xy/public/utils/get_color_picker.test.tsx new file mode 100644 index 0000000000000..c2377b42bb1c2 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/get_color_picker.test.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { LegendColorPickerProps, XYChartSeriesIdentifier } from '@elastic/charts'; +import { EuiPopover } from '@elastic/eui'; +import { mountWithIntl } from '@kbn/test/jest'; +import { ComponentType, ReactWrapper } from 'enzyme'; +import { getColorPicker } from './get_color_picker'; +import { ColorPicker } from '../../../charts/public'; +import type { PersistedState } from '../../../visualizations/public'; + +jest.mock('@elastic/charts', () => { + const original = jest.requireActual('@elastic/charts'); + + return { + ...original, + getSpecId: jest.fn(() => {}), + }; +}); + +describe('getColorPicker', function () { + const mockState = new Map(); + const uiState = ({ + get: jest + .fn() + .mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)), + set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)), + emit: jest.fn(), + setSilent: jest.fn(), + } as unknown) as PersistedState; + + let wrapperProps: LegendColorPickerProps; + const Component: ComponentType = getColorPicker( + 'left', + jest.fn(), + jest.fn().mockImplementation((seriesIdentifier) => seriesIdentifier.seriesKeys[0]), + 'default', + uiState + ); + let wrapper: ReactWrapper; + + beforeAll(() => { + wrapperProps = { + color: 'rgb(109, 204, 177)', + onClose: jest.fn(), + onChange: jest.fn(), + anchor: document.createElement('div'), + seriesIdentifiers: [ + { + yAccessor: 'col-2-1', + splitAccessors: {}, + seriesKeys: ['Logstash Airways', 'col-2-1'], + specId: 'histogram-col-2-1', + key: + 'groupId{__pseudo_stacked_group-ValueAxis-1__}spec{histogram-col-2-1}yAccessor{col-2-1}splitAccessors{col-1-3-Logstash Airways}', + } as XYChartSeriesIdentifier, + ], + }; + }); + + it('renders the color picker', () => { + wrapper = mountWithIntl(); + expect(wrapper.find(ColorPicker).length).toBe(1); + }); + + it('renders the color picker with the colorIsOverwritten prop set to false if color is not overwritten for the specific series', () => { + wrapper = mountWithIntl(); + expect(wrapper.find(ColorPicker).prop('colorIsOverwritten')).toBe(false); + }); + + it('renders the color picker with the colorIsOverwritten prop set to true if color is overwritten for the specific series', () => { + uiState.set('vis.colors', { 'Logstash Airways': '#6092c0' }); + wrapper = mountWithIntl(); + expect(wrapper.find(ColorPicker).prop('colorIsOverwritten')).toBe(true); + }); + + it('renders the picker on the correct position', () => { + wrapper = mountWithIntl(); + expect(wrapper.find(EuiPopover).prop('anchorPosition')).toEqual('rightCenter'); + }); + + it('renders the picker for kibana palette with useLegacyColors set to true', () => { + const LegacyPaletteComponent: ComponentType = getColorPicker( + 'left', + jest.fn(), + jest.fn(), + 'kibana_palette', + uiState + ); + wrapper = mountWithIntl(); + expect(wrapper.find(ColorPicker).prop('useLegacyColors')).toBe(true); + }); +}); diff --git a/src/plugins/vis_type_xy/public/utils/get_color_picker.tsx b/src/plugins/vis_type_xy/public/utils/get_color_picker.tsx new file mode 100644 index 0000000000000..4805d89068e86 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/get_color_picker.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; + +import { LegendColorPicker, Position, XYChartSeriesIdentifier, SeriesName } from '@elastic/charts'; +import { PopoverAnchorPosition, EuiWrappingPopover, EuiOutsideClickDetector } from '@elastic/eui'; +import type { PersistedState } from '../../../visualizations/public'; +import { ColorPicker } from '../../../charts/public'; + +function getAnchorPosition(legendPosition: Position): PopoverAnchorPosition { + switch (legendPosition) { + case Position.Bottom: + return 'upCenter'; + case Position.Top: + return 'downCenter'; + case Position.Left: + return 'rightCenter'; + default: + return 'leftCenter'; + } +} + +const KEY_CODE_ENTER = 13; + +export const getColorPicker = ( + legendPosition: Position, + setColor: (newColor: string | null, seriesKey: string | number) => void, + getSeriesName: (series: XYChartSeriesIdentifier) => SeriesName, + paletteName: string, + uiState: PersistedState +): LegendColorPicker => ({ + anchor, + color, + onClose, + onChange, + seriesIdentifiers: [seriesIdentifier], +}) => { + const seriesName = getSeriesName(seriesIdentifier as XYChartSeriesIdentifier); + const overwriteColors: Record = uiState?.get('vis.colors', {}); + const colorIsOverwritten = Object.keys(overwriteColors).includes(seriesName as string); + let keyDownEventOn = false; + + const handleChange = (newColor: string | null) => { + if (!seriesName) { + return; + } + if (newColor) { + onChange(newColor); + } + setColor(newColor, seriesName); + // close the popover if no color is applied or the user has clicked a color + if (!newColor || !keyDownEventOn) { + onClose(); + } + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.keyCode === KEY_CODE_ENTER) { + onClose?.(); + } + keyDownEventOn = true; + }; + + const handleOutsideClick = useCallback(() => { + onClose?.(); + }, [onClose]); + + return ( + + + + + + ); +}; diff --git a/src/plugins/vis_type_xy/public/utils/index.tsx b/src/plugins/vis_type_xy/public/utils/index.tsx index 82e16a639daeb..d68a6e8068fa8 100644 --- a/src/plugins/vis_type_xy/public/utils/index.tsx +++ b/src/plugins/vis_type_xy/public/utils/index.tsx @@ -11,6 +11,6 @@ export { getTimeZone } from './get_time_zone'; export { getLegendActions } from './get_legend_actions'; export { getSeriesNameFn } from './get_series_name_fn'; export { getXDomain, getAdjustedDomain } from './domain'; -export { useColorPicker } from './use_color_picker'; +export { getColorPicker } from './get_color_picker'; export { getXAccessor } from './accessors'; export { getAllSeries } from './get_all_series'; diff --git a/src/plugins/vis_type_xy/public/utils/use_color_picker.tsx b/src/plugins/vis_type_xy/public/utils/use_color_picker.tsx deleted file mode 100644 index 5028bc379c375..0000000000000 --- a/src/plugins/vis_type_xy/public/utils/use_color_picker.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { BaseSyntheticEvent, useCallback, useMemo } from 'react'; - -import { LegendColorPicker, Position, XYChartSeriesIdentifier, SeriesName } from '@elastic/charts'; -import { PopoverAnchorPosition, EuiWrappingPopover, EuiOutsideClickDetector } from '@elastic/eui'; - -import { ColorPicker } from '../../../charts/public'; - -function getAnchorPosition(legendPosition: Position): PopoverAnchorPosition { - switch (legendPosition) { - case Position.Bottom: - return 'upCenter'; - case Position.Top: - return 'downCenter'; - case Position.Left: - return 'rightCenter'; - default: - return 'leftCenter'; - } -} - -export const useColorPicker = ( - legendPosition: Position, - setColor: ( - newColor: string | null, - seriesKey: string | number, - event: BaseSyntheticEvent - ) => void, - getSeriesName: (series: XYChartSeriesIdentifier) => SeriesName -): LegendColorPicker => - useMemo( - () => ({ anchor, color, onClose, onChange, seriesIdentifiers: [seriesIdentifier] }) => { - const seriesName = getSeriesName(seriesIdentifier as XYChartSeriesIdentifier); - const handlChange = (newColor: string | null, event: BaseSyntheticEvent) => { - if (!seriesName) { - return; - } - if (newColor) { - onChange(newColor); - } - setColor(newColor, seriesName, event); - // must be called after onChange - onClose(); - }; - - // rule doesn't know this is inside a functional component - // eslint-disable-next-line react-hooks/rules-of-hooks - const handleOutsideClick = useCallback(() => { - onClose?.(); - }, [onClose]); - - return ( - - - - - - ); - }, - [getSeriesName, legendPosition, setColor] - ); diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index ab398101bac9d..5da5ffcc637c6 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -6,15 +6,7 @@ * Side Public License, v 1. */ -import React, { - BaseSyntheticEvent, - KeyboardEvent, - memo, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { Chart, @@ -28,7 +20,6 @@ import { AccessorFn, Accessor, } from '@elastic/charts'; -import { keys } from '@elastic/eui'; import { compact } from 'lodash'; import { @@ -50,7 +41,7 @@ import { renderAllSeries, getSeriesNameFn, getLegendActions, - useColorPicker, + getColorPicker, getXAccessor, getAllSeries, } from './utils'; @@ -86,16 +77,6 @@ const VisComponent = (props: VisComponentProps) => { return props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) as boolean; }); const [palettesRegistry, setPalettesRegistry] = useState(null); - useEffect(() => { - const fn = () => { - props?.uiState?.emit?.('reload'); - }; - props?.uiState?.on?.('change', fn); - - return () => { - props?.uiState?.off?.('change', fn); - }; - }, [props?.uiState]); const onRenderChange = useCallback( (isRendered) => { @@ -203,11 +184,7 @@ const VisComponent = (props: VisComponentProps) => { }, [props.uiState]); const setColor = useCallback( - (newColor: string | null, seriesLabel: string | number, event: BaseSyntheticEvent) => { - if ((event as KeyboardEvent).key && (event as KeyboardEvent).key !== keys.ENTER) { - return; - } - + (newColor: string | null, seriesLabel: string | number) => { const colors = props.uiState?.get('vis.colors') || {}; if (colors[seriesLabel] === newColor || !newColor) { delete colors[seriesLabel]; @@ -337,6 +314,18 @@ const VisComponent = (props: VisComponentProps) => { xAccessor, ] ); + + const legendColorPicker = useMemo( + () => + getColorPicker( + legendPosition, + setColor, + getSeriesName, + visParams.palette.name, + props.uiState + ), + [getSeriesName, legendPosition, props.uiState, setColor, visParams.palette.name] + ); return (
{ legendPosition={legendPosition} xDomain={xDomain} adjustedXDomain={adjustedXDomain} - legendColorPicker={useColorPicker(legendPosition, setColor, getSeriesName)} + legendColorPicker={legendColorPicker} onElementClick={handleFilterClick( visData, xAccessor, diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/dashboard_state.ts index 9b2be2e6b5a00..acb2bd869819d 100644 --- a/test/functional/apps/dashboard/dashboard_state.ts +++ b/test/functional/apps/dashboard/dashboard_state.ts @@ -76,7 +76,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await queryBar.clickQuerySubmitButton(); await PageObjects.visChart.openLegendOptionColors('Count', `[data-title="${visName}"]`); - await PageObjects.visChart.selectNewLegendColorChoice('#EA6460'); + const overwriteColor = isNewChartsLibraryEnabled ? '#d36086' : '#EA6460'; + await PageObjects.visChart.selectNewLegendColorChoice(overwriteColor); await PageObjects.dashboard.saveDashboard(dashboarName); @@ -89,7 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } const colorChoiceRetained = await PageObjects.visChart.doesSelectedLegendColorExist( - '#EA6460' + overwriteColor ); expect(colorChoiceRetained).to.be(true); diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index abd5975b95d0a..cd1c5cf318e63 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -408,7 +408,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr await this.waitForVisualizationRenderingStabilized(); // arbitrary color chosen, any available would do - const isOpen = await this.doesLegendColorChoiceExist('#EF843C'); + const arbitraryColor = (await this.isVisTypeXYChart()) ? '#d36086' : '#EF843C'; + const isOpen = await this.doesLegendColorChoiceExist(arbitraryColor); if (!isOpen) { throw new Error('legend color selector not open'); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a558834ab67f6..d9c162e89020a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -262,7 +262,6 @@ "charts.colormaps.greysText": "グレー", "charts.colormaps.redsText": "赤", "charts.colormaps.yellowToRedText": "黄色から赤", - "charts.colorPicker.clearColor": "色のクリア", "charts.colorPicker.setColor.screenReaderDescription": "値 {legendDataLabel} の色を設定", "charts.countText": "カウント", "charts.functions.palette.args.colorHelpText": "パレットの色です。{html} カラー名、{hex}、{hsl}、{hsla}、{rgb}、または {rgba} を使用できます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9113b44d6ad31..325200723e122 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -265,7 +265,6 @@ "charts.colormaps.greysText": "灰色", "charts.colormaps.redsText": "红色", "charts.colormaps.yellowToRedText": "黄到红", - "charts.colorPicker.clearColor": "清除颜色", "charts.colorPicker.setColor.screenReaderDescription": "为值 {legendDataLabel} 设置颜色", "charts.countText": "计数", "charts.functions.palette.args.colorHelpText": "调色板颜色。接受 {html} 颜色名称 {hex}、{hsl}、{hsla}、{rgb} 或 {rgba}。", From 36ca26d2905ab2479f011da3dd26c94c69f49fcc Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 16 Mar 2021 11:08:02 -0700 Subject: [PATCH 26/44] skip flaky suite (#94666) --- x-pack/test/accessibility/apps/ml.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index 08762353132e7..4689180c457a5 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const ml = getService('ml'); - describe('ml', () => { + // Failing: See https://github.com/elastic/kibana/issues/94666 + describe.skip('ml', () => { const esArchiver = getService('esArchiver'); before(async () => { From 70bdb28d795e259974831da514be4594c895f483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= Date: Tue, 16 Mar 2021 19:24:26 +0100 Subject: [PATCH 27/44] [APM] Settings: Update layout and update/add descriptions (#94398) * [APM] Align naming * [APM] Add agent config description * [APM] Update layout and styles * [APM] Update text styles and spacing * [APM] Updating styles and layout * [APM] Update layout and styles * [APM] Update description * [APM] Update layout and styles * [APM] Update i18n name * [APM] Add i18n description * [APM] Update layout and styles * Update x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx Co-authored-by: Brandon Morelli * [APM] Update agent config description * Update x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx Co-authored-by: Brandon Morelli * [APM] Remove empty state description Not needed as we have a description by the main title * [APM] Remove unneeded translations Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Brandon Morelli --- .../AgentConfigurationCreateEdit/index.tsx | 4 +--- .../Settings/AgentConfigurations/List/index.tsx | 8 -------- .../app/Settings/AgentConfigurations/index.tsx | 11 +++++++++-- .../components/app/Settings/ApmIndices/index.tsx | 4 ++-- .../Settings/CustomizeUI/CustomLink/index.tsx | 16 ++++++++-------- .../app/Settings/CustomizeUI/index.tsx | 10 ++++++++-- .../app/Settings/anomaly_detection/index.tsx | 4 ++-- .../app/Settings/anomaly_detection/jobs_list.tsx | 5 ++--- .../plugins/translations/translations/ja-JP.json | 1 - .../plugins/translations/translations/zh-CN.json | 1 - 10 files changed, 32 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx index 734eb7c236fdf..07afb2fece283 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -123,9 +123,7 @@ export function AgentConfigurationCreateEdit({ {i18n.translate('xpack.apm.agentConfig.newConfig.description', { - defaultMessage: `This allows you to fine-tune your agent configuration directly in - Kibana. Best of all, changes are automatically propagated to your APM - agents so there’s no need to redeploy.`, + defaultMessage: `Fine-tune your agent configuration from within the APM app. Changes are automatically propagated to your APM agents, so there’s no need to redeploy.`, })} diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index d0b8e6fd8fba2..bef0dfc22280c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -61,14 +61,6 @@ export function AgentConfigurationList({ status, data, refetch }: Props) { )}

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�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 literal 0 HcmV?d00001 diff --git a/docs/apm/images/correlations.png b/docs/apm/images/correlations.png new file mode 100644 index 0000000000000000000000000000000000000000..e35e800cb9e010c997e5169eb62b9b43c626c78d GIT binary patch literal 517392 zcmb@t1ymeCwmyyq3!30=!QI{6-625G;O+!>3$Byk4ud-hHi1EcySuyFf3mx8-@fgv8#Q&r#n>fYPcQL4%^sK^A!P*6~)a4O2?

} - body={ -

- {i18n.translate('xpack.apm.agentConfig.configTable.emptyPromptText', { - defaultMessage: - "Let's change that! You can fine-tune agent configuration directly from Kibana without having to redeploy. Get started by creating your first configuration.", - })} -

- } actions={

{i18n.translate('xpack.apm.agentConfig.titleText', { - defaultMessage: 'Agent remote configuration', + defaultMessage: 'Agent central configuration', })}

+ + + {i18n.translate('xpack.apm.settings.agentConfig.descriptionText', { + defaultMessage: `Fine-tune your agent configuration from within the APM app. Changes are automatically propagated to your APM agents, so there’s no need to redeploy.`, + })} + - +

{i18n.translate( 'xpack.apm.agentConfig.configurationsPanelTitle', diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index e93aced10a744..9722c99990e3f 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -178,8 +178,8 @@ export function ApmIndices() { })}

- - + + {i18n.translate('xpack.apm.settings.apmIndices.description', { defaultMessage: `The APM UI uses index patterns to query your APM indices. If you've customized the index names that APM Server writes events to, you may need to update these patterns for the APM UI to work. Settings here take precedence over those set in kibana.yml.`, })} diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index 90bc83eeffde9..4b4bc2e8feeab 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -9,9 +9,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, - EuiSpacer, EuiTitle, EuiText, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; @@ -82,14 +82,14 @@ export function CustomLinkOverview() { /> )} - + - + - + @@ -117,11 +117,11 @@ export function CustomLinkOverview() { )} - - + + {i18n.translate('xpack.apm.settings.customizeUI.customLink.info', { defaultMessage: - 'These links will be shown in the Actions context menu for transactions.', + 'These links will be shown in the Actions context menu in selected areas of the app, e.g. by the transactions detail.', })} {hasValidLicense ? ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx index a408fbe6c09b4..fabd70cec6647 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { CustomLinkOverview } from './CustomLink'; @@ -15,11 +15,17 @@ export function CustomizeUI() { <>

- {i18n.translate('xpack.apm.settings.customizeApp', { + {i18n.translate('xpack.apm.settings.customizeApp.title', { defaultMessage: 'Customize app', })}

+ + + {i18n.translate('xpack.apm.settings.customizeApp.description', { + defaultMessage: `Extend the APM app experience with the following settings.`, + })} + diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 0c93c7e3a7aba..72f0249f07bf6 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -73,8 +73,8 @@ export function AnomalyDetection() { })}
- - + + {i18n.translate('xpack.apm.settings.anomalyDetection.descriptionText', { defaultMessage: `Machine Learning's anomaly detection integration enables application health status indicators for services in each configured environment by identifying anomalies in latency.`, })} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 6be4a5889211e..9c69d692876b0 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -69,7 +69,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { - +

{i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.environments', @@ -91,8 +91,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { - - + Date: Tue, 16 Mar 2021 18:32:43 +0000 Subject: [PATCH 28/44] chore(NA): upgrade bazel rules nodejs to v3.2.2 (#94726) --- WORKSPACE.bazel | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 7c5b59aa15b16..9f0e6e0231feb 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -10,15 +10,15 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch Node.js rules http_archive( name = "build_bazel_rules_nodejs", - sha256 = "bfacf15161d96a6a39510e7b3d3b522cf61cb8b82a31e79400a84c5abcab5347", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.1/rules_nodejs-3.2.1.tar.gz"], + sha256 = "55a25a762fcf9c9b88ab54436581e671bc9f4f523cb5a1bd32459ebec7be68a8", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/3.2.2/rules_nodejs-3.2.2.tar.gz"], ) # Now that we have the rules let's import from them to complete the work load("@build_bazel_rules_nodejs//:index.bzl", "check_rules_nodejs_version", "node_repositories", "yarn_install") # Assure we have at least a given rules_nodejs version -check_rules_nodejs_version(minimum_version_string = "3.2.1") +check_rules_nodejs_version(minimum_version_string = "3.2.2") # Setup the Node.js toolchain for the architectures we want to support # From ec41ae3c49bee9b20ead36fcea1eb0d277f8f0bf Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 16 Mar 2021 11:54:47 -0700 Subject: [PATCH 29/44] [Reporting-CSV Export] Re-write CSV Export using SearchSource (#88303) * [Reporting-CSV Export] Re-write CSV Export using SearchSource * replace PIT solution with scan-and-scroll * update tests * cleanup * simplify pr * update docs * update docs * update telemetry schema * use getSearchRequestBody instead of flatten * Revert "update docs" This reverts commit ab9f4d9642136fb0f53213dbe06037efaa89b6b9. * optimize some async calls * cleanup * --wip-- [skip ci] * fix telemetry schema * fix telemetry tests * fix snapshot * api docs * api doc updates * use import type * format the data through chains of maps * add another saved search to reporting/ecommerce_kibana * add a failing test * add error logging to query failures * put clear scroll in a finally so the ES error can be captured * log dat error * set dat fieldsFromSource * --wip-- [skip ci] * Revert "add another saved search to reporting/ecommerce_kibana" This reverts commit 6edf26eff255189c4a2c88a93a327497bd833410. * functional test fixes * clean up ecommerce test archive * add test for new search with fieldsFromSource set * add tests and refactor tests * cleanup redundant conditionals * add GenerateCsv.getFields * fix some tests * fix double-escaping * fix test snapshots and refactoring * fix other tests * fix test * fix default index pattern in functional tests * fix ts and sort fields when they come from API response * --wip-- [skip ci] * fix formatting and increase maxSizeBytes for testing * remove client-side logic for sanitizing fields * do not prepend timefield name if it already is a column * test the logic to prepend timeField * test the logic to sort the fields * fix functional test * preserve the error from data.search * add functional test for ES returning an error * fix snapshot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- api_docs/data.json | 50 +- api_docs/data_search.json | 177 ++ api_docs/discover.json | 19 + api_docs/reporting.json | 130 +- .../components/top_nav/get_top_nav_links.ts | 3 +- .../helpers/get_sharing_data.test.ts | 155 +- .../application/helpers/get_sharing_data.ts | 73 +- x-pack/plugins/reporting/common/constants.ts | 10 +- x-pack/plugins/reporting/common/types.ts | 3 +- .../components/reporting_panel_content.tsx | 10 +- .../get_csv_panel_action.test.ts | 15 +- .../panel_actions/get_csv_panel_action.tsx | 62 +- .../register_csv_reporting.tsx | 20 +- x-pack/plugins/reporting/server/core.ts | 15 + .../server/export_types/common/index.ts | 1 - .../generate_csv/check_cells_for_formulas.ts | 2 +- .../export_types/csv/generate_csv/index.ts | 11 +- .../server/export_types/csv/types.d.ts | 7 +- .../csv_from_savedobject/create_job.ts | 95 - .../csv_from_savedobject/execute_job.ts | 80 - .../lib/get_csv_job.test.ts | 340 --- .../csv_from_savedobject/lib/get_csv_job.ts | 155 - .../lib/get_data_source.ts | 56 - .../lib/get_filters.test.ts | 208 -- .../csv_from_savedobject/lib/get_filters.ts | 55 - .../csv_from_savedobject/types.d.ts | 153 - .../csv_searchsource/create_job.ts | 30 + .../csv_searchsource/execute_job.test.ts | 75 + .../csv_searchsource/execute_job.ts | 49 + .../__snapshots__/generate_csv.test.ts.snap | 163 ++ .../generate_csv/cell_has_formula.ts | 0 .../generate_csv/escape_value.test.ts | 0 .../generate_csv/escape_value.ts | 0 .../generate_csv/generate_csv.test.ts | 645 ++++ .../generate_csv/generate_csv.ts | 400 +++ .../generate_csv/get_export_settings.test.ts | 83 + .../generate_csv/get_export_settings.ts | 85 + .../csv_searchsource/generate_csv/index.ts | 8 + .../max_size_string_builder.test.ts | 0 .../generate_csv/max_size_string_builder.ts | 0 .../export_types/csv_searchsource/index.ts | 40 + .../metadata.ts | 6 +- .../export_types/csv_searchsource/types.d.ts | 18 + .../csv_searchsource_immediate/execute_job.ts | 81 + .../index.ts | 13 +- .../csv_searchsource_immediate/metadata.ts | 13 + .../csv_searchsource_immediate/types.d.ts | 24 + .../reporting/server/lib/enqueue_job.ts | 6 +- .../server/lib/export_types_registry.ts | 8 +- x-pack/plugins/reporting/server/plugin.ts | 2 + ...diate.ts => csv_searchsource_immediate.ts} | 44 +- .../reporting/server/routes/generation.ts | 2 +- .../server/routes/lib/get_document_payload.ts | 4 +- .../routes/lib/get_job_params_from_request.ts | 22 - .../create_mock_reportingplugin.ts | 35 +- x-pack/plugins/reporting/server/types.ts | 7 +- .../reporting_usage_collector.test.ts.snap | 24 + .../server/usage/decorate_range_stats.ts | 8 +- .../usage/reporting_usage_collector.test.ts | 154 + .../plugins/reporting/server/usage/schema.ts | 2 + .../plugins/reporting/server/usage/types.ts | 4 +- .../schema/xpack_plugins.json | 176 ++ .../reporting/__snapshots__/download_csv.snap | 53 + .../apps/dashboard/reporting/download_csv.ts | 160 +- .../discover/__snapshots__/reporting.snap | 106 +- .../functional/apps/discover/reporting.ts | 100 +- .../reporting/ecommerce/data.json.gz | Bin 957893 -> 956882 bytes .../reporting/ecommerce/mappings.json | 1015 ------- .../reporting/ecommerce_kibana/data.json | 456 ++- .../reporting/hugedata/data.json.gz | Bin 33745 -> 33885 bytes .../reporting/hugedata/mappings.json | 2590 ++++++++++++++--- .../reporting/scripted_small2/data.json.gz | Bin 4436 -> 0 bytes .../reporting/scripted_small2/mappings.json | 2217 -------------- .../reporting_api_integration/fixtures.ts | 261 -- .../reporting_and_security.config.ts | 2 +- .../csv_searchsource_immediate.snap | 250 ++ .../csv_saved_search.ts | 411 --- .../csv_searchsource_immediate.ts | 512 ++++ .../reporting_and_security/index.ts | 2 +- 79 files changed, 6393 insertions(+), 5878 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts delete mode 100644 x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/create_job.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap rename x-pack/plugins/reporting/server/export_types/{csv => csv_searchsource}/generate_csv/cell_has_formula.ts (100%) rename x-pack/plugins/reporting/server/export_types/{csv => csv_searchsource}/generate_csv/escape_value.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/{csv => csv_searchsource}/generate_csv/escape_value.ts (100%) create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/index.ts rename x-pack/plugins/reporting/server/export_types/{csv => csv_searchsource}/generate_csv/max_size_string_builder.test.ts (100%) rename x-pack/plugins/reporting/server/export_types/{csv => csv_searchsource}/generate_csv/max_size_string_builder.ts (100%) create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/index.ts rename x-pack/plugins/reporting/server/export_types/{csv_from_savedobject => csv_searchsource}/metadata.ts (65%) create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts rename x-pack/plugins/reporting/server/export_types/{csv_from_savedobject => csv_searchsource_immediate}/index.ts (75%) create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/metadata.ts create mode 100644 x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts rename x-pack/plugins/reporting/server/routes/{generate_from_savedobject_immediate.ts => csv_searchsource_immediate.ts} (55%) delete mode 100644 x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts create mode 100644 x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap delete mode 100644 x-pack/test/functional/es_archives/reporting/scripted_small2/data.json.gz delete mode 100644 x-pack/test/functional/es_archives/reporting/scripted_small2/mappings.json create mode 100644 x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap delete mode 100644 x-pack/test/reporting_api_integration/reporting_and_security/csv_saved_search.ts create mode 100644 x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts diff --git a/api_docs/data.json b/api_docs/data.json index 7989768e180ce..24bc790bbafa7 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -21764,7 +21764,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 245 + "lineNumber": 246 }, "signature": [ "typeof ", @@ -21785,7 +21785,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 246 + "lineNumber": 247 }, "signature": [ "typeof ", @@ -21806,7 +21806,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 247 + "lineNumber": 248 }, "signature": [ "({ display: string; val: string; enabled(agg: ", @@ -21828,7 +21828,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 248 + "lineNumber": 249 }, "signature": [ "typeof ", @@ -21849,7 +21849,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 249 + "lineNumber": 250 }, "signature": [ "typeof ", @@ -21870,7 +21870,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 250 + "lineNumber": 251 }, "signature": [ "typeof ", @@ -21891,7 +21891,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 251 + "lineNumber": 252 }, "signature": [ "(agg: ", @@ -21913,7 +21913,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 252 + "lineNumber": 253 }, "signature": [ "(agg: ", @@ -21935,7 +21935,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 253 + "lineNumber": 254 }, "signature": [ "(...types: string[]) => (agg: ", @@ -21957,7 +21957,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 254 + "lineNumber": 255 }, "signature": [ "typeof ", @@ -21978,7 +21978,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 255 + "lineNumber": 256 }, "signature": [ "typeof ", @@ -21999,7 +21999,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 256 + "lineNumber": 257 } }, { @@ -22010,7 +22010,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 257 + "lineNumber": 258 }, "signature": [ "typeof ", @@ -22031,7 +22031,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 258 + "lineNumber": 259 }, "signature": [ "typeof ", @@ -22052,7 +22052,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 259 + "lineNumber": 260 }, "signature": [ "typeof ", @@ -22073,7 +22073,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 260 + "lineNumber": 261 } }, { @@ -22084,7 +22084,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 261 + "lineNumber": 262 }, "signature": [ "string[]" @@ -22098,7 +22098,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 262 + "lineNumber": 263 }, "signature": [ "typeof ", @@ -22119,7 +22119,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 263 + "lineNumber": 264 }, "signature": [ "typeof ", @@ -22137,7 +22137,7 @@ "label": "aggs", "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 244 + "lineNumber": 245 } }, { @@ -22148,7 +22148,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 265 + "lineNumber": 266 }, "signature": [ "typeof ", @@ -22169,7 +22169,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 266 + "lineNumber": 267 }, "signature": [ "typeof ", @@ -22190,7 +22190,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 267 + "lineNumber": 268 }, "signature": [ "typeof ", @@ -22211,7 +22211,7 @@ "description": [], "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 268 + "lineNumber": 269 }, "signature": [ "typeof ", @@ -22229,7 +22229,7 @@ "label": "search", "source": { "path": "src/plugins/data/server/index.ts", - "lineNumber": 243 + "lineNumber": 244 }, "initialIsOpen": false }, diff --git a/api_docs/data_search.json b/api_docs/data_search.json index 0bdfcadd338ea..6dc7c105051f5 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -1573,6 +1573,183 @@ } ], "interfaces": [ + { + "id": "def-server.IScopedSearchClient", + "type": "Interface", + "label": "IScopedSearchClient", + "signature": [ + { + "pluginId": "data", + "scope": "server", + "docId": "kibDataSearchPluginApi", + "section": "def-server.IScopedSearchClient", + "text": "IScopedSearchClient" + }, + " extends ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.ISearchClient", + "text": "ISearchClient" + } + ], + "description": [], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-server.IScopedSearchClient.saveSession", + "type": "Function", + "label": "saveSession", + "description": [], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 90 + }, + "signature": [ + "(sessionId: string, attributes: Partial) => Promise<", + { + "pluginId": "core", + "scope": "common", + "docId": "kibCorePluginApi", + "section": "def-common.SavedObject", + "text": "SavedObject" + }, + " | undefined>" + ] + }, + { + "tags": [], + "id": "def-server.IScopedSearchClient.getSession", + "type": "Function", + "label": "getSession", + "description": [], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 91 + }, + "signature": [ + "(sessionId: string) => Promise<", + { + "pluginId": "core", + "scope": "common", + "docId": "kibCorePluginApi", + "section": "def-common.SavedObject", + "text": "SavedObject" + }, + ">" + ] + }, + { + "tags": [], + "id": "def-server.IScopedSearchClient.findSessions", + "type": "Function", + "label": "findSessions", + "description": [], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 92 + }, + "signature": [ + "(options: Pick<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptions", + "text": "SavedObjectsFindOptions" + }, + ", \"filter\" | \"fields\" | \"searchAfter\" | \"search\" | \"page\" | \"perPage\" | \"sortField\" | \"sortOrder\" | \"searchFields\" | \"rootSearchFields\" | \"hasReference\" | \"hasReferenceOperator\" | \"defaultSearchOperator\" | \"namespaces\" | \"typeToNamespacesMap\" | \"preference\" | \"pit\">) => Promise<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindResponse", + "text": "SavedObjectsFindResponse" + }, + ">" + ] + }, + { + "tags": [], + "id": "def-server.IScopedSearchClient.updateSession", + "type": "Function", + "label": "updateSession", + "description": [], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 93 + }, + "signature": [ + "(sessionId: string, attributes: Partial) => Promise<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsUpdateResponse", + "text": "SavedObjectsUpdateResponse" + }, + ">" + ] + }, + { + "tags": [], + "id": "def-server.IScopedSearchClient.cancelSession", + "type": "Function", + "label": "cancelSession", + "description": [], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 94 + }, + "signature": [ + "(sessionId: string) => Promise<{}>" + ] + }, + { + "tags": [], + "id": "def-server.IScopedSearchClient.deleteSession", + "type": "Function", + "label": "deleteSession", + "description": [], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 95 + }, + "signature": [ + "(sessionId: string) => Promise<{}>" + ] + }, + { + "tags": [], + "id": "def-server.IScopedSearchClient.extendSession", + "type": "Function", + "label": "extendSession", + "description": [], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 96 + }, + "signature": [ + "(sessionId: string, expires: Date) => Promise<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsUpdateResponse", + "text": "SavedObjectsUpdateResponse" + }, + ">" + ] + } + ], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 89 + }, + "initialIsOpen": false + }, { "id": "def-server.ISearchSessionService", "type": "Interface", diff --git a/api_docs/discover.json b/api_docs/discover.json index 69e1d0366a712..adcecddfcc444 100644 --- a/api_docs/discover.json +++ b/api_docs/discover.json @@ -40,6 +40,25 @@ "lineNumber": 18 }, "initialIsOpen": false + }, + { + "id": "def-public.loadSharingDataHelpers", + "type": "Function", + "label": "loadSharingDataHelpers", + "signature": [ + "() => Promise" + ], + "description": [], + "children": [], + "tags": [], + "returnComment": [], + "source": { + "path": "src/plugins/discover/public/shared/index.ts", + "lineNumber": 12 + }, + "initialIsOpen": false } ], "interfaces": [ diff --git a/api_docs/reporting.json b/api_docs/reporting.json index e07e3493a9d85..44050591f71cb 100644 --- a/api_docs/reporting.json +++ b/api_docs/reporting.json @@ -851,7 +851,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 65 + "lineNumber": 69 } }, { @@ -873,7 +873,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 65 + "lineNumber": 69 } } ], @@ -881,7 +881,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 65 + "lineNumber": 69 } }, { @@ -917,7 +917,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 75 + "lineNumber": 79 } } ], @@ -925,7 +925,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 75 + "lineNumber": 79 } }, { @@ -961,7 +961,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 89 + "lineNumber": 93 } } ], @@ -969,7 +969,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 89 + "lineNumber": 93 } }, { @@ -985,7 +985,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 102 + "lineNumber": 106 } }, { @@ -1001,7 +1001,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 113 + "lineNumber": 117 } }, { @@ -1017,7 +1017,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 120 + "lineNumber": 124 } }, { @@ -1053,7 +1053,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 127 + "lineNumber": 131 } } ], @@ -1061,7 +1061,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 127 + "lineNumber": 131 } }, { @@ -1079,7 +1079,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 135 + "lineNumber": 139 } }, { @@ -1102,7 +1102,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 155 + "lineNumber": 159 } }, { @@ -1126,7 +1126,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 165 + "lineNumber": 169 } }, { @@ -1149,7 +1149,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 173 + "lineNumber": 177 } }, { @@ -1210,7 +1210,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 177 + "lineNumber": 181 } } ], @@ -1218,7 +1218,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 177 + "lineNumber": 181 } }, { @@ -1242,7 +1242,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 181 + "lineNumber": 185 } }, { @@ -1266,7 +1266,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 185 + "lineNumber": 189 } }, { @@ -1290,7 +1290,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 195 + "lineNumber": 199 } }, { @@ -1313,7 +1313,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 204 + "lineNumber": 208 } }, { @@ -1336,7 +1336,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 211 + "lineNumber": 216 } }, { @@ -1382,7 +1382,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 220 + "lineNumber": 225 } } ], @@ -1390,7 +1390,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 220 + "lineNumber": 225 } }, { @@ -1435,7 +1435,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 226 + "lineNumber": 231 } }, { @@ -1454,7 +1454,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 226 + "lineNumber": 231 } } ], @@ -1462,7 +1462,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 226 + "lineNumber": 231 } }, { @@ -1500,7 +1500,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 240 + "lineNumber": 245 } }, { @@ -1513,7 +1513,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 240 + "lineNumber": 245 } }, { @@ -1532,7 +1532,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 240 + "lineNumber": 245 } } ], @@ -1540,7 +1540,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 240 + "lineNumber": 245 } }, { @@ -1593,7 +1593,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 260 + "lineNumber": 265 } }, { @@ -1612,7 +1612,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 260 + "lineNumber": 265 } } ], @@ -1620,7 +1620,55 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 260 + "lineNumber": 265 + } + }, + { + "id": "def-server.ReportingCore.getDataService", + "type": "Function", + "label": "getDataService", + "signature": [ + "() => Promise<", + { + "pluginId": "data", + "scope": "server", + "docId": "kibDataPluginApi", + "section": "def-server.DataPluginStart", + "text": "DataPluginStart" + }, + ">" + ], + "description": [], + "children": [], + "tags": [], + "returnComment": [], + "source": { + "path": "x-pack/plugins/reporting/server/core.ts", + "lineNumber": 275 + } + }, + { + "id": "def-server.ReportingCore.getEsClient", + "type": "Function", + "label": "getEsClient", + "signature": [ + "() => Promise<", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.IClusterClient", + "text": "IClusterClient" + }, + ">" + ], + "description": [], + "children": [], + "tags": [], + "returnComment": [], + "source": { + "path": "x-pack/plugins/reporting/server/core.ts", + "lineNumber": 280 } }, { @@ -1642,7 +1690,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 270 + "lineNumber": 285 } } ], @@ -1650,7 +1698,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 270 + "lineNumber": 285 } }, { @@ -1672,7 +1720,7 @@ "description": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 274 + "lineNumber": 289 } } ], @@ -1680,7 +1728,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 274 + "lineNumber": 289 } }, { @@ -1696,13 +1744,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 278 + "lineNumber": 293 } } ], "source": { "path": "x-pack/plugins/reporting/server/core.ts", - "lineNumber": 54 + "lineNumber": 58 }, "initialIsOpen": false }, diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts index a1215836f9c5f..65fef2e4d030f 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts @@ -97,8 +97,7 @@ export const getTopNavLinks = ({ const sharingData = await getSharingData( searchSource, state.appStateContainer.getState(), - services.uiSettings, - getFieldCounts + services.uiSettings ); services.share.toggleShareContextMenu({ anchorElement, diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts index 5e0e48e619a27..ebb1946b524cd 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.test.ts @@ -11,59 +11,130 @@ import { getSharingData, showPublicUrlSwitch } from './get_sharing_data'; import { IUiSettingsClient } from 'kibana/public'; import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks'; import { indexPatternMock } from '../../__mocks__/index_pattern'; -import { SORT_DEFAULT_ORDER_SETTING } from '../../../common'; +import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; +import { IndexPattern } from 'src/plugins/data/public'; describe('getSharingData', () => { + let mockConfig: IUiSettingsClient; + + beforeEach(() => { + mockConfig = ({ + get: (key: string) => { + if (key === SORT_DEFAULT_ORDER_SETTING) { + return 'desc'; + } + if (key === DOC_HIDE_TIME_COLUMN_SETTING) { + return false; + } + return false; + }, + } as unknown) as IUiSettingsClient; + }); + test('returns valid data for sharing', async () => { const searchSourceMock = createSearchSourceMock({ index: indexPatternMock }); + const result = await getSharingData(searchSourceMock, { columns: [] }, mockConfig); + expect(result).toMatchInlineSnapshot(` + Object { + "searchSource": Object { + "index": "the-index-pattern-id", + "sort": Array [ + Object { + "_score": "desc", + }, + ], + }, + } + `); + }); + + test('fields have prepended timeField', async () => { + const index = { ...indexPatternMock } as IndexPattern; + index.timeFieldName = 'cool-timefield'; + + const searchSourceMock = createSearchSourceMock({ index }); const result = await getSharingData( searchSourceMock, - { columns: [] }, - ({ - get: (key: string) => { - if (key === SORT_DEFAULT_ORDER_SETTING) { - return 'desc'; - } - return false; - }, - } as unknown) as IUiSettingsClient, - () => Promise.resolve({}) + { + columns: [ + 'cool-field-1', + 'cool-field-2', + 'cool-field-3', + 'cool-field-4', + 'cool-field-5', + 'cool-field-6', + ], + }, + mockConfig ); expect(result).toMatchInlineSnapshot(` Object { - "conflictedTypesFields": Array [], - "fields": Array [], - "indexPatternId": "the-index-pattern-id", - "metaFields": Array [ - "_index", - "_score", + "searchSource": Object { + "fields": Array [ + "cool-timefield", + "cool-field-1", + "cool-field-2", + "cool-field-3", + "cool-field-4", + "cool-field-5", + "cool-field-6", + ], + "index": "the-index-pattern-id", + "sort": Array [ + Object { + "_doc": "desc", + }, + ], + }, + } + `); + }); + + test('fields conditionally do not have prepended timeField', async () => { + mockConfig = ({ + get: (key: string) => { + if (key === DOC_HIDE_TIME_COLUMN_SETTING) { + return true; + } + return false; + }, + } as unknown) as IUiSettingsClient; + + const index = { ...indexPatternMock } as IndexPattern; + index.timeFieldName = 'cool-timefield'; + + const searchSourceMock = createSearchSourceMock({ index }); + const result = await getSharingData( + searchSourceMock, + { + columns: [ + 'cool-field-1', + 'cool-field-2', + 'cool-field-3', + 'cool-field-4', + 'cool-field-5', + 'cool-field-6', ], - "searchRequest": Object { - "body": Object { - "_source": Object {}, - "fields": Array [], - "query": Object { - "bool": Object { - "filter": Array [], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, + }, + mockConfig + ); + expect(result).toMatchInlineSnapshot(` + Object { + "searchSource": Object { + "fields": Array [ + "cool-field-1", + "cool-field-2", + "cool-field-3", + "cool-field-4", + "cool-field-5", + "cool-field-6", + ], + "index": "the-index-pattern-id", + "sort": Array [ + Object { + "_doc": false, }, - "runtime_mappings": Object {}, - "script_fields": Object {}, - "sort": Array [ - Object { - "_score": Object { - "order": "desc", - }, - }, - ], - "stored_fields": Array [ - "*", - ], - }, - "index": "the-index-pattern-title", + ], }, } `); diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts index 2455589cf69fc..f0e07ccc38deb 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts @@ -6,57 +6,28 @@ * Side Public License, v 1. */ -import { Capabilities, IUiSettingsClient } from 'kibana/public'; +import type { Capabilities, IUiSettingsClient } from 'kibana/public'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; import { getSortForSearchSource } from '../angular/doc_table'; import { ISearchSource } from '../../../../data/common'; import { AppState } from '../angular/discover_state'; -import { SortOrder } from '../../saved_searches/types'; - -const getSharingDataFields = async ( - getFieldCounts: () => Promise>, - selectedFields: string[], - timeFieldName: string, - hideTimeColumn: boolean -) => { - if ( - selectedFields.length === 0 || - (selectedFields.length === 1 && selectedFields[0] === '_source') - ) { - const fieldCounts = await getFieldCounts(); - return { - searchFields: undefined, - selectFields: Object.keys(fieldCounts).sort(), - }; - } - - const fields = - timeFieldName && !hideTimeColumn ? [timeFieldName, ...selectedFields] : selectedFields; - return { - searchFields: fields, - selectFields: fields, - }; -}; +import type { SavedSearch, SortOrder } from '../../saved_searches/types'; /** * Preparing data to share the current state as link or CSV/Report */ export async function getSharingData( currentSearchSource: ISearchSource, - state: AppState, - config: IUiSettingsClient, - getFieldCounts: () => Promise> + state: AppState | SavedSearch, + config: IUiSettingsClient ) { const searchSource = currentSearchSource.createCopy(); const index = searchSource.getField('index')!; + const fields = { + fields: searchSource.getField('fields'), + fieldsFromSource: searchSource.getField('fieldsFromSource'), + }; - const { searchFields, selectFields } = await getSharingDataFields( - getFieldCounts, - state.columns || [], - index.timeFieldName || '', - config.get(DOC_HIDE_TIME_COLUMN_SETTING) - ); - searchSource.setField('fieldsFromSource', searchFields); searchSource.setField( 'sort', getSortForSearchSource(state.sort as SortOrder[], index, config.get(SORT_DEFAULT_ORDER_SETTING)) @@ -66,17 +37,27 @@ export async function getSharingData( searchSource.removeField('aggs'); searchSource.removeField('size'); - const body = await searchSource.getSearchRequestBody(); + // fields get re-set to match the saved search columns + let columns = state.columns || []; + + if (columns && columns.length > 0) { + // conditionally add the time field column: + let timeFieldName: string | undefined; + const hideTimeColumn = config.get(DOC_HIDE_TIME_COLUMN_SETTING); + if (!hideTimeColumn && index && index.timeFieldName) { + timeFieldName = index.timeFieldName; + } + if (timeFieldName && !columns.includes(timeFieldName)) { + columns = [timeFieldName, ...columns]; + } + + // if columns were selected in the saved search, use them for the searchSource's fields + const fieldsKey = fields.fieldsFromSource ? 'fieldsFromSource' : 'fields'; + searchSource.setField(fieldsKey, columns); + } return { - searchRequest: { - index: index.title, - body, - }, - fields: selectFields, - metaFields: index.metaFields, - conflictedTypesFields: index.fields.filter((f) => f.type === 'conflict').map((f) => f.name), - indexPatternId: index.id, + searchSource: searchSource.getSerializedFields(true), }; } diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index e3f58dd20cecb..2a95557473fc0 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -50,6 +50,7 @@ export const KBN_SCREENSHOT_HEADER_BLOCK_LIST_STARTS_WITH_PATTERN = ['proxy-']; export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo'; export const UI_SETTINGS_CSV_SEPARATOR = 'csv:separator'; export const UI_SETTINGS_CSV_QUOTE_VALUES = 'csv:quoteValues'; +export const UI_SETTINGS_DATEFORMAT_TZ = 'dateFormat:tz'; export const LAYOUT_TYPES = { PRESERVE_LAYOUT: 'preserve_layout', @@ -57,13 +58,16 @@ export const LAYOUT_TYPES = { }; // Export Type Definitions +export const CSV_REPORT_TYPE = 'CSV'; +export const CSV_JOB_TYPE = 'csv_searchsource'; + export const PDF_REPORT_TYPE = 'printablePdf'; export const PDF_JOB_TYPE = 'printable_pdf'; export const PNG_REPORT_TYPE = 'PNG'; export const PNG_JOB_TYPE = 'PNG'; -export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; +export const CSV_SEARCHSOURCE_IMMEDIATE_TYPE = 'csv_searchsource_immediate'; // This is deprecated because it lacks support for runtime fields // but the extension points are still needed for pre-existing scripted automation, until 8.0 @@ -86,9 +90,9 @@ export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; export const API_LIST_URL = `${API_BASE_URL}/jobs`; export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; -// hacky endpoint +// hacky endpoint: download CSV without queueing a report export const API_BASE_URL_V1 = '/api/reporting/v1'; // -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv_searchsource`; // Management UI route export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 3af329cbf0303..5e20381e35898 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -47,9 +47,10 @@ export interface ReportDocumentHead { export interface TaskRunResult { content_type: string | null; content: string | null; - csv_contains_formulas?: boolean; size: number; + csv_contains_formulas?: boolean; max_size_reached?: boolean; + needs_sorting?: boolean; warnings?: string[]; } diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 6f6cf2dc9351b..399b503fe48d3 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -11,11 +11,7 @@ import React, { Component, ReactElement } from 'react'; import { ToastsSetup } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { - CSV_REPORT_TYPE_DEPRECATED, - PDF_REPORT_TYPE, - PNG_REPORT_TYPE, -} from '../../common/constants'; +import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -177,8 +173,8 @@ class ReportingPanelContentUi extends Component { switch (this.props.reportType) { case PDF_REPORT_TYPE: return 'PDF'; - case 'csv': - return CSV_REPORT_TYPE_DEPRECATED; + case 'csv_searchsource': + return CSV_REPORT_TYPE; case 'png': return PNG_REPORT_TYPE; default: diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index f452719e91713..4e1b9ccd2642f 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -52,7 +52,20 @@ describe('GetCsvReportPanelAction', () => { context = { embeddable: { type: 'search', - getSavedSearch: () => ({ id: 'lebowski' }), + getSavedSearch: () => { + const searchSource = { + createCopy: () => searchSource, + removeField: jest.fn(), + setField: jest.fn(), + getField: jest.fn().mockImplementation((key: string) => { + if (key === 'index') { + return 'my-test-index-*'; + } + }), + getSerializedFields: jest.fn().mockImplementation(() => ({})), + }; + return { searchSource }; + }, getTitle: () => `The Dude`, getInspectorAdapters: () => null, getInput: () => ({ diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index cc1da146eff32..d440edc3f3fe9 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; import moment from 'moment-timezone'; import { CoreSetup } from 'src/core/public'; import { + loadSharingDataHelpers, ISearchEmbeddable, + SavedSearch, SEARCH_EMBEDDABLE_TYPE, } from '../../../../../src/plugins/discover/public'; import { IEmbeddable, ViewMode } from '../../../../../src/plugins/embeddable/public'; @@ -21,6 +21,7 @@ import { } from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; +import { JobParamsDownloadCSV } from '../../server/export_types/csv_searchsource_immediate/types'; import { checkLicense } from '../lib/license_check'; function isSavedSearchEmbeddable( @@ -61,17 +62,16 @@ export class GetCsvReportPanelAction implements ActionDefinition }); } - public getSearchRequestBody({ searchEmbeddable }: { searchEmbeddable: any }) { - const adapters = searchEmbeddable.getInspectorAdapters(); - if (!adapters) { - return {}; - } - - if (adapters.requests.requests.length === 0) { - return {}; - } + public async getSearchSource(savedSearch: SavedSearch, embeddable: ISearchEmbeddable) { + const { getSharingData } = await loadSharingDataHelpers(); + const searchSource = savedSearch.searchSource.createCopy(); + const { searchSource: serializedSearchSource } = await getSharingData( + searchSource, + savedSearch, // TODO: get unsaved state (using embeddale.searchScope): https://github.com/elastic/kibana/issues/43977 + this.core.uiSettings + ); - return searchEmbeddable.getSavedSearch().searchSource.getSearchRequestBody(); + return serializedSearchSource; } public isCompatible = async (context: ActionContext) => { @@ -95,34 +95,18 @@ export class GetCsvReportPanelAction implements ActionDefinition return; } - const { - timeRange: { to, from }, - } = embeddable.getInput(); + const savedSearch = embeddable.getSavedSearch(); + const searchSource = await this.getSearchSource(savedSearch, embeddable); - const searchEmbeddable = embeddable; - const searchRequestBody = await this.getSearchRequestBody({ searchEmbeddable }); - const state = _.pick(searchRequestBody, ['sort', 'docvalue_fields', 'query']); const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz'); + const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; + const immediateJobParams: JobParamsDownloadCSV = { + searchSource, + browserTimezone, + title: savedSearch.title, + }; - const id = `search:${embeddable.getSavedSearch().id}`; - const timezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; - const fromTime = dateMath.parse(from); - const toTime = dateMath.parse(to, { roundUp: true }); - - if (!fromTime || !toTime) { - return this.onGenerationFail( - new Error(`Invalid time range: From: ${fromTime}, To: ${toTime}`) - ); - } - - const body = JSON.stringify({ - timerange: { - min: fromTime.format(), - max: toTime.format(), - timezone, - }, - state, - }); + const body = JSON.stringify(immediateJobParams); this.isDownloading = true; @@ -137,11 +121,11 @@ export class GetCsvReportPanelAction implements ActionDefinition }); await this.core.http - .post(`${API_GENERATE_IMMEDIATE}/${id}`, { body }) + .post(`${API_GENERATE_IMMEDIATE}`, { body }) .then((rawResponse: string) => { this.isDownloading = false; - const download = `${embeddable.getSavedSearch().title}.csv`; + const download = `${savedSearch.title}.csv`; const blob = new Blob([rawResponse], { type: 'text/csv;charset=utf-8;' }); // Hack for IE11 Support diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 31c86ae4c5669..97433f7a4f0c1 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -11,10 +11,8 @@ import React from 'react'; import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../../licensing/public'; -import { - JobParamsDeprecatedCSV, - SearchRequestDeprecatedCSV, -} from '../../server/export_types/csv/types'; +import { CSV_JOB_TYPE } from '../../common/constants'; +import { JobParamsCSV } from '../../server/export_types/csv_searchsource/types'; import { ReportingPanelContent } from '../components/reporting_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -56,22 +54,18 @@ export const csvReportingProvider = ({ objectType, objectId, sharingData, - isDirty, onClose, + isDirty, }: ShareContext) => { if ('search' !== objectType) { return []; } - const jobParams: JobParamsDeprecatedCSV = { + const jobParams: JobParamsCSV = { browserTimezone, - objectType, title: sharingData.title as string, - indexPatternId: sharingData.indexPatternId as string, - searchRequest: sharingData.searchRequest as SearchRequestDeprecatedCSV, - fields: sharingData.fields as string[], - metaFields: sharingData.metaFields as string[], - conflictedTypesFields: sharingData.conflictedTypesFields as string[], + objectType, + searchSource: sharingData.searchSource, }; const getJobParams = () => jobParams; @@ -99,7 +93,7 @@ export const csvReportingProvider = ({ ; @@ -48,6 +50,8 @@ export interface ReportingInternalStart { store: ReportingStore; savedObjects: SavedObjectsServiceStart; uiSettings: UiSettingsServiceStart; + esClient: IClusterClient; + data: DataPluginStart; taskManager: TaskManagerStartContract; } @@ -208,6 +212,7 @@ export class ReportingCore { return this.pluginSetupDeps; } + // NOTE: Uses the Legacy API public getElasticsearchService() { return this.getPluginSetupDeps().elasticsearch; } @@ -267,6 +272,16 @@ export class ReportingCore { return await this.getUiSettingsServiceFactory(savedObjectsClient); } + public async getDataService() { + const startDeps = await this.getPluginStartDeps(); + return startDeps.data; + } + + public async getEsClient() { + const startDeps = await this.getPluginStartDeps(); + return startDeps.esClient; + } + public trackReport(reportId: string) { this.executing.add(reportId); } diff --git a/x-pack/plugins/reporting/server/export_types/common/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts index 1003ecf83601c..8832577281bb2 100644 --- a/x-pack/plugins/reporting/server/export_types/common/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -12,7 +12,6 @@ export { omitBlockedHeaders } from './omit_blocked_headers'; export { validateUrls } from './validate_urls'; export interface TimeRangeParams { - timezone: string; min?: Date | string | number | null; max?: Date | string | number | null; } diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts index 942739f0d9945..f650bbaed1271 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts @@ -6,7 +6,7 @@ */ import { pick, keys, values, some } from 'lodash'; -import { cellHasFormulas } from './cell_has_formula'; +import { cellHasFormulas } from '../../csv_searchsource/generate_csv/cell_has_formula'; interface IFlattened { [header: string]: string; diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index ed05180501e32..629a81df350be 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -13,15 +13,18 @@ import { CSV_BOM_CHARS } from '../../../../common/constants'; import { byteSizeValueToNumber } from '../../../../common/schema_utils'; import { LevelLogger } from '../../../lib'; import { getFieldFormats } from '../../../services'; -import { IndexPatternSavedObjectDeprecatedCSV, SavedSearchGeneratorResult } from '../types'; +import { createEscapeValue } from '../../csv_searchsource/generate_csv/escape_value'; +import { MaxSizeStringBuilder } from '../../csv_searchsource/generate_csv/max_size_string_builder'; +import { + IndexPatternSavedObjectDeprecatedCSV, + SavedSearchGeneratorResultDeprecatedCSV, +} from '../types'; import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; -import { createEscapeValue } from './escape_value'; import { fieldFormatMapFactory } from './field_format_map'; import { createFlattenHit } from './flatten_hit'; import { createFormatCsvValues } from './format_csv_values'; import { getUiSettings } from './get_ui_settings'; import { createHitIterator, EndpointCaller } from './hit_iterator'; -import { MaxSizeStringBuilder } from './max_size_string_builder'; interface SearchRequest { index: string; @@ -55,7 +58,7 @@ export function createGenerateCsv(logger: LevelLogger) { uiSettingsClient: IUiSettingsClient, callEndpoint: EndpointCaller, cancellationToken: CancellationToken - ): Promise { + ): Promise { const settings = await getUiSettings(job.browserTimezone, uiSettingsClient, config, logger); const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts index 4c4f33d0ee9f7..604d451d822b6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -77,15 +77,10 @@ type FormatsMapDeprecatedCSV = Map< } >; -export interface SavedSearchGeneratorResult { +export interface SavedSearchGeneratorResultDeprecatedCSV { content: string; size: number; maxSizeReached: boolean; csvContainsFormulas?: boolean; warnings: string[]; } - -export interface CsvResultFromSearch { - type: string; - result: SavedSearchGeneratorResult; -} diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts deleted file mode 100644 index 75b07e5bca8c8..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.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 { notFound, notImplemented } from '@hapi/boom'; -import { get } from 'lodash'; -import { CsvFromSavedObjectRequest } from '../../routes/generate_from_savedobject_immediate'; -import type { ReportingRequestHandlerContext } from '../../types'; -import { CreateJobFnFactory } from '../../types'; -import { - JobParamsPanelCsv, - JobPayloadPanelCsv, - SavedObject, - SavedObjectReference, - SavedObjectServiceError, - VisObjectAttributesJSON, -} from './types'; - -export type ImmediateCreateJobFn = ( - jobParams: JobParamsPanelCsv, - context: ReportingRequestHandlerContext, - req: CsvFromSavedObjectRequest -) => Promise; - -export const createJobFnFactory: CreateJobFnFactory = function createJobFactoryFn( - reporting, - parentLogger -) { - const logger = parentLogger.clone(['create-job']); - - return async function createJob(jobParams, context, req) { - const { savedObjectType, savedObjectId } = jobParams; - - const panel = await Promise.resolve() - .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) - .then(async (savedObject: SavedObject) => { - const { attributes, references } = savedObject; - const { kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON } = attributes; - const { timerange } = req.body; - - if (!kibanaSavedObjectMetaJSON) { - throw new Error('Could not parse saved object data!'); - } - - const kibanaSavedObjectMeta = { - ...kibanaSavedObjectMetaJSON, - searchSource: JSON.parse(kibanaSavedObjectMetaJSON.searchSourceJSON), - }; - - const { visState: visStateJSON } = attributes as VisObjectAttributesJSON; - if (visStateJSON) { - throw notImplemented('Visualization types are not yet implemented'); - } - - // saved search type - const { searchSource } = kibanaSavedObjectMeta; - if (!searchSource || !references) { - throw new Error('The saved search object is missing configuration fields!'); - } - - const indexPatternMeta = references.find( - (ref: SavedObjectReference) => ref.type === 'index-pattern' - ); - if (!indexPatternMeta) { - throw new Error('Could not find index pattern for the saved search!'); - } - - return { - attributes: { - ...attributes, - kibanaSavedObjectMeta: { searchSource }, - }, - indexPatternSavedObjectId: indexPatternMeta.id, - timerange, - }; - }) - .catch((err: Error) => { - const boomErr = (err as unknown) as { isBoom: boolean }; - if (boomErr.isBoom) { - throw err; - } - const errPayload: SavedObjectServiceError = get(err, 'output.payload', { statusCode: 0 }); - if (errPayload.statusCode === 404) { - throw notFound(errPayload.message); - } - logger.error(err); - throw new Error(`Unable to create a job from saved object data! Error: ${err}`); - }); - - return { ...jobParams, panel }; - }; -}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts deleted file mode 100644 index b79bb063c26f8..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaRequest } from 'src/core/server'; -import { CancellationToken } from '../../../common'; -import { CONTENT_TYPE_CSV } from '../../../common/constants'; -import { TaskRunResult } from '../../lib/tasks'; -import type { ReportingRequestHandlerContext } from '../../types'; -import { RunTaskFnFactory } from '../../types'; -import { createGenerateCsv } from '../csv/generate_csv'; -import { getGenerateCsvParams } from './lib/get_csv_job'; -import { JobPayloadPanelCsv } from './types'; - -/* - * ImmediateExecuteFn receives the job doc payload because the payload was - * generated in the ScheduleFn - */ -export type ImmediateExecuteFn = ( - jobId: null, - job: JobPayloadPanelCsv, - context: ReportingRequestHandlerContext, - req: KibanaRequest -) => Promise; - -export const runTaskFnFactory: RunTaskFnFactory = function executeJobFactoryFn( - reporting, - parentLogger -) { - const config = reporting.getConfig(); - const logger = parentLogger.clone(['execute-job']); - - return async function runTask(jobId, jobPayload, context, req) { - const generateCsv = createGenerateCsv(logger); - const { panel } = jobPayload; - - logger.debug(`Execute job generating saved search CSV`); - - const savedObjectsClient = context.core.savedObjects.client; - const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - const job = await getGenerateCsvParams( - jobPayload, - panel, - savedObjectsClient, - uiSettingsClient, - logger - ); - - const elasticsearch = reporting.getElasticsearchService(); - const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req); - - const { content, maxSizeReached, size, csvContainsFormulas, warnings } = await generateCsv( - job, - config, - uiSettingsClient, - callAsCurrentUser, - new CancellationToken() // can not be cancelled - ); - - if (csvContainsFormulas) { - logger.warn(`CSV may contain formulas whose values have been escaped`); - } - - if (maxSizeReached) { - logger.warn(`Max size reached: CSV output truncated to ${size} bytes`); - } - - return { - content_type: CONTENT_TYPE_CSV, - content, - max_size_reached: maxSizeReached, - size, - csv_contains_formulas: csvContainsFormulas, - warnings, - }; - }; -}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts deleted file mode 100644 index fc6e092962d3b..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts +++ /dev/null @@ -1,340 +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 { createMockLevelLogger } from '../../../test_helpers'; -import { JobParamsPanelCsv, SearchPanel } from '../types'; -import { getGenerateCsvParams } from './get_csv_job'; - -const logger = createMockLevelLogger(); - -describe('Get CSV Job', () => { - let mockJobParams: JobParamsPanelCsv; - let mockSearchPanel: SearchPanel; - let mockSavedObjectsClient: any; - let mockUiSettingsClient: any; - beforeEach(() => { - mockJobParams = { savedObjectType: 'search', savedObjectId: '234-ididid' }; - mockSearchPanel = { - indexPatternSavedObjectId: '123-indexId', - attributes: { - title: 'my search', - sort: [], - kibanaSavedObjectMeta: { - searchSource: { query: { isSearchSourceQuery: true }, filter: [] }, - }, - uiState: 56, - }, - timerange: { timezone: 'PST', min: 0, max: 100 }, - }; - mockSavedObjectsClient = { - get: () => ({ - attributes: { fields: null, title: null, timeFieldName: null }, - }), - }; - mockUiSettingsClient = { - get: () => ({}), - }; - }); - - it('creates a data structure needed by generateCsv', async () => { - const result = await getGenerateCsvParams( - mockJobParams, - mockSearchPanel, - mockSavedObjectsClient, - mockUiSettingsClient, - logger - ); - expect(result).toMatchInlineSnapshot(` - Object { - "browserTimezone": "PST", - "conflictedTypesFields": Array [], - "fields": Array [], - "indexPatternSavedObject": Object { - "attributes": Object { - "fields": null, - "timeFieldName": null, - "title": null, - }, - "fields": Array [], - "timeFieldName": null, - "title": null, - }, - "metaFields": Array [], - "searchRequest": Object { - "body": Object { - "_source": Object { - "includes": Array [], - }, - "docvalue_fields": undefined, - "query": Object { - "bool": Object { - "filter": Array [], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - "script_fields": Object {}, - "sort": Array [], - }, - "index": null, - }, - } - `); - }); - - it('uses query and sort from the payload', async () => { - mockJobParams.post = { - state: { - query: ['this is the query'], - sort: ['this is the sort'], - }, - }; - const result = await getGenerateCsvParams( - mockJobParams, - mockSearchPanel, - mockSavedObjectsClient, - mockUiSettingsClient, - logger - ); - expect(result).toMatchInlineSnapshot(` - Object { - "browserTimezone": "PST", - "conflictedTypesFields": Array [], - "fields": Array [], - "indexPatternSavedObject": Object { - "attributes": Object { - "fields": null, - "timeFieldName": null, - "title": null, - }, - "fields": Array [], - "timeFieldName": null, - "title": null, - }, - "metaFields": Array [], - "searchRequest": Object { - "body": Object { - "_source": Object { - "includes": Array [], - }, - "docvalue_fields": undefined, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "0": "this is the query", - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - "script_fields": Object {}, - "sort": Array [ - "this is the sort", - ], - }, - "index": null, - }, - } - `); - }); - - it('uses timerange timezone from the payload', async () => { - mockJobParams.post = { - timerange: { timezone: 'Africa/Timbuktu', min: 0, max: 9000 }, - }; - const result = await getGenerateCsvParams( - mockJobParams, - mockSearchPanel, - mockSavedObjectsClient, - mockUiSettingsClient, - logger - ); - expect(result).toMatchInlineSnapshot(` - Object { - "browserTimezone": "Africa/Timbuktu", - "conflictedTypesFields": Array [], - "fields": Array [], - "indexPatternSavedObject": Object { - "attributes": Object { - "fields": null, - "timeFieldName": null, - "title": null, - }, - "fields": Array [], - "timeFieldName": null, - "title": null, - }, - "metaFields": Array [], - "searchRequest": Object { - "body": Object { - "_source": Object { - "includes": Array [], - }, - "docvalue_fields": undefined, - "query": Object { - "bool": Object { - "filter": Array [], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - "script_fields": Object {}, - "sort": Array [], - }, - "index": null, - }, - } - `); - }); - - it('uses timerange min and max (numeric) when index pattern has timefieldName', async () => { - mockJobParams.post = { - timerange: { timezone: 'Africa/Timbuktu', min: 0, max: 900000000 }, - }; - mockSavedObjectsClient = { - get: () => ({ - attributes: { fields: null, title: 'test search', timeFieldName: '@test_time' }, - }), - }; - const result = await getGenerateCsvParams( - mockJobParams, - mockSearchPanel, - mockSavedObjectsClient, - mockUiSettingsClient, - logger - ); - expect(result).toMatchInlineSnapshot(` - Object { - "browserTimezone": "Africa/Timbuktu", - "conflictedTypesFields": Array [], - "fields": Array [ - "@test_time", - ], - "indexPatternSavedObject": Object { - "attributes": Object { - "fields": null, - "timeFieldName": "@test_time", - "title": "test search", - }, - "fields": Array [], - "timeFieldName": "@test_time", - "title": "test search", - }, - "metaFields": Array [], - "searchRequest": Object { - "body": Object { - "_source": Object { - "includes": Array [ - "@test_time", - ], - }, - "docvalue_fields": undefined, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@test_time": Object { - "format": "strict_date_time", - "gte": "1970-01-01T00:00:00Z", - "lte": "1970-01-11T10:00:00Z", - }, - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - "script_fields": Object {}, - "sort": Array [], - }, - "index": "test search", - }, - } - `); - }); - - it('uses timerange min and max (string) when index pattern has timefieldName', async () => { - mockJobParams.post = { - timerange: { - timezone: 'Africa/Timbuktu', - min: '1980-01-01T00:00:00Z', - max: '1990-01-01T00:00:00Z', - }, - }; - mockSavedObjectsClient = { - get: () => ({ - attributes: { fields: null, title: 'test search', timeFieldName: '@test_time' }, - }), - }; - const result = await getGenerateCsvParams( - mockJobParams, - mockSearchPanel, - mockSavedObjectsClient, - mockUiSettingsClient, - logger - ); - expect(result).toMatchInlineSnapshot(` - Object { - "browserTimezone": "Africa/Timbuktu", - "conflictedTypesFields": Array [], - "fields": Array [ - "@test_time", - ], - "indexPatternSavedObject": Object { - "attributes": Object { - "fields": null, - "timeFieldName": "@test_time", - "title": "test search", - }, - "fields": Array [], - "timeFieldName": "@test_time", - "title": "test search", - }, - "metaFields": Array [], - "searchRequest": Object { - "body": Object { - "_source": Object { - "includes": Array [ - "@test_time", - ], - }, - "docvalue_fields": undefined, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "range": Object { - "@test_time": Object { - "format": "strict_date_time", - "gte": "1980-01-01T00:00:00Z", - "lte": "1990-01-01T00:00:00Z", - }, - }, - }, - ], - "must": Array [], - "must_not": Array [], - "should": Array [], - }, - }, - "script_fields": Object {}, - "sort": Array [], - }, - "index": "test search", - }, - } - `); - }); -}); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts deleted file mode 100644 index e4570816e26ff..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts +++ /dev/null @@ -1,155 +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 { IUiSettingsClient, SavedObjectsClientContract } from 'kibana/server'; -import { EsQueryConfig } from 'src/plugins/data/server'; -import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/server'; -import { LevelLogger } from '../../../lib'; -import { TimeRangeParams } from '../../common'; -import { GenerateCsvParams } from '../../csv/generate_csv'; -import { - DocValueFields, - IndexPatternField, - JobParamsPanelCsv, - QueryFilter, - SavedSearchObjectAttributes, - SearchPanel, - SearchSource, -} from '../types'; -import { getDataSource } from './get_data_source'; -import { getFilters } from './get_filters'; - -export const getEsQueryConfig = async (config: IUiSettingsClient) => { - const configs = await Promise.all([ - config.get('query:allowLeadingWildcards'), - config.get('query:queryString:options'), - config.get('courier:ignoreFilterIfFieldNotInIndex'), - ]); - const [allowLeadingWildcards, queryStringOptions, ignoreFilterIfFieldNotInIndex] = configs; - return { - allowLeadingWildcards, - queryStringOptions, - ignoreFilterIfFieldNotInIndex, - } as EsQueryConfig; -}; - -/* - * Create a CSV Job object for CSV From SavedObject to use as a job parameter - * for generateCsv - */ -export const getGenerateCsvParams = async ( - jobParams: JobParamsPanelCsv, - panel: SearchPanel, - savedObjectsClient: SavedObjectsClientContract, - uiConfig: IUiSettingsClient, - logger: LevelLogger -): Promise => { - let timerange: TimeRangeParams | null; - if (jobParams.post?.timerange) { - timerange = jobParams.post?.timerange; - } else { - timerange = panel.timerange || null; - } - const { indexPatternSavedObjectId } = panel; - const savedSearchObjectAttr = panel.attributes as SavedSearchObjectAttributes; - const { indexPatternSavedObject } = await getDataSource( - savedObjectsClient, - indexPatternSavedObjectId - ); - const esQueryConfig = await getEsQueryConfig(uiConfig); - - const { - kibanaSavedObjectMeta: { - searchSource: { - filter: [searchSourceFilter], - query: searchSourceQuery, - }, - }, - } = savedSearchObjectAttr as { kibanaSavedObjectMeta: { searchSource: SearchSource } }; - - const { - timeFieldName: indexPatternTimeField, - title: esIndex, - fields: indexPatternFields, - } = indexPatternSavedObject; - - if (!indexPatternFields || indexPatternFields.length === 0) { - logger.error( - new Error( - `No fields are selected in the saved search! Please select fields as columns in the saved search and try again.` - ) - ); - } - - let payloadQuery: QueryFilter | undefined; - let payloadSort: any[] = []; - let docValueFields: DocValueFields[] | undefined; - if (jobParams.post && jobParams.post.state) { - ({ - post: { - state: { query: payloadQuery, sort: payloadSort = [], docvalue_fields: docValueFields }, - }, - } = jobParams); - } - const { includes, combinedFilter } = getFilters( - indexPatternSavedObjectId, - indexPatternTimeField, - timerange, - savedSearchObjectAttr, - searchSourceFilter, - payloadQuery - ); - - const savedSortConfigs = savedSearchObjectAttr.sort; - const sortConfig = [...payloadSort]; - savedSortConfigs.forEach(([savedSortField, savedSortOrder]) => { - sortConfig.push({ [savedSortField]: { order: savedSortOrder } }); - }); - - const scriptFieldsConfig = - indexPatternFields && - indexPatternFields - .filter((f: IndexPatternField) => f.scripted) - .reduce((accum: any, curr: IndexPatternField) => { - return { - ...accum, - [curr.name]: { - script: { - source: curr.script, - lang: curr.lang, - }, - }, - }; - }, {}); - - const searchRequest = { - index: esIndex, - body: { - _source: { includes }, - docvalue_fields: docValueFields, - query: esQuery.buildEsQuery( - // compromise made while factoring out IIndexPattern type - // @ts-expect-error - indexPatternSavedObject, - (searchSourceQuery as unknown) as Query, - (combinedFilter as unknown) as Filter, - esQueryConfig - ), - script_fields: scriptFieldsConfig, - sort: sortConfig, - }, - }; - - return { - browserTimezone: timerange?.timezone, - indexPatternSavedObject, - searchRequest, - fields: includes, - metaFields: [], - conflictedTypesFields: [], - }; -}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts deleted file mode 100644 index d903a1d8ba9e8..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_data_source.ts +++ /dev/null @@ -1,56 +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 { IndexPatternSavedObjectDeprecatedCSV } from '../../csv/types'; -import { SavedObjectReference, SavedSearchObjectAttributesJSON, SearchSource } from '../types'; - -export async function getDataSource( - savedObjectsClient: any, - indexPatternId?: string, - savedSearchObjectId?: string -): Promise<{ - indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV; - searchSource: SearchSource | null; -}> { - let indexPatternSavedObject: IndexPatternSavedObjectDeprecatedCSV; - let searchSource: SearchSource | null = null; - - if (savedSearchObjectId) { - try { - const { attributes, references } = (await savedObjectsClient.get( - 'search', - savedSearchObjectId - )) as { attributes: SavedSearchObjectAttributesJSON; references: SavedObjectReference[] }; - searchSource = JSON.parse(attributes.kibanaSavedObjectMeta.searchSourceJSON); - const { id: indexPatternFromSearchId } = references.find( - ({ type }) => type === 'index-pattern' - ) as { id: string }; - ({ indexPatternSavedObject } = await getDataSource( - savedObjectsClient, - indexPatternFromSearchId - )); - return { searchSource, indexPatternSavedObject }; - } catch (err) { - throw new Error(`Could not get saved search info! ${err}`); - } - } - try { - const { attributes } = await savedObjectsClient.get('index-pattern', indexPatternId); - const { fields, title, timeFieldName } = attributes; - const parsedFields = fields ? JSON.parse(fields) : []; - - indexPatternSavedObject = { - fields: parsedFields, - title, - timeFieldName, - attributes, - }; - } catch (err) { - throw new Error(`Could not get index pattern saved object! ${err}`); - } - return { indexPatternSavedObject, searchSource }; -} diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts deleted file mode 100644 index ca5bf12e1d510..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts +++ /dev/null @@ -1,208 +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 { TimeRangeParams } from '../../common'; -import { QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types'; -import { getFilters } from './get_filters'; - -interface Args { - indexPatternId: string; - indexPatternTimeField: string | null; - timerange: TimeRangeParams | null; - savedSearchObjectAttr: SavedSearchObjectAttributes; - searchSourceFilter: SearchSourceFilter; - queryFilter: QueryFilter; -} - -describe('CSV from Saved Object: get_filters', () => { - let args: Args; - beforeEach(() => { - args = { - indexPatternId: 'logs-test-*', - indexPatternTimeField: 'testtimestamp', - timerange: { - timezone: 'UTC', - min: '1901-01-01T00:00:00.000Z', - max: '1902-01-01T00:00:00.000Z', - }, - savedSearchObjectAttr: { - title: 'test', - sort: [{ sortField: { order: 'asc' } }], - kibanaSavedObjectMeta: { - searchSource: { - query: { isSearchSourceQuery: true }, - filter: ['hello searchSource filter 1'], - }, - }, - columns: ['larry'], - uiState: null, - }, - searchSourceFilter: { isSearchSourceFilter: true, isFilter: true }, - queryFilter: { isQueryFilter: true, isFilter: true }, - }; - }); - - describe('search', () => { - it('for timebased search', () => { - const filters = getFilters( - args.indexPatternId, - args.indexPatternTimeField, - args.timerange, - args.savedSearchObjectAttr, - args.searchSourceFilter, - args.queryFilter - ); - - expect(filters).toEqual({ - combinedFilter: [ - { - range: { - testtimestamp: { - format: 'strict_date_time', - gte: '1901-01-01T00:00:00Z', - lte: '1902-01-01T00:00:00Z', - }, - }, - }, - { isFilter: true, isSearchSourceFilter: true }, - { isFilter: true, isQueryFilter: true }, - ], - includes: ['testtimestamp', 'larry'], - timezone: 'UTC', - }); - }); - - it('for non-timebased search', () => { - args.indexPatternTimeField = null; - args.timerange = null; - - const filters = getFilters( - args.indexPatternId, - args.indexPatternTimeField, - args.timerange, - args.savedSearchObjectAttr, - args.searchSourceFilter, - args.queryFilter - ); - - expect(filters).toEqual({ - combinedFilter: [ - { isFilter: true, isSearchSourceFilter: true }, - { isFilter: true, isQueryFilter: true }, - ], - includes: ['larry'], - timezone: null, - }); - }); - }); - - describe('errors', () => { - it('throw if timebased and timerange is missing', () => { - args.timerange = null; - - const throwFn = () => - getFilters( - args.indexPatternId, - args.indexPatternTimeField, - args.timerange, - args.savedSearchObjectAttr, - args.searchSourceFilter, - args.queryFilter - ); - - expect(throwFn).toThrow( - 'Time range params are required for index pattern [logs-test-*], using time field [testtimestamp]' - ); - }); - }); - - it('composes the defined filters', () => { - expect( - getFilters( - args.indexPatternId, - args.indexPatternTimeField, - args.timerange, - args.savedSearchObjectAttr, - undefined, - undefined - ) - ).toEqual({ - combinedFilter: [ - { - range: { - testtimestamp: { - format: 'strict_date_time', - gte: '1901-01-01T00:00:00Z', - lte: '1902-01-01T00:00:00Z', - }, - }, - }, - ], - includes: ['testtimestamp', 'larry'], - timezone: 'UTC', - }); - - expect( - getFilters( - args.indexPatternId, - args.indexPatternTimeField, - args.timerange, - args.savedSearchObjectAttr, - undefined, - args.queryFilter - ) - ).toEqual({ - combinedFilter: [ - { - range: { - testtimestamp: { - format: 'strict_date_time', - gte: '1901-01-01T00:00:00Z', - lte: '1902-01-01T00:00:00Z', - }, - }, - }, - { isFilter: true, isQueryFilter: true }, - ], - includes: ['testtimestamp', 'larry'], - timezone: 'UTC', - }); - }); - - describe('timefilter', () => { - it('formats the datetime to the provided timezone', () => { - args.timerange = { - timezone: 'MST', - min: '1901-01-01T00:00:00Z', - max: '1902-01-01T00:00:00Z', - }; - - expect( - getFilters( - args.indexPatternId, - args.indexPatternTimeField, - args.timerange, - args.savedSearchObjectAttr - ) - ).toEqual({ - combinedFilter: [ - { - range: { - testtimestamp: { - format: 'strict_date_time', - gte: '1900-12-31T17:00:00-07:00', - lte: '1901-12-31T17:00:00-07:00', - }, - }, - }, - ], - includes: ['testtimestamp', 'larry'], - timezone: 'MST', - }); - }); - }); -}); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts deleted file mode 100644 index c252b66952360..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts +++ /dev/null @@ -1,55 +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 { badRequest } from '@hapi/boom'; -import moment from 'moment-timezone'; -import { TimeRangeParams } from '../../common'; -import { Filter, QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types'; - -export function getFilters( - indexPatternId: string, - indexPatternTimeField: string | null, - timerange: TimeRangeParams | null, - savedSearchObjectAttr: SavedSearchObjectAttributes, - searchSourceFilter?: SearchSourceFilter, - queryFilter?: QueryFilter -) { - let includes: string[]; - let timeFilter: any | null; - let timezone: string | null; - - if (indexPatternTimeField) { - if (!timerange || timerange.min == null || timerange.max == null) { - throw badRequest( - `Time range params are required for index pattern [${indexPatternId}], using time field [${indexPatternTimeField}]` - ); - } - - timezone = timerange.timezone; - const { min: gte, max: lte } = timerange; - timeFilter = { - range: { - [indexPatternTimeField]: { - format: 'strict_date_time', - gte: moment.tz(moment(gte), timezone).format(), - lte: moment.tz(moment(lte), timezone).format(), - }, - }, - }; - - const savedSearchCols = savedSearchObjectAttr.columns || []; - includes = [indexPatternTimeField, ...savedSearchCols]; - } else { - includes = savedSearchObjectAttr.columns || []; - timeFilter = null; - timezone = null; - } - - const combinedFilter: Filter[] = [timeFilter, searchSourceFilter, queryFilter].filter(Boolean); // builds an array of defined filters - - return { timezone, combinedFilter, includes }; -} diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts deleted file mode 100644 index a4fbdb69bbbba..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts +++ /dev/null @@ -1,153 +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 { TimeRangeParams } from '../common'; - -export interface FakeRequest { - headers: Record; -} - -export interface JobParamsPanelCsvPost { - timerange?: TimeRangeParams; - state?: any; -} - -export interface SearchPanel { - indexPatternSavedObjectId: string; - attributes: SavedSearchObjectAttributes; - timerange?: TimeRangeParams; -} - -export interface JobPayloadPanelCsv extends JobParamsPanelCsv { - panel: SearchPanel; -} - -export interface JobParamsPanelCsv { - savedObjectType: string; - savedObjectId: string; - post?: JobParamsPanelCsvPost; - visType?: string; -} - -export interface SavedObjectServiceError { - statusCode: number; - error?: string; - message?: string; -} - -export interface SavedObjectMetaJSON { - searchSourceJSON: string; -} - -export interface SavedObjectMeta { - searchSource: SearchSource; -} - -export interface SavedSearchObjectAttributesJSON { - title: string; - sort: any[]; - columns: string[]; - kibanaSavedObjectMeta: SavedObjectMetaJSON; - uiState: any; -} - -export interface SavedSearchObjectAttributes { - title: string; - sort: any[]; - columns?: string[]; - kibanaSavedObjectMeta: SavedObjectMeta; - uiState: any; -} - -export interface VisObjectAttributesJSON { - title: string; - visState: string; // JSON string - type: string; - params: any; - uiStateJSON: string; // also JSON string - aggs: any[]; - sort: any[]; - kibanaSavedObjectMeta: SavedObjectMeta; -} - -export interface VisObjectAttributes { - title: string; - visState: string; // JSON string - type: string; - params: any; - uiState: { - vis: { - params: { - sort: { - columnIndex: string; - direction: string; - }; - }; - }; - }; - aggs: any[]; - sort: any[]; - kibanaSavedObjectMeta: SavedObjectMeta; -} - -export interface SavedObjectReference { - name: string; // should be kibanaSavedObjectMeta.searchSourceJSON.index - type: string; // should be index-pattern - id: string; -} - -export interface SavedObject { - attributes: any; - references: SavedObjectReference[]; -} - -export interface VisPanel { - indexPatternSavedObjectId?: string; - savedSearchObjectId?: string; - attributes: VisObjectAttributes; - timerange: TimeRangeParams; -} - -export interface DocValueFields { - field: string; - format: string; -} - -export interface SearchSourceQuery { - isSearchSourceQuery: boolean; -} - -export interface SearchSource { - query: SearchSourceQuery; - filter: any[]; -} - -/* - * These filter types are stub types to help ensure things get passed to - * non-Typescript functions in the right order. An actual structure is not - * needed because the code doesn't look into the properties; just combines them - * and passes them through to other non-TS modules. - */ -export interface Filter { - isFilter: boolean; -} -export interface TimeFilter extends Filter { - isTimeFilter: boolean; -} -export interface QueryFilter extends Filter { - isQueryFilter: boolean; -} -export interface SearchSourceFilter extends Filter { - isSearchSourceFilter: boolean; -} - -export interface IndexPatternField { - scripted: boolean; - lang?: string; - script?: string; - name: string; -} diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/create_job.ts new file mode 100644 index 0000000000000..a389f2a3252ca --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/create_job.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CSV_JOB_TYPE } from '../../../common/constants'; +import { cryptoFactory } from '../../lib'; +import { CreateJobFn, CreateJobFnFactory } from '../../types'; +import { JobParamsCSV, TaskPayloadCSV } from './types'; + +export const createJobFnFactory: CreateJobFnFactory< + CreateJobFn +> = function createJobFactoryFn(reporting, parentLogger) { + const logger = parentLogger.clone([CSV_JOB_TYPE, 'create-job']); + + const config = reporting.getConfig(); + const crypto = cryptoFactory(config.get('encryptionKey')); + + return async function createJob(jobParams, context, request) { + const serializedEncryptedHeaders = await crypto.encrypt(request.headers); + + return { + headers: serializedEncryptedHeaders, + spaceId: reporting.getSpaceId(request, logger), + ...jobParams, + }; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts new file mode 100644 index 0000000000000..1c2e15ebc5d9b --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts @@ -0,0 +1,75 @@ +/* + * 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('./generate_csv/generate_csv', () => ({ + CsvGenerator: class CsvGeneratorMock { + generateData() { + return { + content: 'test\n123', + }; + } + }, +})); + +import nodeCrypto from '@elastic/node-crypto'; +import { ReportingCore } from '../../'; +import { CancellationToken } from '../../../common'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../../test_helpers'; +import { runTaskFnFactory } from './execute_job'; + +const logger = createMockLevelLogger(); +const encryptionKey = 'tetkey'; +const headers = { sid: 'cooltestheaders' }; +let encryptedHeaders: string; +let reportingCore: ReportingCore; + +beforeAll(async () => { + const crypto = nodeCrypto({ encryptionKey }); + const config = createMockConfig( + createMockConfigSchema({ + encryptionKey, + csv: { + checkForFormulas: true, + escapeFormulaValues: true, + maxSizeBytes: 180000, + scroll: { size: 500, duration: '30s' }, + }, + }) + ); + + encryptedHeaders = await crypto.encrypt(headers); + + reportingCore = await createMockReportingCore(config); +}); + +test('gets the csv content from job parameters', async () => { + const runTask = runTaskFnFactory(reportingCore, logger); + + const payload = await runTask( + 'cool-job-id', + { + headers: encryptedHeaders, + browserTimezone: 'US/Alaska', + searchSource: {}, + objectType: 'search', + title: 'Test Search', + }, + new CancellationToken() + ); + + expect(payload).toMatchInlineSnapshot(` + Object { + "content": "test + 123", + } + `); +}); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts new file mode 100644 index 0000000000000..ff50377ab13c5 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts @@ -0,0 +1,49 @@ +/* + * 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 { CSV_JOB_TYPE } from '../../../common/constants'; +import { getFieldFormats } from '../../services'; +import { RunTaskFn, RunTaskFnFactory } from '../../types'; +import { decryptJobHeaders } from '../common'; +import { CsvGenerator } from './generate_csv/generate_csv'; +import { TaskPayloadCSV } from './types'; + +export const runTaskFnFactory: RunTaskFnFactory> = ( + reporting, + parentLogger +) => { + const config = reporting.getConfig(); + + return async function runTask(jobId, job, cancellationToken) { + const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job', jobId]); + + const encryptionKey = config.get('encryptionKey'); + const headers = await decryptJobHeaders(encryptionKey, job.headers, logger); + const fakeRequest = reporting.getFakeRequest({ headers }, job.spaceId, logger); + const uiSettings = await reporting.getUiSettingsClient(fakeRequest, logger); + const dataPluginStart = await reporting.getDataService(); + const fieldFormatsRegistry = await getFieldFormats().fieldFormatServiceFactory(uiSettings); + + const [es, searchSourceStart] = await Promise.all([ + (await reporting.getEsClient()).asScoped(fakeRequest), + await dataPluginStart.search.searchSource.asScoped(fakeRequest), + ]); + + const clients = { + uiSettings, + data: dataPluginStart.search.asScoped(fakeRequest), + es, + }; + const dependencies = { + searchSourceStart, + fieldFormatsRegistry, + }; + + const csv = new CsvGenerator(job, config, clients, dependencies, cancellationToken, logger); + return await csv.generateData(); + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap new file mode 100644 index 0000000000000..62c9ecff830ff --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/__snapshots__/generate_csv.test.ts.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fields cells can be multi-value 1`] = ` +"\\"_id\\",sku +\\"my-cool-id\\",\\"This is a cool SKU., This is also a cool SKU.\\" +" +`; + +exports[`fields provides top-level underscored fields as columns 1`] = ` +"\\"_id\\",\\"_index\\",date,message +\\"my-cool-id\\",\\"my-cool-index\\",\\"2020-12-31T00:14:28.000Z\\",\\"it's nice to see you\\" +" +`; + +exports[`fields sorts the fields when they are to be used as table column names 1`] = ` +"\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",date,\\"message_t\\",\\"message_u\\",\\"message_v\\",\\"message_w\\",\\"message_x\\",\\"message_y\\",\\"message_z\\" +\\"my-cool-id\\",\\"my-cool-index\\",\\"'-\\",\\"'-\\",\\"2020-12-31T00:14:28.000Z\\",\\"test field T\\",\\"test field U\\",\\"test field V\\",\\"test field W\\",\\"test field X\\",\\"test field Y\\",\\"test field Z\\" +" +`; + +exports[`formats a search result to CSV content 1`] = ` +"date,ip,message +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"This is a great message!\\" +" +`; + +exports[`formats an empty search result to CSV content 1`] = ` +"date,ip,message +" +`; + +exports[`formulas can check for formulas, without escaping them 1`] = ` +"date,ip,message +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"=SUM(A1:A2)\\" +" +`; + +exports[`formulas escapes formula values in a cell, doesn't warn the csv contains formulas 1`] = ` +"date,ip,message +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"'=SUM(A1:A2)\\" +" +`; + +exports[`formulas escapes formula values in a header, doesn't warn the csv contains formulas 1`] = ` +"date,ip,\\"'=SUM(A1:A2)\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"This is great data\\" +" +`; + +exports[`uses the scrollId to page all the data 1`] = ` +"date,ip,message +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from the initial search\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"hit from a subsequent scroll\\" +" +`; + +exports[`warns if max size was reached 1`] = ` +"date,ip,message +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"super cali fragile istic XPLA docious\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"super cali fragile istic XPLA docious\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"super cali fragile istic XPLA docious\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"super cali fragile istic XPLA docious\\" +\\"2020-12-31T00:14:28.000Z\\",\\"110.135.176.89\\",\\"super cali fragile istic XPLA docious\\" +" +`; diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/cell_has_formula.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/cell_has_formula.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/generate_csv/cell_has_formula.ts rename to x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/cell_has_formula.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/escape_value.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.test.ts rename to x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/escape_value.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/escape_value.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/generate_csv/escape_value.ts rename to x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/escape_value.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts new file mode 100644 index 0000000000000..0193eaaff2c8d --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -0,0 +1,645 @@ +/* + * 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 * as Rx from 'rxjs'; +import { identity, range } from 'lodash'; +import { IScopedClusterClient, IUiSettingsClient, SearchResponse } from 'src/core/server'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, + uiSettingsServiceMock, +} from 'src/core/server/mocks'; +import { FieldFormatsRegistry, ISearchStartSearchSource } from 'src/plugins/data/common'; +import { searchSourceInstanceMock } from 'src/plugins/data/common/search/search_source/mocks'; +import { IScopedSearchClient } from 'src/plugins/data/server'; +import { dataPluginMock } from 'src/plugins/data/server/mocks'; +import { ReportingConfig } from '../../../'; +import { CancellationToken } from '../../../../common'; +import { + UI_SETTINGS_CSV_QUOTE_VALUES, + UI_SETTINGS_CSV_SEPARATOR, + UI_SETTINGS_DATEFORMAT_TZ, +} from '../../../../common/constants'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, +} from '../../../test_helpers'; +import { JobParamsCSV } from '../types'; +import { CsvGenerator } from './generate_csv'; + +const createMockJob = (baseObj: any = {}): JobParamsCSV => ({ + ...baseObj, +}); + +let mockEsClient: IScopedClusterClient; +let mockDataClient: IScopedSearchClient; +let mockConfig: ReportingConfig; +let uiSettingsClient: IUiSettingsClient; + +const searchSourceMock = { ...searchSourceInstanceMock }; +const mockSearchSourceService: jest.Mocked = { + create: jest.fn().mockReturnValue(searchSourceMock), + createEmpty: jest.fn().mockReturnValue(searchSourceMock), +}; +const mockDataClientSearchDefault = jest.fn().mockImplementation( + (): Rx.Observable<{ rawResponse: SearchResponse }> => + Rx.of({ + rawResponse: { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, failed: 0, skipped: 0 }, + hits: { + hits: [], + total: 0, + max_score: 0, + }, + }, + }) +); +const mockSearchSourceGetFieldDefault = jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'fields': + return ['date', 'ip', 'message']; + case 'index': + return { + fields: { + getByName: jest.fn().mockImplementation(() => []), + getByType: jest.fn().mockImplementation(() => []), + }, + getFormatterForField: jest.fn(), + }; + } +}); + +const mockFieldFormatsRegistry = ({ + deserialize: jest + .fn() + .mockImplementation(() => ({ id: 'string', convert: jest.fn().mockImplementation(identity) })), +} as unknown) as FieldFormatsRegistry; + +beforeEach(async () => { + mockEsClient = elasticsearchServiceMock.createScopedClusterClient(); + mockDataClient = dataPluginMock.createStartContract().search.asScoped({} as any); + mockDataClient.search = mockDataClientSearchDefault; + + uiSettingsClient = uiSettingsServiceMock + .createStartContract() + .asScopedToClient(savedObjectsClientMock.create()); + uiSettingsClient.get = jest.fn().mockImplementation((key): any => { + switch (key) { + case UI_SETTINGS_CSV_QUOTE_VALUES: + return true; + case UI_SETTINGS_CSV_SEPARATOR: + return ','; + case UI_SETTINGS_DATEFORMAT_TZ: + return 'Browser'; + } + }); + + mockConfig = createMockConfig( + createMockConfigSchema({ + csv: { + checkForFormulas: true, + escapeFormulaValues: true, + maxSizeBytes: 180000, + scroll: { size: 500, duration: '30s' }, + }, + }) + ); + + searchSourceMock.getField = mockSearchSourceGetFieldDefault; +}); + +const logger = createMockLevelLogger(); + +it('formats an empty search result to CSV content', async () => { + const generateCsv = new CsvGenerator( + createMockJob({}), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + expect(csvResult.content).toMatchSnapshot(); + expect(csvResult.csv_contains_formulas).toBe(false); +}); + +it('formats a search result to CSV content', async () => { + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + fields: { + date: `["2020-12-31T00:14:28.000Z"]`, + ip: `["110.135.176.89"]`, + message: `["This is a great message!"]`, + }, + }, + ], + total: 1, + }, + }, + }) + ); + const generateCsv = new CsvGenerator( + createMockJob({}), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + expect(csvResult.content).toMatchSnapshot(); + expect(csvResult.csv_contains_formulas).toBe(false); +}); + +const HITS_TOTAL = 100; + +it('calculates the bytes of the content', async () => { + searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { + if (key === 'fields') { + return ['message']; + } + return mockSearchSourceGetFieldDefault(key); + }); + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: range(0, HITS_TOTAL).map((hit, i) => ({ + fields: { + message: ['this is a great message'], + }, + })), + total: HITS_TOTAL, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({}), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + expect(csvResult.size).toBe(2608); + expect(csvResult.max_size_reached).toBe(false); + expect(csvResult.warnings).toEqual([]); +}); + +it('warns if max size was reached', async () => { + const TEST_MAX_SIZE = 500; + + mockConfig = createMockConfig( + createMockConfigSchema({ + csv: { + checkForFormulas: true, + escapeFormulaValues: true, + maxSizeBytes: TEST_MAX_SIZE, + scroll: { size: 500, duration: '30s' }, + }, + }) + ); + + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: range(0, HITS_TOTAL).map((hit, i) => ({ + fields: { + date: ['2020-12-31T00:14:28.000Z'], + ip: ['110.135.176.89'], + message: ['super cali fragile istic XPLA docious'], + }, + })), + total: HITS_TOTAL, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({}), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + expect(csvResult.max_size_reached).toBe(true); + expect(csvResult.warnings).toEqual([]); + expect(csvResult.content).toMatchSnapshot(); +}); + +it('uses the scrollId to page all the data', async () => { + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + _scroll_id: 'awesome-scroll-hero', + hits: { + hits: range(0, HITS_TOTAL / 10).map((hit, i) => ({ + fields: { + date: ['2020-12-31T00:14:28.000Z'], + ip: ['110.135.176.89'], + message: ['hit from the initial search'], + }, + })), + total: HITS_TOTAL, + }, + }, + }) + ); + mockEsClient.asCurrentUser.scroll = jest.fn().mockResolvedValue({ + body: { + hits: { + hits: range(0, HITS_TOTAL / 10).map((hit, i) => ({ + fields: { + date: ['2020-12-31T00:14:28.000Z'], + ip: ['110.135.176.89'], + message: ['hit from a subsequent scroll'], + }, + })), + }, + }, + }); + + const generateCsv = new CsvGenerator( + createMockJob({}), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + expect(csvResult.warnings).toEqual([]); + expect(csvResult.content).toMatchSnapshot(); +}); + +describe('fields', () => { + it('cells can be multi-value', async () => { + searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { + if (key === 'fields') { + return ['_id', 'sku']; + } + return mockSearchSourceGetFieldDefault(key); + }); + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + sku: [`This is a cool SKU.`, `This is also a cool SKU.`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ searchSource: {} }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + }); + + it('provides top-level underscored fields as columns', async () => { + searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { + if (key === 'fields') { + return ['_id', '_index', 'date', 'message']; + } + return mockSearchSourceGetFieldDefault(key); + }); + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + date: ['2020-12-31T00:14:28.000Z'], + message: [`it's nice to see you`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ + searchSource: { + query: { query: '', language: 'kuery' }, + sort: [{ '@date': 'desc' }], + index: '93f4bc50-6662-11eb-98bc-f550e2308366', + fields: ['_id', '_index', '@date', 'message'], + filter: [], + }, + }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + expect(csvResult.csv_contains_formulas).toBe(false); + }); + + it('sorts the fields when they are to be used as table column names', async () => { + searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { + if (key === 'fields') { + return ['*']; + } + return mockSearchSourceGetFieldDefault(key); + }); + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + _id: 'my-cool-id', + _index: 'my-cool-index', + _version: 4, + fields: { + date: ['2020-12-31T00:14:28.000Z'], + message_z: [`test field Z`], + message_y: [`test field Y`], + message_x: [`test field X`], + message_w: [`test field W`], + message_v: [`test field V`], + message_u: [`test field U`], + message_t: [`test field T`], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({ + searchSource: { + query: { query: '', language: 'kuery' }, + sort: [{ '@date': 'desc' }], + index: '93f4bc50-6662-11eb-98bc-f550e2308366', + fields: ['*'], + filter: [], + }, + }), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + expect(csvResult.csv_contains_formulas).toBe(false); + }); +}); + +describe('formulas', () => { + const TEST_FORMULA = '=SUM(A1:A2)'; + + it(`escapes formula values in a cell, doesn't warn the csv contains formulas`, async () => { + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + fields: { + date: ['2020-12-31T00:14:28.000Z'], + ip: ['110.135.176.89'], + message: [TEST_FORMULA], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({}), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + expect(csvResult.csv_contains_formulas).toBe(false); + }); + + it(`escapes formula values in a header, doesn't warn the csv contains formulas`, async () => { + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + fields: { + date: ['2020-12-31T00:14:28.000Z'], + ip: ['110.135.176.89'], + [TEST_FORMULA]: 'This is great data', + }, + }, + ], + total: 1, + }, + }, + }) + ); + + searchSourceMock.getField = jest.fn().mockImplementation((key: string) => { + if (key === 'fields') { + return ['date', 'ip', TEST_FORMULA]; + } + return mockSearchSourceGetFieldDefault(key); + }); + + const generateCsv = new CsvGenerator( + createMockJob({}), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + expect(csvResult.csv_contains_formulas).toBe(false); + }); + + it('can check for formulas, without escaping them', async () => { + mockConfig = createMockConfig( + createMockConfigSchema({ + csv: { + checkForFormulas: true, + escapeFormulaValues: false, + maxSizeBytes: 180000, + scroll: { size: 500, duration: '30s' }, + }, + }) + ); + mockDataClient.search = jest.fn().mockImplementation(() => + Rx.of({ + rawResponse: { + hits: { + hits: [ + { + fields: { + date: ['2020-12-31T00:14:28.000Z'], + ip: ['110.135.176.89'], + message: [TEST_FORMULA], + }, + }, + ], + total: 1, + }, + }, + }) + ); + + const generateCsv = new CsvGenerator( + createMockJob({}), + mockConfig, + { + es: mockEsClient, + data: mockDataClient, + uiSettings: uiSettingsClient, + }, + { + searchSourceStart: mockSearchSourceService, + fieldFormatsRegistry: mockFieldFormatsRegistry, + }, + new CancellationToken(), + logger + ); + + const csvResult = await generateCsv.generateData(); + + expect(csvResult.content).toMatchSnapshot(); + expect(csvResult.csv_contains_formulas).toBe(true); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts new file mode 100644 index 0000000000000..370fc42921acf --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -0,0 +1,400 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { IScopedClusterClient, IUiSettingsClient } from 'src/core/server'; +import { IScopedSearchClient } from 'src/plugins/data/server'; +import { Datatable } from 'src/plugins/expressions/server'; +import { ReportingConfig } from '../../..'; +import { + ES_SEARCH_STRATEGY, + FieldFormat, + FieldFormatConfig, + IFieldFormatsRegistry, + IndexPattern, + ISearchSource, + ISearchStartSearchSource, + SearchFieldValue, + tabifyDocs, +} from '../../../../../../../src/plugins/data/common'; +import { KbnServerError } from '../../../../../../../src/plugins/kibana_utils/server'; +import { CancellationToken } from '../../../../common'; +import { CONTENT_TYPE_CSV } from '../../../../common/constants'; +import { byteSizeValueToNumber } from '../../../../common/schema_utils'; +import { LevelLogger } from '../../../lib'; +import { TaskRunResult } from '../../../lib/tasks'; +import { JobParamsCSV } from '../types'; +import { cellHasFormulas } from './cell_has_formula'; +import { CsvExportSettings, getExportSettings } from './get_export_settings'; +import { MaxSizeStringBuilder } from './max_size_string_builder'; + +interface Clients { + es: IScopedClusterClient; + data: IScopedSearchClient; + uiSettings: IUiSettingsClient; +} + +interface Dependencies { + searchSourceStart: ISearchStartSearchSource; + fieldFormatsRegistry: IFieldFormatsRegistry; +} + +// Function to check if the field name values can be used as the header row +function isPlainStringArray( + fields: SearchFieldValue[] | string | boolean | undefined +): fields is string[] { + let result = true; + if (Array.isArray(fields)) { + fields.forEach((field) => { + if (typeof field !== 'string' || field === '*' || field === '_source') { + result = false; + } + }); + } + return result; +} + +export class CsvGenerator { + private _formatters: Record | null = null; + private csvContainsFormulas = false; + private maxSizeReached = false; + private csvRowCount = 0; + + constructor( + private job: JobParamsCSV, + private config: ReportingConfig, + private clients: Clients, + private dependencies: Dependencies, + private cancellationToken: CancellationToken, + private logger: LevelLogger + ) {} + + private async scan( + index: IndexPattern, + searchSource: ISearchSource, + scrollSettings: CsvExportSettings['scroll'] + ) { + const searchBody = await searchSource.getSearchRequestBody(); + this.logger.debug(`executing search request`); + const searchParams = { + params: { + body: searchBody, + index: index.title, + scroll: scrollSettings.duration, + size: scrollSettings.size, + }, + }; + const results = ( + await this.clients.data.search(searchParams, { strategy: ES_SEARCH_STRATEGY }).toPromise() + ).rawResponse; + + return results; + } + + private async scroll(scrollId: string, scrollSettings: CsvExportSettings['scroll']) { + this.logger.debug(`executing scroll request`); + const results = ( + await this.clients.es.asCurrentUser.scroll({ + scroll: scrollSettings.duration, + scroll_id: scrollId, + }) + ).body as SearchResponse; + return results; + } + + /* + * Load field formats for each field in the list + */ + private getFormatters(table: Datatable) { + if (this._formatters) { + return this._formatters; + } + + // initialize field formats + const formatters: Record = {}; + table.columns.forEach((c) => { + const fieldFormat = this.dependencies.fieldFormatsRegistry.deserialize(c.meta.params); + formatters[c.id] = fieldFormat; + }); + + this._formatters = formatters; + return this._formatters; + } + + private escapeValues(settings: CsvExportSettings) { + return (value: string) => { + if (settings.checkForFormulas && cellHasFormulas(value)) { + this.csvContainsFormulas = true; // set warning if cell value has a formula + } + return settings.escapeValue(value); + }; + } + + // use fields/fieldsFromSource from the searchSource to get the ordering of columns + // otherwise use the table columns as they are + private getFields(searchSource: ISearchSource, table: Datatable): string[] { + const fieldValues: Record = { + fields: searchSource.getField('fields'), + fieldsFromSource: searchSource.getField('fieldsFromSource'), + }; + const fieldSource = fieldValues.fieldsFromSource ? 'fieldsFromSource' : 'fields'; + this.logger.debug(`Getting search source fields from: '${fieldSource}'`); + + const fields = fieldValues[fieldSource]; + // Check if field name values are string[] and if the fields are user-defined + if (isPlainStringArray(fields)) { + return fields; + } + + // Default to using the table column IDs as the fields + const columnIds = table.columns.map((c) => c.id); + // Fields in the API response don't come sorted - they need to be sorted client-side + columnIds.sort(); + return columnIds; + } + + private formatCellValues(formatters: Record) { + return ({ + column: tableColumn, + data: dataTableCell, + }: { + column: string; + data: any; + }): string => { + let cell: string[] | string | object; + // check truthiness to guard against _score, _type, etc + if (tableColumn && dataTableCell) { + try { + cell = formatters[tableColumn].convert(dataTableCell); + } catch (err) { + this.logger.error(err); + cell = '-'; + } + + try { + // expected values are a string of JSON where the value(s) is in an array + cell = JSON.parse(cell); + } catch (e) { + // ignore + } + + // We have to strip singular array values out of their array wrapper, + // So that the value appears the visually the same as seen in Discover + if (Array.isArray(cell)) { + cell = cell.map((c) => (typeof c === 'object' ? JSON.stringify(c) : c)).join(', '); + } + + // Check for object-type value (geoip) + if (typeof cell === 'object') { + cell = JSON.stringify(cell); + } + + return cell; + } + + return '-'; // Unknown field: it existed in searchSource but has no value in the result + }; + } + + /* + * Use the list of fields to generate the header row + */ + private generateHeader( + fields: string[], + table: Datatable, + builder: MaxSizeStringBuilder, + settings: CsvExportSettings + ) { + this.logger.debug(`Building CSV header row...`); + const header = fields.map(this.escapeValues(settings)).join(settings.separator) + '\n'; + + if (!builder.tryAppend(header)) { + return { + size: 0, + content: '', + maxSizeReached: true, + warnings: [], + }; + } + } + + /* + * Format a Datatable into rows of CSV content + */ + private generateRows( + fields: string[], + table: Datatable, + builder: MaxSizeStringBuilder, + formatters: Record, + settings: CsvExportSettings + ) { + this.logger.debug(`Building ${table.rows.length} CSV data rows...`); + for (const dataTableRow of table.rows) { + if (this.cancellationToken.isCancelled()) { + break; + } + + const row = + fields + .map((f) => ({ column: f, data: dataTableRow[f] })) + .map(this.formatCellValues(formatters)) + .map(this.escapeValues(settings)) + .join(settings.separator) + '\n'; + + if (!builder.tryAppend(row)) { + this.logger.warn(`Max Size Reached after ${this.csvRowCount} rows.`); + this.maxSizeReached = true; + if (this.cancellationToken) { + this.cancellationToken.cancel(); + } + break; + } + + this.csvRowCount++; + } + } + + public async generateData(): Promise { + const [settings, searchSource] = await Promise.all([ + getExportSettings( + this.clients.uiSettings, + this.config, + this.job.browserTimezone, + this.logger + ), + this.dependencies.searchSourceStart.create(this.job.searchSource), + ]); + + const index = searchSource.getField('index'); + + if (!index) { + throw new Error(`The search must have a revference to an index pattern!`); + } + + const { maxSizeBytes, bom, escapeFormulaValues, scroll: scrollSettings } = settings; + + const builder = new MaxSizeStringBuilder(byteSizeValueToNumber(maxSizeBytes), bom); + const warnings: string[] = []; + let first = true; + let currentRecord = -1; + let totalRecords = 0; + let scrollId: string | undefined; + + // apply timezone from the job to all date field formatters + try { + index.fields.getByType('date').forEach(({ name }) => { + this.logger.debug(`setting timezone on ${name}`); + const format: FieldFormatConfig = { + ...index.fieldFormatMap[name], + id: index.fieldFormatMap[name]?.id || 'date', // allow id: date_nanos + params: { + ...index.fieldFormatMap[name]?.params, + timezone: settings.timezone, + }, + }; + index.setFieldFormat(name, format); + }); + } catch (err) { + this.logger.error(err); + } + + try { + do { + if (this.cancellationToken.isCancelled()) { + break; + } + let results: SearchResponse | undefined; + if (scrollId == null) { + // open a scroll cursor in Elasticsearch + results = await this.scan(index, searchSource, scrollSettings); + scrollId = results?._scroll_id; + if (results.hits?.total != null) { + totalRecords = results.hits.total; + this.logger.debug(`Total search results: ${totalRecords}`); + } + } else { + // use the scroll cursor in Elasticsearch + results = await this.scroll(scrollId, scrollSettings); + } + + if (!results) { + this.logger.warning(`Search results are undefined!`); + break; + } + + let table: Datatable | undefined; + try { + table = tabifyDocs(results, index, { shallow: true, meta: true }); + } catch (err) { + this.logger.error(err); + } + + if (!table) { + break; + } + + const fields = this.getFields(searchSource, table); + + if (first) { + first = false; + this.generateHeader(fields, table, builder, settings); + } + + if (table.rows.length < 1) { + break; // empty report with just the header + } + + const formatters = this.getFormatters(table); + this.generateRows(fields, table, builder, formatters, settings); + + // update iterator + currentRecord += table.rows.length; + } while (currentRecord < totalRecords - 1); + + // Add warnings to be logged + if (this.csvContainsFormulas && escapeFormulaValues) { + warnings.push( + i18n.translate('xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues', { + defaultMessage: 'CSV may contain formulas whose values have been escaped', + }) + ); + } + } catch (err) { + this.logger.error(err); + if (err instanceof KbnServerError && err.errBody) { + throw JSON.stringify(err.errBody.error); + } + } finally { + // clear scrollID + if (scrollId) { + this.logger.debug(`executing clearScroll request`); + try { + await this.clients.es.asCurrentUser.clearScroll({ scroll_id: [scrollId] }); + } catch (err) { + this.logger.error(err); + } + } else { + this.logger.warn(`No scrollId to clear!`); + } + } + + const size = builder.getSizeInBytes(); + this.logger.debug( + `Finished generating. Total size in bytes: ${size}. Row count: ${this.csvRowCount}.` + ); + + return { + content: builder.getString(), + content_type: CONTENT_TYPE_CSV, + csv_contains_formulas: this.csvContainsFormulas && !escapeFormulaValues, + max_size_reached: this.maxSizeReached, + size, + warnings, + }; + } +} diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts new file mode 100644 index 0000000000000..efdb583a89dc8 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { + UI_SETTINGS_DATEFORMAT_TZ, + UI_SETTINGS_CSV_QUOTE_VALUES, + UI_SETTINGS_CSV_SEPARATOR, +} from '../../../../common/constants'; +import { IUiSettingsClient } from 'kibana/server'; +import { savedObjectsClientMock, uiSettingsServiceMock } from 'src/core/server/mocks'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, +} from '../../../test_helpers'; +import { getExportSettings } from './get_export_settings'; + +describe('getExportSettings', () => { + let uiSettingsClient: IUiSettingsClient; + const config = createMockConfig(createMockConfigSchema({})); + const logger = createMockLevelLogger(); + + beforeEach(() => { + uiSettingsClient = uiSettingsServiceMock + .createStartContract() + .asScopedToClient(savedObjectsClientMock.create()); + uiSettingsClient.get = jest.fn().mockImplementation((key: string) => { + switch (key) { + case UI_SETTINGS_CSV_QUOTE_VALUES: + return true; + case UI_SETTINGS_CSV_SEPARATOR: + return ','; + case UI_SETTINGS_DATEFORMAT_TZ: + return 'Browser'; + } + + return 'helo world'; + }); + }); + + test('getExportSettings: returns the expected result', async () => { + expect(await getExportSettings(uiSettingsClient, config, '', logger)).toMatchInlineSnapshot(` + Object { + "bom": "", + "checkForFormulas": undefined, + "escapeFormulaValues": undefined, + "escapeValue": [Function], + "maxSizeBytes": undefined, + "scroll": Object { + "duration": undefined, + "size": undefined, + }, + "separator": ",", + "timezone": "UTC", + } + `); + }); + + test('escapeValue function', async () => { + const { escapeValue } = await getExportSettings(uiSettingsClient, config, '', logger); + expect(escapeValue(`test`)).toBe(`test`); + expect(escapeValue(`this is, a test`)).toBe(`"this is, a test"`); + expect(escapeValue(`"tet"`)).toBe(`"""tet"""`); + expect(escapeValue(`@foo`)).toBe(`"@foo"`); + }); + + test('non-default timezone', async () => { + uiSettingsClient.get = jest.fn().mockImplementation((key: string) => { + switch (key) { + case UI_SETTINGS_DATEFORMAT_TZ: + return `America/Aruba`; + } + }); + + expect( + await getExportSettings(uiSettingsClient, config, '', logger).then(({ timezone }) => timezone) + ).toBe(`America/Aruba`); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts new file mode 100644 index 0000000000000..17a10f3034242 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts @@ -0,0 +1,85 @@ +/* + * 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 { ByteSizeValue } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { IUiSettingsClient } from 'kibana/server'; +import { ReportingConfig } from '../../../'; +import { + CSV_BOM_CHARS, + UI_SETTINGS_DATEFORMAT_TZ, + UI_SETTINGS_CSV_QUOTE_VALUES, + UI_SETTINGS_CSV_SEPARATOR, +} from '../../../../common/constants'; +import { LevelLogger } from '../../../lib'; +import { createEscapeValue } from './escape_value'; + +export interface CsvExportSettings { + timezone: string; + scroll: { + size: number; + duration: string; + }; + bom: string; + separator: string; + maxSizeBytes: number | ByteSizeValue; + checkForFormulas: boolean; + escapeFormulaValues: boolean; + escapeValue: (value: string) => string; +} + +export const getExportSettings = async ( + client: IUiSettingsClient, + config: ReportingConfig, + timezone: string | undefined, + logger: LevelLogger +): Promise => { + // Timezone + let setTimezone: string; + // timezone in job params? + if (timezone) { + setTimezone = timezone; + } else { + // timezone in settings? + setTimezone = await client.get(UI_SETTINGS_DATEFORMAT_TZ); + if (setTimezone === 'Browser') { + // if `Browser`, hardcode it to 'UTC' so the export has data that makes sense + logger.warn( + i18n.translate('xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting', { + defaultMessage: + 'Kibana Advanced Setting "{dateFormatTimezone}" is set to "Browser". Dates will be formatted as UTC to avoid ambiguity.', + values: { dateFormatTimezone: 'dateFormat:tz' }, + }) + ); + setTimezone = 'UTC'; + } + } + + // Separator, QuoteValues + const [separator, quoteValues] = await Promise.all([ + client.get(UI_SETTINGS_CSV_SEPARATOR), + client.get(UI_SETTINGS_CSV_QUOTE_VALUES), + ]); + + const escapeFormulaValues = config.get('csv', 'escapeFormulaValues'); + const escapeValue = createEscapeValue(quoteValues, escapeFormulaValues); + const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; + + return { + timezone: setTimezone, + scroll: { + size: config.get('csv', 'scroll', 'size'), + duration: config.get('csv', 'scroll', 'duration'), + }, + bom, + separator, + maxSizeBytes: config.get('csv', 'maxSizeBytes'), + checkForFormulas: config.get('csv', 'checkForFormulas'), + escapeFormulaValues, + escapeValue, + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/index.ts new file mode 100644 index 0000000000000..4e08ff2a222dc --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/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 { CsvGenerator } from './generate_csv'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/max_size_string_builder.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.test.ts rename to x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/max_size_string_builder.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/max_size_string_builder.ts similarity index 100% rename from x-pack/plugins/reporting/server/export_types/csv/generate_csv/max_size_string_builder.ts rename to x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/max_size_string_builder.ts diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/index.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/index.ts new file mode 100644 index 0000000000000..65126a0a62cb8 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CSV_JOB_TYPE as jobType, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_ENTERPRISE, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_TRIAL, +} from '../../../common/constants'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; +import { runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; +import { JobParamsCSV, TaskPayloadCSV } from './types'; + +export const getExportType = (): ExportTypeDefinition< + CreateJobFn, + RunTaskFn +> => ({ + ...metadata, + jobType, + jobContentExtension: 'csv', + createJobFnFactory, + runTaskFnFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + LICENSE_TYPE_ENTERPRISE, + ], +}); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/metadata.ts similarity index 65% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts rename to x-pack/plugins/reporting/server/export_types/csv_searchsource/metadata.ts index 76bf106acb5de..187d64d872a9d 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/metadata.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; +import { CSV_JOB_TYPE } from '../../../common/constants'; export const metadata = { - id: CSV_FROM_SAVEDOBJECT_JOB_TYPE, - name: CSV_FROM_SAVEDOBJECT_JOB_TYPE, + id: 'csv_searchsource', + name: CSV_JOB_TYPE, }; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts new file mode 100644 index 0000000000000..f0ad4e00ebd5c --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/types.d.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { BaseParams, BasePayload } from '../../types'; + +export type RawValue = string | object | null | undefined; + +interface BaseParamsCSV { + browserTimezone: string; + searchSource: any; +} + +export type JobParamsCSV = BaseParamsCSV & BaseParams; +export type TaskPayloadCSV = BaseParamsCSV & BasePayload; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts new file mode 100644 index 0000000000000..c8475e85bd847 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest } from 'src/core/server'; +import { CancellationToken } from '../../../common'; +import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; +import { TaskRunResult } from '../../lib/tasks'; +import { getFieldFormats } from '../../services'; +import { ReportingRequestHandlerContext, RunTaskFnFactory } from '../../types'; +import { CsvGenerator } from '../csv_searchsource/generate_csv/generate_csv'; +import { JobParamsDownloadCSV } from './types'; + +/* + * ImmediateExecuteFn receives the job doc payload because the payload was + * generated in the ScheduleFn + */ +export type ImmediateExecuteFn = ( + jobId: null, + job: JobParamsDownloadCSV, + context: ReportingRequestHandlerContext, + req: KibanaRequest +) => Promise; + +export const runTaskFnFactory: RunTaskFnFactory = function executeJobFactoryFn( + reporting, + parentLogger +) { + const config = reporting.getConfig(); + const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE, 'execute-job']); + + return async function runTask(jobId, immediateJobParams, context, req) { + const job = { + objectType: 'immediate-search', + ...immediateJobParams, + }; + + const savedObjectsClient = context.core.savedObjects.client; + const uiSettings = await reporting.getUiSettingsServiceFactory(savedObjectsClient); + const dataPluginStart = await reporting.getDataService(); + const fieldFormatsRegistry = await getFieldFormats().fieldFormatServiceFactory(uiSettings); + + const [es, searchSourceStart] = await Promise.all([ + (await reporting.getEsClient()).asScoped(req), + await dataPluginStart.search.searchSource.asScoped(req), + ]); + const clients = { + uiSettings, + data: dataPluginStart.search.asScoped(req), + es, + }; + const dependencies = { + fieldFormatsRegistry, + searchSourceStart, + }; + const cancellationToken = new CancellationToken(); + + const csv = new CsvGenerator(job, config, clients, dependencies, cancellationToken, logger); + const result = await csv.generateData(); + + if (result.csv_contains_formulas) { + logger.warn(`CSV may contain formulas whose values have been escaped`); + } + + if (result.max_size_reached) { + logger.warn(`Max size reached: CSV output truncated to ${result.size} bytes`); + } + + const { warnings } = result; + if (warnings) { + warnings.forEach((warning) => { + logger.warning(warning); + }); + } + + return result; + }; +}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/index.ts similarity index 75% rename from x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts rename to x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/index.ts index c3a0df9529a4d..9d915db4797b3 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/index.ts @@ -6,7 +6,7 @@ */ import { - CSV_FROM_SAVEDOBJECT_JOB_TYPE, + CSV_SEARCHSOURCE_IMMEDIATE_TYPE, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -15,7 +15,6 @@ import { LICENSE_TYPE_TRIAL, } from '../../../common/constants'; import { ExportTypeDefinition } from '../../types'; -import { createJobFnFactory, ImmediateCreateJobFn } from './create_job'; import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; @@ -23,17 +22,13 @@ import { metadata } from './metadata'; * These functions are exported to share with the API route handler that * generates csv from saved object immediately on request. */ -export { createJobFnFactory } from './create_job'; export { runTaskFnFactory } from './execute_job'; -export const getExportType = (): ExportTypeDefinition< - ImmediateCreateJobFn, - ImmediateExecuteFn -> => ({ +export const getExportType = (): ExportTypeDefinition => ({ ...metadata, - jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, + jobType: CSV_SEARCHSOURCE_IMMEDIATE_TYPE, jobContentExtension: 'csv', - createJobFnFactory, + createJobFnFactory: null, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/metadata.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/metadata.ts new file mode 100644 index 0000000000000..c27b8484697dd --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/metadata.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; + +export const metadata = { + id: CSV_SEARCHSOURCE_IMMEDIATE_TYPE, + name: CSV_SEARCHSOURCE_IMMEDIATE_TYPE, +}; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts new file mode 100644 index 0000000000000..276016dd61233 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/types.d.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimeRangeParams } from '../common'; + +export interface FakeRequest { + headers: Record; +} + +export interface JobParamsDownloadCSV { + browserTimezone: string; + title: string; + searchSource: any; +} + +export interface SavedObjectServiceError { + statusCode: number; + error?: string; + message?: string; +} diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 5ac644298796d..b0e5d7bafb03c 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -38,13 +38,17 @@ export function enqueueJobFactory( throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); } + if (!exportType.createJobFnFactory) { + throw new Error(`Export type ${exportTypeId} is not an async job type!`); + } + const [createJob, store] = await Promise.all([ exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), reporting.getStore(), ]); const config = reporting.getConfig(); - const job = await createJob(jobParams, context, request); + const job = await createJob!(jobParams, context, request); // 1. Add the report to ReportingStore to show as pending const report = await store.addReport( diff --git a/x-pack/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/plugins/reporting/server/lib/export_types_registry.ts index 5502692306319..890af43297751 100644 --- a/x-pack/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/plugins/reporting/server/lib/export_types_registry.ts @@ -6,8 +6,9 @@ */ import { isString } from 'lodash'; -import { getExportType as getTypeCsv } from '../export_types/csv'; -import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_from_savedobject'; +import { getExportType as getTypeCsvDeprecated } from '../export_types/csv'; +import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_searchsource_immediate'; +import { getExportType as getTypeCsv } from '../export_types/csv_searchsource'; import { getExportType as getTypePng } from '../export_types/png'; import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf'; import { CreateJobFn, ExportTypeDefinition } from '../types'; @@ -82,8 +83,9 @@ export function getExportTypesRegistry(): ExportTypesRegistry { const registry = new ExportTypesRegistry(); type CreateFnType = CreateJobFn; // can not specify params types because different type of params are not assignable to each other type RunFnType = any; // can not specify because ImmediateExecuteFn is not assignable to RunTaskFn - const getTypeFns: Array<() => ExportTypeDefinition> = [ + const getTypeFns: Array<() => ExportTypeDefinition> = [ getTypeCsv, + getTypeCsvDeprecated, getTypeCsvFromSavedObject, getTypePng, getTypePrintablePdf, diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index e910fecb76988..bef60545d89b8 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -122,6 +122,8 @@ export class ReportingPlugin savedObjects: core.savedObjects, uiSettings: core.uiSettings, store, + esClient: core.elasticsearch.client, + data: plugins.data, taskManager: plugins.taskManager, }); diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts similarity index 55% rename from x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts rename to x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts index 6d000cffb9195..55092b5236ce6 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts @@ -8,26 +8,17 @@ import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; -import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job'; -import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; -import { - JobParamsPanelCsv, - JobParamsPanelCsvPost, -} from '../export_types/csv_from_savedobject/types'; +import { runTaskFnFactory } from '../export_types/csv_searchsource_immediate/execute_job'; +import { JobParamsDownloadCSV } from '../export_types/csv_searchsource_immediate/types'; import { LevelLogger as Logger } from '../lib'; import { TaskRunResult } from '../lib/tasks'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; -import { getJobParamsFromRequest } from './lib/get_job_params_from_request'; import { HandlerErrorFunction } from './types'; const API_BASE_URL_V1 = '/api/reporting/v1'; const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; -export type CsvFromSavedObjectRequest = KibanaRequest< - JobParamsPanelCsv, - unknown, - JobParamsPanelCsvPost ->; +export type CsvFromSavedObjectRequest = KibanaRequest; /* * This function registers API Endpoints for immediate Reporting jobs. The API inputs are: @@ -47,43 +38,28 @@ export function registerGenerateCsvFromSavedObjectImmediate( const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; - /* - * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: - * - re-use the createJob function to build up es query config - * - re-use the runTask function to run the scan and scroll queries and capture the entire CSV in a result object. - */ + // This API calls run the SearchSourceImmediate export type's runTaskFn directly router.post( { - path: `${API_BASE_GENERATE_V1}/immediate/csv/saved-object/{savedObjectType}:{savedObjectId}`, + path: `${API_BASE_GENERATE_V1}/immediate/csv_searchsource`, validate: { - params: schema.object({ - savedObjectType: schema.string({ minLength: 5 }), - savedObjectId: schema.string({ minLength: 5 }), - }), body: schema.object({ - state: schema.object({}, { unknowns: 'allow' }), - timerange: schema.object({ - timezone: schema.string({ defaultValue: 'UTC' }), - min: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), - max: schema.nullable(schema.oneOf([schema.number(), schema.string({ minLength: 5 })])), - }), + searchSource: schema.object({}, { unknowns: 'allow' }), + browserTimezone: schema.string({ defaultValue: 'UTC' }), + title: schema.string(), }), }, }, userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => { - const logger = parentLogger.clone(['savedobject-csv']); - const jobParams = getJobParamsFromRequest(req); - const createJob = createJobFnFactory(reporting, logger); + const logger = parentLogger.clone(['csv_searchsource_immediate']); const runTaskFn = runTaskFnFactory(reporting, logger); try { - // FIXME: no create job for immediate download - const payload = await createJob(jobParams, context, req); const { content_type: jobOutputContentType, content: jobOutputContent, size: jobOutputSize, - }: TaskRunResult = await runTaskFn(null, payload, context, req); + }: TaskRunResult = await runTaskFn(null, req.body, context, req); logger.info(`Job output size: ${jobOutputSize} bytes`); diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts index 3edd898609f8c..5c9fd25b76c39 100644 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ b/x-pack/plugins/reporting/server/routes/generation.ts @@ -13,7 +13,7 @@ import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; import { enqueueJobFactory } from '../lib/enqueue_job'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; -import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; +import { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate'; import { HandlerFunction } from './types'; const esErrors = elasticsearchErrors as Record; diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index 26f1a64a7ef63..4e8e888e4e266 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -8,7 +8,7 @@ // @ts-ignore import contentDisposition from 'content-disposition'; import { get } from 'lodash'; -import { CSV_JOB_TYPE_DEPRECATED } from '../../../common/constants'; +import { CSV_JOB_TYPE, CSV_JOB_TYPE_DEPRECATED } from '../../../common/constants'; import { ExportTypesRegistry, statuses } from '../../lib'; import { ReportDocument } from '../../lib/store'; import { TaskRunResult } from '../../lib/tasks'; @@ -34,7 +34,7 @@ const getTitle = (exportType: ExportTypeDefinition, title?: string): string => const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeDefinition) => { const metaDataHeaders: Record = {}; - if (exportType.jobType === CSV_JOB_TYPE_DEPRECATED) { + if (exportType.jobType === CSV_JOB_TYPE || exportType.jobType === CSV_JOB_TYPE_DEPRECATED) { const csvContainsFormulas = get(output, 'csv_contains_formulas', false); const maxSizedReach = get(output, 'max_size_reached', false); diff --git a/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts b/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts deleted file mode 100644 index 8dce491e3df09..0000000000000 --- a/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { JobParamsPanelCsv } from '../../export_types/csv_from_savedobject/types'; -import { CsvFromSavedObjectRequest } from '../generate_from_savedobject_immediate'; - -export function getJobParamsFromRequest(request: CsvFromSavedObjectRequest): JobParamsPanelCsv { - const { savedObjectType, savedObjectId } = request.params; - const { timerange, state } = request.body; - - const post = timerange || state ? { timerange, state } : undefined; - - return { - savedObjectType, - savedObjectId, - post, - }; -} diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 0700fbaff0fe3..e42d87c50e118 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -11,7 +11,10 @@ jest.mock('../browsers'); import _ from 'lodash'; import * as Rx from 'rxjs'; -import { coreMock } from 'src/core/server/mocks'; +import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { fieldFormats } from 'src/plugins/data/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { dataPluginMock } from 'src/plugins/data/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; import { featuresPluginMock } from '../../../features/server/mocks'; import { @@ -22,6 +25,7 @@ import { import { ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; +import { setFieldFormats } from '../services'; import { createMockLevelLogger } from './create_mock_levellogger'; (initializeBrowserDriverFactory as jest.Mock< @@ -45,15 +49,22 @@ export const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup = const logger = createMockLevelLogger(); -const createMockPluginStart = ( - mockReportingCore: ReportingCore, +const createMockReportingStore = () => ({} as ReportingStore); + +export const createMockPluginStart = ( + mockReportingCore: ReportingCore | undefined, startMock?: any ): ReportingInternalStart => { - const store = new ReportingStore(mockReportingCore, logger); + const store = mockReportingCore + ? new ReportingStore(mockReportingCore, logger) + : createMockReportingStore(); + return { browserDriverFactory: startMock.browserDriverFactory, + esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() }, uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) }, + data: startMock.data || dataPluginMock.createStartContract(), store, taskManager: { schedule: jest.fn().mockImplementation(() => ({ id: 'taskId' })), @@ -124,11 +135,18 @@ export const createMockReportingCore = async ( setupDepsMock: ReportingInternalSetup | undefined = undefined, startDepsMock: ReportingInternalStart | undefined = undefined ) => { - config = config || {}; + const mockReportingCore = ({ + getConfig: () => config, + getElasticsearchService: () => setupDepsMock?.elasticsearch, + getDataService: () => startDepsMock?.data, + } as unknown) as ReportingCore; if (!setupDepsMock) { setupDepsMock = createMockPluginSetup({}); } + if (!startDepsMock) { + startDepsMock = createMockPluginStart(mockReportingCore, {}); + } const context = coreMock.createPluginInitializerContext(createMockConfigSchema()); const core = new ReportingCore(logger, context); @@ -143,5 +161,12 @@ export const createMockReportingCore = async ( await core.pluginStart(startDepsMock); await core.pluginStartsUp(); + setFieldFormats({ + fieldFormatServiceFactory() { + const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry(); + return Promise.resolve(fieldFormatsRegistry); + }, + }); + return core; }; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 1b762c96079fa..2a9cbaeaa6755 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -83,13 +83,16 @@ export type RunTaskFnFactory = ( logger: LevelLogger ) => RunTaskFnType; -export interface ExportTypeDefinition { +export interface ExportTypeDefinition< + CreateJobFnType = CreateJobFn | null, + RunTaskFnType = RunTaskFn +> { id: string; name: string; jobType: string; jobContentEncoding?: string; jobContentExtension: string; - createJobFnFactory: CreateJobFnFactory; + createJobFnFactory: CreateJobFnFactory | null; // immediate job does not have a "create" phase runTaskFnFactory: RunTaskFnFactory; validLicenses: string[]; } diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index ed2637d7a1bcb..150154fa996c5 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -13,6 +13,10 @@ Object { "available": true, "total": 0, }, + "csv_searchsource": Object { + "available": true, + "total": 0, + }, "enabled": true, "last7Days": Object { "PNG": Object { @@ -24,6 +28,10 @@ Object { "available": true, "total": 0, }, + "csv_searchsource": Object { + "available": true, + "total": 0, + }, "printable_pdf": Object { "app": Object { "dashboard": 0, @@ -75,6 +83,10 @@ Object { "available": true, "total": 0, }, + "csv_searchsource": Object { + "available": true, + "total": 0, + }, "enabled": true, "last7Days": Object { "PNG": Object { @@ -86,6 +98,10 @@ Object { "available": true, "total": 0, }, + "csv_searchsource": Object { + "available": true, + "total": 0, + }, "printable_pdf": Object { "app": Object { "dashboard": 0, @@ -166,6 +182,10 @@ Object { "available": true, "total": 1, }, + "csv_searchsource": Object { + "available": true, + "total": 0, + }, "enabled": true, "last7Days": Object { "PNG": Object { @@ -177,6 +197,10 @@ Object { "available": true, "total": 1, }, + "csv_searchsource": Object { + "available": true, + "total": 0, + }, "printable_pdf": Object { "app": Object { "canvas workpad": 1, diff --git a/x-pack/plugins/reporting/server/usage/decorate_range_stats.ts b/x-pack/plugins/reporting/server/usage/decorate_range_stats.ts index a750e9e196b20..9fc0141ab742e 100644 --- a/x-pack/plugins/reporting/server/usage/decorate_range_stats.ts +++ b/x-pack/plugins/reporting/server/usage/decorate_range_stats.ts @@ -6,7 +6,12 @@ */ import { uniq } from 'lodash'; -import { CSV_JOB_TYPE_DEPRECATED, PDF_JOB_TYPE, PNG_JOB_TYPE } from '../../common/constants'; +import { + CSV_JOB_TYPE, + CSV_JOB_TYPE_DEPRECATED, + PDF_JOB_TYPE, + PNG_JOB_TYPE, +} from '../../common/constants'; import { AvailableTotal, ExportType, FeatureAvailabilityMap, RangeStats } from './types'; function getForFeature( @@ -55,6 +60,7 @@ export const decorateRangeStats = ( // combine the known types with any unknown type found in reporting data const keysBasic = uniq([ + CSV_JOB_TYPE, CSV_JOB_TYPE_DEPRECATED, PNG_JOB_TYPE, ...Object.keys(rangeStatsBasic), diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 9335fee766740..05b80bc8acc75 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -354,11 +354,13 @@ describe('data modeling', () => { available: true, browser_type: 'chromium', csv: { available: true, total: 4 }, + csv_searchsource: { available: true, total: 4 }, enabled: true, last7Days: { PNG: { available: true, total: 0 }, _all: 0, csv: { available: true, total: 0 }, + csv_searchsource: { available: true, total: 0 }, printable_pdf: { app: { dashboard: 0, visualization: 0 }, available: true, @@ -389,11 +391,13 @@ describe('data modeling', () => { available: true, browser_type: 'chromium', csv: { available: true, total: 0 }, + csv_searchsource: { available: true, total: 0 }, enabled: true, last7Days: { PNG: { available: true, total: 3 }, _all: 4, csv: { available: true, total: 0 }, + csv_searchsource: { available: true, total: 0 }, printable_pdf: { app: { 'canvas workpad': 1, dashboard: 0, visualization: 0 }, available: true, @@ -431,6 +435,7 @@ describe('data modeling', () => { layout: { preserve_layout: 0, print: 0 }, }, csv: { available: true, total: 0 }, + csv_searchsource: { available: true, total: 0 }, PNG: { available: true, total: 0 }, }, _all: 0, @@ -443,6 +448,7 @@ describe('data modeling', () => { layout: { preserve_layout: 0, print: 0 }, }, csv: { available: true, total: 0 }, + csv_searchsource: { available: true, total: 0 }, PNG: { available: true, total: 0 }, }); }); @@ -491,6 +497,14 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "available": Object { + "type": "boolean", + }, + "total": Object { + "type": "long", + }, + }, "enabled": Object { "type": "boolean", }, @@ -514,6 +528,14 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "available": Object { + "type": "boolean", + }, + "total": Object { + "type": "long", + }, + }, "printable_pdf": Object { "app": Object { "canvas workpad": Object { @@ -585,6 +607,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -620,6 +653,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -655,6 +699,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -690,6 +745,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -725,6 +791,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -760,6 +837,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -845,6 +933,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -880,6 +979,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -915,6 +1025,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -950,6 +1071,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -985,6 +1117,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", @@ -1020,6 +1163,17 @@ describe('Ready for collection observable', () => { "type": "long", }, }, + "csv_searchsource": Object { + "canvas workpad": Object { + "type": "long", + }, + "dashboard": Object { + "type": "long", + }, + "visualization": Object { + "type": "long", + }, + }, "printable_pdf": Object { "canvas workpad": Object { "type": "long", diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index ec15ae4b1ac47..8528543b09e07 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -16,6 +16,7 @@ const appCountsSchema: MakeSchemaFrom = { const byAppCountsSchema: MakeSchemaFrom = { csv: appCountsSchema, + csv_searchsource: appCountsSchema, PNG: appCountsSchema, printable_pdf: appCountsSchema, }; @@ -27,6 +28,7 @@ const availableTotalSchema: MakeSchemaFrom = { const jobTypesSchema: MakeSchemaFrom = { csv: availableTotalSchema, + csv_searchsource: availableTotalSchema, PNG: availableTotalSchema, printable_pdf: { ...availableTotalSchema, diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index 5970df6ccae43..58def60a24ccb 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -59,7 +59,7 @@ export interface AvailableTotal { total: number; } -type BaseJobTypes = 'csv' | 'PNG' | 'printable_pdf'; +type BaseJobTypes = 'csv' | 'csv_searchsource' | 'PNG' | 'printable_pdf'; export interface LayoutCounts { print: number; preserve_layout: number; @@ -106,7 +106,7 @@ export type ReportingUsageType = RangeStats & { last7Days: RangeStats; }; -export type ExportType = 'csv' | 'printable_pdf' | 'PNG'; +export type ExportType = 'csv' | 'csv_searchsource' | 'printable_pdf' | 'PNG'; export type FeatureAvailabilityMap = { [F in ExportType]: boolean }; export interface KeyCountBucket { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 4a0ab555f8f40..8367fcb6deef2 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -2436,6 +2436,16 @@ } } }, + "csv_searchsource": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + } + } + }, "PNG": { "properties": { "available": { @@ -2521,6 +2531,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -2564,6 +2587,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -2607,6 +2643,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -2650,6 +2699,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -2693,6 +2755,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -2736,6 +2811,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -2787,6 +2875,16 @@ } } }, + "csv_searchsource": { + "properties": { + "available": { + "type": "boolean" + }, + "total": { + "type": "long" + } + } + }, "PNG": { "properties": { "available": { @@ -2872,6 +2970,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -2915,6 +3026,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -2958,6 +3082,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -3001,6 +3138,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -3044,6 +3194,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { @@ -3087,6 +3250,19 @@ } } }, + "csv_searchsource": { + "properties": { + "canvas workpad": { + "type": "long" + }, + "dashboard": { + "type": "long" + }, + "visualization": { + "type": "long" + } + } + }, "PNG": { "properties": { "canvas workpad": { diff --git a/x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap b/x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap new file mode 100644 index 0000000000000..10384b865c82e --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/__snapshots__/download_csv.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dashboard Reporting Download CSV E-Commerce Data Download CSV export of a saved search panel 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,19,716724,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Shoes, Women's Clothing\\",EUR,45,591503,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0006400064, ZO0150601506\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0638206382, ZO0038800388\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0297602976, ZO0565605656\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0561405614, ZO0281602816\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,41,591636,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0385003850, ZO0408604086\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0505605056, ZO0513605136\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0276702767, ZO0291702917\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0046600466, ZO0050800508\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,48,590970,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0455604556, ZO0680806808\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,46,591299,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0229002290, ZO0674406744\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0529905299, ZO0617006170\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,13,591175,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0299402994, ZO0433504335\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,21,591297,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0257502575, ZO0451704517\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,14,591149,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0584905849, ZO0578405784\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,27,591754,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0335803358, ZO0325903259\\" +" +`; + +exports[`dashboard Reporting Download CSV E-Commerce Data Downloads a filtered CSV export of a saved search panel 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,19,716724,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Shoes, Women's Clothing\\",EUR,45,591503,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0006400064, ZO0150601506\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0638206382, ZO0038800388\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0297602976, ZO0565605656\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0561405614, ZO0281602816\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,41,591636,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0385003850, ZO0408604086\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0505605056, ZO0513605136\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0276702767, ZO0291702917\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0046600466, ZO0050800508\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,48,590970,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0455604556, ZO0680806808\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,46,591299,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0229002290, ZO0674406744\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0529905299, ZO0617006170\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,13,591175,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0299402994, ZO0433504335\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,21,591297,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0257502575, ZO0451704517\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,14,591149,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0584905849, ZO0578405784\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,27,591754,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0335803358, ZO0325903259\\" +" +`; + +exports[`dashboard Reporting Download CSV Field Formatters and Scripted Fields Download CSV export of a saved search panel 1`] = ` +"date,\\"_id\\",name,gender,value,year,\\"years_ago\\",\\"date_informal\\" +\\"Jan 1, 1984 @ 00:00:00.000\\",\\"1984-Fethany-F\\",Fethany,F,5,1984,\\"35.00000000000000000000\\",\\"Jan 1st 84\\" +\\"Jan 1, 1983 @ 00:00:00.000\\",\\"1983-Fethany-F\\",Fethany,F,\\"1,043\\",1983,\\"36.00000000000000000000\\",\\"Jan 1st 83\\" +\\"Jan 1, 1982 @ 00:00:00.000\\",\\"1982-Fethany-F\\",Fethany,F,780,1982,\\"37.00000000000000000000\\",\\"Jan 1st 82\\" +\\"Jan 1, 1981 @ 00:00:00.000\\",\\"1981-Fethany-F\\",Fethany,F,655,1981,\\"38.00000000000000000000\\",\\"Jan 1st 81\\" +\\"Jan 1, 1980 @ 00:00:00.000\\",\\"1980-Fethany-F\\",Fethany,F,702,1980,\\"39.00000000000000000000\\",\\"Jan 1st 80\\" +" +`; diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index 72f07ef90d703..d4a909f6a0474 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -9,91 +9,139 @@ import { REPO_ROOT } from '@kbn/utils'; import expect from '@kbn/expect'; import fs from 'fs'; import path from 'path'; -import * as Rx from 'rxjs'; -import { filter, first, map, timeout } from 'rxjs/operators'; import { FtrProviderContext } from '../../../ftr_provider_context'; -const csvPath = path.resolve(REPO_ROOT, 'target/functional-tests/downloads/Ecommerce Data.csv'); - -// checks every 100ms for the file to exist in the download dir -// just wait up to 5 seconds -const getDownload$ = (filePath: string) => { - return Rx.interval(100).pipe( - map(() => fs.existsSync(filePath)), - filter((value) => value === true), - first(), - timeout(5000) - ); -}; - export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const dashboardPanelActions = getService('dashboardPanelActions'); const log = getService('log'); const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); const find = getService('find'); - const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); + const retry = getService('retry'); + const PageObjects = getPageObjects(['reporting', 'common', 'dashboard', 'timePicker']); + + const getCsvPath = (name: string) => + path.resolve(REPO_ROOT, `target/functional-tests/downloads/${name}.csv`); + + // checks every 100ms for the file to exist in the download dir + // just wait up to 5 seconds + const getDownload = (filePath: string) => { + return retry.tryForTime(5000, async () => { + expect(fs.existsSync(filePath)).to.be(true); + return fs.readFileSync(filePath).toString(); + }); + }; + + const clickActionsMenu = async (headingTestSubj: string) => { + const savedSearchPanel = await testSubjects.find('embeddablePanelHeading-' + headingTestSubj); + await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + }; + + const clickDownloadCsv = async () => { + log.debug('click "More"'); + await dashboardPanelActions.clickContextMenuMoreItem(); + + const actionItemTestSubj = 'embeddablePanelAction-downloadCsvReport'; + await testSubjects.existOrFail(actionItemTestSubj); // wait for the full panel to display or else the test runner could click the wrong option! + log.debug('click "Download CSV"'); + await testSubjects.click(actionItemTestSubj); + await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel + }; describe('Download CSV', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); - await esArchiver.loadIfNeeded('reporting/ecommerce'); - await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); await browser.setWindowSize(1600, 850); }); - after('clean up archives and previous file download', async () => { - await esArchiver.unload('reporting/ecommerce'); - await esArchiver.unload('reporting/ecommerce_kibana'); - }); - afterEach('remove download', () => { try { - fs.unlinkSync(csvPath); + fs.unlinkSync(getCsvPath('Ecommerce Data')); } catch (e) { // it might not have been there to begin with } }); - it('Downloads a CSV export of a saved search panel', async function () { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); - const savedSearchPanel = await testSubjects.find('embeddablePanelHeading-EcommerceData'); - await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + describe('E-Commerce Data', () => { + before(async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); + }); + after(async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); - const actionExists = await testSubjects.exists('embeddablePanelAction-downloadCsvReport'); - if (!actionExists) { - await dashboardPanelActions.clickContextMenuMoreItem(); - } - await testSubjects.existOrFail('embeddablePanelAction-downloadCsvReport'); // wait for the full panel to display or else the test runner could click the wrong option! - await testSubjects.click('embeddablePanelAction-downloadCsvReport'); - await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel + it('Download CSV export of a saved search panel', async function () { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await clickActionsMenu('EcommerceData'); + await clickDownloadCsv(); + + const csvFile = await getDownload(getCsvPath('Ecommerce Data')); + expectSnapshot(csvFile).toMatch(); + }); - const fileExists = await getDownload$(csvPath).toPromise(); - expect(fileExists).to.be(true); + it('Downloads a filtered CSV export of a saved search panel', async function () { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); - // no need to validate download contents, API Integration tests do that some different variations + // add a filter + await filterBar.addFilter('currency', 'is', 'EUR'); + + await clickActionsMenu('EcommerceData'); + await clickDownloadCsv(); + + const csvFile = await getDownload(getCsvPath('Ecommerce Data')); + expectSnapshot(csvFile).toMatch(); + }); + + it('Gets the correct filename if panel titles are hidden', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard Hidden Panel Titles'); + const savedSearchPanel = await find.byCssSelector( + '[data-test-embeddable-id="94eab06f-60ac-4a85-b771-3a8ed475c9bb"]' + ); // panel title is hidden + await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + + await clickDownloadCsv(); + await testSubjects.existOrFail('csvDownloadStarted'); + + const csvFile = await getDownload(getCsvPath('Ecommerce Data')); // file exists with proper name + expect(csvFile).to.not.be(null); + }); }); - it('Gets the correct filename if panel titles are hidden', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard Hidden Panel Titles'); - const savedSearchPanel = await find.byCssSelector( - '[data-test-embeddable-id="94eab06f-60ac-4a85-b771-3a8ed475c9bb"]' - ); // panel title is hidden - await dashboardPanelActions.toggleContextMenu(savedSearchPanel); - - const actionExists = await testSubjects.exists('embeddablePanelAction-downloadCsvReport'); - if (!actionExists) { - await dashboardPanelActions.clickContextMenuMoreItem(); - } - await testSubjects.existOrFail('embeddablePanelAction-downloadCsvReport'); - await testSubjects.click('embeddablePanelAction-downloadCsvReport'); - await testSubjects.existOrFail('csvDownloadStarted'); + describe('Field Formatters and Scripted Fields', () => { + before(async () => { + await esArchiver.load('reporting/hugedata'); + }); + after(async () => { + await esArchiver.unload('reporting/hugedata'); + }); + + it('Download CSV export of a saved search panel', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('names dashboard'); + await PageObjects.timePicker.setAbsoluteRange( + 'Jan 01, 1980 @ 00:00:00.000', + 'Dec 31, 1984 @ 23:59:59.000' + ); + + await PageObjects.common.sleep(1000); + + await filterBar.addFilter('name.keyword', 'is', 'Fethany'); + + await PageObjects.common.sleep(1000); + + await clickActionsMenu('namessearch'); + await clickDownloadCsv(); - const fileExists = await getDownload$(csvPath).toPromise(); // file exists with proper name - expect(fileExists).to.be(true); + const csvFile = await getDownload(getCsvPath('namessearch')); + expectSnapshot(csvFile).toMatch(); + }); }); }); } diff --git a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap index 43771b00525cc..5ddef936b41ae 100644 --- a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap +++ b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap @@ -1,40 +1,88 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`discover Discover Generate CSV: archived search generates a report with data 1`] = ` -"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"order_date\\",\\"products.created_on\\",sku -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\",\\"\\"Women's Accessories\\"\\",\\"\\"Men's Accessories\\"\\"]\\",EUR,19,716724,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0687606876\\"\\",\\"\\"ZO0290502905\\"\\",\\"\\"ZO0126701267\\"\\",\\"\\"ZO0308503085\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Shoes\\"\\",\\"\\"Women's Clothing\\"\\"]\\",EUR,45,591503,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0006400064\\"\\",\\"\\"ZO0150601506\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0638206382\\"\\",\\"\\"ZO0038800388\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0297602976\\"\\",\\"\\"ZO0565605656\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0561405614\\"\\",\\"\\"ZO0281602816\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\"]\\",EUR,41,591636,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0385003850\\"\\",\\"\\"ZO0408604086\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0505605056\\"\\",\\"\\"ZO0513605136\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0276702767\\"\\",\\"\\"ZO0291702917\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0046600466\\"\\",\\"\\"ZO0050800508\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Clothing\\"\\",\\"\\"Men's Shoes\\"\\"]\\",EUR,48,590970,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0455604556\\"\\",\\"\\"ZO0680806808\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Clothing\\"\\",\\"\\"Women's Shoes\\"\\"]\\",EUR,46,591299,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0229002290\\"\\",\\"\\"ZO0674406744\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0529905299\\"\\",\\"\\"ZO0617006170\\"\\"]\\" +exports[`discover Discover CSV Export Generate CSV: archived search generates a report with data 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,19,716724,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Shoes, Women's Clothing\\",EUR,45,591503,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0006400064, ZO0150601506\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0638206382, ZO0038800388\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0297602976, ZO0565605656\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0561405614, ZO0281602816\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,41,591636,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0385003850, ZO0408604086\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0505605056, ZO0513605136\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0276702767, ZO0291702917\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0046600466, ZO0050800508\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,48,590970,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0455604556, ZO0680806808\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,46,591299,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0229002290, ZO0674406744\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0529905299, ZO0617006170\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,13,591175,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0299402994, ZO0433504335\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,21,591297,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0257502575, ZO0451704517\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,14,591149,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0584905849, ZO0578405784\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,27,591754,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0335803358, ZO0325903259\\" " `; -exports[`discover Discover Generate CSV: archived search generates a report with filtered data 1`] = ` -"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"order_date\\",\\"products.created_on\\",sku -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\",\\"\\"Women's Accessories\\"\\",\\"\\"Men's Accessories\\"\\"]\\",EUR,19,716724,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0687606876\\"\\",\\"\\"ZO0290502905\\"\\",\\"\\"ZO0126701267\\"\\",\\"\\"ZO0308503085\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Shoes\\"\\",\\"\\"Women's Clothing\\"\\"]\\",EUR,45,591503,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0006400064\\"\\",\\"\\"ZO0150601506\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0638206382\\"\\",\\"\\"ZO0038800388\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0297602976\\"\\",\\"\\"ZO0565605656\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0561405614\\"\\",\\"\\"ZO0281602816\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Shoes\\"\\",\\"\\"Men's Clothing\\"\\"]\\",EUR,41,591636,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0385003850\\"\\",\\"\\"ZO0408604086\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0505605056\\"\\",\\"\\"ZO0513605136\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0276702767\\"\\",\\"\\"ZO0291702917\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0046600466\\"\\",\\"\\"ZO0050800508\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Men's Clothing\\"\\",\\"\\"Men's Shoes\\"\\"]\\",EUR,48,590970,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0455604556\\"\\",\\"\\"ZO0680806808\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Women's Clothing\\"\\",\\"\\"Women's Shoes\\"\\"]\\",EUR,46,591299,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0229002290\\"\\",\\"\\"ZO0674406744\\"\\"]\\" -\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Jul 12, 2019 @ 00:00:00.000\\",\\"[\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\",\\"\\"Dec 31, 2016 @ 00:00:00.000\\"\\"]\\",\\"[\\"\\"ZO0529905299\\"\\",\\"\\"ZO0617006170\\"\\"]\\" +exports[`discover Discover CSV Export Generate CSV: archived search generates a report with discover:searchFieldsFromSource = true 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,19,716724,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Shoes, Women's Clothing\\",EUR,45,591503,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0006400064, ZO0150601506\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0638206382, ZO0038800388\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0297602976, ZO0565605656\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0561405614, ZO0281602816\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,41,591636,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0385003850, ZO0408604086\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0505605056, ZO0513605136\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0276702767, ZO0291702917\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0046600466, ZO0050800508\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,48,590970,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0455604556, ZO0680806808\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,46,591299,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0229002290, ZO0674406744\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0529905299, ZO0617006170\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,13,591175,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0299402994, ZO0433504335\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,21,591297,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0257502575, ZO0451704517\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,14,591149,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0584905849, ZO0578405784\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,27,591754,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0335803358, ZO0325903259\\" " `; -exports[`discover Discover Generate CSV: new search generates a report with data 1`] = ` +exports[`discover Discover CSV Export Generate CSV: archived search generates a report with filtered data 1`] = ` +"\\"order_date\\",category,currency,\\"customer_id\\",\\"order_id\\",\\"day_of_week_i\\",\\"products.created_on\\",sku +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,19,716724,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Shoes, Women's Clothing\\",EUR,45,591503,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0006400064, ZO0150601506\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,12,591709,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0638206382, ZO0038800388\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,52,590937,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0297602976, ZO0565605656\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,29,590976,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0561405614, ZO0281602816\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,41,591636,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0385003850, ZO0408604086\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes\\",EUR,30,591539,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0505605056, ZO0513605136\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,41,591598,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0276702767, ZO0291702917\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing\\",EUR,44,590927,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0046600466, ZO0050800508\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing, Men's Shoes\\",EUR,48,590970,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0455604556, ZO0680806808\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,46,591299,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0229002290, ZO0674406744\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,36,591133,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0529905299, ZO0617006170\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,13,591175,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0299402994, ZO0433504335\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Shoes, Men's Clothing\\",EUR,21,591297,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0257502575, ZO0451704517\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Men's Clothing\\",EUR,14,591149,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0584905849, ZO0578405784\\" +\\"Jul 12, 2019 @ 00:00:00.000\\",\\"Women's Clothing, Women's Shoes\\",EUR,27,591754,5,\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"ZO0335803358, ZO0325903259\\" +" +`; + +exports[`discover Discover CSV Export Generate CSV: new search generates a report from a new search with data: default 1`] = ` +"\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user +3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,,Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ + \\"\\"coordinates\\"\\": [ + 54.4, + 24.5 + ], + \\"\\"type\\"\\": \\"\\"Point\\"\\" +}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.39, 32.99, 10.34, 6.11\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"173.96\\",\\"173.96\\",4,4,order,sultan +" +`; + +exports[`discover Discover CSV Export Generate CSV: new search generates a report from a new search with data: discover:searchFieldsFromSource 1`] = ` "\\"_id\\",\\"_index\\",\\"_score\\",\\"_type\\",category,\\"category.keyword\\",currency,\\"customer_first_name\\",\\"customer_first_name.keyword\\",\\"customer_full_name\\",\\"customer_full_name.keyword\\",\\"customer_gender\\",\\"customer_id\\",\\"customer_last_name\\",\\"customer_last_name.keyword\\",\\"customer_phone\\",\\"day_of_week\\",\\"day_of_week_i\\",email,\\"geoip.city_name\\",\\"geoip.continent_name\\",\\"geoip.country_iso_code\\",\\"geoip.location\\",\\"geoip.region_name\\",manufacturer,\\"manufacturer.keyword\\",\\"order_date\\",\\"order_id\\",\\"products._id\\",\\"products._id.keyword\\",\\"products.base_price\\",\\"products.base_unit_price\\",\\"products.category\\",\\"products.category.keyword\\",\\"products.created_on\\",\\"products.discount_amount\\",\\"products.discount_percentage\\",\\"products.manufacturer\\",\\"products.manufacturer.keyword\\",\\"products.min_price\\",\\"products.price\\",\\"products.product_id\\",\\"products.product_name\\",\\"products.product_name.keyword\\",\\"products.quantity\\",\\"products.sku\\",\\"products.tax_amount\\",\\"products.taxful_price\\",\\"products.taxless_price\\",\\"products.unit_discount_amount\\",sku,\\"taxful_total_price\\",\\"taxless_total_price\\",\\"total_quantity\\",\\"total_unique_products\\",type,user +3AMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",EUR,\\"Sultan Al\\",\\"Sultan Al\\",\\"Sultan Al Boone\\",\\"Sultan Al Boone\\",MALE,19,Boone,Boone,,Saturday,5,\\"sultan al@boone-family.zzz\\",\\"Abu Dhabi\\",Asia,AE,\\"{ + \\"\\"coordinates\\"\\": [ + 54.4, + 24.5 + ], + \\"\\"type\\"\\": \\"\\"Point\\"\\" +}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.39, 32.99, 10.34, 6.11\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"173.96\\",\\"173.96\\",4,4,order,sultan " `; diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index dfc44a8e0e12d..d7dd961e2f103 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -12,18 +12,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const es = getService('es'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); const PageObjects = getPageObjects(['reporting', 'common', 'discover', 'timePicker']); const filterBar = getService('filterBar'); - describe('Discover', () => { + const setFieldsFromSource = async (setValue: boolean) => { + await kibanaServer.uiSettings.update({ 'discover:searchFieldsFromSource': setValue }); + }; + + describe('Discover CSV Export', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); - await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); await browser.setWindowSize(1600, 850); }); after('clean up archives', async () => { await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); await es.deleteByQuery({ index: '.reporting-*', refresh: true, @@ -31,7 +38,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('Generate CSV: new search', () => { + describe('Check Available', () => { beforeEach(() => PageObjects.common.navigateToApp('discover')); it('is not available if new', async () => { @@ -63,8 +70,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.reporting.openCsvReportingPanel(); expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); }); + }); - it('generates a report with data', async () => { + describe('Generate CSV: new search', () => { + beforeEach(async () => { + await esArchiver.load('reporting/ecommerce_kibana'); // reload the archive to wipe out changes made by each test + await PageObjects.common.navigateToApp('discover'); + }); + + it('generates a report from a new search with data: default', async () => { await PageObjects.discover.clickNewSearchButton(); await PageObjects.reporting.setTimepickerInDataRange(); await PageObjects.discover.saveSearch('my search - with data - expectReportCanBeCreated'); @@ -79,6 +93,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expectSnapshot(res.text).toMatch(); }); + it('generates a report from a new search with data: discover:searchFieldsFromSource', async () => { + await setFieldsFromSource(true); + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.reporting.setTimepickerInDataRange(); + await PageObjects.discover.saveSearch( + 'my search - with fieldsFromSource data - expectReportCanBeCreated' + ); + await PageObjects.reporting.openCsvReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + expectSnapshot(res.text).toMatch(); + await setFieldsFromSource(false); + }); + it('generates a report with no data', async () => { await PageObjects.reporting.setTimepickerInNoDataRange(); await PageObjects.discover.saveSearch('my search - no data - expectReportCanBeCreated'); @@ -98,6 +131,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); describe('Generate CSV: archived search', () => { + const setupPage = async () => { + const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; + const toTime = 'Aug 23, 2019 @ 16:18:51.821'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + }; + + const getReport = async () => { + await PageObjects.reporting.openCsvReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + return res; + }; + before(async () => { await esArchiver.load('reporting/ecommerce'); await esArchiver.load('reporting/ecommerce_kibana'); @@ -111,41 +162,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(() => PageObjects.common.navigateToApp('discover')); it('generates a report with data', async () => { + await setupPage(); await PageObjects.discover.loadSavedSearch('Ecommerce Data'); - const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; - const toTime = 'Aug 23, 2019 @ 16:18:51.821'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.reporting.openCsvReportingPanel(); - await PageObjects.reporting.clickGenerateReportButton(); - - const url = await PageObjects.reporting.getReportURL(60000); - const res = await PageObjects.reporting.getResponse(url); - - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); - expectSnapshot(res.text).toMatch(); + const { text } = await getReport(); + expectSnapshot(text).toMatch(); }); it('generates a report with filtered data', async () => { + await setupPage(); await PageObjects.discover.loadSavedSearch('Ecommerce Data'); - const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; - const toTime = 'Aug 23, 2019 @ 16:18:51.821'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); // filter and re-save await filterBar.addFilter('currency', 'is', 'EUR'); - await PageObjects.discover.saveSearch(`Ecommerce Data: EUR Filtered`); + await PageObjects.discover.saveSearch(`Ecommerce Data: EUR Filtered`); // renamed the search - await PageObjects.reporting.openCsvReportingPanel(); - await PageObjects.reporting.clickGenerateReportButton(); + const { text } = await getReport(); + expectSnapshot(text).toMatch(); + await PageObjects.discover.saveSearch(`Ecommerce Data`); // rename the search back for the next test + }); - const url = await PageObjects.reporting.getReportURL(60000); - const res = await PageObjects.reporting.getResponse(url); + it('generates a report with discover:searchFieldsFromSource = true', async () => { + await setupPage(); + await PageObjects.discover.loadSavedSearch('Ecommerce Data'); - expect(res.status).to.equal(200); - expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); - expectSnapshot(res.text).toMatch(); + await setFieldsFromSource(true); + await browser.refresh(); + + const { text } = await getReport(); + expectSnapshot(text).toMatch(); + await setFieldsFromSource(false); }); }); }); diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz index 7736287bc9a37e5203c887d89bcfbe7d98fe1dd9..e1710365c4b414dc0c0071e9a2317512580a625b 100644 GIT binary patch delta 58013 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 aYUPK@`ti!#h{gQ_LMGQ5w^2qL!v6rllKrRv diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce/mappings.json b/x-pack/test/functional/es_archives/reporting/ecommerce/mappings.json index 9e3275bd40bfe..6b474059a8e7a 100644 --- a/x-pack/test/functional/es_archives/reporting/ecommerce/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/ecommerce/mappings.json @@ -209,1018 +209,3 @@ } } } - -{ - "type": "index", - "value": { - "aliases": { - ".kibana": { - } - }, - "index": ".kibana_1", - "mappings": { - "_meta": { - "migrationMappingPropertyHashes": { - "apm-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "canvas-element": "7390014e1091044523666d97247392fc", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "namespace": "2f4316de49999235636386fe51dc06c1", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "siem-ui-timeline": "1f6f0860ad7bc0dba3e42467ca40470d", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "space": "25de8c2deec044392922989cfcf24c54", - "telemetry": "e1c8bc94e443aefd9458932cc0697a4d", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "type": "2f4316de49999235636386fe51dc06c1", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "visualization": "52d7a13ad68a150c4525b292d23e12cc" - } - }, - "dynamic": "strict", - "properties": { - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "null_value": 0, - "type": "long" - }, - "go": { - "null_value": 0, - "type": "long" - }, - "java": { - "null_value": 0, - "type": "long" - }, - "js-base": { - "null_value": 0, - "type": "long" - }, - "nodejs": { - "null_value": 0, - "type": "long" - }, - "python": { - "null_value": 0, - "type": "long" - }, - "ruby": { - "null_value": 0, - "type": "long" - }, - "rum-js": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "fields": { - "keyword": { - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "defaultIndex": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - }, - "type": "nested" - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "mapsTotalCount": { - "type": "long" - }, - "timeCaptured": { - "type": "date" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "space": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "index": false, - "type": "keyword" - } - } - }, - "timefilter": { - "enabled": false, - "type": "object" - }, - "title": { - "type": "text" - } - } - }, - "references": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "null_value": true, - "type": "boolean" - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "null_value": 0, - "type": "long" - }, - "indices": { - "null_value": 0, - "type": "long" - }, - "overview": { - "null_value": 0, - "type": "long" - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "null_value": 0, - "type": "long" - }, - "open": { - "null_value": 0, - "type": "long" - }, - "start": { - "null_value": 0, - "type": "long" - }, - "stop": { - "null_value": 0, - "type": "long" - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "fields": { - "keyword": { - "ignore_above": 2048, - "type": "keyword" - } - }, - "type": "text" - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json index a0f384a96e6b4..64d04ec6f49a9 100644 --- a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json +++ b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana/data.json @@ -1,31 +1,449 @@ -{ "type": "doc", "value": { "id": "config:7.0.0", "index": ".kibana_1", "source": { "config": { "buildNum": 9007199254740991, "dateFormat:tz": "UTC", "defaultIndex": "5193f870-d861-11e9-a311-0fa548c5f953" }, "migrationVersion": { "config": "7.13.0" }, "references": [ ], "type": "config", "updated_at": "2019-09-16T09:06:51.201Z" } } } +{ + "type": "doc", + "value": { + "id": "config:7.0.0", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": 9007199254740991, + "dateFormat:tz": "UTC", + "defaultIndex": "5193f870-d861-11e9-a311-0fa548c5f953" + }, + "migrationVersion": { + "config": "7.13.0" + }, + "references": [], + "type": "config", + "updated_at": "2019-09-16T09:06:51.201Z" + } + } +} -{ "type": "doc", "value": { "id": "config:8.0.0", "index": ".kibana_1", "source": { "config": { "buildNum": 9007199254740991, "dateFormat:tz": "UTC", "defaultIndex": "5193f870-d861-11e9-a311-0fa548c5f953" }, "migrationVersion": { "config": "7.13.0" }, "references": [ ], "type": "config", "updated_at": "2019-12-11T23:22:12.698Z" } } } +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": 9007199254740991, + "dateFormat:tz": "UTC", + "defaultIndex": "5193f870-d861-11e9-a311-0fa548c5f953" + }, + "migrationVersion": { + "config": "7.13.0" + }, + "references": [], + "type": "config", + "updated_at": "2019-12-11T23:22:12.698Z" + } + } +} -{ "type": "doc", "value": { "id": "index-pattern:5193f870-d861-11e9-a311-0fa548c5f953", "index": ".kibana_1", "source": { "index-pattern": { "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"category\"}}},{\"name\":\"currency\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_birth_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_first_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_first_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_first_name\"}}},{\"name\":\"customer_full_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_full_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_full_name\"}}},{\"name\":\"customer_gender\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_last_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_last_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_last_name\"}}},{\"name\":\"customer_phone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week_i\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"manufacturer\"}}},{\"name\":\"order_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"order_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products._id\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products._id.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products._id\"}}},{\"name\":\"products.base_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.base_unit_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.category\"}}},{\"name\":\"products.created_on\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_percentage\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.manufacturer\"}}},{\"name\":\"products.min_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.product_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.product_name\"}}},{\"name\":\"products.quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.tax_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxful_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxless_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.unit_discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxful_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxless_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_unique_products\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", "timeFieldName": "order_date", "title": "ecommerce" }, "migrationVersion": { "index-pattern": "7.11.0" }, "references": [ ], "type": "index-pattern", "updated_at": "2019-12-11T23:24:13.381Z" } } } +{ + "type": "doc", + "value": { + "id": "index-pattern:5193f870-d861-11e9-a311-0fa548c5f953", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"category\"}}},{\"name\":\"currency\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_birth_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_first_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_first_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_first_name\"}}},{\"name\":\"customer_full_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_full_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_full_name\"}}},{\"name\":\"customer_gender\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"customer_last_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"customer_last_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"customer_last_name\"}}},{\"name\":\"customer_phone\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"day_of_week_i\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"email\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.city_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.continent_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.country_iso_code\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.location\",\"type\":\"geo_point\",\"esTypes\":[\"geo_point\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geoip.region_name\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"manufacturer\"}}},{\"name\":\"order_date\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"order_id\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products._id\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products._id.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products._id\"}}},{\"name\":\"products.base_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.base_unit_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.category\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.category.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.category\"}}},{\"name\":\"products.created_on\",\"type\":\"date\",\"esTypes\":[\"date\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.discount_percentage\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.manufacturer\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.manufacturer.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.manufacturer\"}}},{\"name\":\"products.min_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_id\",\"type\":\"number\",\"esTypes\":[\"long\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.product_name\",\"type\":\"string\",\"esTypes\":[\"text\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"products.product_name.keyword\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true,\"subType\":{\"multi\":{\"parent\":\"products.product_name\"}}},{\"name\":\"products.quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.tax_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxful_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.taxless_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"products.unit_discount_amount\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"sku\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":1,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxful_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"taxless_total_price\",\"type\":\"number\",\"esTypes\":[\"half_float\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_quantity\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"total_unique_products\",\"type\":\"number\",\"esTypes\":[\"integer\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"user\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "order_date", + "title": "ecommerce" + }, + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2019-12-11T23:24:13.381Z" + } + } +} -{ "type": "doc", "value": { "id": "search:6091ead0-1c6d-11ea-a100-8589bb9d7c6b", "index": ".kibana_1", "source": { "migrationVersion": { "search": "7.9.3" }, "references": [ { "id": "5193f870-d861-11e9-a311-0fa548c5f953", "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } ], "search": { "columns": [ "category", "currency", "customer_id", "order_id", "day_of_week_i", "order_date", "products.created_on", "sku" ], "description": "", "hits": 0, "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" }, "sort": [ [ "order_date", "desc" ] ], "title": "Ecommerce Data", "version": 1 }, "type": "search", "updated_at": "2019-12-11T23:24:28.540Z" } } } +{ + "type": "doc", + "value": { + "id": "search:6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "search": "7.9.3" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "search": { + "columns": [ + "order_date", + "category", + "currency", + "customer_id", + "order_id", + "day_of_week_i", + "products.created_on", + "sku" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "sort": [ + [ + "order_date", + "desc" + ] + ], + "title": "Ecommerce Data", + "version": 1 + }, + "type": "search", + "updated_at": "2019-12-11T23:24:28.540Z" + } + } +} -{ "type": "doc", "value": { "id": "dashboard:constructed-sample-saved-object-id", "index": ".kibana_1", "source": { "dashboard": { "description": "", "hits": 0, "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" }, "optionsJSON": "{\"hidePanelTitles\":true,\"useMargins\":true}", "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":33,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":41,\"w\":11,\"h\":10,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":11,\"y\":41,\"w\":5,\"h\":10,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"}]", "refreshInterval": { "pause": true, "value": 0 }, "timeFrom": "2019-06-26T06:20:28.066Z", "timeRestore": true, "timeTo": "2019-06-26T07:27:58.573Z", "title": "Ecom Dashboard Hidden Panel Titles", "version": 1 }, "migrationVersion": { "dashboard": "7.11.0" }, "references": [ { "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", "name": "panel_0", "type": "visualization" }, { "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", "name": "panel_1", "type": "visualization" }, { "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", "name": "panel_2", "type": "search" }, { "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", "name": "panel_3", "type": "visualization" }, { "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", "name": "panel_4", "type": "visualization" }, { "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", "name": "panel_5", "type": "visualization" } ], "type": "dashboard", "updated_at": "2020-04-10T00:37:48.462Z" } } } +{ + "type": "doc", + "value": { + "id": "dashboard:constructed-sample-saved-object-id", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":true,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":33,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":41,\"w\":11,\"h\":10,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":11,\"y\":41,\"w\":5,\"h\":10,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-06-26T06:20:28.066Z", + "timeRestore": true, + "timeTo": "2019-06-26T07:27:58.573Z", + "title": "Ecom Dashboard Hidden Panel Titles", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.11.0" + }, + "references": [ + { + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "panel_2", + "type": "search" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_5", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2020-04-10T00:37:48.462Z" + } + } +} -{ "type": "doc", "value": { "id": "visualization:0a464230-79f0-11ea-ae7f-13c5d6e410a0", "index": ".kibana_1", "source": { "migrationVersion": { "visualization": "7.12.0" }, "references": [ { "id": "5193f870-d861-11e9-a311-0fa548c5f953", "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } ], "type": "visualization", "updated_at": "2020-04-08T23:24:05.971Z", "visualization": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" }, "title": "e-commerce area chart", "uiStateJSON": "{}", "version": 1, "visState": "{\"type\":\"area\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"title\":\"e-commerce area chart\"}" } } } } +{ + "type": "doc", + "value": { + "id": "visualization:0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.12.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-08T23:24:05.971Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "e-commerce area chart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"area\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{},\"isVislibVis\":true,\"detailedTooltip\":true,\"fittingFunction\":\"zero\"},\"title\":\"e-commerce area chart\"}" + } + } + } +} -{ "type": "doc", "value": { "id": "visualization:200609c0-79f0-11ea-ae7f-13c5d6e410a0", "index": ".kibana_1", "source": { "migrationVersion": { "visualization": "7.12.0" }, "references": [ { "id": "5193f870-d861-11e9-a311-0fa548c5f953", "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } ], "type": "visualization", "updated_at": "2020-04-08T23:24:42.460Z", "visualization": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" }, "title": "e-commerce pie chart", "uiStateJSON": "{}", "version": 1, "visState": "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"title\":\"e-commerce pie chart\"}" } } } } +{ + "type": "doc", + "value": { + "id": "visualization:200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.12.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-08T23:24:42.460Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "e-commerce pie chart", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"order_date\",\"timeRange\":{\"from\":\"2019-06-26T06:20:28.066Z\",\"to\":\"2019-06-26T07:27:58.573Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"title\":\"e-commerce pie chart\"}" + } + } + } +} -{ "type": "doc", "value": { "id": "visualization:ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", "index": ".kibana_1", "source": { "migrationVersion": { "visualization": "7.12.0" }, "references": [ { "id": "5193f870-d861-11e9-a311-0fa548c5f953", "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } ], "type": "visualization", "updated_at": "2020-04-10T00:33:44.909Z", "visualization": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" }, "title": "게이지", "uiStateJSON": "{}", "version": 1, "visState": "{\"type\":\"gauge\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"rgba(105,112,125,0.2)\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"rgba(105,112,125,0.2)\",\"bgColor\":true,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"게이지\"}" } } } } +{ + "type": "doc", + "value": { + "id": "visualization:ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.12.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-10T00:33:44.909Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" + }, + "title": "게이지", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"gauge\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":false,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"rgba(105,112,125,0.2)\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"rgba(105,112,125,0.2)\",\"bgColor\":true,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"게이지\"}" + } + } + } +} -{ "type": "doc", "value": { "id": "visualization:132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", "index": ".kibana_1", "source": { "migrationVersion": { "visualization": "7.12.0" }, "references": [ { "id": "5193f870-d861-11e9-a311-0fa548c5f953", "name": "kibanaSavedObjectMeta.searchSourceJSON.index", "type": "index-pattern" } ], "type": "visualization", "updated_at": "2020-04-10T00:34:44.700Z", "visualization": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" }, "title": "Українська", "uiStateJSON": "{}", "version": 1, "visState": "{\"type\":\"metric\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"Українська\"}" } } } } +{ + "type": "doc", + "value": { + "id": "visualization:132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.12.0" + }, + "references": [ + { + "id": "5193f870-d861-11e9-a311-0fa548c5f953", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2020-04-10T00:34:44.700Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"filter\":[]}" + }, + "title": "Українська", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"metric\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}],\"params\":{\"addTooltip\":true,\"addLegend\":false,\"type\":\"metric\",\"metric\":{\"percentageMode\":false,\"useRanges\":false,\"colorSchema\":\"Green to Red\",\"metricColorMode\":\"None\",\"colorsRange\":[{\"from\":0,\"to\":10000}],\"labels\":{\"show\":true},\"invertColors\":false,\"style\":{\"bgFill\":\"#000\",\"bgColor\":false,\"labelColor\":false,\"subText\":\"\",\"fontSize\":60}}},\"title\":\"Українська\"}" + } + } + } +} -{ "type": "doc", "value": { "id": "visualization:4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", "index": ".kibana_1", "source": { "migrationVersion": { "visualization": "7.12.0" }, "references": [ ], "type": "visualization", "updated_at": "2020-04-10T00:36:17.053Z", "visualization": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" }, "title": "Tiểu thuyết", "uiStateJSON": "{}", "version": 1, "visState": "{\"type\":\"markdown\",\"aggs\":[],\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"Tiểu thuyết là một thể loại văn xuôi có hư cấu, thông qua nhân vật, hoàn cảnh, sự việc để phản ánh bức tranh xã hội rộng lớn và những vấn đề của cuộc sống con người, biểu hiện tính chất tường thuật, tính chất kể chuyện bằng ngôn ngữ văn xuôi theo những chủ đề xác định.\\n\\nTrong một cách hiểu khác, nhận định của Belinski: \\\"tiểu thuyết là sử thi của đời tư\\\" chỉ ra khái quát nhất về một dạng thức tự sự, trong đó sự trần thuật tập trung vào số phận của một cá nhân trong quá trình hình thành và phát triển của nó. Sự trần thuật ở đây được khai triển trong không gian và thời gian nghệ thuật đến mức đủ để truyền đạt cơ cấu của nhân cách[1].\\n\\n\\n[1]^ Mục từ Tiểu thuyết trong cuốn 150 thuật ngữ văn học, Lại Nguyên Ân biên soạn, Nhà xuất bản Đại học Quốc gia Hà Nội, in lần thứ 2 có sửa đổi bổ sung. H. 2003. Trang 326.\"},\"title\":\"Tiểu thuyết\"}" } } } } +{ + "type": "doc", + "value": { + "id": "visualization:4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.12.0" + }, + "references": [], + "type": "visualization", + "updated_at": "2020-04-10T00:36:17.053Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "Tiểu thuyết", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"type\":\"markdown\",\"aggs\":[],\"params\":{\"fontSize\":12,\"openLinksInNewTab\":false,\"markdown\":\"Tiểu thuyết là một thể loại văn xuôi có hư cấu, thông qua nhân vật, hoàn cảnh, sự việc để phản ánh bức tranh xã hội rộng lớn và những vấn đề của cuộc sống con người, biểu hiện tính chất tường thuật, tính chất kể chuyện bằng ngôn ngữ văn xuôi theo những chủ đề xác định.\\n\\nTrong một cách hiểu khác, nhận định của Belinski: \\\"tiểu thuyết là sử thi của đời tư\\\" chỉ ra khái quát nhất về một dạng thức tự sự, trong đó sự trần thuật tập trung vào số phận của một cá nhân trong quá trình hình thành và phát triển của nó. Sự trần thuật ở đây được khai triển trong không gian và thời gian nghệ thuật đến mức đủ để truyền đạt cơ cấu của nhân cách[1].\\n\\n\\n[1]^ Mục từ Tiểu thuyết trong cuốn 150 thuật ngữ văn học, Lại Nguyên Ân biên soạn, Nhà xuất bản Đại học Quốc gia Hà Nội, in lần thứ 2 có sửa đổi bổ sung. H. 2003. Trang 326.\"},\"title\":\"Tiểu thuyết\"}" + } + } + } +} -{ "type": "doc", "value": { "id": "space:default", "index": ".kibana_1", "source": { "space": { "_reserved": true, "description": "This is the default space", "disabledFeatures": [ ], "name": "Default Space" }, "type": "space", "updated_at": "2021-01-07T00:17:12.785Z" } } } +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "space": { + "_reserved": true, + "description": "This is the default space", + "disabledFeatures": [], + "name": "Default Space" + }, + "type": "space", + "updated_at": "2021-01-07T00:17:12.785Z" + } + } +} -{ "type": "doc", "value": { "id": "ui-counter:visualize:06012021:click:tagcloud", "index": ".kibana_1", "source": { "type": "ui-counter", "ui-counter": { "count": 1 }, "updated_at": "2021-01-07T00:18:52.592Z" } } } +{ + "type": "doc", + "value": { + "id": "dashboard:6c263e00-1c6d-11ea-a100-8589bb9d7c6b", + "index": ".kibana_1", + "source": { + "dashboard": { + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + }, + "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", + "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":35,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":23,\"w\":16,\"h\":12,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":16,\"y\":23,\"w\":12,\"h\":12,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":28,\"y\":23,\"w\":20,\"h\":12,\"i\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\"},\"panelIndex\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]", + "refreshInterval": { + "pause": true, + "value": 0 + }, + "timeFrom": "2019-03-23T03:06:17.785Z", + "timeRestore": true, + "timeTo": "2019-10-04T02:33:16.708Z", + "title": "Ecom Dashboard", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.11.0" + }, + "references": [ + { + "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_0", + "type": "visualization" + }, + { + "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", + "name": "panel_1", + "type": "visualization" + }, + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "panel_2", + "type": "search" + }, + { + "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", + "name": "panel_4", + "type": "visualization" + }, + { + "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", + "name": "panel_5", + "type": "visualization" + }, + { + "id": "1bba55f0-507e-11eb-9c0d-97106882b997", + "name": "panel_6", + "type": "visualization" + } + ], + "type": "dashboard", + "updated_at": "2021-01-07T00:22:16.102Z" + } + } +} -{ "type": "doc", "value": { "id": "ui-counter:data_plugin:06012021:click:discover:query_submitted", "index": ".kibana_1", "source": { "type": "ui-counter", "ui-counter": { "count": 1 }, "updated_at": "2021-01-07T00:18:52.592Z" } } } - -{ "type": "doc", "value": { "id": "dashboard:6c263e00-1c6d-11ea-a100-8589bb9d7c6b", "index": ".kibana_1", "source": { "dashboard": { "description": "", "hits": 0, "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" }, "optionsJSON": "{\"hidePanelTitles\":false,\"useMargins\":true}", "panelsJSON": "[{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\"},\"panelIndex\":\"1c12c2f2-80c2-4d5c-b722-55b2415006e1\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_0\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\"},\"panelIndex\":\"1c4b99e1-7785-444f-a1c5-f592893b1a96\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_1\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":35,\"w\":48,\"h\":18,\"i\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\"},\"panelIndex\":\"94eab06f-60ac-4a85-b771-3a8ed475c9bb\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_2\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":15,\"w\":48,\"h\":8,\"i\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\"},\"panelIndex\":\"52c19b6b-7117-42ac-a74e-c507a1c3ffc0\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_3\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":0,\"y\":23,\"w\":16,\"h\":12,\"i\":\"a1e889dc-b80e-4937-a576-979f34d1859b\"},\"panelIndex\":\"a1e889dc-b80e-4937-a576-979f34d1859b\",\"embeddableConfig\":{\"enhancements\":{},\"vis\":null},\"panelRefName\":\"panel_4\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":16,\"y\":23,\"w\":12,\"h\":12,\"i\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\"},\"panelIndex\":\"4930b035-d756-4cc5-9a18-1af9e67d6f31\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_5\"},{\"version\":\"8.0.0\",\"gridData\":{\"x\":28,\"y\":23,\"w\":20,\"h\":12,\"i\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\"},\"panelIndex\":\"55112375-d6f0-44f7-a8fb-867c8f7d464d\",\"embeddableConfig\":{\"enhancements\":{}},\"panelRefName\":\"panel_6\"}]", "refreshInterval": { "pause": true, "value": 0 }, "timeFrom": "2019-03-23T03:06:17.785Z", "timeRestore": true, "timeTo": "2019-10-04T02:33:16.708Z", "title": "Ecom Dashboard", "version": 1 }, "migrationVersion": { "dashboard": "7.11.0" }, "references": [ { "id": "0a464230-79f0-11ea-ae7f-13c5d6e410a0", "name": "panel_0", "type": "visualization" }, { "id": "200609c0-79f0-11ea-ae7f-13c5d6e410a0", "name": "panel_1", "type": "visualization" }, { "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", "name": "panel_2", "type": "search" }, { "id": "4a36acd0-7ac3-11ea-b69c-cf0d7935cd67", "name": "panel_3", "type": "visualization" }, { "id": "ef8757d0-7ac2-11ea-b69c-cf0d7935cd67", "name": "panel_4", "type": "visualization" }, { "id": "132ab9c0-7ac3-11ea-b69c-cf0d7935cd67", "name": "panel_5", "type": "visualization" }, { "id": "1bba55f0-507e-11eb-9c0d-97106882b997", "name": "panel_6", "type": "visualization" } ], "type": "dashboard", "updated_at": "2021-01-07T00:22:16.102Z" } } } - -{ "type": "doc", "value": { "id": "visualization:1bba55f0-507e-11eb-9c0d-97106882b997", "index": ".kibana_1", "source": { "migrationVersion": { "visualization": "7.12.0" }, "references": [ { "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", "name": "search_0", "type": "search" } ], "type": "visualization", "updated_at": "2021-01-07T00:23:04.624Z", "visualization": { "description": "", "kibanaSavedObjectMeta": { "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" }, "savedSearchRefName": "search_0", "title": "Tag Cloud of Names", "uiStateJSON": "{}", "version": 1, "visState": "{\"title\":\"Tag Cloud of Names\",\"type\":\"tagcloud\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"customer_first_name.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true}}" } } } } - -{ "type": "doc", "value": { "id": "ui-counter:DashboardPanelVersionInUrl:06012021:loaded:8.0.0", "index": ".kibana_1", "source": { "type": "ui-counter", "ui-counter": { "count": 85 }, "updated_at": "2021-01-07T00:23:25.741Z" } } } +{ + "type": "doc", + "value": { + "id": "visualization:1bba55f0-507e-11eb-9c0d-97106882b997", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.12.0" + }, + "references": [ + { + "id": "6091ead0-1c6d-11ea-a100-8589bb9d7c6b", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization", + "updated_at": "2021-01-07T00:23:04.624Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Tag Cloud of Names", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"Tag Cloud of Names\",\"type\":\"tagcloud\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"customer_first_name.keyword\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":10,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":72,\"showLabel\":true}}" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz b/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz index 83da642be87622f3b3d7a7b0b92f62e88fed5977..e5fb8a73234e4fca80958028dc282642ece8b96a 100644 GIT binary patch literal 33885 zcmV)tK$pKCiwFo)GcaHP17u-zVJ>QOZ*Bn8U0HJ@HQINMx|sMXGx2320sQgR#l_|N z@jk!2zPOw&Czo&Vz;D_70gwCvf54x`6wgkUi&=4Vt*yemd&?T|vA5U475fSNQ+wtFQhz|JBs;Po`r_%#4hbVFa5!9$BvC zNQ|eNkeQyEsrWN5?y$LA*mkdRE659hhx-=Uooiqv#n0Jq`Si!}r+l*hK3`{7Rl|OE z68-PbKmKr4iGA>d4MgtL=ssJo^Kx;yz2d30p@0~(+E$9s6(&FviiufLhk-(Oy@%ZL2U`^(?*as}VLJFr@oYxwI| zxC6z9kMEt3?=PP|z-eb!aPW`a$M=`-FX7C*Ha2&pvNpH}PRxpYK3%;nmv@y+`1JSq zI^1m^-Mg+nt|rSeuRqq8;%C$f_o)&HK2$HteZ;ruKHTSU!(#0|yO-uZ;hWihaIa|l zr0tt|fBC_QIbY*5uYbkAoK!#Mvme~!p+zrIYH0VK((XGa0QZ?*?tiXT=0J&R9l6{) z^ueo1>ta23#)F8gYQxw&U%zR%f;ODs#_pco)o^x*!lGf zJkE=J-k~=+@D)xszge%#Rc$3sEm6BC%Li1ZYx=aOZ;ca`_B9UI`qwyKX`}aWt(%Q= z&ouS${L}smc5SJ;*6*|Xx;@Vx?OF$7KirLL`?=4`>~2+^ts!!kkT>Ltc>aNIAGYKL zkF4+YG^u{b*1!)RQLggoKg#;jWm()5)dO$W>-+1kzn(8A+5C37T3<`WNPYkMcCj(t zue1C6ucz7ScD&5W>B?O&&Q|%~;iB4qJ-s;;_51I?zxZx>@yjnLaua2zEx+!+3iO=L zk-OWG7F>)jK>2_D3nKNG4_7;vgpSf(b)OZBIhg96AG?0;s4Cxgh;Q{p`E<`|JG;%s z^L%$KySXXzn{0h}w9K<9y5jGalYgQ_i193&ukxoi$D8BYf$!!e?RfT<_uI04&F!@0 z!~Xj!p+&`A{uW=`#w-qA==SUnHGO&Jf_IK}oQ)rybA36BJaj?l`d22eq>>T-b)GL) z*J3QJq0&)FnvhE3!wtvOfJ zV|UkY>fJ`=niOU4%3}3j50lS1Xf<5xO!e56^K6{YPd{)8#4baX+W-EKx5a9`yn#;Q z_>EN`5@?mvFVHM79ei%~uwMEg$A3>pzhwn|Pi^A$qmEzdRNeWSZL_`C9{INOl;+I0 zSoJ4ozDMU*Prt}-vHQ4K&MwLvm^}27=f70f^8}#VpzQ8C8{(g8Is7Plj(U-YS36nW z-OmAxZ+C{)y#4u+!@u|cI{2D8@;K}{+zA(9HJ##oFRk&}UGgko*xYw{dGtGfD^?HL zy!evU&bsjxwO@Vi`aFI%n?Sb*k&W+icfp_YbskSwiS<|aYR~fSo=4m-SH-4-S}*T? zm#p%#$X6T99B1W6=)J%eyw6+RF8}jCWig!>3)g`iF_p{Tzo}uWgg?U_2es)LI?x$o zjNJ0ESmS zxAuy`{0)7Hqt~~^^uRst+P@!BZ=XH&bO?u9zL9jkSpDmlQxvY9%-BAi8SU85Xz~Dn zF4m(1y7QvU*qd$zc7`~5u?@aF!MC}=k0c{uBKCrSif}x2$g*K z;x+>ha|gEWk{eIkuh>sDx&t3ETk_`5G~BiGu;pUg@)^%FN!e_a3qePM5j8SdrbmR3 zF%{J085?_Be%_5qT23+7{5(YeDF^>v=FJ}VS>p3zXIW<^KhJmkFBp`m@7ws%`jz^f zVV!k7)0w{@oc9Vr{X#egdxj%!!h6aaGkYZkiSWkM8a{fPKQ0%av(ejzx80>>0^Zgo zAF3x25RbkitXQPgrAC^nKez*1d+MG&mT zBwn>h99SU2aj>M)IHGQ}AOtWF4X7v)9~g8Y0C8PNoZ-SQ?yIdpL2;C`9I*C@#1(T%KirtPS6u z0H`?69t27QP=<3ul;PCaCSjn=15_)1eGGk2d@mSld?3RsvA+~(#hPFktpQnKzW6O) z6w|)>`$miaXu#fu)j;EomfGqjW|VYckqFvs)-znQS;!Wxlo+>LAg78q<3gV)CWlF+ zfl0In6Ih=jmMJSJ6~MBZRMf|v0_DO>ccP#{4NDyxMt!U7L95E(XYuajH@>eM`& z_pDU&y@B^8R2ChzHpDOpb*xbhq-ZBvg_(vqz|(iC!O0NJVrj{V2H&orOO(1v9DF@t z@?3+1D_*f8u?p!GH;hsU02wouGEY`UcYza6gHxQZ3gE;Uqudz61(kvd-K6c3b;1&u z11isZCZZ_9@Odb8VD+6}011vmB@|uifS#CCPE4<%4uqs?PSoa>I#blaK-k8MF=ejn-ypfC0bG@ zv_3CxZY3v^2CZo23ZiAWlAKyAwIGyHpEY)&TorMD?47wk9*2s94+5AJP^Be%|t`mIhKOTGx#e<3ws*Ev$TOzF|diJK++fu9k zxqA>SsV(JNf__oPEwv4>e7>qB>i+mCn|$t_y*nvF=?0}OgoY4ETdt`U=T%{~MBpDk zEeD`rAVkO>hcV=Z&TBD!=-GFH1R?g$G4a5tB^o}NW`f3-A5HGLDx~^m4j{qnaPo_r zo-J^36LMF>8TCp4RGf1U12r7KzIa^T6@3q;RzeJ4vkvTu$Z$ zSW*)uHCN!Kq(#)uBMhx%Ws;y3N3KqwH4t{?RO_Z#_pMqWLJ%yiaX3}tR`w1R$`C?p z7}G9AGPvRh{7JZmQ<1#7UEcSuTzhNg_!53x3B$q1Q%^!-x$* zuRiswQ=eXgKE-1crAr}Isujd&Ek|B zVj#8wcisex`?6PUr20V8vfoS}N)RwY#kG_5Gg{nqKskuM~P`d#X89?yaK4Sl-g(t5e>vLzEr5U z3v8oC6y@xBOu~r}3jT&M6ON?hJO3SSE6l!-h zb1(^1k?txCm6D7B4V6Z6Ym_It>1tSh;7~E872{xr7TKsLwh>;s&<NWvakc@BbQ!;z^o0|@vA znz{9bf$dQC-k0e+0hSb0Yr?F+vMb)qe{$Q2XRj6Ow&#kEN`~WB$T4KrQW=G7&w0`@ zs*_-lB||Hci-yr++5%YCVu7|+#xvvDJBhcAf%!ymMO$JRE}##B8jh1DsA3-S&z=R}?xvjx|@*5?sQ)Nl0lYiwz(jE$;e`b3lSn|2K>k zqgf)T;?yb()nHbzNP<+HuL>ijfGQY8C6UT*-f1=oOBLmIf^c*6;}D3O90O#wCq*60~d^$rrY!bczaPL4i=f}DYLg***V zasE9Blop&@4K7Pb0zTdo;dEB}@wiTq6OHrkC_*SD3Uq;SELW4n7}{hFvXeSBmI|ym zXLSNBN(^uYMif>yaL-l~-txyb!knJh=nHOmHkjSvh6~cO%(@4nPGUfj0;8yOaR4Kx zr4Kgh!D?Ot%oykh1vQy5eBULR)odtEmwbmx_jd&qtwr@k|?4*~@Xiz1?yg zB2drzy7T6^CGYA)5}4xD={QW3putf|rNyPzRdnQn|5TOXhQVooN zC@Bb+tcjSn&Z-Rxl?t#Z5_J+Vf_wS`kZ^YzTe?)6D@0(^*CV1^txwy>Q`H zjNzwYHf}z_t`hN%KD4a_=oN4XO2IUVz}@GY>4vU$07wN`yt5Aj#&9VHh<<=`Dc004 z@ACGodHcS%EjfvoAd-WMXSWZpHpn5^<7+U-Dh<)|E-xs-ai|PoLK57- zglHTyP3Qz5+N0tT0V{x)sza$te{2;MLi2$vHcao zT2C$~*ybhLt%vfY!>H(^sj!On>*1U-uDWUjlh6q=b!O2=8`Uvsz>0QPL9hl=gIt?o z0xlYDhrt~O2eLqlv&19=Jdvh&Vn7U4#Snq(^l70zWiqU*wY3w$6-l#B42M}xnB<%( z36Y_1m6YzXrb*C>bX}*=f`G>mMX~AuYezlNPG@0s?}LYd&9EtxQV8%@&>M2__|1HS zcL}U^tg!dN!vIjYqcg)D!wnV6aCN3ewPS$24;_Xyic88Fu0c*Mu7c&E@0~1_i9n^; zj3W-I@Ln8-NDf^eF_OcNXY9V4DJCKjRB<8Ui5+nUva&C#Ro_bW3U*`^Nnnaos31Q; z->eM*pALbuIg?JcQZ;&)E#m!o98xGkD8s-klgw&OwE_)mVCBmjw~~xW090HE7=~#O zi5Q*+r#KH4#Az5?-vCZ|+4n9%C_zZ0B$hL991=8=dFCi}vgE%*>T^ENdrq&{LWJ-d z#1;w6;gtfesBor|A(XG8z&x>528cTLfz2U_u2Kq#;EL6**PvVi(MkbU6k~8Agj8!V z8}5RneQ&qxL3F(vQ*zipJERMHLtvH)P3NQQf-SOARq7JKtgqH1SgY0*EQ4UzwW z3@cT=WuAtY#rBr@DOxs+8lD=nVln{4K!^Zd16eJvvd4Tr@7*Fnh5%f{sm?@E69nY3 z?miFZF9uRZP@Chr{}2ge7``#m8m`;k3kjSMA;5;QSo%i42BBaf4WouY2#K3QX(I9bO4cGC1*yXv1-JSp}vk zeRI^hn$8EZ-VR|9={7v*HjFyq${*muN+F43`AxV~CrQ@V0oTC7?G$juhTCDd1~JdCc3cDSTd!t_sP9?sC%_Vt zG0r(dZx6pb9b#8YS~@`!m|-lbMsZDVJ`P3qm?qXJGt(B1wT^lPs+2>BS%ynojfgAP?mlGny=cNQr zFF~^ySR_4j4VZRD8_VA3E0=*8xSW6qa-*3LmI~rocD)lr6p4_EjC(>zF$4V})0F>z zdvBv#$B}CbKhXt>Wc|LteGl2zBs*3lK$bJ3JpCoRx09J{Znp5Tx~dF(IZmR%0AqR` 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˾C_Vojy7lQE09P4q(~ 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

f^VA3z+;f_!$%;Zz)J#6V%kmwclVlopxg2ynoE{ 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 literal 33745 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 { - return await supertestSvc - .post(`/api/reporting/v1/generate/${isImmediate ? 'immediate/' : ''}csv/saved-object/${id}`) - .set('kbn-xsrf', 'xxx') - .send({ timerange, state }); - }, - }; - - describe('Generation from Saved Search ID', () => { - after(async () => { - await reportingAPI.deleteAllReports(); - }); - - describe('Saved Search Features', () => { - it('With filters and timebased data, explicit UTC format', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/logs'); - await esArchiver.load('logstash_functional'); - - const res = (await generateAPI.getCsvFromSavedSearch( - 'search:d7a79750-3edd-11e9-99cc-4d80163ee9e7', - { - timerange: { - timezone: 'UTC', - min: '2015-09-19T10:00:00.000Z', - max: '2015-09-21T10:00:00.000Z', - }, - state: {}, - } - )) as supertest.Response; - const { status: resStatus, text: resText, type: resType } = res; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_TIMEBASED_UTC); - - await esArchiver.unload('reporting/logs'); - await esArchiver.unload('logstash_functional'); - }); - - it('With filters and timebased data, default to UTC', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/logs'); - await esArchiver.load('logstash_functional'); - - const res = (await generateAPI.getCsvFromSavedSearch( - 'search:d7a79750-3edd-11e9-99cc-4d80163ee9e7', - { - // @ts-expect-error: timerange.timezone is missing from post params - timerange: { - min: '2015-09-19T10:00:00.000Z', - max: '2015-09-21T10:00:00.000Z', - }, - state: {}, - } - )) as supertest.Response; - const { status: resStatus, text: resText, type: resType } = res; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_TIMEBASED_UTC); - - await esArchiver.unload('reporting/logs'); - await esArchiver.unload('logstash_functional'); - }); - - it('With filters and timebased data, custom timezone', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/logs'); - await esArchiver.load('logstash_functional'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - 'search:d7a79750-3edd-11e9-99cc-4d80163ee9e7', - { - timerange: { - timezone: 'America/Phoenix', - min: '2015-09-19T10:00:00.000Z', - max: '2015-09-21T10:00:00.000Z', - }, - state: {}, - } - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_TIMEBASED_CUSTOM); - - await esArchiver.unload('reporting/logs'); - await esArchiver.unload('logstash_functional'); - }); - - it('With filters and non-timebased data', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/sales'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - 'search:71e3ee20-3f99-11e9-b8ee-6b9604f2f877', - { - state: {}, - } - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_TIMELESS); - - await esArchiver.unload('reporting/sales'); - }); - - it('With scripted fields and field formatters', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/scripted_small2'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - 'search:a6d51430-ace2-11ea-815f-39e12f89a8c2', - { - timerange: { - timezone: 'UTC', - min: '1979-01-01T10:00:00Z', - max: '1981-01-01T10:00:00Z', - }, - state: {}, - } - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_SCRIPTED); - - await esArchiver.unload('reporting/scripted_small2'); - }); - - it('Formatted date_nanos data, UTC timezone', async () => { - await esArchiver.load('reporting/nanos'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - 'search:e4035040-a295-11e9-a900-ef10e0ac769e', - { - state: {}, - } - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_NANOS); - - await esArchiver.unload('reporting/nanos'); - }); - - it('Formatted date_nanos data, custom time zone', async () => { - await esArchiver.load('reporting/nanos'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - 'search:e4035040-a295-11e9-a900-ef10e0ac769e', - { - state: {}, - timerange: { timezone: 'America/New_York' }, - } - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_NANOS_CUSTOM); - - await esArchiver.unload('reporting/nanos'); - }); - }); - - describe('API Features', () => { - it('Return a 404', async () => { - const { body } = (await generateAPI.getCsvFromSavedSearch('search:gobbledygook', { - timerange: { timezone: 'UTC', min: 63097200000, max: 126255599999 }, - state: {}, - })) as supertest.Response; - const expectedBody = { - error: 'Not Found', - message: 'Saved object [search/gobbledygook] not found', - statusCode: 404, - }; - expect(body).to.eql(expectedBody); - }); - - it('Return 400 if time range param is needed but missing', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/logs'); - await esArchiver.load('logstash_functional'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - 'search:d7a79750-3edd-11e9-99cc-4d80163ee9e7', - { state: {} } - )) as supertest.Response; - - expect(resStatus).to.eql(400); - expect(resType).to.eql('application/json'); - const { message: errorMessage } = JSON.parse(resText); - expect(errorMessage).to.eql( - 'Time range params are required for index pattern [logstash-*], using time field [@timestamp]' - ); - - await esArchiver.unload('reporting/logs'); - await esArchiver.unload('logstash_functional'); - }); - - it('Stops at Max Size Reached', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/hugedata'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - 'search:f34bf440-5014-11e9-bce7-4dabcb8bef24', - { - timerange: { - timezone: 'UTC', - min: '1960-01-01T10:00:00Z', - max: '1999-01-01T10:00:00Z', - }, - state: {}, - } - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_HUGE); - - await esArchiver.unload('reporting/hugedata'); - }); - }); - - describe('Merge user state into the query', () => { - it('for query', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/scripted_small2'); - - const params = { - searchId: 'search:a6d51430-ace2-11ea-815f-39e12f89a8c2', - postPayload: { - timerange: { timezone: 'UTC', min: '1979-01-01T10:00:00Z', max: '1981-01-01T10:00:00Z' }, // prettier-ignore - state: { query: { bool: { filter: [ { bool: { filter: [ { bool: { minimum_should_match: 1, should: [{ query_string: { fields: ['name'], query: 'Fel*' } }] } } ] } } ] } } }, // prettier-ignore - }, - isImmediate: true, - }; - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - params.searchId, - params.postPayload, - params.isImmediate - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_SCRIPTED_REQUERY); - - await esArchiver.unload('reporting/scripted_small2'); - }); - - it('for sort', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/hugedata'); - - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - 'search:f34bf440-5014-11e9-bce7-4dabcb8bef24', - { - timerange: { - timezone: 'UTC', - min: '1979-01-01T10:00:00Z', - max: '1981-01-01T10:00:00Z', - }, - state: { sort: [{ name: { order: 'asc', unmapped_type: 'boolean' } }] }, - } - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_SCRIPTED_RESORTED); - - await esArchiver.unload('reporting/hugedata'); - }); - - it('for docvalue_fields', async () => { - // load test data that contains a saved search and documents - await esArchiver.load('reporting/ecommerce'); - await esArchiver.load('reporting/ecommerce_kibana'); - - const params = { - searchId: 'search:6091ead0-1c6d-11ea-a100-8589bb9d7c6b', - postPayload: { - timerange: { - min: '2019-05-28T00:00:00Z', - max: '2019-06-26T00:00:00Z', - timezone: 'UTC', - }, - state: { - sort: [ - { order_date: { order: 'desc', unmapped_type: 'boolean' } }, - { order_id: { order: 'asc', unmapped_type: 'boolean' } }, - ], - docvalue_fields: [ - { field: 'customer_birth_date', format: 'date_time' }, - { field: 'order_date', format: 'date_time' }, - { field: 'products.created_on', format: 'date_time' }, - ], - query: { - bool: { - must: [], - filter: [ - { match_all: {} }, - { match_all: {} }, - { - range: { - order_date: { - gte: '2019-05-28T00:00:00.000Z', - lte: '2019-06-26T00:00:00.000Z', - format: 'strict_date_optional_time', - }, - }, - }, - ], - should: [], - must_not: [], - }, - }, - }, - }, - isImmediate: true, - }; - const { - status: resStatus, - text: resText, - type: resType, - } = (await generateAPI.getCsvFromSavedSearch( - params.searchId, - params.postPayload, - params.isImmediate - )) as supertest.Response; - - expect(resStatus).to.eql(200); - expect(resType).to.eql('text/csv'); - expect(resText).to.eql(fixtures.CSV_RESULT_DOCVALUE); - - await esArchiver.unload('reporting/ecommerce'); - await esArchiver.unload('reporting/ecommerce_kibana'); - }); - }); - }); -} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts new file mode 100644 index 0000000000000..27c6a05f740bf --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts @@ -0,0 +1,512 @@ +/* + * 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 supertest from 'supertest'; +import { JobParamsDownloadCSV } from '../../../plugins/reporting/server/export_types/csv_searchsource_immediate/types'; +import { FtrProviderContext } from '../ftr_provider_context'; + +const getMockJobParams = (obj: Partial): JobParamsDownloadCSV => ({ + title: `Mock CSV Title`, + ...(obj as any), +}); + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const supertestSvc = getService('supertest'); + const reportingAPI = getService('reportingAPI'); + + const generateAPI = { + getCSVFromSearchSource: async (job: JobParamsDownloadCSV) => { + return await supertestSvc + .post(`/api/reporting/v1/generate/immediate/csv_searchsource`) + .set('kbn-xsrf', 'xxx') + .send(job); + }, + }; + + describe('CSV Generation from SearchSource', () => { + before(async () => { + await kibanaServer.uiSettings.update({ + 'csv:quoteValues': false, + 'dateFormat:tz': 'UTC', + defaultIndex: 'logstash-*', + }); + }); + after(async () => { + await reportingAPI.deleteAllReports(); + }); + + it('Exports CSV with almost all fields when using fieldsFromSource', async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + query: { query: '', language: 'kuery' }, + index: '5193f870-d861-11e9-a311-0fa548c5f953', + sort: [{ order_date: 'desc' }], + fieldsFromSource: [ + '_id', + '_index', + '_score', + '_source', + '_type', + 'category', + 'category.keyword', + 'currency', + 'customer_birth_date', + 'customer_first_name', + 'customer_first_name.keyword', + 'customer_full_name', + 'customer_full_name.keyword', + 'customer_gender', + 'customer_id', + 'customer_last_name', + 'customer_last_name.keyword', + 'customer_phone', + 'day_of_week', + 'day_of_week_i', + 'email', + 'geoip.city_name', + 'geoip.continent_name', + 'geoip.country_iso_code', + 'geoip.location', + 'geoip.region_name', + 'manufacturer', + 'manufacturer.keyword', + 'order_date', + 'order_id', + 'products._id', + 'products._id.keyword', + 'products.base_price', + 'products.base_unit_price', + 'products.category', + 'products.category.keyword', + 'products.created_on', + 'products.discount_amount', + 'products.discount_percentage', + 'products.manufacturer', + 'products.manufacturer.keyword', + 'products.min_price', + 'products.price', + 'products.product_id', + 'products.product_name', + 'products.product_name.keyword', + 'products.quantity', + 'products.sku', + 'products.tax_amount', + 'products.taxful_price', + 'products.taxless_price', + 'products.unit_discount_amount', + 'sku', + 'taxful_total_price', + 'taxless_total_price', + 'total_quantity', + 'total_unique_products', + 'type', + 'user', + ], + filter: [], + parent: { + query: { language: 'kuery', query: '' }, + filter: [], + parent: { + filter: [ + { + meta: { index: '5193f870-d861-11e9-a311-0fa548c5f953', params: {} }, + range: { + order_date: { + gte: '2019-03-23T03:06:17.785Z', + lte: '2019-10-04T02:33:16.708Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + browserTimezone: 'UTC', + title: 'testfooyu78yt90-', + }) + )) as supertest.Response; + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + it('Exports CSV with all fields when using defaults', async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + query: { query: '', language: 'kuery' }, + index: '5193f870-d861-11e9-a311-0fa548c5f953', + sort: [{ order_date: 'desc' }], + fields: ['*'], + filter: [], + parent: { + query: { language: 'kuery', query: '' }, + filter: [], + parent: { + filter: [ + { + meta: { index: '5193f870-d861-11e9-a311-0fa548c5f953', params: {} }, + range: { + order_date: { + gte: '2019-03-23T03:06:17.785Z', + lte: '2019-10-04T02:33:16.708Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + browserTimezone: 'UTC', + title: 'testfooyu78yt90-', + }) + )) as supertest.Response; + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + it('Logs the error explanation if the search query returns an error', async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); + + const { status: resStatus, text: resText } = (await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + query: { query: '', language: 'kuery' }, + index: '5193f870-d861-11e9-a311-0fa548c5f953', + sort: [{ order_date: 'desc' }], + fields: ['order_date', 'products'], // products is a non-leaf field + filter: [], + parent: { + query: { language: 'kuery', query: '' }, + filter: [], + parent: { + filter: [ + { + meta: { index: '5193f870-d861-11e9-a311-0fa548c5f953', params: {} }, + range: { + order_date: { + gte: '2019-03-23T03:06:17.785Z', + lte: '2019-10-04T02:33:16.708Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + browserTimezone: 'UTC', + title: 'testfooyu78yt90-', + }) + )) as supertest.Response; + expect(resStatus).to.eql(500); + expectSnapshot(resText).toMatch(); + + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + describe('date formatting', () => { + before(async () => { + // load test data that contains a saved search and documents + await esArchiver.load('reporting/logs'); + await esArchiver.load('logstash_functional'); + }); + after(async () => { + await esArchiver.unload('reporting/logs'); + await esArchiver.unload('logstash_functional'); + }); + + it('With filters and timebased data, default to UTC', async () => { + const res = (await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + fields: ['@timestamp', 'clientip', 'extension'], + filter: [ + { + range: { + '@timestamp': { + gte: '2015-09-20T10:19:40.307Z', + lt: '2015-09-20T10:26:56.221Z', + }, + }, + }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2015-01-12T07:00:55.654Z', + lte: '2016-01-29T21:08:10.881Z', + }, + }, + }, + ], + index: 'logstash-*', + query: { language: 'kuery', query: '' }, + sort: [{ '@timestamp': 'desc' }], + }, + }) + )) as supertest.Response; + const { status: resStatus, text: resText, type: resType } = res; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + }); + + it('With filters and timebased data, non-default timezone', async () => { + const res = (await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + browserTimezone: 'America/Phoenix', + searchSource: { + fields: ['@timestamp', 'clientip', 'extension'], + filter: [ + { + range: { + '@timestamp': { + gte: '2015-09-20T10:19:40.307Z', + lt: '2015-09-20T10:26:56.221Z', + }, + }, + }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2015-01-12T07:00:55.654Z', + lte: '2016-01-29T21:08:10.881Z', + }, + }, + }, + ], + index: 'logstash-*', + query: { language: 'kuery', query: '' }, + sort: [{ '@timestamp': 'desc' }], + }, + }) + )) as supertest.Response; + const { status: resStatus, text: resText, type: resType } = res; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + }); + + it('Formatted date_nanos data, UTC timezone', async () => { + await esArchiver.load('reporting/nanos'); + + const res = await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + query: { query: '', language: 'kuery' }, + version: true, + index: '907bc200-a294-11e9-a900-ef10e0ac769e', + sort: [{ date: 'desc' }], + fields: ['date', 'message'], + filter: [], + }, + }) + ); + const { status: resStatus, text: resText, type: resType } = res; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + + await esArchiver.unload('reporting/nanos'); + }); + + it('Formatted date_nanos data, custom timezone (New York)', async () => { + await esArchiver.load('reporting/nanos'); + + const res = await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + browserTimezone: 'America/New_York', + searchSource: { + query: { query: '', language: 'kuery' }, + version: true, + index: '907bc200-a294-11e9-a900-ef10e0ac769e', + sort: [{ date: 'desc' }], + fields: ['date', 'message'], + filter: [], + }, + }) + ); + const { status: resStatus, text: resText, type: resType } = res; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + + await esArchiver.unload('reporting/nanos'); + }); + }); + + describe('non-timebased', () => { + it('Handle _id and _index columns', async () => { + await esArchiver.load('reporting/nanos'); + + const res = await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + query: { query: '', language: 'kuery' }, + version: true, + index: '907bc200-a294-11e9-a900-ef10e0ac769e', + sort: [{ date: 'desc' }], + fields: ['date', 'message', '_id', '_index'], + filter: [], + }, + }) + ); + const { status: resStatus, text: resText, type: resType } = res; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + + await esArchiver.unload('reporting/nanos'); + }); + + it('With filters and non-timebased data', async () => { + // load test data that contains a saved search and documents + await esArchiver.load('reporting/sales'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + query: { query: '', language: 'kuery' }, + version: true, + index: 'timeless-sales', + sort: [{ power: 'asc' }], + fields: ['name', 'power'], + filter: [ + { + range: { power: { gte: 1, lt: null } }, + }, + ], + }, + }) + )) as supertest.Response; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + + await esArchiver.unload('reporting/sales'); + }); + }); + + describe('validation', () => { + it('Return a 404', async () => { + const { body } = (await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + index: 'gobbledygook', + }, + }) + )) as supertest.Response; + const expectedBody = { + error: 'Not Found', + message: 'Saved object [index-pattern/gobbledygook] not found', + statusCode: 404, + }; + expect(body).to.eql(expectedBody); + }); + + it(`Searches large amount of data, stops at Max Size Reached`, async () => { + await esArchiver.load('reporting/ecommerce'); + await esArchiver.load('reporting/ecommerce_kibana'); + + const { + status: resStatus, + text: resText, + type: resType, + } = (await generateAPI.getCSVFromSearchSource( + getMockJobParams({ + searchSource: { + version: true, + query: { query: '', language: 'kuery' }, + index: '5193f870-d861-11e9-a311-0fa548c5f953', + sort: [{ order_date: 'desc' }], + fields: [ + 'order_date', + 'category', + 'currency', + 'customer_id', + 'order_id', + 'day_of_week_i', + 'products.created_on', + 'sku', + ], + filter: [], + parent: { + query: { language: 'kuery', query: '' }, + filter: [], + parent: { + filter: [ + { + meta: { index: '5193f870-d861-11e9-a311-0fa548c5f953', params: {} }, + range: { + order_date: { + gte: '2019-03-23T03:06:17.785Z', + lte: '2019-10-04T02:33:16.708Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + browserTimezone: 'UTC', + title: 'Ecommerce Data', + }) + )) as supertest.Response; + + expect(resStatus).to.eql(200); + expect(resType).to.eql('text/csv'); + expectSnapshot(resText).toMatch(); + + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index 2981ff81d66eb..b4e05e37d3fda 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -12,7 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Reporting APIs', function () { this.tags('ciGroup2'); loadTestFile(require.resolve('./csv_job_params')); - loadTestFile(require.resolve('./csv_saved_search')); + loadTestFile(require.resolve('./csv_searchsource_immediate')); loadTestFile(require.resolve('./network_policy')); loadTestFile(require.resolve('./spaces')); loadTestFile(require.resolve('./usage')); From 21587dc79efe99294dadb90113ae998d1f8e3693 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 16 Mar 2021 12:03:24 -0700 Subject: [PATCH 30/44] [Alerts] Replaces legacy es client with the ElasticsearchClient for alerts and triggers_actions_ui plugins. (#93364) * [Alerts] Replaces legasy es client with the ElasticsearchClient * fixed build * fixed build * fixed ci build * fixed ci build * fixed infra callCLuster * fixed infra callCLuster * fixed infra callCLuster * fixed ci build * fixed ci build * fixed ci build * fixed infra tests * fixed security tests * fixed security tests * fixed security tests * fixed tests * fixed monitoring unit tests * fixed monitoring unit tests * fixed type checks * fixed type checks * fixed type checks * migrated lists plugin * fixed type checks * fixed tests * fixed security tests * fixed type checks * fixed tests * fixed type checks * fixed tests * fixed tests * fixed tests * fixed due to comments * fixed tests * fixed comment * fixed tests * fixed tests * fixed searh * fixed searh * fixed test * fixed due to comment * fixed detections failing test and replaces scopedClusterClient exposure with IScopedClusterClient instead of ElasticsearchClient asCurrentUser * fixed test * fixed test * fixed test * fixed typecheck * fixed typecheck * fixed typecheck * fixed merge --- x-pack/plugins/alerting/README.md | 3 +- x-pack/plugins/alerting/server/mocks.ts | 4 +- x-pack/plugins/alerting/server/plugin.ts | 7 +- .../server/routes/_mock_handler_arguments.ts | 10 +- .../alerting/server/routes/health.test.ts | 51 ++- .../plugins/alerting/server/routes/health.ts | 17 +- .../server/task_runner/task_runner.test.ts | 2 +- x-pack/plugins/alerting/server/types.ts | 11 +- .../server/usage/alerts_telemetry.test.ts | 28 +- .../alerting/server/usage/alerts_telemetry.ts | 15 +- x-pack/plugins/alerting/server/usage/task.ts | 18 +- .../server/lib/alerts/alerting_es_client.ts | 5 +- .../register_error_count_alert_type.test.ts | 45 ++- .../alerts/register_error_count_alert_type.ts | 2 +- ...egister_transaction_duration_alert_type.ts | 2 +- ..._transaction_error_rate_alert_type.test.ts | 59 ++- ...ister_transaction_error_rate_alert_type.ts | 2 +- .../framework/kibana_framework_adapter.ts | 63 +++- .../evaluate_condition.ts | 13 +- .../inventory_metric_threshold_executor.ts | 10 +- ...review_inventory_metric_threshold_alert.ts | 8 +- .../log_threshold/log_threshold_executor.ts | 37 +- .../metric_threshold/lib/evaluate_alert.ts | 14 +- .../metric_threshold_executor.test.ts | 38 +- .../metric_threshold_executor.ts | 2 +- .../preview_metric_threshold_alert.test.ts | 33 +- .../preview_metric_threshold_alert.ts | 12 +- .../infra/server/routes/alerting/preview.ts | 9 +- .../server/utils/get_all_composite_data.ts | 9 +- x-pack/plugins/lists/server/plugin.ts | 10 +- .../services/items/create_list_item.mock.ts | 6 +- .../services/items/create_list_item.test.ts | 19 +- .../server/services/items/create_list_item.ts | 8 +- .../items/create_list_items_bulk.mock.ts | 6 +- .../items/create_list_items_bulk.test.ts | 8 +- .../services/items/create_list_items_bulk.ts | 8 +- .../services/items/delete_list_item.mock.ts | 6 +- .../services/items/delete_list_item.test.ts | 2 +- .../server/services/items/delete_list_item.ts | 10 +- .../items/delete_list_item_by_value.mock.ts | 6 +- .../items/delete_list_item_by_value.test.ts | 4 +- .../items/delete_list_item_by_value.ts | 12 +- .../services/items/find_list_item.mock.ts | 12 +- .../services/items/find_list_item.test.ts | 51 ++- .../server/services/items/find_list_item.ts | 24 +- .../services/items/get_list_item.test.ts | 25 +- .../server/services/items/get_list_item.ts | 12 +- .../items/get_list_item_by_value.mock.ts | 6 +- .../services/items/get_list_item_by_value.ts | 8 +- .../items/get_list_item_by_values.mock.ts | 6 +- .../items/get_list_item_by_values.test.ts | 18 +- .../services/items/get_list_item_by_values.ts | 21 +- .../items/search_list_item_by_values.mock.ts | 6 +- .../items/search_list_item_by_values.test.ts | 25 +- .../items/search_list_item_by_values.ts | 22 +- .../services/items/update_list_item.mock.ts | 6 +- .../services/items/update_list_item.test.ts | 9 +- .../server/services/items/update_list_item.ts | 10 +- .../write_lines_to_bulk_list_items.mock.ts | 8 +- .../items/write_lines_to_bulk_list_items.ts | 18 +- .../items/write_list_items_to_stream.test.ts | 68 ++-- .../items/write_list_items_to_stream.ts | 46 +-- .../items/write_list_items_to_streams.mock.ts | 10 +- .../server/services/lists/create_list.mock.ts | 6 +- .../server/services/lists/create_list.test.ts | 25 +- .../server/services/lists/create_list.ts | 8 +- .../lists/create_list_if_it_does_not_exist.ts | 10 +- .../server/services/lists/delete_list.mock.ts | 6 +- .../server/services/lists/delete_list.test.ts | 10 +- .../server/services/lists/delete_list.ts | 16 +- .../lists/server/services/lists/find_list.ts | 22 +- .../server/services/lists/get_list.test.ts | 18 +- .../lists/server/services/lists/get_list.ts | 12 +- .../server/services/lists/list_client.mock.ts | 6 +- .../server/services/lists/list_client.ts | 148 ++++---- .../services/lists/list_client_types.ts | 4 +- .../server/services/lists/update_list.mock.ts | 6 +- .../server/services/lists/update_list.test.ts | 15 +- .../server/services/lists/update_list.ts | 10 +- .../services/utils/get_search_after_scroll.ts | 11 +- .../services/utils/scroll_to_start_page.ts | 12 +- x-pack/plugins/lists/server/types.ts | 4 +- .../monitoring/server/alerts/base_alert.ts | 39 +- .../alerts/ccr_read_exceptions_alert.ts | 5 +- .../alerts/cluster_health_alert.test.ts | 3 +- .../server/alerts/cluster_health_alert.ts | 5 +- .../server/alerts/cpu_usage_alert.test.ts | 3 +- .../server/alerts/cpu_usage_alert.ts | 5 +- .../server/alerts/disk_usage_alert.test.ts | 3 +- .../server/alerts/disk_usage_alert.ts | 5 +- ...asticsearch_version_mismatch_alert.test.ts | 3 +- .../elasticsearch_version_mismatch_alert.ts | 5 +- .../kibana_version_mismatch_alert.test.ts | 3 +- .../alerts/kibana_version_mismatch_alert.ts | 5 +- .../server/alerts/large_shard_size_alert.ts | 5 +- .../alerts/license_expiration_alert.test.ts | 3 +- .../server/alerts/license_expiration_alert.ts | 5 +- .../logstash_version_mismatch_alert.test.ts | 3 +- .../alerts/logstash_version_mismatch_alert.ts | 5 +- .../server/alerts/memory_usage_alert.ts | 5 +- .../missing_monitoring_data_alert.test.ts | 3 +- .../alerts/missing_monitoring_data_alert.ts | 5 +- .../server/alerts/nodes_changed_alert.test.ts | 3 +- .../server/alerts/nodes_changed_alert.ts | 5 +- .../thread_pool_rejections_alert_base.ts | 5 +- .../collectors/get_usage_collector.test.ts | 8 +- .../collectors/get_usage_collector.ts | 8 +- .../lib/alerts/fetch_available_ccs.test.ts | 42 ++- .../server/lib/alerts/fetch_available_ccs.ts | 19 +- .../lib/alerts/fetch_ccr_read_exceptions.ts | 5 +- .../lib/alerts/fetch_cluster_health.test.ts | 33 +- .../server/lib/alerts/fetch_cluster_health.ts | 5 +- .../server/lib/alerts/fetch_clusters.test.ts | 73 ++-- .../server/lib/alerts/fetch_clusters.ts | 49 ++- .../alerts/fetch_cpu_usage_node_stats.test.ts | 41 ++- .../lib/alerts/fetch_cpu_usage_node_stats.ts | 5 +- .../fetch_disk_usage_node_stats.test.ts | 16 +- .../lib/alerts/fetch_disk_usage_node_stats.ts | 5 +- .../fetch_elasticsearch_versions.test.ts | 16 +- .../alerts/fetch_elasticsearch_versions.ts | 5 +- .../lib/alerts/fetch_index_shard_size.ts | 5 +- .../lib/alerts/fetch_kibana_versions.test.ts | 15 +- .../lib/alerts/fetch_kibana_versions.ts | 5 +- .../server/lib/alerts/fetch_licenses.test.ts | 46 ++- .../server/lib/alerts/fetch_licenses.ts | 5 +- .../alerts/fetch_logstash_versions.test.ts | 15 +- .../lib/alerts/fetch_logstash_versions.ts | 5 +- .../alerts/fetch_memory_usage_node_stats.ts | 5 +- .../fetch_missing_monitoring_data.test.ts | 36 +- .../alerts/fetch_missing_monitoring_data.ts | 5 +- .../alerts/fetch_nodes_from_cluster_stats.ts | 5 +- .../fetch_thread_pool_rejections_stats.ts | 5 +- .../elasticsearch/verify_alerting_security.ts | 9 +- .../index/create_bootstrap_index.ts | 24 +- .../index/delete_all_index.ts | 19 +- .../detection_engine/index/delete_policy.ts | 14 +- .../detection_engine/index/delete_template.ts | 14 +- .../index/get_index_exists.test.ts | 38 +- .../index/get_index_exists.ts | 13 +- .../index/get_policy_exists.ts | 6 +- .../index/get_template_exists.ts | 13 +- .../lib/detection_engine/index/read_index.ts | 10 +- .../lib/detection_engine/index/set_policy.ts | 18 +- .../detection_engine/index/set_template.ts | 17 +- .../notifications/get_signals.ts | 8 +- .../notifications/get_signals_count.ts | 12 +- .../rules_notification_alert_type.test.ts | 36 +- .../rules_notification_alert_type.ts | 2 +- .../routes/index/create_index_route.ts | 16 +- .../routes/index/delete_index_route.ts | 16 +- .../routes/index/get_index_version.ts | 11 +- .../routes/index/read_index_route.ts | 6 +- .../rules/add_prepackaged_rules_route.test.ts | 19 +- .../rules/add_prepackaged_rules_route.ts | 4 +- .../rules/create_rules_bulk_route.test.ts | 12 +- .../routes/rules/create_rules_bulk_route.ts | 4 +- .../routes/rules/create_rules_route.test.ts | 12 +- .../routes/rules/create_rules_route.ts | 4 +- .../routes/rules/import_rules_route.test.ts | 21 +- .../routes/rules/import_rules_route.ts | 4 +- .../signals/build_events_query.test.ts | 34 +- .../signals/build_events_query.ts | 5 +- .../signals/search_after_bulk_create.test.ts | 344 +++++++++++++----- .../signals/signal_rule_alert_type.test.ts | 22 +- .../signals/signal_rule_alert_type.ts | 15 +- .../signals/single_bulk_create.test.ts | 58 +-- .../signals/single_bulk_create.ts | 4 +- .../signals/single_search_after.test.ts | 56 +-- .../signals/single_search_after.ts | 5 +- .../threat_mapping/build_threat_enrichment.ts | 2 +- .../threat_mapping/create_threat_signals.ts | 6 +- .../signals/threat_mapping/get_threat_list.ts | 17 +- .../signals/threat_mapping/types.ts | 6 +- .../lib/detection_engine/signals/utils.ts | 32 +- .../common/build_sorted_events_query.test.ts | 28 +- .../common/build_sorted_events_query.ts | 4 +- .../alert_types/es_query/alert_type.test.ts | 128 ++++--- .../server/alert_types/es_query/alert_type.ts | 5 +- .../geo_containment/es_query_builder.ts | 20 +- .../geo_containment/geo_containment.ts | 16 +- .../tests/geo_containment.test.ts | 2 +- .../alert_types/index_threshold/alert_type.ts | 4 +- .../server/data/lib/time_series_query.test.ts | 9 +- .../server/data/lib/time_series_query.ts | 17 +- .../server/data/routes/fields.ts | 21 +- .../server/data/routes/indices.ts | 21 +- .../server/data/routes/time_series_query.ts | 2 +- .../server/lib/alerts/status_check.test.ts | 4 +- .../server/lib/alerts/uptime_alert_wrapper.ts | 5 +- .../plugins/alerts/server/alert_types.ts | 17 +- .../tests/alerting/alerts.ts | 2 - 191 files changed, 2025 insertions(+), 1275 deletions(-) diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 6332985112c4c..19322fed7363e 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -116,9 +116,8 @@ This is the primary function for an alert type. Whenever the alert needs to exec |Property|Description| |---|---| -|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but in the context of the user who created the alert when security is enabled.| +|services.scopedClusterClient|This is an instance of the Elasticsearch client. Use this to do Elasticsearch queries in the context of the user who created the alert when security is enabled.| |services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user who created the alert (only when security isenabled).| -|services.getLegacyScopedClusterClient|This function returns an instance of the LegacyScopedClusterClient scoped to the user who created the alert when security is enabled.| |services.alertInstanceFactory(id)|This [alert instance factory](#alert-instance-factory) creates instances of alerts and must be used in order to execute actions. The id you give to the alert instance factory is a unique identifier to the alert instance.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| |startedAt|The date and time the alert type started execution.| diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index f3c40a6788967..df9a3c5ddf169 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -70,10 +70,8 @@ const createAlertServicesMock = < alertInstanceFactory: jest .fn>, [string]>() .mockReturnValue(alertInstanceFactoryMock), - callCluster: elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser, - getLegacyScopedClusterClient: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient().asCurrentUser, + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), }; }; export type AlertServicesMock = ReturnType; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index d85622f301171..ff36ebcd84ba5 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -31,7 +31,6 @@ import { SavedObjectsServiceStart, IContextProvider, ElasticsearchServiceStart, - ILegacyClusterClient, StatusServiceSetup, ServiceStatus, SavedObjectsBulkGetObject, @@ -420,12 +419,8 @@ export class AlertingPlugin { elasticsearch: ElasticsearchServiceStart ): (request: KibanaRequest) => Services { return (request) => ({ - callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: this.getScopedClientWithAlertSavedObjectType(savedObjects, request), - scopedClusterClient: elasticsearch.client.asScoped(request).asCurrentUser, - getLegacyScopedClusterClient(clusterClient: ILegacyClusterClient) { - return clusterClient.asScoped(request); - }, + scopedClusterClient: elasticsearch.client.asScoped(request), }); } diff --git a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts index a5cc192a337b7..cd1c32a9b2d8f 100644 --- a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { KibanaRequest, KibanaResponseFactory, ILegacyClusterClient } from 'kibana/server'; +import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { identity } from 'lodash'; import type { MethodKeysOf } from '@kbn/utility-types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ScopedClusterClientMock } from '../../../../../src/core/server/elasticsearch/client/mocks'; import { httpServerMock } from '../../../../../src/core/server/mocks'; import { alertsClientMock, AlertsClientMock } from '../alerts_client.mock'; import { AlertsHealth, AlertType } from '../../common'; @@ -18,12 +20,12 @@ export function mockHandlerArguments( { alertsClient = alertsClientMock.create(), listTypes: listTypesRes = [], - esClient = elasticsearchServiceMock.createLegacyClusterClient(), + esClient = elasticsearchServiceMock.createScopedClusterClient(), getFrameworkHealth, }: { alertsClient?: AlertsClientMock; listTypes?: AlertType[]; - esClient?: jest.Mocked; + esClient?: jest.Mocked; getFrameworkHealth?: jest.MockInstance, []> & (() => Promise); }, @@ -37,7 +39,7 @@ export function mockHandlerArguments( const listTypes = jest.fn(() => listTypesRes); return [ ({ - core: { elasticsearch: { legacy: { client: esClient } } }, + core: { elasticsearch: { client: esClient } }, alerting: { listTypes, getAlertsClient() { diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index 22df0e6a00046..75c621e4a0abf 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -15,6 +15,8 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv import { alertsClientMock } from '../alerts_client.mock'; import { HealthStatus } from '../types'; import { alertsMock } from '../mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../src/core/server/elasticsearch/client/mocks'; const alertsClient = alertsClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ @@ -63,8 +65,10 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockReturnValue(Promise.resolve({})); + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}) + ); const [context, req, res] = mockHandlerArguments({ esClient, alertsClient }, {}, ['ok']); @@ -72,9 +76,8 @@ describe('healthRoute', () => { expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); - expect(esClient.callAsInternalUser.mock.calls[0]).toMatchInlineSnapshot(` + expect(esClient.asInternalUser.transport.request.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "transport.request", Object { "method": "GET", "path": "/_xpack/usage", @@ -91,8 +94,10 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockReturnValue(Promise.resolve({})); + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}) + ); const [context, req, res] = mockHandlerArguments( { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, @@ -130,8 +135,10 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockReturnValue(Promise.resolve({})); + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({}) + ); const [context, req, res] = mockHandlerArguments( { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, @@ -169,8 +176,10 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: {} })); + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ security: {} }) + ); const [context, req, res] = mockHandlerArguments( { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, @@ -208,8 +217,10 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: { enabled: true } })); + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ security: { enabled: true } }) + ); const [context, req, res] = mockHandlerArguments( { esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth }, @@ -247,9 +258,11 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockReturnValue( - Promise.resolve({ security: { enabled: true, ssl: {} } }) + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + security: { enabled: true, ssl: {} }, + }) ); const [context, req, res] = mockHandlerArguments( @@ -288,9 +301,11 @@ describe('healthRoute', () => { healthRoute(router, licenseState, encryptedSavedObjects); const [, handler] = router.get.mock.calls[0]; - const esClient = elasticsearchServiceMock.createLegacyClusterClient(); - esClient.callAsInternalUser.mockReturnValue( - Promise.resolve({ security: { enabled: true, ssl: { http: { enabled: true } } } }) + const esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asInternalUser.transport.request.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + security: { enabled: true, ssl: { http: { enabled: true } } }, + }) ); const [context, req, res] = mockHandlerArguments( diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index 9e1f01041e091..de0b14465c5ac 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ApiResponse } from '@elastic/elasticsearch'; import type { AlertingRouter } from '../types'; import { ILicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; @@ -39,14 +40,14 @@ export function healthRoute( } try { const { - security: { - enabled: isSecurityEnabled = false, - ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, - } = {}, - }: XPackUsageSecurity = await context.core.elasticsearch.legacy.client - // `transport.request` is potentially unsafe when combined with untrusted user input. - // Do not augment with such input. - .callAsInternalUser('transport.request', { + body: { + security: { + enabled: isSecurityEnabled = false, + ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, + } = {}, + }, + }: ApiResponse = await context.core.elasticsearch.client.asInternalUser.transport // Do not augment with such input. // `transport.request` is potentially unsafe when combined with untrusted user input. + .request({ method: 'GET', path: '/_xpack/usage', }); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index bb5e0e5830159..a3a7e9bbd9da5 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -206,7 +206,7 @@ describe('Task Runner', () => { expect(call.createdBy).toBe('alert-creator'); expect(call.updatedBy).toBe('alert-updater'); expect(call.services.alertInstanceFactory).toBeTruthy(); - expect(call.services.callCluster).toBeTruthy(); + expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); const logger = taskRunnerFactoryInitializerParams.logger; diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 2b749b866d3a0..23aed1070a31a 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -13,9 +13,7 @@ import { PluginSetupContract, PluginStartContract } from './plugin'; import { AlertsClient } from './alerts_client'; export * from '../common'; import { - ElasticsearchClient, - ILegacyClusterClient, - ILegacyScopedClusterClient, + IScopedClusterClient, KibanaRequest, SavedObjectAttributes, SavedObjectsClientContract, @@ -63,13 +61,8 @@ export interface AlertingRequestHandlerContext extends RequestHandlerContext { export type AlertingRouter = IRouter; export interface Services { - /** - * @deprecated Use `scopedClusterClient` instead. - */ - callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; - scopedClusterClient: ElasticsearchClient; - getLegacyScopedClusterClient(clusterClient: ILegacyClusterClient): ILegacyScopedClusterClient; + scopedClusterClient: IScopedClusterClient; } export interface AlertServices< diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts index 8a3870c894e4e..3c9decdf7ba96 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.test.ts @@ -5,27 +5,31 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../src/core/server/elasticsearch/client/mocks'; import { getTotalCountInUse } from './alerts_telemetry'; describe('alerts telemetry', () => { test('getTotalCountInUse should replace first "." symbol to "__" in alert types names', async () => { - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue({ - aggregations: { - byAlertTypeId: { - value: { - types: { '.index-threshold': 2, 'logs.alert.document.count': 1, 'document.test.': 1 }, + const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser; + mockEsClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + aggregations: { + byAlertTypeId: { + value: { + types: { '.index-threshold': 2, 'logs.alert.document.count': 1, 'document.test.': 1 }, + }, }, }, - }, - hits: { - hits: [], - }, - }); + hits: { + hits: [], + }, + }) + ); const telemetry = await getTotalCountInUse(mockEsClient, 'test'); - expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(mockEsClient.search).toHaveBeenCalledTimes(1); expect(telemetry).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index c66110f2647c6..93bed31ce7d50 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; +import { ElasticsearchClient } from 'kibana/server'; import { AlertsUsage } from './types'; const alertTypeMetric = { @@ -36,7 +35,7 @@ const alertTypeMetric = { }; export async function getTotalCountAggregations( - callCluster: LegacyAPICaller, + esClient: ElasticsearchClient, kibanaInex: string ): Promise< Pick< @@ -223,7 +222,7 @@ export async function getTotalCountAggregations( }, }; - const results = await callCluster('search', { + const { body: results } = await esClient.search({ index: kibanaInex, body: { query: { @@ -256,7 +255,7 @@ export async function getTotalCountAggregations( return { count_total: totalAlertsCount, count_by_type: Object.keys(results.aggregations.byAlertTypeId.value.types).reduce( - // ES DSL aggregations are returned as `any` by callCluster + // ES DSL aggregations are returned as `any` by esClient.search // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, @@ -295,8 +294,8 @@ export async function getTotalCountAggregations( }; } -export async function getTotalCountInUse(callCluster: LegacyAPICaller, kibanaInex: string) { - const searchResult: SearchResponse = await callCluster('search', { +export async function getTotalCountInUse(esClient: ElasticsearchClient, kibanaInex: string) { + const { body: searchResult } = await esClient.search({ index: kibanaInex, body: { query: { @@ -316,7 +315,7 @@ export async function getTotalCountInUse(callCluster: LegacyAPICaller, kibanaIne 0 ), countByType: Object.keys(searchResult.aggregations.byAlertTypeId.value.types).reduce( - // ES DSL aggregations are returned as `any` by callCluster + // ES DSL aggregations are returned as `any` by esClient.search // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, diff --git a/x-pack/plugins/alerting/server/usage/task.ts b/x-pack/plugins/alerting/server/usage/task.ts index d03697f2bb11b..043d970ddd231 100644 --- a/x-pack/plugins/alerting/server/usage/task.ts +++ b/x-pack/plugins/alerting/server/usage/task.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger, CoreSetup, LegacyAPICaller } from 'kibana/server'; +import { Logger, CoreSetup } from 'kibana/server'; import moment from 'moment'; import { RunContext, @@ -65,17 +65,21 @@ async function scheduleTasks(logger: Logger, taskManager: TaskManagerStartContra export function telemetryTaskRunner(logger: Logger, core: CoreSetup, kibanaIndex: string) { return ({ taskInstance }: RunContext) => { const { state } = taskInstance; - const callCluster = (...args: Parameters) => { - return core.getStartServices().then(([{ elasticsearch: { legacy: { client } } }]) => - client.callAsInternalUser(...args) + const getEsClient = () => + core.getStartServices().then( + ([ + { + elasticsearch: { client }, + }, + ]) => client.asInternalUser ); - }; return { async run() { + const esClient = await getEsClient(); return Promise.all([ - getTotalCountAggregations(callCluster, kibanaIndex), - getTotalCountInUse(callCluster, kibanaIndex), + getTotalCountAggregations(esClient, kibanaIndex), + getTotalCountInUse(esClient, kibanaIndex), ]) .then(([totalCountAggregations, totalInUse]) => { return { diff --git a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts index d5706ac9063ed..c4fef64f515d1 100644 --- a/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts +++ b/x-pack/plugins/apm/server/lib/alerts/alerting_es_client.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ApiResponse } from '@elastic/elasticsearch'; import { ThresholdMetActionGroupId } from '../../../common/alert_types'; import { ESSearchRequest, @@ -23,8 +24,8 @@ export function alertingEsClient( ThresholdMetActionGroupId >, params: TParams -): Promise> { - return services.callCluster('search', { +): Promise>> { + return services.scopedClusterClient.asCurrentUser.search({ ...params, ignore_unavailable: true, }); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts index 4d403be84a2b2..167cb133102f2 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.test.ts @@ -13,6 +13,9 @@ import { AlertingPlugin } from '../../../../alerting/server'; import { APMConfig } from '../..'; import { registerErrorCountAlertType } from './register_error_count_alert_type'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; type Operator = (source: Rx.Observable) => Rx.Observable; const pipeClosure = (fn: Operator): Operator => { @@ -43,16 +46,20 @@ describe('Error count alert', () => { expect(alertExecutor).toBeDefined(); const services = { - callCluster: jest.fn(() => ({ + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(), + }; + const params = { threshold: 1 }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { total: { value: 0, }, }, - })), - alertInstanceFactory: jest.fn(), - }; - const params = { threshold: 1 }; + }) + ); await alertExecutor!({ services, params }); expect(services.alertInstanceFactory).not.toBeCalled(); @@ -74,7 +81,13 @@ describe('Error count alert', () => { const scheduleActions = jest.fn(); const services = { - callCluster: jest.fn(() => ({ + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 1, windowSize: 5, windowUnit: 'm' }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { total: { value: 2, @@ -98,10 +111,8 @@ describe('Error count alert', () => { ], }, }, - })), - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), - }; - const params = { threshold: 1, windowSize: 5, windowUnit: 'm' }; + }) + ); await alertExecutor!({ services, params }); [ @@ -158,7 +169,13 @@ describe('Error count alert', () => { const scheduleActions = jest.fn(); const services = { - callCluster: jest.fn(() => ({ + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 1, windowSize: 5, windowUnit: 'm' }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { total: { value: 2, @@ -176,10 +193,8 @@ describe('Error count alert', () => { ], }, }, - })), - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), - }; - const params = { threshold: 1, windowSize: 5, windowUnit: 'm' }; + }) + ); await alertExecutor!({ services, params }); ['apm.error_rate_foo', 'apm.error_rate_bar'].forEach((instanceName) => diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 70b41da6917ef..0120891a8f868 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -127,7 +127,7 @@ export function registerErrorCountAlertType({ }, }; - const response = await alertingEsClient(services, searchParams); + const { body: response } = await alertingEsClient(services, searchParams); const errorCount = response.hits.total.value; if (errorCount > alertParams.threshold) { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index bb8e67574e9ad..500e0744d5638 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -122,7 +122,7 @@ export function registerTransactionDurationAlertType({ }, }; - const response = await alertingEsClient(services, searchParams); + const { body: response } = await alertingEsClient(services, searchParams); if (!response.aggregations) { return; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts index 068eb9b1ccef4..c18f29b6267e0 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.test.ts @@ -11,6 +11,9 @@ import { toArray, map } from 'rxjs/operators'; import { AlertingPlugin } from '../../../../alerting/server'; import { APMConfig } from '../..'; import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; type Operator = (source: Rx.Observable) => Rx.Observable; const pipeClosure = (fn: Operator): Operator => { @@ -41,16 +44,20 @@ describe('Transaction error rate alert', () => { expect(alertExecutor).toBeDefined(); const services = { - callCluster: jest.fn(() => ({ + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(), + }; + const params = { threshold: 1 }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { total: { value: 0, }, }, - })), - alertInstanceFactory: jest.fn(), - }; - const params = { threshold: 1 }; + }) + ); await alertExecutor!({ services, params }); expect(services.alertInstanceFactory).not.toBeCalled(); @@ -72,7 +79,13 @@ describe('Transaction error rate alert', () => { const scheduleActions = jest.fn(); const services = { - callCluster: jest.fn(() => ({ + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 10, windowSize: 5, windowUnit: 'm' }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { total: { value: 4, @@ -113,10 +126,8 @@ describe('Transaction error rate alert', () => { ], }, }, - })), - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), - }; - const params = { threshold: 10, windowSize: 5, windowUnit: 'm' }; + }) + ); await alertExecutor!({ services, params }); [ @@ -177,7 +188,13 @@ describe('Transaction error rate alert', () => { const scheduleActions = jest.fn(); const services = { - callCluster: jest.fn(() => ({ + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 10, windowSize: 5, windowUnit: 'm' }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { total: { value: 4, @@ -204,10 +221,8 @@ describe('Transaction error rate alert', () => { ], }, }, - })), - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), - }; - const params = { threshold: 10, windowSize: 5, windowUnit: 'm' }; + }) + ); await alertExecutor!({ services, params }); [ @@ -251,7 +266,13 @@ describe('Transaction error rate alert', () => { const scheduleActions = jest.fn(); const services = { - callCluster: jest.fn(() => ({ + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + alertInstanceFactory: jest.fn(() => ({ scheduleActions })), + }; + const params = { threshold: 10, windowSize: 5, windowUnit: 'm' }; + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { total: { value: 4, @@ -265,10 +286,8 @@ describe('Transaction error rate alert', () => { buckets: [{ key: 'foo' }, { key: 'bar' }], }, }, - })), - alertInstanceFactory: jest.fn(() => ({ scheduleActions })), - }; - const params = { threshold: 10, windowSize: 5, windowUnit: 'm' }; + }) + ); await alertExecutor!({ services, params }); [ diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index ef5407500349d..0b2684cdaf083 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -134,7 +134,7 @@ export function registerTransactionErrorRateAlertType({ }, }; - const response = await alertingEsClient(services, searchParams); + const { body: response } = await alertingEsClient(services, searchParams); if (!response.aggregations) { return; } diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 557831780008a..2cb00644f56d4 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -5,6 +5,13 @@ * 2.0. */ +import { + IndicesExistsAlias, + IndicesGet, + MlGetBuckets, + Msearch, +} from '@elastic/elasticsearch/api/requestParams'; +import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { InfraRouteConfig, InfraTSVBResponse, @@ -134,10 +141,58 @@ export class KibanaFramework { } : {}; - return elasticsearch.legacy.client.callAsCurrentUser(endpoint, { - ...params, - ...frozenIndicesParams, - }); + let apiResult; + switch (endpoint) { + case 'search': + apiResult = elasticsearch.client.asCurrentUser.search({ + ...params, + ...frozenIndicesParams, + }); + break; + case 'msearch': + apiResult = elasticsearch.client.asCurrentUser.msearch({ + ...params, + ...frozenIndicesParams, + } as Msearch); + break; + case 'fieldCaps': + apiResult = elasticsearch.client.asCurrentUser.fieldCaps({ + ...params, + ...frozenIndicesParams, + }); + break; + case 'indices.existsAlias': + apiResult = elasticsearch.client.asCurrentUser.indices.existsAlias({ + ...params, + ...frozenIndicesParams, + } as IndicesExistsAlias); + break; + case 'indices.getAlias': + apiResult = elasticsearch.client.asCurrentUser.indices.getAlias({ + ...params, + ...frozenIndicesParams, + }); + break; + case 'indices.get': + apiResult = elasticsearch.client.asCurrentUser.indices.get({ + ...params, + ...frozenIndicesParams, + } as IndicesGet); + break; + case 'transport.request': + apiResult = elasticsearch.client.asCurrentUser.transport.request({ + ...params, + ...frozenIndicesParams, + } as TransportRequestParams); + break; + case 'ml.getBuckets': + apiResult = elasticsearch.client.asCurrentUser.ml.getBuckets({ + ...params, + ...frozenIndicesParams, + } as MlGetBuckets); + break; + } + return apiResult ? (await apiResult).body : undefined; } public getIndexPatternsService( diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 6b6cf5f1d563c..615de182662f1 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -7,6 +7,7 @@ import { mapValues, last, first } from 'lodash'; import moment from 'moment'; +import { ElasticsearchClient } from 'kibana/server'; import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; import { isTooManyBucketsPreviewException, @@ -17,7 +18,6 @@ import { CallWithRequestParams, } from '../../adapters/framework/adapter_types'; import { Comparator, InventoryMetricConditions } from './types'; -import { AlertServices } from '../../../../../alerting/server'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraTimerangeInput, SnapshotRequest } from '../../../../common/http_api/snapshot_api'; import { InfraSource } from '../../sources'; @@ -36,7 +36,7 @@ export const evaluateCondition = async ( condition: InventoryMetricConditions, nodeType: InventoryItemType, source: InfraSource, - callCluster: AlertServices['callCluster'], + esClient: ElasticsearchClient, filterQuery?: string, lookbackSize?: number ): Promise> => { @@ -53,7 +53,7 @@ export const evaluateCondition = async ( } const currentValues = await getData( - callCluster, + esClient, nodeType, metric, timerange, @@ -96,7 +96,7 @@ const getCurrentValue: (value: any) => number = (value) => { type DataValue = number | null | Array; const getData = async ( - callCluster: AlertServices['callCluster'], + esClient: ElasticsearchClient, nodeType: InventoryItemType, metric: SnapshotMetricType, timerange: InfraTimerangeInput, @@ -104,9 +104,10 @@ const getData = async ( filterQuery?: string, customMetric?: SnapshotCustomMetricInput ) => { - const client = ( + const client = async ( options: CallWithRequestParams - ): Promise> => callCluster('search', options); + ): Promise> => + (await esClient.search(options)).body as InfraDatabaseSearchResponse; const metrics = [ metric === 'custom' ? (customMetric as SnapshotCustomMetricInput) : { type: metric }, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index f4fadd09efdf5..632ba9cd6f282 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -69,7 +69,15 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = ); const results = await Promise.all( - criteria.map((c) => evaluateCondition(c, nodeType, source, services.callCluster, filterQuery)) + criteria.map((c) => + evaluateCondition( + c, + nodeType, + source, + services.scopedClusterClient.asCurrentUser, + filterQuery + ) + ) ); const inventoryItems = Object.keys(first(results)!); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 6f3299a2cc126..472f9d408694c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -13,7 +13,7 @@ import { TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, isTooManyBucketsPreviewException, } from '../../../../common/alerting/metrics'; -import { ILegacyScopedClusterClient } from '../../../../../../../src/core/server'; +import { ElasticsearchClient } from '../../../../../../../src/core/server'; import { InfraSource } from '../../../../common/http_api/source_api'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { InventoryItemType } from '../../../../common/inventory_models/types'; @@ -27,7 +27,7 @@ interface InventoryMetricThresholdParams { } interface PreviewInventoryMetricThresholdAlertParams { - callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; + esClient: ElasticsearchClient; params: InventoryMetricThresholdParams; source: InfraSource; lookback: Unit; @@ -40,7 +40,7 @@ interface PreviewInventoryMetricThresholdAlertParams { export const previewInventoryMetricThresholdAlert: ( params: PreviewInventoryMetricThresholdAlertParams ) => Promise = async ({ - callCluster, + esClient, params, source, lookback, @@ -68,7 +68,7 @@ export const previewInventoryMetricThresholdAlert: ( try { const results = await Promise.all( criteria.map((c) => - evaluateCondition(c, nodeType, source, callCluster, filterQuery, lookbackSize) + evaluateCondition(c, nodeType, source, esClient, filterQuery, lookbackSize) ) ); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 565dfa6d6d2aa..0dff7e1070971 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { AlertExecutorOptions, AlertServices, @@ -67,7 +68,7 @@ const checkValueAgainstComparatorMap: { export const createLogThresholdExecutor = (libs: InfraBackendLibs) => async function ({ services, params }: LogThresholdAlertExecutorOptions) { - const { alertInstanceFactory, savedObjectsClient, callCluster } = services; + const { alertInstanceFactory, savedObjectsClient, scopedClusterClient } = services; const { sources } = libs; const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); @@ -82,7 +83,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => validatedParams, timestampField, indexPattern, - callCluster, + scopedClusterClient.asCurrentUser, alertInstanceFactory ); } else { @@ -90,7 +91,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => validatedParams, timestampField, indexPattern, - callCluster, + scopedClusterClient.asCurrentUser, alertInstanceFactory ); } @@ -103,7 +104,7 @@ async function executeAlert( alertParams: CountAlertParams, timestampField: string, indexPattern: string, - callCluster: LogThresholdAlertServices['callCluster'], + esClient: ElasticsearchClient, alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory'] ) { const query = getESQuery(alertParams, timestampField, indexPattern); @@ -114,14 +115,14 @@ async function executeAlert( if (hasGroupBy(alertParams)) { processGroupByResults( - await getGroupedResults(query, callCluster), + await getGroupedResults(query, esClient), alertParams, alertInstanceFactory, updateAlertInstance ); } else { processUngroupedResults( - await getUngroupedResults(query, callCluster), + await getUngroupedResults(query, esClient), alertParams, alertInstanceFactory, updateAlertInstance @@ -133,7 +134,7 @@ async function executeRatioAlert( alertParams: RatioAlertParams, timestampField: string, indexPattern: string, - callCluster: LogThresholdAlertServices['callCluster'], + esClient: ElasticsearchClient, alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory'] ) { // Ratio alert params are separated out into two standard sets of alert params @@ -155,8 +156,8 @@ async function executeRatioAlert( } if (hasGroupBy(alertParams)) { - const numeratorGroupedResults = await getGroupedResults(numeratorQuery, callCluster); - const denominatorGroupedResults = await getGroupedResults(denominatorQuery, callCluster); + const numeratorGroupedResults = await getGroupedResults(numeratorQuery, esClient); + const denominatorGroupedResults = await getGroupedResults(denominatorQuery, esClient); processGroupByRatioResults( numeratorGroupedResults, denominatorGroupedResults, @@ -165,8 +166,8 @@ async function executeRatioAlert( updateAlertInstance ); } else { - const numeratorUngroupedResults = await getUngroupedResults(numeratorQuery, callCluster); - const denominatorUngroupedResults = await getUngroupedResults(denominatorQuery, callCluster); + const numeratorUngroupedResults = await getUngroupedResults(numeratorQuery, esClient); + const denominatorUngroupedResults = await getUngroupedResults(denominatorQuery, esClient); processUngroupedRatioResults( numeratorUngroupedResults, denominatorUngroupedResults, @@ -605,17 +606,11 @@ const getQueryMappingForComparator = (comparator: Comparator) => { return queryMappings[comparator]; }; -const getUngroupedResults = async ( - query: object, - callCluster: LogThresholdAlertServices['callCluster'] -) => { - return decodeOrThrow(UngroupedSearchQueryResponseRT)(await callCluster('search', query)); +const getUngroupedResults = async (query: object, esClient: ElasticsearchClient) => { + return decodeOrThrow(UngroupedSearchQueryResponseRT)((await esClient.search(query)).body); }; -const getGroupedResults = async ( - query: object, - callCluster: LogThresholdAlertServices['callCluster'] -) => { +const getGroupedResults = async (query: object, esClient: ElasticsearchClient) => { let compositeGroupBuckets: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] = []; let lastAfterKey: GroupedSearchQueryResponse['aggregations']['groups']['after_key'] | undefined; @@ -623,7 +618,7 @@ const getGroupedResults = async ( const queryWithAfterKey: any = { ...query }; queryWithAfterKey.body.aggregations.groups.composite.after = lastAfterKey; const groupResponse: GroupedSearchQueryResponse = decodeOrThrow(GroupedSearchQueryResponseRT)( - await callCluster('search', queryWithAfterKey) + (await esClient.search(queryWithAfterKey)).body ); compositeGroupBuckets = [ ...compositeGroupBuckets, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index 3f6bb075c8f92..b7d3dbb1f7adb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -6,6 +6,7 @@ */ import { mapValues, first, last, isNaN } from 'lodash'; +import { ElasticsearchClient } from 'kibana/server'; import { isTooManyBucketsPreviewException, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, @@ -13,7 +14,6 @@ import { import { InfraSource } from '../../../../../common/http_api/source_api'; import { InfraDatabaseSearchResponse } from '../../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../../utils/create_afterkey_handler'; -import { AlertServices } from '../../../../../../alerting/server'; import { getAllCompositeData } from '../../../../utils/get_all_composite_data'; import { DOCUMENT_COUNT_I18N } from '../../common/messages'; import { UNGROUPED_FACTORY_KEY } from '../../common/utils'; @@ -43,7 +43,7 @@ export interface EvaluatedAlertParams { } export const evaluateAlert = ( - callCluster: AlertServices['callCluster'], + esClient: ElasticsearchClient, params: Params, config: InfraSource['configuration'], timeframe?: { start: number; end: number } @@ -52,7 +52,7 @@ export const evaluateAlert = { const currentValues = await getMetric( - callCluster, + esClient, criterion, config.metricAlias, config.fields.timestamp, @@ -91,7 +91,7 @@ export const evaluateAlert = Promise> = async function ( - callCluster, + esClient, params, index, timefield, @@ -127,7 +127,7 @@ const getMetric: ( (response) => response.aggregations?.groupings?.after_key ); const compositeBuckets = (await getAllCompositeData( - (body) => callCluster('search', { body, index }), + (body) => esClient.search({ body, index }), searchBody, bucketSelector, afterKeyHandler @@ -142,7 +142,7 @@ const getMetric: ( {} ); } - const result = await callCluster('search', { + const { body: result } = await esClient.search({ body: searchBody, index, }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 8d052f725fe20..9086d6436c2a2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -16,6 +16,8 @@ import { } from '../../../../../alerting/server/mocks'; import { InfraSources } from '../../sources'; import { MetricThresholdAlertExecutorOptions } from './register_metric_threshold_alert_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; interface AlertTestInstance { instance: AlertInstanceMock; @@ -439,26 +441,36 @@ const mockLibs: any = { const executor = createMetricThresholdExecutor(mockLibs); const services: AlertServicesMock = alertsMock.createAlertServices(); -services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { - if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; - const metric = body.query.bool.filter[1]?.exists.field; - if (body.aggs.groupings) { - if (body.aggs.groupings.composite.after) { - return mocks.compositeEndResponse; +services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => { + if (params.index === 'alternatebeat-*') return mocks.changedSourceIdResponse; + const metric = params?.body.query.bool.filter[1]?.exists.field; + if (params?.body.aggs.groupings) { + if (params?.body.aggs.groupings.composite.after) { + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.compositeEndResponse + ); } if (metric === 'test.metric.2') { - return mocks.alternateCompositeResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.alternateCompositeResponse + ); } - return mocks.basicCompositeResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.basicCompositeResponse + ); } if (metric === 'test.metric.2') { - return mocks.alternateMetricResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.alternateMetricResponse + ); } else if (metric === 'test.metric.3') { - return body.aggs.aggregatedIntervals.aggregations.aggregatedValue_max - ? mocks.emptyRateResponse - : mocks.emptyMetricResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise( + params?.body.aggs.aggregatedIntervals.aggregations.aggregatedValue_max + ? mocks.emptyRateResponse + : mocks.emptyMetricResponse + ); } - return mocks.basicMetricResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise(mocks.basicMetricResponse); }); services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { if (sourceId === 'alternate') diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 934d6cc4293ad..190d8e028fe0d 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -44,7 +44,7 @@ export const createMetricThresholdExecutor = ( ); const config = source.configuration; const alertResults = await evaluateAlert( - services.callCluster, + services.scopedClusterClient.asCurrentUser, params as EvaluatedAlertParams, config ); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts index 551116ddac091..49cb8d70f6020 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts @@ -9,6 +9,8 @@ import * as mocks from './test_mocks'; import { Comparator, Aggregators, MetricExpressionParams } from './types'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import { previewMetricThresholdAlert } from './preview_metric_threshold_alert'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; describe('Previewing the metric threshold alert type', () => { describe('querying the entire infrastructure', () => { @@ -163,21 +165,32 @@ describe('Previewing the metric threshold alert type', () => { }); const services: AlertServicesMock = alertsMock.createAlertServices(); -services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { - const metric = body.query.bool.filter[1]?.exists.field; - if (body.aggs.groupings) { - if (body.aggs.groupings.composite.after) { - return mocks.compositeEndResponse; + +services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => { + const metric = params?.body.query.bool.filter[1]?.exists.field; + if (params?.body.aggs.groupings) { + if (params?.body.aggs.groupings.composite.after) { + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.compositeEndResponse + ); } - return mocks.basicCompositePreviewResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.basicCompositePreviewResponse + ); } if (metric === 'test.metric.2') { - return mocks.alternateMetricPreviewResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.alternateMetricPreviewResponse + ); } if (metric === 'test.metric.3') { - return mocks.repeatingMetricPreviewResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.repeatingMetricPreviewResponse + ); } - return mocks.basicMetricPreviewResponse; + return elasticsearchClientMock.createSuccessTransportRequestPromise( + mocks.basicMetricPreviewResponse + ); }); const baseCriterion = { @@ -197,7 +210,7 @@ const config = { } as any; const baseParams = { - callCluster: services.callCluster, + esClient: services.scopedClusterClient.asCurrentUser, params: { criteria: [baseCriterion], groupBy: undefined, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index fe2a88d89bf4a..064804b661b74 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -11,7 +11,7 @@ import { TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, isTooManyBucketsPreviewException, } from '../../../../common/alerting/metrics'; -import { ILegacyScopedClusterClient } from '../../../../../../../src/core/server'; +import { ElasticsearchClient } from '../../../../../../../src/core/server'; import { InfraSource } from '../../../../common/http_api/source_api'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { PreviewResult } from '../common/types'; @@ -21,7 +21,7 @@ import { evaluateAlert } from './lib/evaluate_alert'; const MAX_ITERATIONS = 50; interface PreviewMetricThresholdAlertParams { - callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; + esClient: ElasticsearchClient; params: { criteria: MetricExpressionParams[]; groupBy: string | undefined | string[]; @@ -43,7 +43,7 @@ export const previewMetricThresholdAlert: ( precalculatedNumberOfGroups?: number ) => Promise = async ( { - callCluster, + esClient, params, config, lookback, @@ -79,7 +79,7 @@ export const previewMetricThresholdAlert: ( // Get a date histogram using the bucket interval and the lookback interval try { - const alertResults = await evaluateAlert(callCluster, params, config, timeframe); + const alertResults = await evaluateAlert(esClient, params, config, timeframe); const groups = Object.keys(first(alertResults)!); // Now determine how to interpolate this histogram based on the alert interval @@ -174,7 +174,7 @@ export const previewMetricThresholdAlert: ( // If there's too much data on the first request, recursively slice the lookback interval // until all the data can be retrieved const basePreviewParams = { - callCluster, + esClient, params, config, lookback, @@ -187,7 +187,7 @@ export const previewMetricThresholdAlert: ( // If this is still the first iteration, try to get the number of groups in order to // calculate max buckets. If this fails, just estimate based on 1 group const currentAlertResults = !precalculatedNumberOfGroups - ? await evaluateAlert(callCluster, params, config) + ? await evaluateAlert(esClient, params, config) : []; const numberOfGroups = precalculatedNumberOfGroups ?? Math.max(Object.keys(first(currentAlertResults)!).length, 1); diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index d1807583acd39..6622df1a8333a 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -26,7 +26,6 @@ import { InfraBackendLibs } from '../../lib/infra_types'; import { assertHasInfraMlPlugins } from '../../utils/request_context'; export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => { - const { callWithRequest } = framework; framework.registerRoute( { method: 'post', @@ -46,9 +45,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) alertNotifyWhen, } = request.body; - const callCluster = (endpoint: string, opts: Record) => { - return callWithRequest(requestContext, endpoint, opts); - }; + const esClient = requestContext.core.elasticsearch.client.asCurrentUser; const source = await sources.getSourceConfiguration( requestContext.core.savedObjects.client, @@ -64,7 +61,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) filterQuery, } = request.body as MetricThresholdAlertPreviewRequestParams; const previewResult = await previewMetricThresholdAlert({ - callCluster, + esClient, params: { criteria, filterQuery, groupBy }, lookback, config: source.configuration, @@ -86,7 +83,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) filterQuery, } = request.body as InventoryAlertPreviewRequestParams; const previewResult = await previewInventoryMetricThresholdAlert({ - callCluster, + esClient, params: { criteria, filterQuery, nodeType }, lookback, source, diff --git a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts index fbe8a36f5038c..df97c91aacd04 100644 --- a/x-pack/plugins/infra/server/utils/get_all_composite_data.ts +++ b/x-pack/plugins/infra/server/utils/get_all_composite_data.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; export const getAllCompositeData = async < @@ -12,13 +13,15 @@ export const getAllCompositeData = async < Bucket = {}, Options extends object = {} >( - callCluster: (options: Options) => Promise>, + esClientSearch: ( + options: Options + ) => Promise>>, options: Options, bucketSelector: (response: InfraDatabaseSearchResponse<{}, Aggregation>) => Bucket[], onAfterKey: (options: Options, response: InfraDatabaseSearchResponse<{}, Aggregation>) => Options, previousBuckets: Bucket[] = [] ): Promise => { - const response = await callCluster(options); + const { body: response } = await esClientSearch(options); // Nothing available, return the previous buckets. if (response.hits.total.value === 0) { @@ -40,7 +43,7 @@ export const getAllCompositeData = async < // There is possibly more data, concat previous and current buckets and call ourselves recursively. const newOptions = onAfterKey(options, response); return getAllCompositeData( - callCluster, + esClientSearch, newOptions, bucketSelector, onAfterKey, diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index b79d6a0b89a57..004677852d020 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -58,10 +58,10 @@ export class ListPlugin user, }); }, - getListClient: (callCluster, spaceId, user): ListClient => { + getListClient: (esClient, spaceId, user): ListClient => { return new ListClient({ - callCluster, config, + esClient, spaceId, user, }); @@ -86,9 +86,7 @@ export class ListPlugin core: { savedObjects: { client: savedObjectsClient }, elasticsearch: { - legacy: { - client: { callAsCurrentUser: callCluster }, - }, + client: { asCurrentUser: esClient }, }, }, } = context; @@ -105,8 +103,8 @@ export class ListPlugin }), getListClient: (): ListClient => new ListClient({ - callCluster, config, + esClient, spaceId, user, }), diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.mock.ts b/x-pack/plugins/lists/server/services/items/create_list_item.mock.ts index fba978d80d0bf..31befdc2122d3 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.mock.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.mock.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { CreateListItemOptions } from '../items'; import { DATE_NOW, @@ -19,9 +21,9 @@ import { } from '../../../common/constants.mock'; export const getCreateListItemOptionsMock = (): CreateListItemOptions => ({ - callCluster: getCallClusterMock(), dateNow: DATE_NOW, deserializer: undefined, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, id: LIST_ITEM_ID, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts index cced16b88433e..a13163d8f774a 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -5,6 +5,9 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { getIndexESListItemMock } from '../../../common/schemas/elastic_query/index_es_list_item_schema.mock'; import { LIST_ITEM_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; @@ -23,13 +26,17 @@ describe('crete_list_item', () => { test('it returns a list item as expected with the id changed out for the elastic id', async () => { const options = getCreateListItemOptionsMock(); - const listItem = await createListItem(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.index.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) + ); + const listItem = await createListItem({ ...options, esClient }); const expected = getListItemResponseMock(); expected.id = 'elastic-id-123'; expect(listItem).toEqual(expected); }); - test('It calls "callCluster" with body, index, and listIndex', async () => { + test('It calls "esClient" with body, index, and listIndex', async () => { const options = getCreateListItemOptionsMock(); await createListItem(options); const body = getIndexESListItemMock(); @@ -39,13 +46,17 @@ describe('crete_list_item', () => { index: LIST_ITEM_INDEX, refresh: 'wait_for', }; - expect(options.callCluster).toBeCalledWith('index', expected); + expect(options.esClient.index).toBeCalledWith(expected); }); test('It returns an auto-generated id if id is sent in undefined', async () => { const options = getCreateListItemOptionsMock(); options.id = undefined; - const list = await createListItem(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.index.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) + ); + const list = await createListItem({ ...options, esClient }); const expected = getListItemResponseMock(); expected.id = 'elastic-id-123'; expect(list).toEqual(expected); diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index bac2958857124..a5369bbfe7ca4 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { CreateDocumentResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { DeserializerOrUndefined, @@ -28,7 +28,7 @@ export interface CreateListItemOptions { listId: string; type: Type; value: string; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -43,7 +43,7 @@ export const createListItem = async ({ listId, type, value, - callCluster, + esClient, listItemIndex, user, meta, @@ -69,7 +69,7 @@ export const createListItem = async ({ ...baseBody, ...elasticQuery, }; - const response = await callCluster('index', { + const { body: response } = await esClient.index({ body, id, index: listItemIndex, diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.mock.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.mock.ts index d6a752df38efc..d2ceb32b91951 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.mock.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.mock.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { CreateListItemsBulkOptions } from '../items'; import { DATE_NOW, @@ -20,9 +22,9 @@ import { } from '../../../common/constants.mock'; export const getCreateListItemBulkOptionsMock = (): CreateListItemsBulkOptions => ({ - callCluster: getCallClusterMock(), dateNow: DATE_NOW, deserializer: undefined, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, meta: META, diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts index 38e22e9b19ef6..f9f9728798a0b 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -20,13 +20,13 @@ describe('crete_list_item_bulk', () => { jest.clearAllMocks(); }); - test('It calls "callCluster" with body, index, and the bulk items', async () => { + test('It calls "esClient" with body, index, and the bulk items', async () => { const options = getCreateListItemBulkOptionsMock(); await createListItemsBulk(options); const firstRecord = getIndexESListItemMock(); const secondRecord = getIndexESListItemMock(VALUE_2); [firstRecord.tie_breaker_id, secondRecord.tie_breaker_id] = TIE_BREAKERS; - expect(options.callCluster).toBeCalledWith('bulk', { + expect(options.esClient.bulk).toBeCalledWith({ body: [ { create: { _index: LIST_ITEM_INDEX } }, firstRecord, @@ -41,7 +41,7 @@ describe('crete_list_item_bulk', () => { test('It should not call the dataClient when the values are empty', async () => { const options = getCreateListItemBulkOptionsMock(); options.value = []; - expect(options.callCluster).not.toBeCalled(); + expect(options.esClient.bulk).not.toBeCalled(); }); test('It should skip over a value if it is not able to add that item because it is not parsable such as an ip_range with a serializer that only matches one ip', async () => { @@ -52,7 +52,7 @@ describe('crete_list_item_bulk', () => { value: ['127.0.0.1', '127.0.0.2'], }; await createListItemsBulk(options); - expect(options.callCluster).toBeCalledWith('bulk', { + expect(options.esClient.bulk).toBeCalledWith({ body: [ { create: { _index: LIST_ITEM_INDEX } }, { diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 0f8b5b7a08595..86d8d9a698b1f 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -6,7 +6,7 @@ */ import uuid from 'uuid'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { transformListItemToElasticQuery } from '../utils'; import { @@ -24,7 +24,7 @@ export interface CreateListItemsBulkOptions { listId: string; type: Type; value: string[]; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -38,7 +38,7 @@ export const createListItemsBulk = async ({ deserializer, serializer, value, - callCluster, + esClient, listItemIndex, user, meta, @@ -82,7 +82,7 @@ export const createListItemsBulk = async ({ [] ); try { - await callCluster('bulk', { + await esClient.bulk({ body, index: listItemIndex, refresh: 'wait_for', diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.mock.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.mock.ts index 9755afcd5422f..89331d02dc3ff 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.mock.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.mock.ts @@ -5,12 +5,14 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { DeleteListItemOptions } from '../items'; import { LIST_ITEM_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; export const getDeleteListItemOptionsMock = (): DeleteListItemOptions => ({ - callCluster: getCallClusterMock(), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, id: LIST_ITEM_ID, listItemIndex: LIST_ITEM_INDEX, }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts index 364b575587d42..de5b6540eee40 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts @@ -50,6 +50,6 @@ describe('delete_list_item', () => { index: LIST_ITEM_INDEX, refresh: 'wait_for', }; - expect(options.callCluster).toBeCalledWith('delete', deleteQuery); + expect(options.esClient.delete).toBeCalledWith(deleteQuery); }); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index 8f1728c5b4a70..f2e9949c82c3e 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Id, ListItemSchema } from '../../../common/schemas'; @@ -13,20 +13,20 @@ import { getListItem } from '.'; export interface DeleteListItemOptions { id: Id; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; } export const deleteListItem = async ({ id, - callCluster, + esClient, listItemIndex, }: DeleteListItemOptions): Promise => { - const listItem = await getListItem({ callCluster, id, listItemIndex }); + const listItem = await getListItem({ esClient, id, listItemIndex }); if (listItem == null) { return null; } else { - await callCluster('delete', { + await esClient.delete({ id, index: listItemIndex, refresh: 'wait_for', diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.mock.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.mock.ts index ef9cc0b46c0c2..54bfe3bae0811 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.mock.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.mock.ts @@ -5,12 +5,14 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { DeleteListItemByValueOptions } from '../items'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from '../../../common/constants.mock'; export const getDeleteListItemByValueOptionsMock = (): DeleteListItemByValueOptions => ({ - callCluster: getCallClusterMock(), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts index c2c7fae942ac3..2755ff8e7aba6 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -61,8 +61,8 @@ describe('delete_list_item_by_value', () => { }, }, index: '.items', - refresh: 'wait_for', + refresh: false, }; - expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); + expect(options.esClient.deleteByQuery).toBeCalledWith(deleteByQuery); }); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index bf02d30b324b8..1c7ac3afb3ee3 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { ListItemArraySchema, Type } from '../../../common/schemas'; import { getQueryFilterFromTypeValue } from '../utils'; @@ -16,7 +16,7 @@ export interface DeleteListItemByValueOptions { listId: string; type: Type; value: string; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; } @@ -24,11 +24,11 @@ export const deleteListItemByValue = async ({ listId, value, type, - callCluster, + esClient, listItemIndex, }: DeleteListItemByValueOptions): Promise => { const listItems = await getListItemByValues({ - callCluster, + esClient, listId, listItemIndex, type, @@ -40,7 +40,7 @@ export const deleteListItemByValue = async ({ type, value: values, }); - await callCluster('deleteByQuery', { + await esClient.deleteByQuery({ body: { query: { bool: { @@ -49,7 +49,7 @@ export const deleteListItemByValue = async ({ }, }, index: listItemIndex, - refresh: 'wait_for', + refresh: false, }); return listItems; }; diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.mock.ts b/x-pack/plugins/lists/server/services/items/find_list_item.mock.ts index 81b9375bb7c4a..4bf62982b2a9f 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.mock.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.mock.ts @@ -6,11 +6,10 @@ */ import { Client } from 'elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { getSearchListMock } from '../../../common/schemas/elastic_response/search_es_list_schema.mock'; import { getShardMock } from '../../../common/get_shard.mock'; -import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; -import { getCallClusterMockMultiTimes } from '../../../common/get_call_cluster.mock'; import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; import { FindListItemOptions } from './find_list_item'; @@ -23,14 +22,9 @@ export const getFindCount = (): ReturnType => { }; export const getFindListItemOptionsMock = (): FindListItemOptions => { - const callCluster = getCallClusterMockMultiTimes([ - getSearchListMock(), - getFindCount(), - getSearchListItemMock(), - ]); return { - callCluster, currentIndexPosition: 0, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, filter: '', listId: LIST_ID, listIndex: LIST_INDEX, diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.test.ts b/x-pack/plugins/lists/server/services/items/find_list_item.test.ts index 4cd7e4aaef00a..29e6f2f845002 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.test.ts @@ -5,9 +5,12 @@ * 2.0. */ -import { getEmptySearchListMock } from '../../../common/schemas/elastic_response/search_es_list_schema.mock'; -import { getCallClusterMockMultiTimes } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +import { getShardMock } from '../../../common/get_shard.mock'; import { getFoundListItemSchemaMock } from '../../../common/schemas/response/found_list_item_schema.mock'; +import { getEmptySearchListMock } from '../../../common/schemas/elastic_response/search_es_list_schema.mock'; import { getFindListItemOptionsMock } from './find_list_item.mock'; import { findListItem } from './find_list_item'; @@ -15,15 +18,53 @@ import { findListItem } from './find_list_item'; describe('find_list_item', () => { test('should find a simple single list item', async () => { const options = getFindListItemOptionsMock(); - const item = await findListItem(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.count.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ count: 1 }) + ); + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + _scroll_id: '123', + _shards: getShardMock(), + hits: { + hits: [ + { + _id: 'some-list-item-id', + _source: { + _version: 'undefined', + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'some user', + date_range: '127.0.0.1', + deserializer: undefined, + list_id: 'some-list-id', + meta: {}, + serializer: undefined, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: '2020-04-20T15:25:31.830Z', + updated_by: 'some user', + }, + }, + ], + max_score: 0, + total: 1, + }, + timed_out: false, + took: 10, + }) + ); + const item = await findListItem({ ...options, esClient }); const expected = getFoundListItemSchemaMock(); expect(item).toEqual(expected); }); test('should return null if the list is null', async () => { const options = getFindListItemOptionsMock(); - options.callCluster = getCallClusterMockMultiTimes([getEmptySearchListMock()]); - const item = await findListItem(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(getEmptySearchListMock()) + ); + const item = await findListItem({ ...options, esClient }); expect(item).toEqual(null); }); }); diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.ts b/x-pack/plugins/lists/server/services/items/find_list_item.ts index e0639bc51ce7b..727c55d53e459 100644 --- a/x-pack/plugins/lists/server/services/items/find_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/find_list_item.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { @@ -37,13 +37,13 @@ export interface FindListItemOptions { page: Page; sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listIndex: string; listItemIndex: string; } export const findListItem = async ({ - callCluster, + esClient, currentIndexPosition, filter, listId, @@ -55,7 +55,7 @@ export const findListItem = async ({ listItemIndex, sortOrder, }: FindListItemOptions): Promise => { - const list = await getList({ callCluster, id: listId, listIndex }); + const list = await getList({ esClient, id: listId, listIndex }); if (list == null) { return null; } else { @@ -63,8 +63,8 @@ export const findListItem = async ({ const sortField = sortFieldWithPossibleValue === 'value' ? list.type : sortFieldWithPossibleValue; const scroll = await scrollToStartPage({ - callCluster, currentIndexPosition, + esClient, filter, hopSize: 100, index: listItemIndex, @@ -75,25 +75,25 @@ export const findListItem = async ({ sortOrder, }); - const { count } = await callCluster('count', { + const { body: respose } = await esClient.count<{ count: number }>({ body: { query, }, - ignoreUnavailable: true, + ignore_unavailable: true, index: listItemIndex, }); if (scroll.validSearchAfterFound) { - // Note: This typing of response = await callCluster> + // Note: This typing of response = await esClient> // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have // to explicitly define the type . - const response = await callCluster>('search', { + const { body: response } = await esClient.search>({ body: { query, search_after: scroll.searchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), }, - ignoreUnavailable: true, + ignore_unavailable: true, index: listItemIndex, seq_no_primary_term: true, size: perPage, @@ -107,7 +107,7 @@ export const findListItem = async ({ data: transformElasticToListItem({ response, type: list.type }), page, per_page: perPage, - total: count, + total: respose.count, }; } else { return { @@ -115,7 +115,7 @@ export const findListItem = async ({ data: [], page, per_page: perPage, - total: count, + total: respose.count, }; } } diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts index 34425f10ec5ad..f92031cae02ca 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts @@ -5,9 +5,11 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { DATE_NOW, LIST_ID, @@ -30,8 +32,11 @@ describe('get_list_item', () => { test('it returns a list item as expected if the list item is found', async () => { const data = getSearchListItemMock(); - const callCluster = getCallClusterMock(data); - const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); + const list = await getListItem({ esClient, id: LIST_ID, listItemIndex: LIST_INDEX }); const expected = getListItemResponseMock(); expect(list).toEqual(expected); }); @@ -39,8 +44,11 @@ describe('get_list_item', () => { test('it returns null if the search is empty', async () => { const data = getSearchListItemMock(); data.hits.hits = []; - const callCluster = getCallClusterMock(data); - const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); + const list = await getListItem({ esClient, id: LIST_ID, listItemIndex: LIST_INDEX }); expect(list).toEqual(null); }); @@ -80,8 +88,11 @@ describe('get_list_item', () => { updated_at: DATE_NOW, updated_by: USER, }; - const callCluster = getCallClusterMock(data); - const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); + const list = await getListItem({ esClient, id: LIST_ID, listItemIndex: LIST_INDEX }); expect(list).toEqual(null); }); }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index 2ccf27e0c00dc..eb05a899478a5 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; @@ -14,19 +14,19 @@ import { findSourceType } from '../utils/find_source_type'; interface GetListItemOptions { id: Id; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; } export const getListItem = async ({ id, - callCluster, + esClient, listItemIndex, }: GetListItemOptions): Promise => { - // Note: This typing of response = await callCluster> + // Note: This typing of response = await esClient> // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have // to explicitly define the type . - const listItemES = await callCluster>('search', { + const { body: listItemES } = await esClient.search>({ body: { query: { term: { @@ -34,7 +34,7 @@ export const getListItem = async ({ }, }, }, - ignoreUnavailable: true, + ignore_unavailable: true, index: listItemIndex, seq_no_primary_term: true, }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.mock.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.mock.ts index 18e7f044bc4ca..3cd329fca3708 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.mock.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.mock.ts @@ -5,12 +5,14 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { GetListItemByValueOptions } from '../items'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from '../../../common/constants.mock'; export const getListItemByValueOptionsMocks = (): GetListItemByValueOptions => ({ - callCluster: getCallClusterMock(), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts index 8af7e3bdc4156..7d3fe81babe59 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { ListItemArraySchema, Type } from '../../../common/schemas'; @@ -13,7 +13,7 @@ import { getListItemByValues } from '.'; export interface GetListItemByValueOptions { listId: string; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; type: Type; value: string; @@ -21,13 +21,13 @@ export interface GetListItemByValueOptions { export const getListItemByValue = async ({ listId, - callCluster, + esClient, listItemIndex, type, value, }: GetListItemByValueOptions): Promise => getListItemByValues({ - callCluster, + esClient, listId, listItemIndex, type, diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.mock.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.mock.ts index 9496e175dd9a5..169934b2ee256 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.mock.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.mock.ts @@ -5,12 +5,14 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { GetListItemByValuesOptions } from '../items'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from '../../../common/constants.mock'; export const getListItemByValuesOptionsMocks = (): GetListItemByValuesOptions => ({ - callCluster: getCallClusterMock(), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts index b5db19451063b..aa22049ce6fe4 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts @@ -5,8 +5,10 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { DATE_NOW, LIST_ID, @@ -34,9 +36,12 @@ describe('get_list_item_by_values', () => { test('Returns a an empty array if the ES query is also empty', async () => { const data = getSearchListItemMock(); data.hits.hits = []; - const callCluster = getCallClusterMock(data); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); const listItem = await getListItemByValues({ - callCluster, + esClient, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, @@ -48,9 +53,12 @@ describe('get_list_item_by_values', () => { test('Returns transformed list item if the data exists within ES', async () => { const data = getSearchListItemMock(); - const callCluster = getCallClusterMock(data); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); const listItem = await getListItemByValues({ - callCluster, + esClient, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts index 6b76f55f4ccc5..c00ee2b13426a 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -5,14 +5,18 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; -import { getQueryFilterFromTypeValue, transformElasticToListItem } from '../utils'; +import { + TransformElasticToListItemOptions, + getQueryFilterFromTypeValue, + transformElasticToListItem, +} from '../utils'; export interface GetListItemByValuesOptions { listId: string; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; type: Type; value: string[]; @@ -20,12 +24,12 @@ export interface GetListItemByValuesOptions { export const getListItemByValues = async ({ listId, - callCluster, + esClient, listItemIndex, type, value, }: GetListItemByValuesOptions): Promise => { - const response = await callCluster('search', { + const { body: response } = await esClient.search({ body: { query: { bool: { @@ -33,9 +37,12 @@ export const getListItemByValues = async ({ }, }, }, - ignoreUnavailable: true, + ignore_unavailable: true, index: listItemIndex, size: 10000, // TODO: This has a limit on the number which is 10,000 the default of Elastic but we might want to provide a way to increase that number }); - return transformElasticToListItem({ response, type }); + return transformElasticToListItem(({ + response, + type, + } as unknown) as TransformElasticToListItemOptions); }; diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.mock.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.mock.ts index 8b8a6a3041351..656b569502fbb 100644 --- a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.mock.ts +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.mock.ts @@ -5,12 +5,14 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { SearchListItemByValuesOptions } from '../items'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from '../../../common/constants.mock'; export const searchListItemByValuesOptionsMocks = (): SearchListItemByValuesOptions => ({ - callCluster: getCallClusterMock(), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts index d989dd6c92e3a..0d084c50b5745 100644 --- a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts @@ -5,9 +5,11 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { SearchListItemArraySchema } from '../../../common/schemas'; import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from '../../../common/constants.mock'; import { searchListItemByValues } from './search_list_item_by_values'; @@ -24,9 +26,12 @@ describe('search_list_item_by_values', () => { test('Returns a an empty array of items if the value is empty', async () => { const data = getSearchListItemMock(); data.hits.hits = []; - const callCluster = getCallClusterMock(data); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); const listItem = await searchListItemByValues({ - callCluster, + esClient, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, @@ -39,9 +44,12 @@ describe('search_list_item_by_values', () => { test('Returns a an empty array of items if the ES query is also empty', async () => { const data = getSearchListItemMock(); data.hits.hits = []; - const callCluster = getCallClusterMock(data); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); const listItem = await searchListItemByValues({ - callCluster, + esClient, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, @@ -57,9 +65,12 @@ describe('search_list_item_by_values', () => { test('Returns transformed list item if the data exists within ES', async () => { const data = getSearchListItemMock(); - const callCluster = getCallClusterMock(data); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); const listItem = await searchListItemByValues({ - callCluster, + esClient, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts index f15f57dfbbd07..4f8808d06e425 100644 --- a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts @@ -5,14 +5,18 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { SearchEsListItemSchema, SearchListItemArraySchema, Type } from '../../../common/schemas'; -import { getQueryFilterFromTypeValue, transformElasticNamedSearchToListItem } from '../utils'; +import { + TransformElasticMSearchToListItemOptions, + getQueryFilterFromTypeValue, + transformElasticNamedSearchToListItem, +} from '../utils'; export interface SearchListItemByValuesOptions { listId: string; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; type: Type; value: unknown[]; @@ -20,12 +24,12 @@ export interface SearchListItemByValuesOptions { export const searchListItemByValues = async ({ listId, - callCluster, + esClient, listItemIndex, type, value, }: SearchListItemByValuesOptions): Promise => { - const response = await callCluster('search', { + const { body: response } = await esClient.search({ body: { query: { bool: { @@ -33,9 +37,13 @@ export const searchListItemByValues = async ({ }, }, }, - ignoreUnavailable: true, + ignore_unavailable: true, index: listItemIndex, size: 10000, // TODO: This has a limit on the number which is 10,000 the default of Elastic but we might want to provide a way to increase that number }); - return transformElasticNamedSearchToListItem({ response, type, value }); + return transformElasticNamedSearchToListItem(({ + response, + type, + value, + } as unknown) as TransformElasticMSearchToListItemOptions); }; diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts b/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts index c69f087c96a72..705e207853543 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.mock.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { UpdateListItemOptions } from '../items'; import { DATE_NOW, @@ -18,8 +20,8 @@ import { export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({ _version: undefined, - callCluster: getCallClusterMock(), dateNow: DATE_NOW, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, id: LIST_ITEM_ID, listItemIndex: LIST_ITEM_INDEX, meta: META, diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts index ec44ae1a3b5fd..ae6b6ad3faecf 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts @@ -5,6 +5,9 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { ListItemSchema } from '../../../common/schemas'; import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; @@ -29,7 +32,11 @@ describe('update_list_item', () => { const listItem = getListItemResponseMock(); ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(listItem); const options = getUpdateListItemOptionsMock(); - const updatedList = await updateListItem(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.update.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) + ); + const updatedList = await updateListItem({ ...options, esClient }); const expected: ListItemSchema = { ...getListItemResponseMock(), id: 'elastic-id-123' }; expect(updatedList).toEqual(expected); }); diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 7da17ba3c3eb6..645508691acc8 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -6,7 +6,7 @@ */ import { CreateDocumentResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Id, @@ -25,7 +25,7 @@ export interface UpdateListItemOptions { _version: _VersionOrUndefined; id: Id; value: string | null | undefined; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -36,14 +36,14 @@ export const updateListItem = async ({ _version, id, value, - callCluster, + esClient, listItemIndex, user, meta, dateNow, }: UpdateListItemOptions): Promise => { const updatedAt = dateNow ?? new Date().toISOString(); - const listItem = await getListItem({ callCluster, id, listItemIndex }); + const listItem = await getListItem({ esClient, id, listItemIndex }); if (listItem == null) { return null; } else { @@ -62,7 +62,7 @@ export const updateListItem = async ({ ...elasticQuery, }; - const response = await callCluster('update', { + const { body: response } = await esClient.update({ ...decodeVersion(_version), body: { doc, diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts index c59f95e152ba8..949b7a5c1a691 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { ImportListItemsToStreamOptions, WriteBufferToItemsOptions } from '../items'; import { LIST_ID, @@ -21,9 +23,9 @@ import { getConfigMockDecoded } from '../../config.mock'; import { TestReadable } from './test_readable.mock'; export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ - callCluster: getCallClusterMock(), config: getConfigMockDecoded(), deserializer: undefined, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listIndex: LIST_INDEX, listItemIndex: LIST_ITEM_INDEX, @@ -37,8 +39,8 @@ export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStream export const getWriteBufferToItemsOptionsMock = (): WriteBufferToItemsOptions => ({ buffer: [], - callCluster: getCallClusterMock(), deserializer: undefined, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, meta: META, diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 1dd9aa6d97368..8450890cfa355 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -7,7 +7,7 @@ import { Readable } from 'stream'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist'; import { @@ -31,7 +31,7 @@ export interface ImportListItemsToStreamOptions { deserializer: DeserializerOrUndefined; serializer: SerializerOrUndefined; stream: Readable; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; type: Type; user: string; @@ -45,7 +45,7 @@ export const importListItemsToStream = ({ serializer, listId, stream, - callCluster, + esClient, listItemIndex, listIndex, type, @@ -62,9 +62,9 @@ export const importListItemsToStream = ({ fileName = fileNameEmitted; if (listId == null) { list = await createListIfItDoesNotExist({ - callCluster, description: `File uploaded from file system of ${fileNameEmitted}`, deserializer, + esClient, id: fileNameEmitted, immutable: false, listIndex, @@ -83,8 +83,8 @@ export const importListItemsToStream = ({ if (listId != null) { await writeBufferToItems({ buffer: lines, - callCluster, deserializer, + esClient, listId, listItemIndex, meta, @@ -95,8 +95,8 @@ export const importListItemsToStream = ({ } else if (fileName != null) { await writeBufferToItems({ buffer: lines, - callCluster, deserializer, + esClient, listId: fileName, listItemIndex, meta, @@ -117,7 +117,7 @@ export interface WriteBufferToItemsOptions { listId: string; deserializer: DeserializerOrUndefined; serializer: SerializerOrUndefined; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; buffer: string[]; type: Type; @@ -131,7 +131,7 @@ export interface LinesResult { export const writeBufferToItems = async ({ listId, - callCluster, + esClient, deserializer, serializer, listItemIndex, @@ -141,8 +141,8 @@ export const writeBufferToItems = async ({ meta, }: WriteBufferToItemsOptions): Promise => { await createListItemsBulk({ - callCluster, deserializer, + esClient, listId, listItemIndex, meta, diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts index 2f161369c84fb..b096adb2d1a13 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -5,8 +5,10 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { LIST_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; import { @@ -38,8 +40,11 @@ describe('write_list_items_to_stream', () => { const options = getExportListItemsToStreamOptionsMock(); const firstResponse = getSearchListItemMock(); firstResponse.hits.hits = []; - options.callCluster = getCallClusterMock(firstResponse); - exportListItemsToStream(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(firstResponse) + ); + exportListItemsToStream({ ...options, esClient }); let chunks: string[] = []; options.stream.on('data', (chunk: Buffer) => { @@ -54,7 +59,12 @@ describe('write_list_items_to_stream', () => { test('It exports single list item to the stream', (done) => { const options = getExportListItemsToStreamOptionsMock(); - exportListItemsToStream(options); + const response = getSearchListItemMock(); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(response) + ); + exportListItemsToStream({ ...options, esClient }); let chunks: string[] = []; options.stream.on('data', (chunk: Buffer) => { @@ -72,8 +82,11 @@ describe('write_list_items_to_stream', () => { const firstResponse = getSearchListItemMock(); const secondResponse = getSearchListItemMock(); firstResponse.hits.hits = [...firstResponse.hits.hits, ...secondResponse.hits.hits]; - options.callCluster = getCallClusterMock(firstResponse); - exportListItemsToStream(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(firstResponse) + ); + exportListItemsToStream({ ...options, esClient }); let chunks: string[] = []; options.stream.on('data', (chunk: Buffer) => { @@ -95,12 +108,14 @@ describe('write_list_items_to_stream', () => { const secondResponse = getSearchListItemMock(); secondResponse.hits.hits[0]._source.ip = '255.255.255.255'; - options.callCluster = jest - .fn() - .mockResolvedValueOnce(firstResponse) - .mockResolvedValueOnce(secondResponse); - - exportListItemsToStream(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(firstResponse) + ); + esClient.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(secondResponse) + ); + exportListItemsToStream({ ...options, esClient }); let chunks: string[] = []; options.stream.on('data', (chunk: Buffer) => { @@ -117,7 +132,12 @@ describe('write_list_items_to_stream', () => { describe('writeNextResponse', () => { test('It returns an empty searchAfter response when there is no sort defined', async () => { const options = getWriteNextResponseOptions(); - const searchAfter = await writeNextResponse(options); + const listItem = getSearchListItemMock(); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(listItem) + ); + const searchAfter = await writeNextResponse({ ...options, esClient }); expect(searchAfter).toEqual(undefined); }); @@ -125,8 +145,11 @@ describe('write_list_items_to_stream', () => { const listItem = getSearchListItemMock(); listItem.hits.hits[0].sort = ['sort-value-1']; const options = getWriteNextResponseOptions(); - options.callCluster = getCallClusterMock(listItem); - const searchAfter = await writeNextResponse(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(listItem) + ); + const searchAfter = await writeNextResponse({ ...options, esClient }); expect(searchAfter).toEqual(['sort-value-1']); }); @@ -134,8 +157,11 @@ describe('write_list_items_to_stream', () => { const listItem = getSearchListItemMock(); listItem.hits.hits = []; const options = getWriteNextResponseOptions(); - options.callCluster = getCallClusterMock(listItem); - const searchAfter = await writeNextResponse(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(listItem) + ); + const searchAfter = await writeNextResponse({ ...options, esClient }); expect(searchAfter).toEqual(undefined); }); }); @@ -183,11 +209,11 @@ describe('write_list_items_to_stream', () => { search_after: ['string 1', 'string 2'], sort: [{ tie_breaker_id: 'asc' }], }, - ignoreUnavailable: true, + ignore_unavailable: true, index: LIST_ITEM_INDEX, size: 100, }; - expect(options.callCluster).toBeCalledWith('search', expected); + expect(options.esClient.search).toBeCalledWith(expected); }); test('It returns a simple response with expected values and size changed', async () => { @@ -201,11 +227,11 @@ describe('write_list_items_to_stream', () => { search_after: ['string 1', 'string 2'], sort: [{ tie_breaker_id: 'asc' }], }, - ignoreUnavailable: true, + ignore_unavailable: true, index: LIST_ITEM_INDEX, size: 33, }; - expect(options.callCluster).toBeCalledWith('search', expected); + expect(options.esClient.search).toBeCalledWith(expected); }); }); diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts index 58b09d9e466d3..9bdcb58835ab0 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts @@ -8,7 +8,7 @@ import { PassThrough } from 'stream'; import { SearchResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { SearchEsListItemSchema } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; @@ -22,7 +22,7 @@ export const SIZE = 100; export interface ExportListItemsToStreamOptions { listId: string; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; stream: PassThrough; stringToAppend: string | null | undefined; @@ -30,7 +30,7 @@ export interface ExportListItemsToStreamOptions { export const exportListItemsToStream = ({ listId, - callCluster, + esClient, stream, listItemIndex, stringToAppend, @@ -39,7 +39,7 @@ export const exportListItemsToStream = ({ // and prevent the async await from bubbling up to the caller setTimeout(async () => { let searchAfter = await writeNextResponse({ - callCluster, + esClient, listId, listItemIndex, searchAfter: undefined, @@ -48,7 +48,7 @@ export const exportListItemsToStream = ({ }); while (searchAfter != null) { searchAfter = await writeNextResponse({ - callCluster, + esClient, listId, listItemIndex, searchAfter, @@ -62,7 +62,7 @@ export const exportListItemsToStream = ({ export interface WriteNextResponseOptions { listId: string; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listItemIndex: string; stream: PassThrough; searchAfter: string[] | undefined; @@ -71,14 +71,14 @@ export interface WriteNextResponseOptions { export const writeNextResponse = async ({ listId, - callCluster, + esClient, stream, listItemIndex, searchAfter, stringToAppend, }: WriteNextResponseOptions): Promise => { const response = await getResponse({ - callCluster, + esClient, listId, listItemIndex, searchAfter, @@ -102,7 +102,7 @@ export const getSearchAfterFromResponse = ({ : undefined; export interface GetResponseOptions { - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listId: string; searchAfter: undefined | string[]; listItemIndex: string; @@ -110,26 +110,28 @@ export interface GetResponseOptions { } export const getResponse = async ({ - callCluster, + esClient, searchAfter, listId, listItemIndex, size = SIZE, }: GetResponseOptions): Promise> => { - return callCluster('search', { - body: { - query: { - term: { - list_id: listId, + return (( + await esClient.search({ + body: { + query: { + term: { + list_id: listId, + }, }, + search_after: searchAfter, + sort: [{ tie_breaker_id: 'asc' }], }, - search_after: searchAfter, - sort: [{ tie_breaker_id: 'asc' }], - }, - ignoreUnavailable: true, - index: listItemIndex, - size, - }); + ignore_unavailable: true, + index: listItemIndex, + size, + }) + ).body as unknown) as SearchResponse; }; export interface WriteResponseHitsToStreamOptions { diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts index 676613a205042..3de8fdb0c9df6 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_streams.mock.ts @@ -7,8 +7,10 @@ import { Stream } from 'stream'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { ExportListItemsToStreamOptions, GetResponseOptions, @@ -18,7 +20,7 @@ import { import { LIST_ID, LIST_ITEM_INDEX } from '../../../common/constants.mock'; export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStreamOptions => ({ - callCluster: getCallClusterMock(getSearchListItemMock()), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, stream: new Stream.PassThrough(), @@ -26,7 +28,7 @@ export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStream }); export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ - callCluster: getCallClusterMock(getSearchListItemMock()), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, searchAfter: [], @@ -35,7 +37,7 @@ export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ }); export const getResponseOptionsMock = (): GetResponseOptions => ({ - callCluster: getCallClusterMock(), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, searchAfter: [], diff --git a/x-pack/plugins/lists/server/services/lists/create_list.mock.ts b/x-pack/plugins/lists/server/services/lists/create_list.mock.ts index c3ddb3bfc56ae..5e9c8e38c3f5e 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.mock.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.mock.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { CreateListOptions } from '../lists'; import { DATE_NOW, @@ -22,10 +24,10 @@ import { } from '../../../common/constants.mock'; export const getCreateListOptionsMock = (): CreateListOptions => ({ - callCluster: getCallClusterMock(), dateNow: DATE_NOW, description: DESCRIPTION, deserializer: undefined, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, id: LIST_ID, immutable: IMMUTABLE, listIndex: LIST_INDEX, diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts index dbbb7d6e6c5f1..6fc556955fae3 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -5,6 +5,9 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { ListSchema } from '../../../common/schemas'; import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; import { getIndexESListMock } from '../../../common/schemas/elastic_query/index_es_list_schema.mock'; @@ -24,7 +27,11 @@ describe('crete_list', () => { test('it returns a list as expected with the id changed out for the elastic id', async () => { const options = getCreateListOptionsMock(); - const list = await createList(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.index.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) + ); + const list = await createList({ ...options, esClient }); const expected: ListSchema = { ...getListResponseMock(), id: 'elastic-id-123' }; expect(list).toEqual(expected); }); @@ -35,7 +42,11 @@ describe('crete_list', () => { deserializer: '{{value}}', serializer: '(?)', }; - const list = await createList(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.index.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) + ); + const list = await createList({ ...options, esClient }); const expected: ListSchema = { ...getListResponseMock(), deserializer: '{{value}}', @@ -45,7 +56,7 @@ describe('crete_list', () => { expect(list).toEqual(expected); }); - test('It calls "callCluster" with body, index, and listIndex', async () => { + test('It calls "esClient" with body, index, and listIndex', async () => { const options = getCreateListOptionsMock(); await createList(options); const body = getIndexESListMock(); @@ -55,13 +66,17 @@ describe('crete_list', () => { index: LIST_INDEX, refresh: 'wait_for', }; - expect(options.callCluster).toBeCalledWith('index', expected); + expect(options.esClient.index).toBeCalledWith(expected); }); test('It returns an auto-generated id if id is sent in undefined', async () => { const options = getCreateListOptionsMock(); options.id = undefined; - const list = await createList(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.index.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) + ); + const list = await createList({ ...options, esClient }); const expected: ListSchema = { ...getListResponseMock(), id: 'elastic-id-123' }; expect(list).toEqual(expected); }); diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 999b29bcb08fd..2671a23266ec9 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { CreateDocumentResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { encodeHitVersion } from '../utils/encode_hit_version'; import { @@ -31,7 +31,7 @@ export interface CreateListOptions { type: Type; name: Name; description: Description; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listIndex: string; user: string; meta: MetaOrUndefined; @@ -48,7 +48,7 @@ export const createList = async ({ name, type, description, - callCluster, + esClient, listIndex, user, meta, @@ -73,7 +73,7 @@ export const createList = async ({ updated_by: user, version, }; - const response = await callCluster('index', { + const { body: response } = await esClient.index({ body, id, index: listIndex, diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts index 0f1fd196d3dc2..5325d951626c7 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Description, @@ -31,7 +31,7 @@ export interface CreateListIfItDoesNotExistOptions { serializer: SerializerOrUndefined; description: Description; immutable: Immutable; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listIndex: string; user: string; meta: MetaOrUndefined; @@ -46,7 +46,7 @@ export const createListIfItDoesNotExist = async ({ type, description, deserializer, - callCluster, + esClient, listIndex, user, meta, @@ -56,13 +56,13 @@ export const createListIfItDoesNotExist = async ({ version, immutable, }: CreateListIfItDoesNotExistOptions): Promise => { - const list = await getList({ callCluster, id, listIndex }); + const list = await getList({ esClient, id, listIndex }); if (list == null) { return createList({ - callCluster, dateNow, description, deserializer, + esClient, id, immutable, listIndex, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.mock.ts b/x-pack/plugins/lists/server/services/lists/delete_list.mock.ts index f231213753762..569083aad40db 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.mock.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.mock.ts @@ -5,12 +5,14 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { DeleteListOptions } from '../lists'; import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock'; export const getDeleteListOptionsMock = (): DeleteListOptions => ({ - callCluster: getCallClusterMock(), + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, id: LIST_ID, listIndex: LIST_INDEX, listItemIndex: LIST_ITEM_INDEX, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts index 8742123238717..9ceecbc299bab 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -48,9 +48,9 @@ describe('delete_list', () => { const deleteByQuery = { body: { query: { term: { list_id: LIST_ID } } }, index: LIST_ITEM_INDEX, - refresh: 'wait_for', + refresh: false, }; - expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); + expect(options.esClient.deleteByQuery).toBeCalledWith(deleteByQuery); }); test('Delete calls "delete" second if a list is returned from getList', async () => { @@ -61,15 +61,15 @@ describe('delete_list', () => { const deleteQuery = { id: LIST_ID, index: LIST_INDEX, - refresh: 'wait_for', + refresh: false, }; - expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); + expect(options.esClient.delete).toHaveBeenNthCalledWith(1, deleteQuery); }); test('Delete does not call data client if the list returns null', async () => { ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); const options = getDeleteListOptionsMock(); await deleteList(options); - expect(options.callCluster).not.toHaveBeenCalled(); + expect(options.esClient.delete).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index cac0189d789b6..4fe200bff436f 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Id, ListSchema } from '../../../common/schemas'; @@ -13,22 +13,22 @@ import { getList } from './get_list'; export interface DeleteListOptions { id: Id; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listIndex: string; listItemIndex: string; } export const deleteList = async ({ id, - callCluster, + esClient, listIndex, listItemIndex, }: DeleteListOptions): Promise => { - const list = await getList({ callCluster, id, listIndex }); + const list = await getList({ esClient, id, listIndex }); if (list == null) { return null; } else { - await callCluster('deleteByQuery', { + await esClient.deleteByQuery({ body: { query: { term: { @@ -37,13 +37,13 @@ export const deleteList = async ({ }, }, index: listItemIndex, - refresh: 'wait_for', + refresh: false, }); - await callCluster('delete', { + await esClient.delete({ id, index: listIndex, - refresh: 'wait_for', + refresh: false, }); return list; } diff --git a/x-pack/plugins/lists/server/services/lists/find_list.ts b/x-pack/plugins/lists/server/services/lists/find_list.ts index c6b995c5102c8..c5a398b0a1ad0 100644 --- a/x-pack/plugins/lists/server/services/lists/find_list.ts +++ b/x-pack/plugins/lists/server/services/lists/find_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { @@ -34,12 +34,12 @@ interface FindListOptions { page: Page; sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listIndex: string; } export const findList = async ({ - callCluster, + esClient, currentIndexPosition, filter, page, @@ -52,8 +52,8 @@ export const findList = async ({ const query = getQueryFilter({ filter }); const scroll = await scrollToStartPage({ - callCluster, currentIndexPosition, + esClient, filter, hopSize: 100, index: listIndex, @@ -64,25 +64,25 @@ export const findList = async ({ sortOrder, }); - const { count } = await callCluster('count', { + const { body: totalCount } = await esClient.count({ body: { query, }, - ignoreUnavailable: true, + ignore_unavailable: true, index: listIndex, }); if (scroll.validSearchAfterFound) { - // Note: This typing of response = await callCluster> + // Note: This typing of response = await esClient> // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have // to explicitly define the type . - const response = await callCluster>('search', { + const { body: response } = await esClient.search>({ body: { query, search_after: scroll.searchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), }, - ignoreUnavailable: true, + ignore_unavailable: true, index: listIndex, seq_no_primary_term: true, size: perPage, @@ -96,7 +96,7 @@ export const findList = async ({ data: transformElasticToList({ response }), page, per_page: perPage, - total: count, + total: totalCount.count, }; } else { return { @@ -104,7 +104,7 @@ export const findList = async ({ data: [], page, per_page: perPage, - total: count, + total: totalCount.count, }; } }; diff --git a/x-pack/plugins/lists/server/services/lists/get_list.test.ts b/x-pack/plugins/lists/server/services/lists/get_list.test.ts index 9d1b8d8d02fe8..930a52266ba41 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.test.ts @@ -5,9 +5,11 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { getSearchListMock } from '../../../common/schemas/elastic_response/search_es_list_schema.mock'; import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { LIST_ID, LIST_INDEX } from '../../../common/constants.mock'; import { getList } from './get_list'; @@ -23,8 +25,11 @@ describe('get_list', () => { test('it returns a list as expected if the list is found', async () => { const data = getSearchListMock(); - const callCluster = getCallClusterMock(data); - const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); + const list = await getList({ esClient, id: LIST_ID, listIndex: LIST_INDEX }); const expected = getListResponseMock(); expect(list).toEqual(expected); }); @@ -32,8 +37,11 @@ describe('get_list', () => { test('it returns null if the search is empty', async () => { const data = getSearchListMock(); data.hits.hits = []; - const callCluster = getCallClusterMock(data); - const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(data) + ); + const list = await getList({ esClient, id: LIST_ID, listIndex: LIST_INDEX }); expect(list).toEqual(null); }); }); diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index a4c45ef6ab0d4..50e6d08dd80ff 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; @@ -13,19 +13,19 @@ import { transformElasticToList } from '../utils/transform_elastic_to_list'; interface GetListOptions { id: Id; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listIndex: string; } export const getList = async ({ id, - callCluster, + esClient, listIndex, }: GetListOptions): Promise => { - // Note: This typing of response = await callCluster> + // Note: This typing of response = await esClient> // is because when you pass in seq_no_primary_term: true it does a "fall through" type and you have // to explicitly define the type . - const response = await callCluster>('search', { + const { body: response } = await esClient.search>({ body: { query: { term: { @@ -33,7 +33,7 @@ export const getList = async ({ }, }, }, - ignoreUnavailable: true, + ignore_unavailable: true, index: listIndex, seq_no_primary_term: true, }); diff --git a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts index c49f73cfb0009..08c14534ac345 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts @@ -5,11 +5,13 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { getFoundListItemSchemaMock } from '../../../common/schemas/response/found_list_item_schema.mock'; import { getFoundListSchemaMock } from '../../../common/schemas/response/found_list_schema.mock'; import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; import { IMPORT_BUFFER_SIZE, IMPORT_TIMEOUT, @@ -63,7 +65,6 @@ export class ListClientMock extends ListClient { export const getListClientMock = (): ListClient => { const mock = new ListClientMock({ - callCluster: getCallClusterMock(), config: { enabled: true, importBufferSize: IMPORT_BUFFER_SIZE, @@ -72,6 +73,7 @@ export const getListClientMock = (): ListClient => { listItemIndex: LIST_ITEM_INDEX, maxImportPayloadBytes: MAX_IMPORT_PAYLOAD_BYTES, }, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, spaceId: 'default', user: 'elastic', }); diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts index 1d10dc8eff926..0b9bfbed28d83 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { FoundListItemSchema, @@ -80,13 +80,13 @@ export class ListClient { private readonly spaceId: string; private readonly user: string; private readonly config: ConfigType; - private readonly callCluster: LegacyAPICaller; + private readonly esClient: ElasticsearchClient; - constructor({ spaceId, user, config, callCluster }: ConstructorOptions) { + constructor({ spaceId, user, config, esClient }: ConstructorOptions) { this.spaceId = spaceId; this.user = user; this.config = config; - this.callCluster = callCluster; + this.esClient = esClient; } public getListIndex = (): string => { @@ -106,9 +106,9 @@ export class ListClient { }; public getList = async ({ id }: GetListOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return getList({ callCluster, id, listIndex }); + return getList({ esClient, id, listIndex }); }; public createList = async ({ @@ -122,12 +122,12 @@ export class ListClient { meta, version, }: CreateListOptions): Promise => { - const { callCluster, user } = this; + const { esClient, user } = this; const listIndex = this.getListIndex(); return createList({ - callCluster, description, deserializer, + esClient, id, immutable, listIndex, @@ -151,12 +151,12 @@ export class ListClient { meta, version, }: CreateListIfItDoesNotExistOptions): Promise => { - const { callCluster, user } = this; + const { esClient, user } = this; const listIndex = this.getListIndex(); return createListIfItDoesNotExist({ - callCluster, description, deserializer, + esClient, id, immutable, listIndex, @@ -170,51 +170,51 @@ export class ListClient { }; public getListIndexExists = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return getIndexExists(callCluster, listIndex); + return getIndexExists(esClient, listIndex); }; public getListItemIndexExists = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); - return getIndexExists(callCluster, listItemIndex); + return getIndexExists(esClient, listItemIndex); }; public createListBootStrapIndex = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return createBootstrapIndex(callCluster, listIndex); + return createBootstrapIndex(esClient, listIndex); }; public createListItemBootStrapIndex = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); - return createBootstrapIndex(callCluster, listItemIndex); + return createBootstrapIndex(esClient, listItemIndex); }; public getListPolicyExists = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return getPolicyExists(callCluster, listIndex); + return getPolicyExists(esClient, listIndex); }; public getListItemPolicyExists = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listsItemIndex = this.getListItemIndex(); - return getPolicyExists(callCluster, listsItemIndex); + return getPolicyExists(esClient, listsItemIndex); }; public getListTemplateExists = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return getTemplateExists(callCluster, listIndex); + return getTemplateExists(esClient, listIndex); }; public getListItemTemplateExists = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); - return getTemplateExists(callCluster, listItemIndex); + return getTemplateExists(esClient, listItemIndex); }; public getListTemplate = (): Record => { @@ -228,71 +228,71 @@ export class ListClient { }; public setListTemplate = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const template = this.getListTemplate(); const listIndex = this.getListIndex(); - return setTemplate(callCluster, listIndex, template); + return setTemplate(esClient, listIndex, template); }; public setListItemTemplate = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const template = this.getListItemTemplate(); const listItemIndex = this.getListItemIndex(); - return setTemplate(callCluster, listItemIndex, template); + return setTemplate(esClient, listItemIndex, template); }; public setListPolicy = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return setPolicy(callCluster, listIndex, listPolicy); + return setPolicy(esClient, listIndex, listPolicy); }; public setListItemPolicy = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); - return setPolicy(callCluster, listItemIndex, listsItemsPolicy); + return setPolicy(esClient, listItemIndex, listsItemsPolicy); }; public deleteListIndex = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return deleteAllIndex(callCluster, `${listIndex}-*`); + return deleteAllIndex(esClient, `${listIndex}-*`); }; public deleteListItemIndex = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); - return deleteAllIndex(callCluster, `${listItemIndex}-*`); + return deleteAllIndex(esClient, `${listItemIndex}-*`); }; public deleteListPolicy = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return deletePolicy(callCluster, listIndex); + return deletePolicy(esClient, listIndex); }; public deleteListItemPolicy = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); - return deletePolicy(callCluster, listItemIndex); + return deletePolicy(esClient, listItemIndex); }; public deleteListTemplate = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); - return deleteTemplate(callCluster, listIndex); + return deleteTemplate(esClient, listIndex); }; public deleteListItemTemplate = async (): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); - return deleteTemplate(callCluster, listItemIndex); + return deleteTemplate(esClient, listItemIndex); }; public deleteListItem = async ({ id }: DeleteListItemOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); - return deleteListItem({ callCluster, id, listItemIndex }); + return deleteListItem({ esClient, id, listItemIndex }); }; public deleteListItemByValue = async ({ @@ -300,10 +300,10 @@ export class ListClient { value, type, }: DeleteListItemByValueOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); return deleteListItemByValue({ - callCluster, + esClient, listId, listItemIndex, type, @@ -312,11 +312,11 @@ export class ListClient { }; public deleteList = async ({ id }: DeleteListOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); const listItemIndex = this.getListItemIndex(); return deleteList({ - callCluster, + esClient, id, listIndex, listItemIndex, @@ -328,10 +328,10 @@ export class ListClient { listId, stream, }: ExportListItemsToStreamOptions): void => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); exportListItemsToStream({ - callCluster, + esClient, listId, listItemIndex, stream, @@ -348,13 +348,13 @@ export class ListClient { meta, version, }: ImportListItemsToStreamOptions): Promise => { - const { callCluster, user, config } = this; + const { esClient, user, config } = this; const listItemIndex = this.getListItemIndex(); const listIndex = this.getListIndex(); return importListItemsToStream({ - callCluster, config, deserializer, + esClient, listId, listIndex, listItemIndex, @@ -372,10 +372,10 @@ export class ListClient { value, type, }: GetListItemByValueOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); return getListItemByValue({ - callCluster, + esClient, listId, listItemIndex, type, @@ -392,11 +392,11 @@ export class ListClient { type, meta, }: CreateListItemOptions): Promise => { - const { callCluster, user } = this; + const { esClient, user } = this; const listItemIndex = this.getListItemIndex(); return createListItem({ - callCluster, deserializer, + esClient, id, listId, listItemIndex, @@ -414,11 +414,11 @@ export class ListClient { value, meta, }: UpdateListItemOptions): Promise => { - const { callCluster, user } = this; + const { esClient, user } = this; const listItemIndex = this.getListItemIndex(); return updateListItem({ _version, - callCluster, + esClient, id, listItemIndex, meta, @@ -435,12 +435,12 @@ export class ListClient { meta, version, }: UpdateListOptions): Promise => { - const { callCluster, user } = this; + const { esClient, user } = this; const listIndex = this.getListIndex(); return updateList({ _version, - callCluster, description, + esClient, id, listIndex, meta, @@ -451,10 +451,10 @@ export class ListClient { }; public getListItem = async ({ id }: GetListItemOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); return getListItem({ - callCluster, + esClient, id, listItemIndex, }); @@ -465,10 +465,10 @@ export class ListClient { listId, value, }: GetListItemsByValueOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); return getListItemByValues({ - callCluster, + esClient, listId, listItemIndex, type, @@ -481,10 +481,10 @@ export class ListClient { listId, value, }: SearchListItemByValuesOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listItemIndex = this.getListItemIndex(); return searchListItemByValues({ - callCluster, + esClient, listId, listItemIndex, type, @@ -501,11 +501,11 @@ export class ListClient { sortOrder, searchAfter, }: FindListOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); return findList({ - callCluster, currentIndexPosition, + esClient, filter, listIndex, page, @@ -526,12 +526,12 @@ export class ListClient { sortOrder, searchAfter, }: FindListItemOptions): Promise => { - const { callCluster } = this; + const { esClient } = this; const listIndex = this.getListIndex(); const listItemIndex = this.getListItemIndex(); return findListItem({ - callCluster, currentIndexPosition, + esClient, filter, listId, listIndex, diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index 54fd4f83e2d83..1efcd2af5420e 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -7,7 +7,7 @@ import { PassThrough, Readable } from 'stream'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Description, @@ -35,7 +35,7 @@ import { import { ConfigType } from '../../config'; export interface ConstructorOptions { - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; config: ConfigType; spaceId: string; user: string; diff --git a/x-pack/plugins/lists/server/services/lists/update_list.mock.ts b/x-pack/plugins/lists/server/services/lists/update_list.mock.ts index 313ab5bb45e2f..5648a8df7dde8 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.mock.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.mock.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { UpdateListOptions } from '../lists'; import { DATE_NOW, @@ -20,9 +22,9 @@ import { export const getUpdateListOptionsMock = (): UpdateListOptions => ({ _version: undefined, - callCluster: getCallClusterMock(), dateNow: DATE_NOW, description: DESCRIPTION, + esClient: elasticsearchClientMock.createScopedClusterClient().asCurrentUser, id: LIST_ID, listIndex: LIST_INDEX, meta: META, diff --git a/x-pack/plugins/lists/server/services/lists/update_list.test.ts b/x-pack/plugins/lists/server/services/lists/update_list.test.ts index ff9a6f598db23..e2d3b09fe518a 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.test.ts @@ -5,6 +5,9 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + import { ListSchema } from '../../../common/schemas'; import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; @@ -29,7 +32,11 @@ describe('update_list', () => { const list = getListResponseMock(); ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); const options = getUpdateListOptionsMock(); - const updatedList = await updateList(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.update.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) + ); + const updatedList = await updateList({ ...options, esClient }); const expected: ListSchema = { ...getListResponseMock(), id: 'elastic-id-123' }; expect(updatedList).toEqual(expected); }); @@ -42,7 +49,11 @@ describe('update_list', () => { }; ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); const options = getUpdateListOptionsMock(); - const updatedList = await updateList(options); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.update.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _id: 'elastic-id-123' }) + ); + const updatedList = await updateList({ ...options, esClient }); const expected: ListSchema = { ...getListResponseMock(), deserializer: '{{value}}', diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 05939d86189c5..aa4eb9a8d834f 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -6,7 +6,7 @@ */ import { CreateDocumentResponse } from 'elasticsearch'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { decodeVersion } from '../utils/decode_version'; import { encodeHitVersion } from '../utils/encode_hit_version'; @@ -26,7 +26,7 @@ import { getList } from '.'; export interface UpdateListOptions { _version: _VersionOrUndefined; id: Id; - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; listIndex: string; user: string; name: NameOrUndefined; @@ -41,7 +41,7 @@ export const updateList = async ({ id, name, description, - callCluster, + esClient, listIndex, user, meta, @@ -49,7 +49,7 @@ export const updateList = async ({ version, }: UpdateListOptions): Promise => { const updatedAt = dateNow ?? new Date().toISOString(); - const list = await getList({ callCluster, id, listIndex }); + const list = await getList({ esClient, id, listIndex }); if (list == null) { return null; } else { @@ -61,7 +61,7 @@ export const updateList = async ({ updated_at: updatedAt, updated_by: user, }; - const response = await callCluster('update', { + const { body: response } = await esClient.update({ ...decodeVersion(_version), body: { doc }, id, diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts index ef9b2b4d93e5f..34359a7a9c697 100644 --- a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts +++ b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; import { Scroll } from '../lists/types'; @@ -16,7 +17,7 @@ import { getSourceWithTieBreaker } from './get_source_with_tie_breaker'; import { TieBreaker, getSearchAfterWithTieBreaker } from './get_search_after_with_tie_breaker'; interface GetSearchAfterOptions { - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; filter: Filter; hops: number; hopSize: number; @@ -27,7 +28,7 @@ interface GetSearchAfterOptions { } export const getSearchAfterScroll = async ({ - callCluster, + esClient, filter, hopSize, hops, @@ -39,14 +40,14 @@ export const getSearchAfterScroll = async ({ const query = getQueryFilter({ filter }); let newSearchAfter = searchAfter; for (let i = 0; i < hops; ++i) { - const response = await callCluster>('search', { + const { body: response } = await esClient.search>>({ body: { _source: getSourceWithTieBreaker({ sortField }), query, search_after: newSearchAfter, sort: getSortWithTieBreaker({ sortField, sortOrder }), }, - ignoreUnavailable: true, + ignore_unavailable: true, index, size: hopSize, }); diff --git a/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts index 502c754615416..2b65c0df54a83 100644 --- a/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts +++ b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas'; import { Scroll } from '../lists/types'; @@ -14,7 +14,7 @@ import { calculateScrollMath } from './calculate_scroll_math'; import { getSearchAfterScroll } from './get_search_after_scroll'; interface ScrollToStartPageOptions { - callCluster: LegacyAPICaller; + esClient: ElasticsearchClient; filter: Filter; sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; @@ -27,7 +27,7 @@ interface ScrollToStartPageOptions { } export const scrollToStartPage = async ({ - callCluster, + esClient, filter, hopSize, currentIndexPosition, @@ -58,7 +58,7 @@ export const scrollToStartPage = async ({ }; } else if (hops > 0) { const scroll = await getSearchAfterScroll({ - callCluster, + esClient, filter, hopSize, hops, @@ -69,7 +69,7 @@ export const scrollToStartPage = async ({ }); if (scroll.validSearchAfterFound && leftOverAfterHops > 0) { return getSearchAfterScroll({ - callCluster, + esClient, filter, hopSize: leftOverAfterHops, hops: 1, @@ -83,7 +83,7 @@ export const scrollToStartPage = async ({ } } else { return getSearchAfterScroll({ - callCluster, + esClient, filter, hopSize: leftOverAfterHops, hops: 1, diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts index c41bfcc0014c8..50d8d4d652a82 100644 --- a/x-pack/plugins/lists/server/types.ts +++ b/x-pack/plugins/lists/server/types.ts @@ -6,9 +6,9 @@ */ import { + ElasticsearchClient, IContextProvider, IRouter, - LegacyAPICaller, RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; @@ -27,7 +27,7 @@ export interface PluginsStart { } export type GetListClientType = ( - dataClient: LegacyAPICaller, + esClient: ElasticsearchClient, spaceId: string, user: string ) => ListClient; diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 9a2efade7b44f..fbe487f240699 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger, LegacyCallAPIOptions } from 'kibana/server'; +import { Logger, ElasticsearchClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { AlertType, @@ -32,7 +32,6 @@ import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; -import { mbSafeQuery } from '../lib/mb_safe_query'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerting/common/parse_duration'; import { Globals } from '../static_globals'; @@ -56,12 +55,6 @@ interface AlertOptions { accessorKey?: string; } -type CallCluster = ( - endpoint: string, - clientParams?: Record | undefined, - options?: LegacyCallAPIOptions | undefined -) => Promise; - const defaultAlertOptions = (): AlertOptions => { return { id: '', @@ -233,29 +226,15 @@ export class BaseAlert { `Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` ); - const useCallCluster = - Globals.app.monitoringCluster?.callAsInternalUser || services.callCluster; - const callCluster = async ( - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await mbSafeQuery(async () => useCallCluster(endpoint, clientParams, options)); - }; - const availableCcs = Globals.app.config.ui.ccs.enabled - ? await fetchAvailableCcs(callCluster) - : []; - const clusters = await this.fetchClusters( - callCluster, - params as CommonAlertParams, - availableCcs - ); - const data = await this.fetchData(params, callCluster, clusters, availableCcs); + const esClient = services.scopedClusterClient.asCurrentUser; + const availableCcs = Globals.app.config.ui.ccs.enabled ? await fetchAvailableCcs(esClient) : []; + const clusters = await this.fetchClusters(esClient, params as CommonAlertParams, availableCcs); + const data = await this.fetchData(params, esClient, clusters, availableCcs); return await this.processData(data, clusters, services, state); } protected async fetchClusters( - callCluster: CallCluster, + esClient: ElasticsearchClient, params: CommonAlertParams, ccs?: string[] ) { @@ -264,7 +243,7 @@ export class BaseAlert { esIndexPattern = getCcsIndexPattern(esIndexPattern, ccs); } if (!params.limit) { - return await fetchClusters(callCluster, esIndexPattern); + return await fetchClusters(esClient, esIndexPattern); } const limit = parseDuration(params.limit); const rangeFilter = this.alertOptions.fetchClustersRange @@ -275,12 +254,12 @@ export class BaseAlert { }, } : undefined; - return await fetchClusters(callCluster, esIndexPattern, rangeFilter); + return await fetchClusters(esClient, esIndexPattern, rangeFilter); } protected async fetchData( params: CommonAlertParams | unknown, - callCluster: CallCluster, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise> { diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts index b089b466564e4..6401c5213ee7d 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -70,7 +71,7 @@ export class CCRReadExceptionsAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -83,7 +84,7 @@ export class CCRReadExceptionsAlert extends BaseAlert { const endMs = +new Date(); const startMs = endMs - duration; const stats = await fetchCCRReadExceptions( - callCluster, + esClient, esIndexPattern, startMs, endMs, diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 1490a6ce58e04..1c39d6d6b9629 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -10,6 +10,7 @@ import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; import { AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; const RealDate = Date; @@ -80,7 +81,7 @@ describe('ClusterHealthAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 15b3a7b486fa2..c5983ae9897fe 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -64,7 +65,7 @@ export class ClusterHealthAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -72,7 +73,7 @@ export class ClusterHealthAlert extends BaseAlert { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const healths = await fetchClusterHealth(callCluster, clusters, esIndexPattern); + const healths = await fetchClusterHealth(esClient, clusters, esIndexPattern); return healths.map((clusterHealth) => { const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; const severity = diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts index 03342099773ca..be10ba15d2674 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts @@ -9,6 +9,7 @@ import { CpuUsageAlert } from './cpu_usage_alert'; import { ALERT_CPU_USAGE } from '../../common/constants'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; const RealDate = Date; @@ -83,7 +84,7 @@ describe('CpuUsageAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index e95c4402c0f90..438d350d366f8 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -68,7 +69,7 @@ export class CpuUsageAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -80,7 +81,7 @@ export class CpuUsageAlert extends BaseAlert { const endMs = +new Date(); const startMs = endMs - duration; const stats = await fetchCpuUsageNodeStats( - callCluster, + esClient, clusters, esIndexPattern, startMs, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts index cdc60faedf0d2..4c40a170e40b4 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts @@ -9,6 +9,7 @@ import { DiskUsageAlert } from './disk_usage_alert'; import { ALERT_DISK_USAGE } from '../../common/constants'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; type IDiskUsageAlertMock = DiskUsageAlert & { defaultParams: { @@ -95,7 +96,7 @@ describe('DiskUsageAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts index 3503195e51b82..8eb36f322168c 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -67,7 +68,7 @@ export class DiskUsageAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -77,7 +78,7 @@ export class DiskUsageAlert extends BaseAlert { } const { duration, threshold } = params; const stats = await fetchDiskUsageNodeStats( - callCluster, + esClient, clusters, esIndexPattern, duration as string, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index a231cec762191..2bd67298e7b5a 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -9,6 +9,7 @@ import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_misma import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; const RealDate = Date; @@ -84,7 +85,7 @@ describe('ElasticsearchVersionMismatchAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index 735e1c43f569a..d51eb99e3a47d 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -53,7 +54,7 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -62,7 +63,7 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } const elasticsearchVersions = await fetchElasticsearchVersions( - callCluster, + esClient, clusters, esIndexPattern, Globals.app.config.ui.max_bucket_size diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 6252fc59ba246..02a8f59aecfbd 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -9,6 +9,7 @@ import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; const RealDate = Date; @@ -87,7 +88,7 @@ describe('KibanaVersionMismatchAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index 04ee29f5e47fc..3d6417e8fd64c 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -66,7 +67,7 @@ export class KibanaVersionMismatchAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -75,7 +76,7 @@ export class KibanaVersionMismatchAlert extends BaseAlert { kibanaIndexPattern = getCcsIndexPattern(kibanaIndexPattern, availableCcs); } const kibanaVersions = await fetchKibanaVersions( - callCluster, + esClient, clusters, kibanaIndexPattern, Globals.app.config.ui.max_bucket_size diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts index b0cbd0edb64f7..2c9e5a04e37e4 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -59,7 +60,7 @@ export class LargeShardSizeAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams & { indexPattern: string }, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -70,7 +71,7 @@ export class LargeShardSizeAlert extends BaseAlert { const { threshold, indexPattern: shardIndexPatterns } = params; const stats = await fetchIndexShardSize( - callCluster, + esClient, clusters, esIndexPattern, threshold!, diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index 0d1c1d20097e5..0bb8ba23cd490 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -10,6 +10,7 @@ import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; const RealDate = Date; @@ -85,7 +86,7 @@ describe('LicenseExpirationAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index 9cf50f372ce4f..f5a6f2f7c7e1d 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -78,7 +79,7 @@ export class LicenseExpirationAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -86,7 +87,7 @@ export class LicenseExpirationAlert extends BaseAlert { if (availableCcs) { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } - const licenses = await fetchLicenses(callCluster, clusters, esIndexPattern); + const licenses = await fetchLicenses(esClient, clusters, esIndexPattern); return licenses.map((license) => { const { clusterUuid, type, expiryDateMS, status, ccs } = license; diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index 50a826b36d58f..7c73c63f293f3 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -9,6 +9,7 @@ import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert' import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; const RealDate = Date; @@ -85,7 +86,7 @@ describe('LogstashVersionMismatchAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 99080b8230ff3..7ee478b17fff8 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -53,7 +54,7 @@ export class LogstashVersionMismatchAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -62,7 +63,7 @@ export class LogstashVersionMismatchAlert extends BaseAlert { logstashIndexPattern = getCcsIndexPattern(logstashIndexPattern, availableCcs); } const logstashVersions = await fetchLogstashVersions( - callCluster, + esClient, clusters, logstashIndexPattern, Globals.app.config.ui.max_bucket_size diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts index 05dc0271cf3f7..06cd90ca80729 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -68,7 +69,7 @@ export class MemoryUsageAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -82,7 +83,7 @@ export class MemoryUsageAlert extends BaseAlert { const startMs = endMs - parsedDuration; const stats = await fetchMemoryUsageNodeStats( - callCluster, + esClient, clusters, esIndexPattern, startMs, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts index 28085f8b5e388..87790ee111326 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts @@ -9,6 +9,7 @@ import { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; import { ALERT_MISSING_MONITORING_DATA } from '../../common/constants'; import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; const RealDate = Date; @@ -87,7 +88,7 @@ describe('MissingMonitoringDataAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts index adf10e4e56dbc..ed35f775b249c 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -66,7 +67,7 @@ export class MissingMonitoringDataAlert extends BaseAlert { } protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -78,7 +79,7 @@ export class MissingMonitoringDataAlert extends BaseAlert { const limit = parseDuration(params.limit!); const now = +new Date(); const missingData = await fetchMissingMonitoringData( - callCluster, + esClient, clusters, indexPattern, Globals.app.config.ui.max_bucket_size, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 848436573fab9..fa97de364d792 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -9,6 +9,7 @@ import { NodesChangedAlert } from './nodes_changed_alert'; import { ALERT_NODES_CHANGED } from '../../common/constants'; import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; const RealDate = Date; @@ -106,7 +107,7 @@ describe('NodesChangedAlert', () => { const getState = jest.fn(); const executorOptions = { services: { - callCluster: jest.fn(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), alertInstanceFactory: jest.fn().mockImplementation(() => { return { replaceState, diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index d040ce1a890ae..b26008ff3860d 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -102,7 +103,7 @@ export class NodesChangedAlert extends BaseAlert { protected async fetchData( params: CommonAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -111,7 +112,7 @@ export class NodesChangedAlert extends BaseAlert { esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); } const nodesFromClusterStats = await fetchNodesFromClusterStats( - callCluster, + esClient, clusters, esIndexPattern ); diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts index cee319504e461..bb91418fc2090 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchClient } from 'kibana/server'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -66,7 +67,7 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { protected async fetchData( params: ThreadPoolRejectionsAlertParams, - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], availableCcs: string[] ): Promise { @@ -78,7 +79,7 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { const { threshold, duration } = params; const stats = await fetchThreadPoolRejectionStats( - callCluster, + esClient, clusters, esIndexPattern, Globals.app.config.ui.max_bucket_size, diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts index e1a93dec8aaee..03a3659b49ce1 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts @@ -6,11 +6,11 @@ */ import { getMonitoringUsageCollector } from './get_usage_collector'; -import { fetchClusters } from '../../lib/alerts/fetch_clusters'; +import { fetchClustersLegacy } from '../../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; jest.mock('../../lib/alerts/fetch_clusters', () => ({ - fetchClusters: jest.fn().mockImplementation(() => { + fetchClustersLegacy: jest.fn().mockImplementation(() => { return [ { clusterUuid: '1abc', @@ -153,7 +153,7 @@ describe('getMonitoringUsageCollector', () => { const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; const args = mock.calls[0]; - (fetchClusters as jest.Mock).mockImplementation(() => { + (fetchClustersLegacy as jest.Mock).mockImplementation(() => { return []; }); @@ -173,7 +173,7 @@ describe('getMonitoringUsageCollector', () => { const mock = (usageCollection.makeUsageCollector as jest.Mock).mock; const args = mock.calls[0]; - (fetchClusters as jest.Mock).mockImplementation(() => { + (fetchClustersLegacy as jest.Mock).mockImplementation(() => { return []; }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts index 1ea7b9b8ac407..6f638b6ff8f0e 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts @@ -8,13 +8,13 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ILegacyClusterClient } from 'src/core/server'; import { MonitoringConfig } from '../../config'; -import { fetchAvailableCcs } from '../../lib/alerts/fetch_available_ccs'; +import { fetchAvailableCcsLegacy } from '../../lib/alerts/fetch_available_ccs'; import { getStackProductsUsage } from './lib/get_stack_products_usage'; import { fetchLicenseType } from './lib/fetch_license_type'; import { MonitoringUsage, StackProductUsage, MonitoringClusterStackProductUsage } from './types'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants'; import { getCcsIndexPattern } from '../../lib/alerts/get_ccs_index_pattern'; -import { fetchClusters } from '../../lib/alerts/fetch_clusters'; +import { fetchClustersLegacy } from '../../lib/alerts/fetch_clusters'; export function getMonitoringUsageCollector( usageCollection: UsageCollectionSetup, @@ -106,9 +106,9 @@ export function getMonitoringUsageCollector( ? legacyEsClient.asScoped(kibanaRequest).callAsCurrentUser : legacyEsClient.callAsInternalUser; const usageClusters: MonitoringClusterStackProductUsage[] = []; - const availableCcs = config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : []; + const availableCcs = config.ui.ccs.enabled ? await fetchAvailableCcsLegacy(callCluster) : []; const elasticsearchIndex = getCcsIndexPattern(INDEX_PATTERN_ELASTICSEARCH, availableCcs); - const clusters = await fetchClusters(callCluster, elasticsearchIndex); + const clusters = await fetchClustersLegacy(callCluster, elasticsearchIndex); for (const cluster of clusters) { const license = await fetchLicenseType(callCluster, availableCcs, cluster.clusterUuid); const stackProducts = await getStackProductsUsage( diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts index 20eea1b5ed8e1..ecfb5fc50a16d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts @@ -5,34 +5,46 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { fetchAvailableCcs } from './fetch_available_ccs'; describe('fetchAvailableCcs', () => { it('should call the `cluster.remoteInfo` api', async () => { - const callCluster = jest.fn(); - await fetchAvailableCcs(callCluster); - expect(callCluster).toHaveBeenCalledWith('cluster.remoteInfo'); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + + await fetchAvailableCcs(esClient); + expect(esClient.cluster.remoteInfo).toHaveBeenCalled(); }); it('should return clusters that are connected', async () => { const connectedRemote = 'myRemote'; - const callCluster = jest.fn().mockImplementation(() => ({ - [connectedRemote]: { - connected: true, - }, - })); - const result = await fetchAvailableCcs(callCluster); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + esClient.cluster.remoteInfo.mockImplementation(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ + [connectedRemote]: { + connected: true, + }, + }) + ); + + const result = await fetchAvailableCcs(esClient); expect(result).toEqual([connectedRemote]); }); it('should not return clusters that are connected', async () => { const disconnectedRemote = 'myRemote'; - const callCluster = jest.fn().mockImplementation(() => ({ - [disconnectedRemote]: { - connected: false, - }, - })); - const result = await fetchAvailableCcs(callCluster); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + esClient.cluster.remoteInfo.mockImplementation(() => + elasticsearchClientMock.createSuccessTransportRequestPromise({ + [disconnectedRemote]: { + connected: false, + }, + }) + ); + + const result = await fetchAvailableCcs(esClient); expect(result.length).toBe(0); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts index 10bc2ead2cb11..0dd0def028e36 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts @@ -5,7 +5,24 @@ * 2.0. */ -export async function fetchAvailableCcs(callCluster: any): Promise { +import { ElasticsearchClient } from 'kibana/server'; + +export async function fetchAvailableCcs(esClient: ElasticsearchClient): Promise { + const availableCcs = []; + const { body: response } = await esClient.cluster.remoteInfo(); + for (const remoteName in response) { + if (!response.hasOwnProperty(remoteName)) { + continue; + } + const remoteInfo = response[remoteName]; + if (remoteInfo.connected) { + availableCcs.push(remoteName); + } + } + return availableCcs; +} + +export async function fetchAvailableCcsLegacy(callCluster: any): Promise { const availableCcs = []; const response = await callCluster('cluster.remoteInfo'); for (const remoteName in response) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts index 8aede7a73e61d..330be4e90ed56 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_ccr_read_exceptions.ts @@ -5,11 +5,12 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { CCRReadExceptionsStats } from '../../../common/types/alerts'; export async function fetchCCRReadExceptions( - callCluster: any, + esClient: ElasticsearchClient, index: string, startMs: number, endMs: number, @@ -92,7 +93,7 @@ export async function fetchCCRReadExceptions( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const stats: CCRReadExceptionsStats[] = []; const { buckets: remoteClusterBuckets = [] } = response.aggregations.remote_clusters; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts index 2fdbbe80b7e89..d326c7f4bedda 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts @@ -5,32 +5,37 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { fetchClusterHealth } from './fetch_cluster_health'; describe('fetchClusterHealth', () => { it('should return the cluster health', async () => { const status = 'green'; const clusterUuid = 'sdfdsaj34434'; - const callCluster = jest.fn(() => ({ - hits: { - hits: [ - { - _index: '.monitoring-es-7', - _source: { - cluster_state: { - status, + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _index: '.monitoring-es-7', + _source: { + cluster_state: { + status, + }, + cluster_uuid: clusterUuid, }, - cluster_uuid: clusterUuid, }, - }, - ], - }, - })); + ], + }, + }) + ); const clusters = [{ clusterUuid, clusterName: 'foo' }]; const index = '.monitoring-es-*'; - const health = await fetchClusterHealth(callCluster, clusters, index); + const health = await fetchClusterHealth(esClient, clusters, index); expect(health).toEqual([ { health: status, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts index bcfa2da0958a2..be91aaa6ec983 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -4,11 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { AlertCluster, AlertClusterHealth } from '../../../common/types/alerts'; import { ElasticsearchSource } from '../../../common/types/es'; export async function fetchClusterHealth( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string ): Promise { @@ -58,7 +59,7 @@ export async function fetchClusterHealth( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { return { health: hit._source.cluster_state?.status, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts index 7a1d0acd73b12..54aa2e68d4ef2 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -5,6 +5,9 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { fetchClusters } from './fetch_clusters'; describe('fetchClusters', () => { @@ -12,54 +15,60 @@ describe('fetchClusters', () => { const clusterName = 'monitoring'; it('return a list of clusters', async () => { - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - cluster_uuid: clusterUuid, - cluster_name: clusterName, + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _source: { + cluster_uuid: clusterUuid, + cluster_name: clusterName, + }, }, - }, - ], - }, - })); + ], + }, + }) + ); const index = '.monitoring-es-*'; - const result = await fetchClusters(callCluster, index); + const result = await fetchClusters(esClient, index); expect(result).toEqual([{ clusterUuid, clusterName }]); }); it('return the metadata name if available', async () => { const metadataName = 'custom-monitoring'; - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - cluster_uuid: clusterUuid, - cluster_name: clusterName, - cluster_settings: { - cluster: { - metadata: { - display_name: metadataName, + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _source: { + cluster_uuid: clusterUuid, + cluster_name: clusterName, + cluster_settings: { + cluster: { + metadata: { + display_name: metadataName, + }, }, }, }, }, - }, - ], - }, - })); + ], + }, + }) + ); const index = '.monitoring-es-*'; - const result = await fetchClusters(callCluster, index); + const result = await fetchClusters(esClient, index); expect(result).toEqual([{ clusterUuid, clusterName: metadataName }]); }); it('should limit the time period in the query', async () => { - const callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; const index = '.monitoring-es-*'; - await fetchClusters(callCluster, index); - const params = callCluster.mock.calls[0][1]; - expect(params.body.query.bool.filter[1].range.timestamp.gte).toBe('now-2m'); + await fetchClusters(esClient, index); + const params = esClient.search.mock.calls[0][0] as any; + expect(params?.body?.query.bool.filter[1].range.timestamp.gte).toBe('now-2m'); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts index 871370977bb38..bbaea8d9f206e 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster } from '../../../common/types/alerts'; @@ -16,7 +17,7 @@ interface RangeFilter { } export async function fetchClusters( - callCluster: any, + esClient: ElasticsearchClient, index: string, rangeFilter: RangeFilter = { timestamp: { gte: 'now-2m' } } ): Promise { @@ -49,6 +50,52 @@ export async function fetchClusters( }, }; + const { body: response } = await esClient.search(params); + return get(response, 'hits.hits', []).map((hit: any) => { + const clusterName: string = + get(hit, '_source.cluster_settings.cluster.metadata.display_name') || + get(hit, '_source.cluster_name') || + get(hit, '_source.cluster_uuid'); + return { + clusterUuid: get(hit, '_source.cluster_uuid'), + clusterName, + }; + }); +} + +export async function fetchClustersLegacy( + callCluster: any, + index: string, + rangeFilter: RangeFilter = { timestamp: { gte: 'now-2m' } } +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_settings.cluster.metadata.display_name', + 'hits.hits._source.cluster_uuid', + 'hits.hits._source.cluster_name', + ], + body: { + size: 1000, + query: { + bool: { + filter: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + range: rangeFilter, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; const response = await callCluster('search', params); return get(response, 'hits.hits', []).map((hit: any) => { const clusterName: string = diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 0f66217180133..2ff9ae3854e4a 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -5,10 +5,12 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { fetchCpuUsageNodeStats } from './fetch_cpu_usage_node_stats'; describe('fetchCpuUsageNodeStats', () => { - let callCluster = jest.fn(); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; const clusters = [ { clusterUuid: 'abc123', @@ -21,8 +23,8 @@ describe('fetchCpuUsageNodeStats', () => { const size = 10; it('fetch normal stats', async () => { - callCluster = jest.fn().mockImplementation((...args) => { - return { + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { buckets: [ @@ -56,9 +58,9 @@ describe('fetchCpuUsageNodeStats', () => { ], }, }, - }; - }); - const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + }) + ); + const result = await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, @@ -74,8 +76,8 @@ describe('fetchCpuUsageNodeStats', () => { }); it('fetch container stats', async () => { - callCluster = jest.fn().mockImplementation((...args) => { - return { + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { buckets: [ @@ -122,9 +124,9 @@ describe('fetchCpuUsageNodeStats', () => { ], }, }, - }; - }); - const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + }) + ); + const result = await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, @@ -140,8 +142,8 @@ describe('fetchCpuUsageNodeStats', () => { }); it('fetch properly return ccs', async () => { - callCluster = jest.fn().mockImplementation((...args) => { - return { + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { buckets: [ @@ -181,18 +183,19 @@ describe('fetchCpuUsageNodeStats', () => { ], }, }, - }; - }); - const result = await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + }) + ); + const result = await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); expect(result[0].ccs).toBe('foo'); }); it('should use consistent params', async () => { let params = null; - callCluster = jest.fn().mockImplementation((...args) => { - params = args[1]; + esClient.search.mockImplementation((...args) => { + params = args[0]; + return elasticsearchClientMock.createSuccessTransportRequestPromise({}); }); - await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); + await fetchCpuUsageNodeStats(esClient, clusters, index, startMs, endMs, size); expect(params).toStrictEqual({ index: '.monitoring-es-*', filterPath: ['aggregations'], diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index dc8e5fc52eadf..1dfbe381b9956 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import moment from 'moment'; import { NORMALIZED_DERIVATIVE_UNIT } from '../../../common/constants'; @@ -23,7 +24,7 @@ interface ClusterBucketESResponse { } export async function fetchCpuUsageNodeStats( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, startMs: number, @@ -140,7 +141,7 @@ export async function fetchCpuUsageNodeStats( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const stats: AlertCpuUsageNodeStats[] = []; const clusterBuckets = get( response, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts index 56b599f73b939..7664d73f6009b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.test.ts @@ -5,10 +5,14 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { fetchDiskUsageNodeStats } from './fetch_disk_usage_node_stats'; describe('fetchDiskUsageNodeStats', () => { - let callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + const clusters = [ { clusterUuid: 'cluster123', @@ -20,8 +24,8 @@ describe('fetchDiskUsageNodeStats', () => { const size = 10; it('fetch normal stats', async () => { - callCluster = jest.fn().mockImplementation(() => { - return { + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { buckets: [ @@ -55,10 +59,10 @@ describe('fetchDiskUsageNodeStats', () => { ], }, }, - }; - }); + }) + ); - const result = await fetchDiskUsageNodeStats(callCluster, clusters, index, duration, size); + const result = await fetchDiskUsageNodeStats(esClient, clusters, index, duration, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts index 912fab19951df..aea4ede825d67 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_disk_usage_node_stats.ts @@ -5,11 +5,12 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertDiskUsageNodeStats } from '../../../common/types/alerts'; export async function fetchDiskUsageNodeStats( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, duration: string, @@ -98,7 +99,7 @@ export async function fetchDiskUsageNodeStats( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const stats: AlertDiskUsageNodeStats[] = []; const { buckets: clusterBuckets = [] } = response.aggregations.clusters; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts index e4f4a4d364ebf..be501ee3d5280 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts @@ -5,10 +5,14 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { fetchElasticsearchVersions } from './fetch_elasticsearch_versions'; describe('fetchElasticsearchVersions', () => { - let callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + const clusters = [ { clusterUuid: 'cluster123', @@ -20,8 +24,8 @@ describe('fetchElasticsearchVersions', () => { const versions = ['8.0.0', '7.2.1']; it('fetch as expected', async () => { - callCluster = jest.fn().mockImplementation(() => { - return { + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ hits: { hits: [ { @@ -37,10 +41,10 @@ describe('fetchElasticsearchVersions', () => { }, ], }, - }; - }); + }) + ); - const result = await fetchElasticsearchVersions(callCluster, clusters, index, size); + const result = await fetchElasticsearchVersions(esClient, clusters, index, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts index 373ddb62aaee8..b4b7739f6731b 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -4,11 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; import { ElasticsearchSource } from '../../../common/types/es'; export async function fetchElasticsearchVersions( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, size: number @@ -59,7 +60,7 @@ export async function fetchElasticsearchVersions( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { const versions = hit._source.cluster_stats?.nodes?.versions; return { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts index 63c2910c46e5d..dfba0c42eef3d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { AlertCluster, IndexShardSizeStats } from '../../../common/types/alerts'; import { ElasticsearchIndexStats, ElasticsearchResponseHit } from '../../../common/types/es'; import { ESGlobPatterns, RegExPatterns } from '../../../common/es_glob_patterns'; @@ -29,7 +30,7 @@ const memoizedIndexPatterns = (globPatterns: string) => { const gbMultiplier = 1000000000; export async function fetchIndexShardSize( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, threshold: number, @@ -113,7 +114,7 @@ export async function fetchIndexShardSize( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const stats: IndexShardSizeStats[] = []; const { buckets: clusterBuckets = [] } = response.aggregations.clusters; const validIndexPatterns = memoizedIndexPatterns(shardIndexPatterns); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts index 518828ef0b1c8..901851d766512 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -6,9 +6,12 @@ */ import { fetchKibanaVersions } from './fetch_kibana_versions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe('fetchKibanaVersions', () => { - let callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; const clusters = [ { clusterUuid: 'cluster123', @@ -19,8 +22,8 @@ describe('fetchKibanaVersions', () => { const size = 10; it('fetch as expected', async () => { - callCluster = jest.fn().mockImplementation(() => { - return { + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { buckets: [ @@ -59,10 +62,10 @@ describe('fetchKibanaVersions', () => { ], }, }, - }; - }); + }) + ); - const result = await fetchKibanaVersions(callCluster, clusters, index, size); + const result = await fetchKibanaVersions(esClient, clusters, index, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts index 2e7fe192df656..a4e1e606702ec 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; @@ -12,7 +13,7 @@ interface ESAggResponse { } export async function fetchKibanaVersions( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, size: number @@ -88,7 +89,7 @@ export async function fetchKibanaVersions( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const indexName = get(response, 'aggregations.index.buckets[0].key', ''); const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; return clusterList.map((cluster) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts index 715c8c50a45e7..69a42812bfe88 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -5,6 +5,9 @@ * 2.0. */ import { fetchLicenses } from './fetch_licenses'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe('fetchLicenses', () => { const clusterName = 'MyCluster'; @@ -16,21 +19,24 @@ describe('fetchLicenses', () => { }; it('return a list of licenses', async () => { - const callCluster = jest.fn().mockImplementation(() => ({ - hits: { - hits: [ - { - _source: { - license, - cluster_uuid: clusterUuid, + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _source: { + license, + cluster_uuid: clusterUuid, + }, }, - }, - ], - }, - })); + ], + }, + }) + ); const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; - const result = await fetchLicenses(callCluster, clusters, index); + const result = await fetchLicenses(esClient, clusters, index); expect(result).toEqual([ { status: license.status, @@ -42,20 +48,20 @@ describe('fetchLicenses', () => { }); it('should only search for the clusters provided', async () => { - const callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; - await fetchLicenses(callCluster, clusters, index); - const params = callCluster.mock.calls[0][1]; - expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + await fetchLicenses(esClient, clusters, index); + const params = esClient.search.mock.calls[0][0] as any; + expect(params?.body?.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); }); it('should limit the time period in the query', async () => { - const callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; const clusters = [{ clusterUuid, clusterName }]; const index = '.monitoring-es-*'; - await fetchLicenses(callCluster, clusters, index); - const params = callCluster.mock.calls[0][1]; - expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + await fetchLicenses(esClient, clusters, index); + const params = esClient.search.mock.calls[0][0] as any; + expect(params?.body?.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts index 6cec7f3296926..5cd4378f0a747 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -4,11 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { AlertLicense, AlertCluster } from '../../../common/types/alerts'; import { ElasticsearchResponse } from '../../../common/types/es'; export async function fetchLicenses( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string ): Promise { @@ -58,7 +59,7 @@ export async function fetchLicenses( }, }; - const response: ElasticsearchResponse = await callCluster('search', params); + const { body: response } = await esClient.search(params); return ( response?.hits?.hits.map((hit) => { const rawLicense = hit._source.license ?? {}; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts index a739593df27e9..e35de6e68866d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -6,9 +6,12 @@ */ import { fetchLogstashVersions } from './fetch_logstash_versions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe('fetchLogstashVersions', () => { - let callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser; const clusters = [ { clusterUuid: 'cluster123', @@ -19,8 +22,8 @@ describe('fetchLogstashVersions', () => { const size = 10; it('fetch as expected', async () => { - callCluster = jest.fn().mockImplementation(() => { - return { + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { index: { buckets: [ @@ -59,10 +62,10 @@ describe('fetchLogstashVersions', () => { ], }, }, - }; - }); + }) + ); - const result = await fetchLogstashVersions(callCluster, clusters, index, size); + const result = await fetchLogstashVersions(esClient, clusters, index, size); expect(result).toEqual([ { clusterUuid: clusters[0].clusterUuid, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts index 8f20c64d6243e..6090ba36d9749 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; @@ -12,7 +13,7 @@ interface ESAggResponse { } export async function fetchLogstashVersions( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, size: number @@ -88,7 +89,7 @@ export async function fetchLogstashVersions( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const indexName = get(response, 'aggregations.index.buckets[0].key', ''); const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; return clusterList.map((cluster) => { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts index 2b2af9572390e..77c17a8ebf3ef 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_memory_usage_node_stats.ts @@ -5,11 +5,12 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertMemoryUsageNodeStats } from '../../../common/types/alerts'; export async function fetchMemoryUsageNodeStats( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, startMs: number, @@ -91,7 +92,7 @@ export async function fetchMemoryUsageNodeStats( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const stats: AlertMemoryUsageNodeStats[] = []; const { buckets: clusterBuckets = [] } = response.aggregations.clusters; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts index 4f907aa628c43..2388abf024eb9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; import { fetchMissingMonitoringData } from './fetch_missing_monitoring_data'; function getResponse( @@ -38,7 +40,8 @@ function getResponse( } describe('fetchMissingMonitoringData', () => { - let callCluster = jest.fn(); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + const index = '.monitoring-*'; const startMs = 100; const size = 10; @@ -51,8 +54,9 @@ describe('fetchMissingMonitoringData', () => { clusterName: 'clusterName1', }, ]; - callCluster = jest.fn().mockImplementation((...args) => { - return { + + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { buckets: clusters.map((cluster) => ({ @@ -80,16 +84,9 @@ describe('fetchMissingMonitoringData', () => { })), }, }, - }; - }); - const result = await fetchMissingMonitoringData( - callCluster, - clusters, - index, - size, - now, - startMs + }) ); + const result = await fetchMissingMonitoringData(esClient, clusters, index, size, now, startMs); expect(result).toEqual([ { nodeId: 'nodeUuid1', @@ -116,8 +113,8 @@ describe('fetchMissingMonitoringData', () => { clusterName: 'clusterName1', }, ]; - callCluster = jest.fn().mockImplementation((...args) => { - return { + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ aggregations: { clusters: { buckets: clusters.map((cluster) => ({ @@ -136,16 +133,9 @@ describe('fetchMissingMonitoringData', () => { })), }, }, - }; - }); - const result = await fetchMissingMonitoringData( - callCluster, - clusters, - index, - size, - now, - startMs + }) ); + const result = await fetchMissingMonitoringData(esClient, clusters, index, size, now, startMs); expect(result).toEqual([ { nodeId: 'nodeUuid1', diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts index fa5f9c6620cf5..cb274848e6c5a 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertMissingData } from '../../../common/types/alerts'; @@ -41,7 +42,7 @@ interface TopHitESResponse { // TODO: only Elasticsearch until we can figure out how to handle upgrades for the rest of the stack // https://github.com/elastic/kibana/issues/83309 export async function fetchMissingMonitoringData( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, size: number, @@ -116,7 +117,7 @@ export async function fetchMissingMonitoringData( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const clusterBuckets = get( response, 'aggregations.clusters.buckets', diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts index c399594c170fa..a97594c8ca995 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { AlertCluster, AlertClusterStatsNodes } from '../../../common/types/alerts'; import { ElasticsearchSource } from '../../../common/types/es'; @@ -23,7 +24,7 @@ function formatNode( } export async function fetchNodesFromClusterStats( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string ): Promise { @@ -87,7 +88,7 @@ export async function fetchNodesFromClusterStats( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const nodes = []; const clusterBuckets = response.aggregations.clusters.buckets; for (const clusterBucket of clusterBuckets) { diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts index 80624b6d5233c..5770721195e14 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_thread_pool_rejections_stats.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import { AlertCluster, AlertThreadPoolRejectionsStats } from '../../../common/types/alerts'; @@ -30,7 +31,7 @@ const getTopHits = (threadType: string, order: string) => ({ }); export async function fetchThreadPoolRejectionStats( - callCluster: any, + esClient: ElasticsearchClient, clusters: AlertCluster[], index: string, size: number, @@ -93,7 +94,7 @@ export async function fetchThreadPoolRejectionStats( }, }; - const response = await callCluster('search', params); + const { body: response } = await esClient.search(params); const stats: AlertThreadPoolRejectionsStats[] = []; const { buckets: clusterBuckets = [] } = response.aggregations.clusters; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts index facb6e29236e3..f5f9c80e0e4d3 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts @@ -34,13 +34,12 @@ export class AlertingSecurity { enabled: isSecurityEnabled = false, ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, } = {}, - }: XPackUsageSecurity = await context.core.elasticsearch.legacy.client.callAsInternalUser( - 'transport.request', - { + } = ( + await context.core.elasticsearch.client.asInternalUser.transport.request({ method: 'GET', path: '/_xpack/usage', - } - ); + }) + ).body as XPackUsageSecurity; return { isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts index f66cf2e0e8ebb..fd9b63152ddd3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/create_bootstrap_index.ts @@ -5,24 +5,26 @@ * 2.0. */ -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; // See the reference(s) below on explanations about why -000001 was chosen and // why the is_write_index is true as well as the bootstrapping step which is needed. // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html export const createBootstrapIndex = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, boolean>, + esClient: ElasticsearchClient, index: string ): Promise => { - return callWithRequest('transport.request', { - path: `/${index}-000001`, - method: 'PUT', - body: { - aliases: { - [index]: { - is_write_index: true, + return ( + await esClient.transport.request({ + path: `/${index}-000001`, + method: 'PUT', + body: { + aliases: { + [index]: { + is_write_index: true, + }, }, }, - }, - }); + }) + ).body; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts index b70ead2b05aff..98a8f8c28d30d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_all_index.ts @@ -5,11 +5,10 @@ * 2.0. */ -import { IndicesDeleteParams, Client } from 'elasticsearch'; -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; export const deleteAllIndex = async ( - callWithRequest: CallWithRequest>, + esClient: ElasticsearchClient, pattern: string, maxAttempts = 5 ): Promise => { @@ -21,10 +20,12 @@ export const deleteAllIndex = async ( } // resolve pattern to concrete index names - const resp = await callWithRequest('indices.getAlias', { - index: pattern, - ignore: 404, - }); + const { body: resp } = await esClient.indices.getAlias( + { + index: pattern, + }, + { ignore: [404] } + ); if (resp.status === 404) { return true; @@ -38,9 +39,9 @@ export const deleteAllIndex = async ( } // delete the concrete indexes we found and try again until this pattern resolves to no indexes - await callWithRequest('indices.delete', { + await esClient.indices.delete({ index: indices, - ignoreUnavailable: true, + ignore_unavailable: true, }); } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts index 63f8648b8e516..d671d256f56aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_policy.ts @@ -5,14 +5,16 @@ * 2.0. */ -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; export const deletePolicy = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, unknown>, + esClient: ElasticsearchClient, policy: string ): Promise => { - return callWithRequest('transport.request', { - path: `/_ilm/policy/${policy}`, - method: 'DELETE', - }); + return ( + await esClient.transport.request({ + path: `/_ilm/policy/${policy}`, + method: 'DELETE', + }) + ).body; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts index 3d9554a826172..e57bbd77120f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/delete_template.ts @@ -4,15 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { IndicesDeleteTemplateParams } from 'elasticsearch'; -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; export const deleteTemplate = async ( - callWithRequest: CallWithRequest, + esClient: ElasticsearchClient, name: string ): Promise => { - return callWithRequest('indices.deleteTemplate', { - name, - }); + return ( + await esClient.indices.deleteTemplate({ + name, + }) + ).body; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts index a162dece4f13d..488ba0dab0b97 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { getIndexExists } from './get_index_exists'; class StatusCode extends Error { @@ -17,29 +19,41 @@ class StatusCode extends Error { describe('get_index_exists', () => { test('it should return a true if you have _shards', async () => { - const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 1 } }); - const indexExists = await getIndexExists(callWithRequest, 'some-index'); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + ); + const indexExists = await getIndexExists(esClient, 'some-index'); expect(indexExists).toEqual(true); }); test('it should return a false if you do NOT have _shards', async () => { - const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 0 } }); - const indexExists = await getIndexExists(callWithRequest, 'some-index'); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + ); + const indexExists = await getIndexExists(esClient, 'some-index'); expect(indexExists).toEqual(false); }); test('it should return a false if it encounters a 404', async () => { - const callWithRequest = jest.fn().mockImplementation(() => { - throw new StatusCode(404, 'I am a 404 error'); - }); - const indexExists = await getIndexExists(callWithRequest, 'some-index'); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createErrorTransportRequestPromise({ + body: new StatusCode(404, 'I am a 404 error'), + }) + ); + const indexExists = await getIndexExists(esClient, 'some-index'); expect(indexExists).toEqual(false); }); test('it should reject if it encounters a non 404', async () => { - const callWithRequest = jest.fn().mockImplementation(() => { - throw new StatusCode(500, 'I am a 500 error'); - }); - await expect(getIndexExists(callWithRequest, 'some-index')).rejects.toThrow('I am a 500 error'); + const esClient = elasticsearchClientMock.createScopedClusterClient().asCurrentUser; + esClient.search.mockReturnValue( + elasticsearchClientMock.createErrorTransportRequestPromise( + new StatusCode(500, 'I am a 500 error') + ) + ); + await expect(getIndexExists(esClient, 'some-index')).rejects.toThrow('I am a 500 error'); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts index 4e9eb1a80566f..b86b58897ee62 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_index_exists.ts @@ -5,17 +5,14 @@ * 2.0. */ -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; export const getIndexExists = async ( - callWithRequest: CallWithRequest< - { index: string; size: number; terminate_after: number; allow_no_indices: boolean }, - { _shards: { total: number } } - >, + esClient: ElasticsearchClient, index: string ): Promise => { try { - const response = await callWithRequest('search', { + const { body: response } = await esClient.search({ index, size: 0, terminate_after: 1, @@ -23,10 +20,10 @@ export const getIndexExists = async ( }); return response._shards.total > 0; } catch (err) { - if (err.status === 404) { + if (err.body?.status === 404) { return false; } else { - throw err; + throw err.body ? err.body : err; } } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts index 75118eb5062f3..c0d7c38a4bb02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_policy_exists.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; export const getPolicyExists = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, unknown>, + esClient: ElasticsearchClient, policy: string ): Promise => { try { - await callWithRequest('transport.request', { + await esClient.transport.request({ path: `/_ilm/policy/${policy}`, method: 'GET', }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts index 7237a5ce58e01..50ec3bfc670d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/get_template_exists.ts @@ -5,14 +5,15 @@ * 2.0. */ -import { IndicesExistsTemplateParams } from 'elasticsearch'; -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; export const getTemplateExists = async ( - callWithRequest: CallWithRequest, + esClient: ElasticsearchClient, template: string ): Promise => { - return callWithRequest('indices.existsTemplate', { - name: template, - }); + return ( + await esClient.indices.existsTemplate({ + name: template, + }) + ).body; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/read_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/read_index.ts index 653c9a2379cc2..7674ca3b48304 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/read_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/read_index.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { IndicesGetSettingsParams } from 'elasticsearch'; -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; -export const readIndex = async ( - callWithRequest: CallWithRequest, - index: string -): Promise => { - return callWithRequest('indices.get', { +export const readIndex = async (esClient: ElasticsearchClient, index: string): Promise => { + return esClient.indices.get({ index, }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts index 1071551170c68..9dbcdd795ac71 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_policy.ts @@ -5,16 +5,18 @@ * 2.0. */ -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; export const setPolicy = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, unknown>, + esClient: ElasticsearchClient, policy: string, - body: unknown + body: Record ): Promise => { - return callWithRequest('transport.request', { - path: `/_ilm/policy/${policy}`, - method: 'PUT', - body, - }); + return ( + await esClient.transport.request({ + path: `/_ilm/policy/${policy}`, + method: 'PUT', + body, + }) + ).body; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts index 11c240fce2356..e63dbbd6c3e8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/index/set_template.ts @@ -5,16 +5,17 @@ * 2.0. */ -import { IndicesPutTemplateParams } from 'elasticsearch'; -import { CallWithRequest } from '../types'; +import { ElasticsearchClient } from 'kibana/server'; export const setTemplate = async ( - callWithRequest: CallWithRequest, + esClient: ElasticsearchClient, name: string, - body: unknown + body: Record ): Promise => { - return callWithRequest('indices.putTemplate', { - name, - body, - }); + return ( + await esClient.indices.putTemplate({ + name, + body, + }) + ).body; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals.ts index b716771d20ac3..b411ac2c69ef2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AlertServices } from '../../../../../alerting/server'; +import { ElasticsearchClient } from 'kibana/server'; import { SignalSearchResponse } from '../signals/types'; import { buildSignalsSearchQuery } from './build_signals_query'; @@ -15,7 +15,7 @@ interface GetSignalsParams { size?: number; ruleId: string; index: string; - callCluster: AlertServices['callCluster']; + esClient: ElasticsearchClient; } export const getSignals = async ({ @@ -24,7 +24,7 @@ export const getSignals = async ({ size, ruleId, index, - callCluster, + esClient, }: GetSignalsParams): Promise => { if (from == null || to == null) { throw Error('"from" or "to" was not provided to signals query'); @@ -38,7 +38,7 @@ export const getSignals = async ({ size, }); - const result: SignalSearchResponse = await callCluster('search', query); + const { body: result } = await esClient.search(query); return result; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals_count.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals_count.ts index 9811e5ce21086..b864919fd7295 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals_count.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/get_signals_count.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AlertServices } from '../../../../../alerting/server'; +import { ElasticsearchClient } from 'kibana/server'; import { buildSignalsSearchQuery } from './build_signals_query'; interface GetSignalsCount { @@ -13,11 +13,7 @@ interface GetSignalsCount { to?: string; ruleId: string; index: string; - callCluster: AlertServices['callCluster']; -} - -interface CountResult { - count: number; + esClient: ElasticsearchClient; } export const getSignalsCount = async ({ @@ -25,7 +21,7 @@ export const getSignalsCount = async ({ to, ruleId, index, - callCluster, + esClient, }: GetSignalsCount): Promise => { if (from == null || to == null) { throw Error('"from" or "to" was not provided to signals count query'); @@ -38,7 +34,7 @@ export const getSignalsCount = async ({ from, }); - const result: CountResult = await callCluster('count', query); + const { body: result } = await esClient.count(query); return result.count; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index 4923aa3d1223e..762d7e724f80a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -17,6 +17,8 @@ import { sampleEmptyDocSearchResults, } from '../signals/__mocks__/es_results'; import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; jest.mock('./build_signals_query'); describe('rules_notification_alert_type', () => { @@ -70,7 +72,11 @@ describe('rules_notification_alert_type', () => { references: [], attributes: ruleAlert, }); - alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId()); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); await alert.executor(payload); @@ -94,7 +100,11 @@ describe('rules_notification_alert_type', () => { references: [], attributes: ruleAlert, }); - alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId()); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); await alert.executor(payload); expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); @@ -118,7 +128,11 @@ describe('rules_notification_alert_type', () => { references: [], attributes: ruleAlert, }); - alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId()); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); await alert.executor(payload); expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); @@ -143,7 +157,11 @@ describe('rules_notification_alert_type', () => { references: [], attributes: ruleAlert, }); - alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId()); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); await alert.executor(payload); expect(alertServices.alertInstanceFactory).toHaveBeenCalled(); @@ -165,7 +183,9 @@ describe('rules_notification_alert_type', () => { references: [], attributes: ruleAlert, }); - alertServices.callCluster.mockResolvedValue(sampleEmptyDocSearchResults()); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(sampleEmptyDocSearchResults()) + ); await alert.executor(payload); @@ -180,7 +200,11 @@ describe('rules_notification_alert_type', () => { references: [], attributes: ruleAlert, }); - alertServices.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortIdNoVersion()); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoVersion() + ) + ); await alert.executor(payload); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 6e03eb45da480..a40cb998eb408 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -64,7 +64,7 @@ export const rulesNotificationAlertType = ({ size: DEFAULT_RULE_NOTIFICATION_QUERY_SIZE, index: ruleParams.outputIndex, ruleId: ruleParams.ruleId, - callCluster: services.callCluster, + esClient: services.scopedClusterClient.asCurrentUser, }); const signals = results.hits.hits.map((hit) => hit._source); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts index 93b5667b9f629..cd1b77862af04 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts @@ -66,9 +66,7 @@ export const createDetectionIndex = async ( context: SecuritySolutionRequestHandlerContext, siemClient: AppClient ): Promise => { - const clusterClient = context.core.elasticsearch.legacy.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const callCluster = clusterClient.callAsCurrentUser; if (!siemClient) { throw new CreateIndexError('', 404); @@ -76,20 +74,20 @@ export const createDetectionIndex = async ( const index = siemClient.getSignalsIndex(); await ensureMigrationCleanupPolicy({ alias: index, esClient }); - const policyExists = await getPolicyExists(callCluster, index); + const policyExists = await getPolicyExists(esClient, index); if (!policyExists) { - await setPolicy(callCluster, index, signalsPolicy); + await setPolicy(esClient, index, signalsPolicy); } if (await templateNeedsUpdate({ alias: index, esClient })) { - await setTemplate(callCluster, index, getSignalsTemplate(index)); + await setTemplate(esClient, index, getSignalsTemplate(index)); } - const indexExists = await getIndexExists(callCluster, index); + const indexExists = await getIndexExists(esClient, index); if (indexExists) { - const indexVersion = await getIndexVersion(callCluster, index); + const indexVersion = await getIndexVersion(esClient, index); if (isOutdated({ current: indexVersion, target: SIGNALS_TEMPLATE_VERSION })) { - await callCluster('indices.rollover', { alias: index }); + await esClient.indices.rollover({ alias: index }); } } else { - await createBootstrapIndex(callCluster, index); + await createBootstrapIndex(esClient, index); } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/delete_index_route.ts index d652bd39c49ce..1a4f00a570424 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -38,16 +38,16 @@ export const deleteIndexRoute = (router: SecuritySolutionPluginRouter) => { const siemResponse = buildSiemResponse(response); try { - const clusterClient = context.core.elasticsearch.legacy.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + const siemClient = context.securitySolution?.getAppClient(); if (!siemClient) { return siemResponse.error({ statusCode: 404 }); } - const callCluster = clusterClient.callAsCurrentUser; const index = siemClient.getSignalsIndex(); - const indexExists = await getIndexExists(callCluster, index); + const indexExists = await getIndexExists(esClient, index); if (!indexExists) { return siemResponse.error({ @@ -55,14 +55,14 @@ export const deleteIndexRoute = (router: SecuritySolutionPluginRouter) => { body: `index: "${index}" does not exist`, }); } else { - await deleteAllIndex(callCluster, `${index}-*`); - const policyExists = await getPolicyExists(callCluster, index); + await deleteAllIndex(esClient, `${index}-*`); + const policyExists = await getPolicyExists(esClient, index); if (policyExists) { - await deletePolicy(callCluster, index); + await deletePolicy(esClient, index); } - const templateExists = await getTemplateExists(callCluster, index); + const templateExists = await getTemplateExists(esClient, index); if (templateExists) { - await deleteTemplate(callCluster, index); + await deleteTemplate(esClient, index); } return response.ok({ body: { acknowledged: true } }); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts index 1ef03b1d9e023..5c626cbe33ac1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_index_version.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { ApiResponse } from '@elastic/elasticsearch'; import { get } from 'lodash'; -import { LegacyAPICaller } from '../../../../../../../../src/core/server'; +import { ElasticsearchClient } from '../../../../../../../../src/core/server'; import { readIndex } from '../../index/read_index'; interface IndicesAliasResponse { @@ -20,10 +21,10 @@ interface IndexAliasResponse { } export const getIndexVersion = async ( - callCluster: LegacyAPICaller, + esClient: ElasticsearchClient, index: string ): Promise => { - const indexAlias: IndicesAliasResponse = await callCluster('indices.getAlias', { + const { body: indexAlias }: ApiResponse = await esClient.indices.getAlias({ index, }); const writeIndex = Object.keys(indexAlias).find( @@ -32,6 +33,6 @@ export const getIndexVersion = async ( if (writeIndex === undefined) { return 0; } - const writeIndexMapping = await readIndex(callCluster, writeIndex); - return get(writeIndexMapping, [writeIndex, 'mappings', '_meta', 'version']) ?? 0; + const writeIndexMapping = await readIndex(esClient, writeIndex); + return get(writeIndexMapping, ['body', writeIndex, 'mappings', '_meta', 'version']) ?? 0; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index 6a2d6c64c211f..01d07f68aa489 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -26,7 +26,7 @@ export const readIndexRoute = (router: SecuritySolutionPluginRouter) => { const siemResponse = buildSiemResponse(response); try { - const clusterClient = context.core.elasticsearch.legacy.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const siemClient = context.securitySolution?.getAppClient(); if (!siemClient) { @@ -34,12 +34,12 @@ export const readIndexRoute = (router: SecuritySolutionPluginRouter) => { } const index = siemClient.getSignalsIndex(); - const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index); + const indexExists = await getIndexExists(esClient, index); if (indexExists) { let mappingOutdated: boolean | null = null; try { - const indexVersion = await getIndexVersion(clusterClient.callAsCurrentUser, index); + const indexVersion = await getIndexVersion(esClient, index); mappingOutdated = isOutdated({ current: indexVersion, target: SIGNALS_TEMPLATE_VERSION, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 50182a795ca93..cf4b0bcf6f2d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -9,7 +9,6 @@ import { getEmptyFindResult, addPrepackagedRulesRequest, getFindResultWithSingleHit, - getEmptyIndex, getNonEmptyIndex, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, createMockConfig, mockGetCurrentUser } from '../__mocks__'; @@ -21,6 +20,8 @@ import { listMock } from '../../../../../../lists/server/mocks'; import { siemMock } from '../../../../mocks'; import { FrameworkRequest } from '../../../framework'; import { ExceptionListClient } from '../../../../../../lists/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -101,6 +102,10 @@ describe('add_prepackaged_rules_route', () => { errors: [], }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + ); addPrepackedRulesRoute(server.router, createMockConfig(), securitySetup); }); @@ -125,8 +130,11 @@ describe('add_prepackaged_rules_route', () => { }); test('it returns a 400 if the index does not exist', async () => { - clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); const request = addPrepackagedRulesRequest(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + ); const response = await server.inject(request, context); expect(response.status).toEqual(400); @@ -179,9 +187,10 @@ describe('add_prepackaged_rules_route', () => { }); test('catches errors if payloads cause errors to be thrown', async () => { - clients.clusterClient.callAsCurrentUser.mockImplementation(() => { - throw new Error('Test error'); - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValue( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('Test error')) + ); const request = addPrepackagedRulesRequest(); const response = await server.inject(request, context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index bccf7f4dfffa0..e7e571647cbe4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -106,7 +106,7 @@ export const createPrepackagedRules = async ( maxTimelineImportExportSize: number, exceptionsClient?: ExceptionListClient ): Promise => { - const clusterClient = context.core.elasticsearch.legacy.client; + const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; const exceptionsListClient = context.lists != null ? context.lists.getExceptionListClient() : exceptionsClient; @@ -126,7 +126,7 @@ export const createPrepackagedRules = async ( const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); const signalsIndex = siemClient.getSignalsIndex(); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { - const signalsIndexExists = await getIndexExists(clusterClient.callAsCurrentUser, signalsIndex); + const signalsIndexExists = await getIndexExists(esClient.asCurrentUser, signalsIndex); if (!signalsIndexExists) { throw new PrepackagedRulesError( `Pre-packaged rules cannot be installed until the signals index is created: ${signalsIndex}`, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index bb9f3ca9c9319..c5cbbeb09ed6d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -10,7 +10,6 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach import { buildMlAuthz } from '../../../machine_learning/authz'; import { getReadBulkRequest, - getEmptyIndex, getNonEmptyIndex, getFindResultWithSingleHit, getEmptyFindResult, @@ -20,6 +19,8 @@ import { import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -37,6 +38,10 @@ describe('create_rules_bulk', () => { clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no existing rules clients.alertsClient.create.mockResolvedValue(getResult()); // successful creation + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + ); createRulesBulkRoute(server.router, ml); }); @@ -84,7 +89,10 @@ describe('create_rules_bulk', () => { }); it('returns an error object if the index does not exist', async () => { - clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + ); const response = await server.inject(getReadBulkRequest(), context); expect(response.status).toEqual(200); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 6b85c7a40743a..e54c9a4cbb03e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -43,7 +43,7 @@ export const createRulesBulkRoute = ( async (context, request, response) => { const siemResponse = buildSiemResponse(response); const alertsClient = context.alerting?.getAlertsClient(); - const clusterClient = context.core.elasticsearch.legacy.client; + const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); @@ -92,7 +92,7 @@ export const createRulesBulkRoute = ( throwHttpError(await mlAuthz.validateRuleType(internalRule.params.type)); const finalIndex = internalRule.params.outputIndex; - const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); + const indexExists = await getIndexExists(esClient.asCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ ruleId: internalRule.params.ruleId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 7b998aa2d4252..dd636d5a180d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -12,7 +12,6 @@ import { getCreateRequest, getFindResultStatus, getNonEmptyIndex, - getEmptyIndex, getFindResultWithSingleHit, createMlRuleRequest, } from '../__mocks__/request_responses'; @@ -22,6 +21,8 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; jest.mock('../../rules/update_rules_notifications'); jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -40,6 +41,10 @@ describe('create_rules', () => { clients.alertsClient.create.mockResolvedValue(getResult()); // creation succeeds clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // needed to transform + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + ); createRulesRoute(server.router, ml); }); @@ -102,7 +107,10 @@ describe('create_rules', () => { describe('unhappy paths', () => { test('it returns a 400 if the index does not exist', async () => { - clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + ); const response = await server.inject(getCreateRequest(), context); expect(response.status).toEqual(400); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 309d1bdbb1471..95539319b5a12 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -45,7 +45,7 @@ export const createRulesRoute = ( } try { const alertsClient = context.alerting?.getAlertsClient(); - const clusterClient = context.core.elasticsearch.legacy.client; + const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); @@ -78,7 +78,7 @@ export const createRulesRoute = ( throwHttpError(await mlAuthz.validateRuleType(internalRule.params.type)); const indexExists = await getIndexExists( - clusterClient.callAsCurrentUser, + esClient.asCurrentUser, internalRule.params.outputIndex ); if (!indexExists) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 4f29f2d0586ee..0a265adf620ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -11,7 +11,6 @@ import { getImportRulesRequestOverwriteTrue, getEmptyFindResult, getResult, - getEmptyIndex, getFindResultWithSingleHit, getNonEmptyIndex, } from '../__mocks__/request_responses'; @@ -25,6 +24,8 @@ import { ruleIdsToNdJsonString, rulesToNdJsonString, } from '../../../../../common/detection_engine/schemas/request/import_rules_schema.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -46,6 +47,10 @@ describe('import_rules_route', () => { clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 1 } }) + ); importRulesRoute(server.router, config, ml); }); @@ -124,7 +129,10 @@ describe('import_rules_route', () => { test('returns an error if the index does not exist', async () => { clients.appClient.getSignalsIndex.mockReturnValue('mockSignalsIndex'); - clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ _shards: { total: 0 } }) + ); const response = await server.inject(request, context); expect(response.status).toEqual(400); expect(response.body).toEqual({ @@ -135,9 +143,12 @@ describe('import_rules_route', () => { }); test('returns an error when cluster throws error', async () => { - clients.clusterClient.callAsCurrentUser.mockImplementation(async () => { - throw new Error('Test error'); - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (context.core.elasticsearch.client.asCurrentUser.search as any).mockResolvedValue( + elasticsearchClientMock.createErrorTransportRequestPromise({ + body: new Error('Test error'), + }) + ); const response = await server.inject(request, context); expect(response.status).toEqual(500); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 27231ab896b7e..b37cc41f1439e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -77,7 +77,7 @@ export const importRulesRoute = ( try { const alertsClient = context.alerting?.getAlertsClient(); - const clusterClient = context.core.elasticsearch.legacy.client; + const esClient = context.core.elasticsearch.client; const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution?.getAppClient(); @@ -101,7 +101,7 @@ export const importRulesRoute = ( }); } const signalsIndex = siemClient.getSignalsIndex(); - const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, signalsIndex); + const indexExists = await getIndexExists(esClient.asCurrentUser, signalsIndex); if (!indexExists) { return siemResponse.error({ statusCode: 400, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts index 8597667f64657..4b74f865c6a53 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts @@ -20,10 +20,10 @@ describe('create_signals', () => { excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['auditbeat-*'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, body: { docvalue_fields: [ { @@ -66,6 +66,7 @@ describe('create_signals', () => { { '@timestamp': { order: 'asc', + unmapped_type: 'date', }, }, ], @@ -84,10 +85,10 @@ describe('create_signals', () => { excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['auditbeat-*'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, body: { docvalue_fields: [ { @@ -130,6 +131,7 @@ describe('create_signals', () => { { '@timestamp': { order: 'asc', + unmapped_type: 'date', }, }, ], @@ -149,10 +151,10 @@ describe('create_signals', () => { excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['auditbeat-*'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, body: { docvalue_fields: [ { @@ -191,14 +193,15 @@ describe('create_signals', () => { include_unmapped: true, }, ], + search_after: [fakeSortId], sort: [ { '@timestamp': { order: 'asc', + unmapped_type: 'date', }, }, ], - search_after: [fakeSortId], }, }); }); @@ -215,10 +218,10 @@ describe('create_signals', () => { excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['auditbeat-*'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, body: { docvalue_fields: [ { @@ -257,14 +260,15 @@ describe('create_signals', () => { include_unmapped: true, }, ], + search_after: [fakeSortIdNumber], sort: [ { '@timestamp': { order: 'asc', + unmapped_type: 'date', }, }, ], - search_after: [fakeSortIdNumber], }, }); }); @@ -280,10 +284,10 @@ describe('create_signals', () => { excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['auditbeat-*'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, body: { docvalue_fields: [ { @@ -326,6 +330,7 @@ describe('create_signals', () => { { '@timestamp': { order: 'asc', + unmapped_type: 'date', }, }, ], @@ -352,10 +357,10 @@ describe('create_signals', () => { excludeDocsWithTimestampOverride: false, }); expect(query).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['auditbeat-*'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, body: { docvalue_fields: [{ field: '@timestamp', format: 'strict_date_optional_time' }], query: { @@ -400,6 +405,7 @@ describe('create_signals', () => { { '@timestamp': { order: 'asc', + unmapped_type: 'date', }, }, ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts index f8fd4ed30d6ee..bce9adc9f0f88 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts @@ -73,10 +73,10 @@ export const buildEventsSearchQuery = ({ const filterWithTime = [filter, { bool: { filter: rangeFilter } }]; const searchQuery = { - allowNoIndices: true, + allow_no_indices: true, index, size, - ignoreUnavailable: true, + ignore_unavailable: true, body: { docvalue_fields: docFields, query: { @@ -100,6 +100,7 @@ export const buildEventsSearchQuery = ({ { [sortField]: { order: sortOrder ?? 'asc', + unmapped_type: 'date', }, }, ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index ead9da533d775..ccefa24e2018c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -26,6 +26,8 @@ import { BulkResponse, RuleRangeTuple } from './types'; import { SearchListItemArraySchema } from '../../../../../lists/common/schemas'; import { getSearchListItemResponseMock } from '../../../../../lists/common/schemas/response/search_list_item_schema.mock'; import { getRuleRangeTuples } from './utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -59,9 +61,14 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success with number of searches less than max signals', async () => { - mockService.callCluster - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) - .mockResolvedValueOnce({ + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -72,8 +79,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -84,8 +99,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -96,8 +119,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -108,7 +139,13 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoHits() + ) + ); const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ @@ -149,15 +186,19 @@ describe('searchAfterAndBulkCreate', () => { buildRuleMessage, }); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(9); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(5); expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('should return success with number of searches less than max signals with gap', async () => { - mockService.callCluster - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) - .mockResolvedValueOnce({ + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)) + ) + ); + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -168,8 +209,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -180,8 +229,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -192,7 +249,13 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoHits() + ) + ); const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ @@ -233,15 +296,20 @@ describe('searchAfterAndBulkCreate', () => { buildRuleMessage, }); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(7); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(4); expect(createdSignalsCount).toEqual(3); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('should return success when no search results are in the allowlist', async () => { - mockService.callCluster - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3))) - .mockResolvedValueOnce({ + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -267,7 +335,13 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoHits() + ) + ); const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ @@ -308,7 +382,7 @@ describe('searchAfterAndBulkCreate', () => { buildRuleMessage, }); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(3); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2); expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); @@ -320,16 +394,22 @@ describe('searchAfterAndBulkCreate', () => { { ...getSearchListItemResponseMock(), value: ['3.3.3.3'] }, ]; listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems); - mockService.callCluster + mockService.scopedClusterClient.asCurrentUser.search .mockResolvedValueOnce( - repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ - '1.1.1.1', - '2.2.2.2', - '2.2.2.2', - '2.2.2.2', - ]) + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '2.2.2.2', + '2.2.2.2', + ]) + ) ) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + .mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoHits() + ) + ); const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ @@ -370,7 +450,7 @@ describe('searchAfterAndBulkCreate', () => { buildRuleMessage, }); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(2); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2); expect(createdSignalsCount).toEqual(0); // should not create any signals because all events were in the allowlist expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); @@ -384,13 +464,15 @@ describe('searchAfterAndBulkCreate', () => { ]; listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems); - mockService.callCluster.mockResolvedValueOnce( - repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3), [ - '1.1.1.1', - '2.2.2.2', - '2.2.2.2', - '2.2.2.2', - ]) + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '2.2.2.2', + '2.2.2.2', + ]) + ) ); const exceptionItem = getExceptionListItemSchemaMock(); @@ -432,7 +514,7 @@ describe('searchAfterAndBulkCreate', () => { buildRuleMessage, }); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(1); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(createdSignalsCount).toEqual(0); // should not create any signals because all events were in the allowlist expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); // I don't like testing log statements since logs change but this is the best @@ -443,9 +525,14 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success when no sortId present but search results are in the allowlist', async () => { - mockService.callCluster - .mockResolvedValueOnce(repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3))) - .mockResolvedValueOnce({ + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -470,7 +557,8 @@ describe('searchAfterAndBulkCreate', () => { }, }, ], - }); + }) + ); const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ @@ -511,7 +599,7 @@ describe('searchAfterAndBulkCreate', () => { buildRuleMessage, }); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(2); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(1); expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); // I don't like testing log statements since logs change but this is the best @@ -522,9 +610,14 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success when no exceptions list provided', async () => { - mockService.callCluster - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3))) - .mockResolvedValueOnce({ + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -550,7 +643,13 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoHits() + ) + ); listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( @@ -587,7 +686,7 @@ describe('searchAfterAndBulkCreate', () => { buildRuleMessage, }); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(3); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(2); expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); @@ -605,9 +704,14 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; - mockService.callCluster - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) - .mockRejectedValue(new Error('bulk failed')); // Added this recently + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)) + ) + ); + mockService.scopedClusterClient.asCurrentUser.bulk.mockRejectedValue( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('bulk failed')) + ); // Added this recently const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, exceptionsList: [exceptionItem], @@ -653,7 +757,9 @@ describe('searchAfterAndBulkCreate', () => { }, }, ]; - mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(sampleEmptyDocSearchResults()) + ); listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ @@ -694,18 +800,20 @@ describe('searchAfterAndBulkCreate', () => { }); test('if returns false when singleSearchAfter throws an exception', async () => { - mockService.callCluster - .mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ - { - create: { - status: 201, + mockService.scopedClusterClient.asCurrentUser.search + .mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + took: 100, + errors: false, + items: [ + { + create: { + status: 201, + }, }, - }, - ], - }) + ], + }) + ) .mockImplementation(() => { throw Error('Fake Error'); // throws the exception we are testing }); @@ -781,11 +889,24 @@ describe('searchAfterAndBulkCreate', () => { }, ], }; - mockService.callCluster - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) - .mockResolvedValueOnce(bulkItem) // adds the response with errors we are testing - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6))) - .mockResolvedValueOnce({ + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(bulkItem) + ); // adds the response with errors we are testing + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -796,8 +917,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -808,8 +937,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -820,7 +957,13 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoHits() + ) + ); const { success, createdSignalsCount, @@ -854,15 +997,20 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(false); expect(errors).toEqual(['error on creation']); - expect(mockService.callCluster).toHaveBeenCalledTimes(9); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(5); expect(createdSignalsCount).toEqual(4); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); it('invokes the enrichment callback with signal search results', async () => { - mockService.callCluster - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) - .mockResolvedValueOnce({ + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -873,8 +1021,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -885,8 +1041,16 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9))) - .mockResolvedValueOnce({ + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9)) + ) + ); + + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ took: 100, errors: false, items: [ @@ -897,7 +1061,13 @@ describe('searchAfterAndBulkCreate', () => { }, ], }) - .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + ); + + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsNoSortIdNoHits() + ) + ); const mockEnrichment = jest.fn((a) => a); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ @@ -941,7 +1111,7 @@ describe('searchAfterAndBulkCreate', () => { }) ); expect(success).toEqual(true); - expect(mockService.callCluster).toHaveBeenCalledTimes(7); + expect(mockService.scopedClusterClient.asCurrentUser.search).toHaveBeenCalledTimes(4); expect(createdSignalsCount).toEqual(3); expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 4079cbb852de4..bcd04ed5e15cd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -30,6 +30,8 @@ import { getExceptionListClientMock } from '../../../../../lists/server/services import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import { getEntryListMock } from '../../../../../lists/common/schemas/types/entry_list.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -140,11 +142,13 @@ describe('rules_notification_alert_type', () => { ), }; }); - alertServices.callCluster.mockResolvedValue({ - hits: { - total: { value: 10 }, - }, - }); + alertServices.scopedClusterClient.asCurrentUser.transport.request.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + total: { value: 10 }, + }, + }) + ); const value: Partial = { statusCode: 200, body: { @@ -160,7 +164,9 @@ describe('rules_notification_alert_type', () => { }, }, }; - alertServices.scopedClusterClient.fieldCaps.mockResolvedValue(value as ApiResponse); + alertServices.scopedClusterClient.asCurrentUser.fieldCaps.mockResolvedValue( + value as ApiResponse + ); const ruleAlert = getResult(); alertServices.savedObjectsClient.get.mockResolvedValue({ id: 'id', @@ -665,7 +671,9 @@ describe('rules_notification_alert_type', () => { }); it('and call ruleStatusService with the default message', async () => { - (searchAfterAndBulkCreate as jest.Mock).mockRejectedValue({}); + (searchAfterAndBulkCreate as jest.Mock).mockRejectedValue( + elasticsearchClientMock.createErrorTransportRequestPromise({}) + ); await alert.executor(payload); expect(logger.error).toHaveBeenCalled(); expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 65efd25c9fba2..cd77cab01bb01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -12,6 +12,7 @@ import isEmpty from 'lodash/isEmpty'; import { chain, tryCatch } from 'fp-ts/lib/TaskEither'; import { flow } from 'fp-ts/lib/function'; +import { ApiResponse } from '@elastic/elasticsearch'; import { performance } from 'perf_hooks'; import { toError, toPromise } from '../../../../common/fp_utils'; @@ -198,7 +199,7 @@ export const signalRulesAlertType = ({ const inputIndices = await getInputIndex(services, version, index); const [privileges, timestampFieldCaps] = await Promise.all([ checkPrivileges(services, inputIndices), - services.scopedClusterClient.fieldCaps({ + services.scopedClusterClient.asCurrentUser.fieldCaps({ index, fields: hasTimestampOverride ? ['@timestamp', timestampOverride as string] @@ -583,7 +584,10 @@ export const signalRulesAlertType = ({ wroteWarningStatus = true; } try { - const signalIndexVersion = await getIndexVersion(services.callCluster, outputIndex); + const signalIndexVersion = await getIndexVersion( + services.scopedClusterClient.asCurrentUser, + outputIndex + ); if (isOutdated({ current: signalIndexVersion, target: MIN_EQL_RULE_INDEX_VERSION })) { throw new Error( `EQL based rules require an update to version ${MIN_EQL_RULE_INDEX_VERSION} of the detection alerts index mapping` @@ -610,10 +614,11 @@ export const signalRulesAlertType = ({ eventCategoryOverride ); const eqlSignalSearchStart = performance.now(); - const response: EqlSignalSearchResponse = await services.callCluster( - 'transport.request', + const { + body: response, + } = (await services.scopedClusterClient.asCurrentUser.transport.request( request - ); + )) as ApiResponse; const eqlSignalSearchEnd = performance.now(); const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart); result.searchAfterTimes = [eqlSearchDuration]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts index 8ade6460cbffc..eecedb02b2687 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -21,6 +21,8 @@ import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { singleBulkCreate, filterDuplicateRules } from './single_bulk_create'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import { buildRuleMessageFactory } from './rule_messages'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -139,15 +141,17 @@ describe('singleBulkCreate', () => { test('create successful bulk create', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }); + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }) + ); const { success, createdItemsCount } = await singleBulkCreate({ filteredEvents: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, @@ -174,15 +178,17 @@ describe('singleBulkCreate', () => { test('create successful bulk create with docs with no versioning', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockResolvedValueOnce({ - took: 100, - errors: false, - items: [ - { - fakeItemValue: 'fakeItemKey', - }, - ], - }); + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + ], + }) + ); const { success, createdItemsCount } = await singleBulkCreate({ filteredEvents: sampleDocSearchResultsNoSortIdNoVersion(), ruleParams: sampleParams, @@ -209,7 +215,9 @@ describe('singleBulkCreate', () => { test('create unsuccessful bulk create due to empty search results', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockResolvedValue(false); + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(false) + ); const { success, createdItemsCount } = await singleBulkCreate({ filteredEvents: sampleEmptyDocSearchResults(), ruleParams: sampleParams, @@ -237,7 +245,9 @@ describe('singleBulkCreate', () => { test('create successful bulk create when bulk create has duplicate errors', async () => { const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateDuplicateResult) + ); const { success, createdItemsCount } = await singleBulkCreate({ filteredEvents: sampleSearchResult(), ruleParams: sampleParams, @@ -267,7 +277,9 @@ describe('singleBulkCreate', () => { test('create failed bulk create when bulk create has multiple error statuses', async () => { const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; - mockService.callCluster.mockResolvedValue(sampleBulkCreateErrorResult); + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValue( + elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateErrorResult) + ); const { success, createdItemsCount, errors } = await singleBulkCreate({ filteredEvents: sampleSearchResult(), ruleParams: sampleParams, @@ -335,7 +347,9 @@ describe('singleBulkCreate', () => { test('create successful and returns proper createdItemsCount', async () => { const sampleParams = sampleRuleAlertParams(); - mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); + mockService.scopedClusterClient.asCurrentUser.bulk.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(sampleBulkCreateDuplicateResult) + ); const { success, createdItemsCount } = await singleBulkCreate({ filteredEvents: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index 4d66a1fe7de92..6c791bc4d0ee3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -158,7 +158,7 @@ export const singleBulkCreate = async ({ }), ]); const start = performance.now(); - const response: BulkResponse = await services.callCluster('bulk', { + const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({ index: signalsIndex, refresh, body: bulkBody, @@ -244,7 +244,7 @@ export const bulkInsertSignals = async ( doc._source, ]); const start = performance.now(); - const response: BulkResponse = await services.callCluster('bulk', { + const { body: response } = await services.scopedClusterClient.asCurrentUser.bulk({ refresh, body: bulkBody, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index c3fb5a2b0a739..a325903c66ec0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -14,6 +14,8 @@ import { singleSearchAfter } from './single_search_after'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; import { ShardError } from '../../types'; import { buildRuleMessageFactory } from './rule_messages'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -29,7 +31,9 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter works without a given sort id', async () => { - mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId()); + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(sampleDocSearchResultsNoSortId()) + ); const { searchResult } = await singleSearchAfter({ searchAfterSortId: undefined, index: [], @@ -46,7 +50,9 @@ describe('singleSearchAfter', () => { expect(searchResult).toEqual(sampleDocSearchResultsNoSortId()); }); test('if singleSearchAfter returns an empty failure array', async () => { - mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId()); + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(sampleDocSearchResultsNoSortId()) + ); const { searchErrors } = await singleSearchAfter({ searchAfterSortId: undefined, index: [], @@ -80,22 +86,24 @@ describe('singleSearchAfter', () => { }, }, ]; - mockService.callCluster.mockResolvedValue({ - took: 10, - timed_out: false, - _shards: { - total: 10, - successful: 10, - failed: 1, - skipped: 0, - failures: errors, - }, - hits: { - total: 100, - max_score: 100, - hits: [], - }, - }); + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 1, + skipped: 0, + failures: errors, + }, + hits: { + total: 100, + max_score: 100, + hits: [], + }, + }) + ); const { searchErrors } = await singleSearchAfter({ searchAfterSortId: undefined, index: [], @@ -115,7 +123,11 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - mockService.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId()); + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + sampleDocSearchResultsWithSortId() + ) + ); const { searchResult } = await singleSearchAfter({ searchAfterSortId, index: [], @@ -133,9 +145,9 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - mockService.callCluster.mockImplementation(async () => { - throw Error('Fake Error'); - }); + mockService.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createErrorTransportRequestPromise(new Error('Fake Error')) + ); await expect( singleSearchAfter({ searchAfterSortId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index f7b30cd7f2e83..b35c68c8deacd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -72,8 +72,9 @@ export const singleSearchAfter = async ({ }); const start = performance.now(); - const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( - 'search', + const { + body: nextSearchAfterResult, + } = await services.scopedClusterClient.asCurrentUser.search( searchAfterQuery ); const end = performance.now(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts index 7d6cd655e336d..3a2a8fcbebf6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts @@ -34,7 +34,7 @@ export const buildThreatEnrichment = ({ }, }; const threatResponse = await getThreatList({ - callCluster: services.callCluster, + esClient: services.scopedClusterClient.asCurrentUser, exceptionItems, threatFilters: [...threatFilters, matchedThreatsFilter], query: threatQuery, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 854c2b8f3fdc1..e0be48458b049 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -66,7 +66,7 @@ export const createThreatSignals = async ({ }; let threatListCount = await getThreatListCount({ - callCluster: services.callCluster, + esClient: services.scopedClusterClient.asCurrentUser, exceptionItems, threatFilters, query: threatQuery, @@ -76,7 +76,7 @@ export const createThreatSignals = async ({ logger.debug(buildRuleMessage(`Total indicator items: ${threatListCount}`)); let threatList = await getThreatList({ - callCluster: services.callCluster, + esClient: services.scopedClusterClient.asCurrentUser, exceptionItems, threatFilters, query: threatQuery, @@ -166,7 +166,7 @@ export const createThreatSignals = async ({ logger.debug(buildRuleMessage(`Indicator items left to check are ${threatListCount}`)); threatList = await getThreatList({ - callCluster: services.callCluster, + esClient: services.scopedClusterClient.asCurrentUser, exceptionItems, query: threatQuery, language: threatLanguage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts index 92d4e5cf8a93b..a2a51d3a060c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ApiResponse } from '@elastic/elasticsearch'; import { SearchResponse } from 'elasticsearch'; import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; import { @@ -21,7 +22,7 @@ import { export const MAX_PER_PAGE = 9000; export const getThreatList = async ({ - callCluster, + esClient, query, language, index, @@ -52,7 +53,7 @@ export const getThreatList = async ({ `Querying the indicator items from the index: "${index}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items` ) ); - const response: SearchResponse = await callCluster('search', { + const { body: response } = await esClient.search>({ body: { query: queryFilter, fields: [ @@ -69,7 +70,7 @@ export const getThreatList = async ({ listItemIndex: listClient.getListItemIndex(), }), }, - ignoreUnavailable: true, + ignore_unavailable: true, index, size: calculatedPerPage, }); @@ -108,7 +109,7 @@ export const getSortWithTieBreaker = ({ }; export const getThreatListCount = async ({ - callCluster, + esClient, query, language, threatFilters, @@ -122,13 +123,15 @@ export const getThreatListCount = async ({ index, exceptionItems ); - const response: { + const { + body: response, + }: ApiResponse<{ count: number; - } = await callCluster('count', { + }> = await esClient.count({ body: { query: queryFilter, }, - ignoreUnavailable: true, + ignore_unavailable: true, index, }); return response.count; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 7d0ab3a2b6d25..0c14f906742d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -29,7 +29,7 @@ import { AlertServices, } from '../../../../../../alerting/server'; import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; -import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/core/server'; +import { ElasticsearchClient, Logger } from '../../../../../../../../src/core/server'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; @@ -148,7 +148,7 @@ export interface BooleanFilter { } export interface GetThreatListOptions { - callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; + esClient: ElasticsearchClient; query: string; language: ThreatLanguageOrUndefined; index: string[]; @@ -164,7 +164,7 @@ export interface GetThreatListOptions { } export interface ThreatListCountOptions { - callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; + esClient: ElasticsearchClient; query: string; language: ThreatLanguageOrUndefined; threatFilters: PartialFilter[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 48e3da7404f51..fa2fa1f102bd1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -155,18 +155,20 @@ export const checkPrivileges = async ( services: AlertServices, indices: string[] ): Promise => - services.callCluster('transport.request', { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - index: [ - { - names: indices ?? [], - privileges: ['read'], - }, - ], - }, - }); + ( + await services.scopedClusterClient.asCurrentUser.transport.request({ + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + index: [ + { + names: indices ?? [], + privileges: ['read'], + }, + ], + }, + }) + ).body as Privilege; export const getNumCatchupIntervals = ({ gap, @@ -205,7 +207,11 @@ export const getListsClient = ({ throw new Error('lists plugin unavailable during rule execution'); } - const listClient = lists.getListClient(services.callCluster, spaceId, updatedByUser ?? 'elastic'); + const listClient = lists.getListClient( + services.scopedClusterClient.asCurrentUser, + spaceId, + updatedByUser ?? 'elastic' + ); const exceptionsClient = lists.getExceptionListClient( savedObjectClient, updatedByUser ?? 'elastic' diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.test.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.test.ts index cc554b8468d1e..39530ea7b7741 100644 --- a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.test.ts +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.test.ts @@ -26,10 +26,10 @@ describe('buildSortedEventsQuery', () => { test('it builds a filter with given date range', () => { expect(buildSortedEventsQuery(query)).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['index-name'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, track_total_hits: false, body: { docvalue_fields: [ @@ -77,10 +77,10 @@ describe('buildSortedEventsQuery', () => { test('it does not include searchAfterSortId if it is an empty string', () => { query.searchAfterSortId = ''; expect(buildSortedEventsQuery(query)).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['index-name'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, track_total_hits: false, body: { docvalue_fields: [ @@ -129,10 +129,10 @@ describe('buildSortedEventsQuery', () => { const sortId = '123456789012'; query.searchAfterSortId = sortId; expect(buildSortedEventsQuery(query)).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['index-name'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, track_total_hits: false, body: { docvalue_fields: [ @@ -182,10 +182,10 @@ describe('buildSortedEventsQuery', () => { const sortId = 123456789012; query.searchAfterSortId = sortId; expect(buildSortedEventsQuery(query)).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['index-name'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, track_total_hits: false, body: { docvalue_fields: [ @@ -240,10 +240,10 @@ describe('buildSortedEventsQuery', () => { }, }; expect(buildSortedEventsQuery(query)).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['index-name'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, track_total_hits: false, body: { docvalue_fields: [ @@ -298,10 +298,10 @@ describe('buildSortedEventsQuery', () => { test('it uses sortOrder if specified', () => { query.sortOrder = 'desc'; expect(buildSortedEventsQuery(query)).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['index-name'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, track_total_hits: false, body: { docvalue_fields: [ @@ -349,10 +349,10 @@ describe('buildSortedEventsQuery', () => { test('it uses track_total_hits if specified', () => { query.track_total_hits = true; expect(buildSortedEventsQuery(query)).toEqual({ - allowNoIndices: true, + allow_no_indices: true, index: ['index-name'], size: 100, - ignoreUnavailable: true, + ignore_unavailable: true, track_total_hits: true, body: { docvalue_fields: [ diff --git a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts index add3e1f59e20e..a4fb54a06ace8 100644 --- a/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts +++ b/x-pack/plugins/stack_alerts/common/build_sorted_events_query.ts @@ -53,10 +53,10 @@ export const buildSortedEventsQuery = ({ const filterWithTime = [filter, { bool: { filter: rangeFilter } }]; const searchQuery = { - allowNoIndices: true, + allow_no_indices: true, index, size, - ignoreUnavailable: true, + ignore_unavailable: true, track_total_hits: track_total_hits ?? false, body: { docvalue_fields: docFields, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 4adc7c05821f9..66984e46de602 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -18,6 +18,8 @@ import { getAlertType, ConditionMetAlertInstanceId, ActionGroupId } from './aler import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; import { ActionContext } from './action_context'; import { ESSearchResponse, ESSearchRequest } from '../../../../../../typings/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; describe('alertType', () => { const logger = loggingSystemMock.create().get(); @@ -132,7 +134,9 @@ describe('alertType', () => { const alertServices: AlertServicesMock = alertsMock.createAlertServices(); const searchResult: ESSearchResponse = generateResults([]); - alertServices.callCluster.mockResolvedValueOnce(searchResult); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) + ); const result = await alertType.executor({ alertId: uuid.v4(), @@ -189,7 +193,9 @@ describe('alertType', () => { 'time-field': newestDocumentTimestamp - 2000, }, ]); - alertServices.callCluster.mockResolvedValueOnce(searchResult); + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) + ); const result = await alertType.executor({ alertId: uuid.v4(), @@ -240,12 +246,14 @@ describe('alertType', () => { const previousTimestamp = Date.now(); const newestDocumentTimestamp = previousTimestamp + 1000; - alertServices.callCluster.mockResolvedValueOnce( - generateResults([ - { - 'time-field': newestDocumentTimestamp, - }, - ]) + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults([ + { + 'time-field': newestDocumentTimestamp, + }, + ]) + ) ); const executorOptions = { @@ -300,15 +308,17 @@ describe('alertType', () => { const oldestDocumentTimestamp = Date.now(); - alertServices.callCluster.mockResolvedValueOnce( - generateResults([ - { - 'time-field': oldestDocumentTimestamp, - }, - { - 'time-field': oldestDocumentTimestamp - 1000, - }, - ]) + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults([ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ]) + ) ); const result = await alertType.executor({ @@ -359,12 +369,14 @@ describe('alertType', () => { const oldestDocumentTimestamp = Date.now(); - alertServices.callCluster.mockResolvedValueOnce( - generateResults([ - { - 'time-field': oldestDocumentTimestamp, - }, - ]) + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults([ + { + 'time-field': oldestDocumentTimestamp, + }, + ]) + ) ); const executorOptions = { @@ -400,15 +412,17 @@ describe('alertType', () => { }); const newestDocumentTimestamp = oldestDocumentTimestamp + 5000; - alertServices.callCluster.mockResolvedValueOnce( - generateResults([ - { - 'time-field': newestDocumentTimestamp, - }, - { - 'time-field': newestDocumentTimestamp - 1000, - }, - ]) + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults([ + { + 'time-field': newestDocumentTimestamp, + }, + { + 'time-field': newestDocumentTimestamp - 1000, + }, + ]) + ) ); const secondResult = await alertType.executor({ @@ -443,17 +457,19 @@ describe('alertType', () => { const oldestDocumentTimestamp = Date.now(); - alertServices.callCluster.mockResolvedValueOnce( - generateResults( - [ - { - 'time-field': oldestDocumentTimestamp, - }, - { - 'time-field': oldestDocumentTimestamp - 1000, - }, - ], - true + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults( + [ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ], + true + ) ) ); @@ -504,18 +520,20 @@ describe('alertType', () => { const oldestDocumentTimestamp = Date.now(); - alertServices.callCluster.mockResolvedValueOnce( - generateResults( - [ - { - 'time-field': oldestDocumentTimestamp, - }, - { - 'time-field': oldestDocumentTimestamp - 1000, - }, - ], - true, - true + alertServices.scopedClusterClient.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults( + [ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ], + true, + true + ) ) ); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 7734c59425a16..d1cbeeb46fac0 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { Logger } from 'src/core/server'; -import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { AlertType, AlertExecutorOptions } from '../../types'; import { ActionContext, EsQueryAlertActionContext, addMessages } from './action_context'; import { @@ -157,7 +156,7 @@ export function getAlertType( const { alertId, name, services, params, state } = options; const previousTimestamp = state.latestTimestamp; - const callCluster = services.callCluster; + const esClient = services.scopedClusterClient.asCurrentUser; const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); const compareFn = ComparatorFns.get(params.thresholdComparator); @@ -215,7 +214,7 @@ export function getAlertType( logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}`); - const searchResult: ESSearchResponse = await callCluster('search', query); + const { body: searchResult } = await esClient.search(query); if (searchResult.hits.hits.length > 0) { const numMatches = searchResult.hits.total.value; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts index b86a8f9284c97..a416056217442 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; +import { ElasticsearchClient } from 'kibana/server'; import { Logger } from 'src/core/server'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { SearchResponse } from 'elasticsearch'; import { Query, IIndexPattern, @@ -39,7 +40,7 @@ export async function getShapesFilters( boundaryIndexTitle: string, boundaryGeoField: string, geoField: string, - callCluster: ILegacyScopedClusterClient['callAsCurrentUser'], + esClient: ElasticsearchClient, log: Logger, alertId: string, boundaryNameField?: string, @@ -48,7 +49,8 @@ export async function getShapesFilters( const filters: Record = {}; const shapesIdsNamesMap: Record = {}; // Get all shapes in index - const boundaryData: SearchResponse> = await callCluster('search', { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { body: boundaryData }: ApiResponse> = await esClient.search({ index: boundaryIndexTitle, body: { size: MAX_SHAPES_QUERY_SIZE, @@ -56,7 +58,7 @@ export async function getShapesFilters( }, }); - boundaryData.hits.hits.forEach(({ _index, _id }) => { + boundaryData.hits.hits.forEach(({ _index, _id }: { _index: string; _id: string }) => { filters[_id] = { geo_shape: { [geoField]: { @@ -101,14 +103,14 @@ export async function executeEsQueryFactory( boundaryNameField?: string; indexQuery?: Query; }, - { callCluster }: { callCluster: ILegacyScopedClusterClient['callAsCurrentUser'] }, + esClient: ElasticsearchClient, log: Logger, shapesFilters: Record ) { return async ( gteDateTime: Date | null, ltDateTime: Date | null - ): Promise | undefined> => { + ): Promise> | undefined> => { let esFormattedQuery; if (indexQuery) { const gteEpochDateTime = gteDateTime ? new Date(gteDateTime).getTime() : null; @@ -192,9 +194,9 @@ export async function executeEsQueryFactory( }, }; - let esResult: SearchResponse | undefined; + let esResult: ApiResponse> | undefined; try { - esResult = await callCluster('search', esQuery); + ({ body: esResult } = await esClient.search(esQuery)); } catch (err) { log.warn(`${err.message}`); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index 3f2421529c346..866b25d239db7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -6,8 +6,9 @@ */ import _ from 'lodash'; -import { SearchResponse } from 'elasticsearch'; import { Logger } from 'src/core/server'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { SearchResponse } from 'elasticsearch'; import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_query_builder'; import { AlertServices } from '../../../../alerting/server'; import { @@ -148,17 +149,22 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ params.boundaryIndexTitle, params.boundaryGeoField, params.geoField, - services.callCluster, + services.scopedClusterClient.asCurrentUser, log, alertId, params.boundaryNameField, params.boundaryIndexQuery ); - const executeEsQuery = await executeEsQueryFactory(params, services, log, shapesFilters); + const executeEsQuery = await executeEsQueryFactory( + params, + services.scopedClusterClient.asCurrentUser, + log, + shapesFilters + ); // Start collecting data only on the first cycle - let currentIntervalResults: SearchResponse | undefined; + let currentIntervalResults: ApiResponse> | undefined; if (!currIntervalStartTime) { log.debug(`alert ${GEO_CONTAINMENT_ID}:${alertId} alert initialized. Collecting data`); // Consider making first time window configurable? @@ -171,7 +177,7 @@ export const getGeoContainmentExecutor = (log: Logger): GeoContainmentAlertType[ } const currLocationMap: Map = transformResults( - currentIntervalResults, + currentIntervalResults?.body, params.dateField, params.geoField ); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts index 62d95e4ed88d8..429331916ea7d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts @@ -9,10 +9,10 @@ import _ from 'lodash'; import sampleJsonResponse from './es_sample_response.json'; import sampleJsonResponseWithNesting from './es_sample_response_with_nesting.json'; import { getActiveEntriesAndGenerateAlerts, transformResults } from '../geo_containment'; -import { SearchResponse } from 'elasticsearch'; import { OTHER_CATEGORY } from '../es_query_builder'; import { alertsMock } from '../../../../../alerting/server/mocks'; import { GeoContainmentInstanceContext, GeoContainmentInstanceState } from '../alert_type'; +import { SearchResponse } from 'elasticsearch'; describe('geo_containment', () => { describe('transformResults', () => { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index d2c910790ea40..4c0fafc95a579 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -146,7 +146,7 @@ export function getAlertType( ); } - const callCluster = services.callCluster; + const esClient = services.scopedClusterClient.asCurrentUser; const date = new Date().toISOString(); // the undefined values below are for config-schema optional types const queryParams: TimeSeriesQuery = { @@ -166,7 +166,7 @@ export function getAlertType( // console.log(`index_threshold: query: ${JSON.stringify(queryParams, null, 4)}`); const result = await (await data).timeSeriesQuery({ logger, - callCluster, + esClient, query: queryParams, }); logger.debug(`alert ${ID}:${alertId} "${name}" query result: ${JSON.stringify(result)}`); diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts index fd0ea1bc9e8f5..86d18d98fa0e1 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts @@ -9,6 +9,8 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { TimeSeriesQueryParameters, TimeSeriesQuery, timeSeriesQuery } from './time_series_query'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; const DefaultQueryParams: TimeSeriesQuery = { index: 'index-name', @@ -27,19 +29,18 @@ const DefaultQueryParams: TimeSeriesQuery = { describe('timeSeriesQuery', () => { let params: TimeSeriesQueryParameters; - const mockCallCluster = jest.fn(); + const esClient = elasticsearchClientMock.createClusterClient().asScoped().asCurrentUser; beforeEach(async () => { - mockCallCluster.mockReset(); params = { logger: loggingSystemMock.create().get(), - callCluster: mockCallCluster, + esClient, query: DefaultQueryParams, }; }); it('fails as expected when the callCluster call fails', async () => { - mockCallCluster.mockRejectedValue(new Error('woopsie')); + esClient.search = jest.fn().mockRejectedValue(new Error('woopsie')); expect(timeSeriesQuery(params)).rejects.toThrowErrorMatchingInlineSnapshot( `"error running search"` ); diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts index 106f665640e41..78462d9969929 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts @@ -6,8 +6,7 @@ */ import { SearchResponse } from 'elasticsearch'; -import { Logger } from 'kibana/server'; -import { LegacyScopedClusterClient } from '../../../../../../src/core/server'; +import { Logger, ElasticsearchClient } from 'kibana/server'; import { DEFAULT_GROUPS } from '../index'; import { getDateRangeInfo } from './date_range_info'; @@ -16,14 +15,14 @@ export { TimeSeriesQuery, TimeSeriesResult } from './time_series_types'; export interface TimeSeriesQueryParameters { logger: Logger; - callCluster: LegacyScopedClusterClient['callAsCurrentUser']; + esClient: ElasticsearchClient; query: TimeSeriesQuery; } export async function timeSeriesQuery( params: TimeSeriesQueryParameters ): Promise { - const { logger, callCluster, query: queryParams } = params; + const { logger, esClient, query: queryParams } = params; const { index, timeWindowSize, @@ -59,9 +58,8 @@ export async function timeSeriesQuery( }, // aggs: {...}, filled in below }, - ignoreUnavailable: true, - allowNoIndices: true, - ignore: [404], + ignore_unavailable: true, + allow_no_indices: true, }; // add the aggregations @@ -127,17 +125,16 @@ export async function timeSeriesQuery( }; } - let esResult: SearchResponse; const logPrefix = 'indexThreshold timeSeriesQuery: callCluster'; logger.debug(`${logPrefix} call: ${JSON.stringify(esQuery)}`); - + let esResult: SearchResponse; // note there are some commented out console.log()'s below, which are left // in, as they are VERY useful when debugging these queries; debug logging // isn't as nice since it's a single long JSON line. // console.log('time_series_query.ts request\n', JSON.stringify(esQuery, null, 4)); try { - esResult = await callCluster('search', esQuery); + esResult = (await esClient.search>(esQuery, { ignore: [404] })).body; } catch (err) { // console.log('time_series_query.ts error\n', JSON.stringify(err, null, 4)); logger.warn(`${logPrefix} error: ${err.message}`); diff --git a/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts index e5cafdd8a0ad7..6a3b5a0c44aba 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts @@ -12,7 +12,7 @@ import { KibanaRequest, IKibanaResponse, KibanaResponseFactory, - ILegacyScopedClusterClient, + ElasticsearchClient, } from 'kibana/server'; import { Logger } from '../../../../../../src/core/server'; @@ -49,7 +49,10 @@ export function createFieldsRoute(logger: Logger, router: IRouter, baseRoute: st } try { - rawFields = await getRawFields(ctx.core.elasticsearch.legacy.client, req.body.indexPatterns); + rawFields = await getRawFields( + ctx.core.elasticsearch.client.asCurrentUser, + req.body.indexPatterns + ); } catch (err) { const indexPatterns = req.body.indexPatterns.join(','); logger.warn( @@ -90,19 +93,15 @@ interface Field { aggregatable: boolean; } -async function getRawFields( - dataClient: ILegacyScopedClusterClient, - indexes: string[] -): Promise { +async function getRawFields(esClient: ElasticsearchClient, indexes: string[]): Promise { const params = { index: indexes, fields: ['*'], - ignoreUnavailable: true, - allowNoIndices: true, - ignore: 404, + ignore_unavailable: true, + allow_no_indices: true, }; - const result = await dataClient.callAsCurrentUser('fieldCaps', params); - return result as RawFields; + const result = await esClient.fieldCaps(params); + return result.body as RawFields; } function getFieldsFromRawFields(rawFields: RawFields): Field[] { diff --git a/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts index 13d6892f37c1b..c029f5b8bdaed 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts @@ -17,9 +17,8 @@ import { KibanaRequest, IKibanaResponse, KibanaResponseFactory, - ILegacyScopedClusterClient, + ElasticsearchClient, } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../src/core/server'; const bodySchema = schema.object({ @@ -54,14 +53,14 @@ export function createIndicesRoute(logger: Logger, router: IRouter, baseRoute: s let aliases: string[] = []; try { - aliases = await getAliasesFromPattern(ctx.core.elasticsearch.legacy.client, pattern); + aliases = await getAliasesFromPattern(ctx.core.elasticsearch.client.asCurrentUser, pattern); } catch (err) { logger.warn(`route ${path} error getting aliases from pattern "${pattern}": ${err.message}`); } let indices: string[] = []; try { - indices = await getIndicesFromPattern(ctx.core.elasticsearch.legacy.client, pattern); + indices = await getIndicesFromPattern(ctx.core.elasticsearch.client.asCurrentUser, pattern); } catch (err) { logger.warn(`route ${path} error getting indices from pattern "${pattern}": ${err.message}`); } @@ -81,13 +80,12 @@ function uniqueCombined(list1: string[], list2: string[], limit: number) { } async function getIndicesFromPattern( - dataClient: ILegacyScopedClusterClient, + esClient: ElasticsearchClient, pattern: string ): Promise { const params = { index: pattern, - ignore: [404], - ignoreUnavailable: true, + ignore_unavailable: true, body: { size: 0, // no hits aggs: { @@ -100,7 +98,7 @@ async function getIndicesFromPattern( }, }, }; - const response: SearchResponse = await dataClient.callAsCurrentUser('search', params); + const { body: response } = await esClient.search(params); // TODO: Investigate when the status field might appear here, type suggests it shouldn't ever happen // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((response as any).status === 404 || !response.aggregations) { @@ -111,17 +109,16 @@ async function getIndicesFromPattern( } async function getAliasesFromPattern( - dataClient: ILegacyScopedClusterClient, + esClient: ElasticsearchClient, pattern: string ): Promise { const params = { index: pattern, - ignoreUnavailable: true, - ignore: [404], + ignore_unavailable: true, }; const result: string[] = []; - const response = await dataClient.callAsCurrentUser('indices.getAlias', params); + const { body: response } = await esClient.indices.getAlias(params); if (response.status === 404) { return result; diff --git a/x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts index dc94d56623bba..da6638db2e457 100644 --- a/x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts @@ -44,7 +44,7 @@ export function createTimeSeriesQueryRoute( const result = await timeSeriesQuery({ logger, - callCluster: ctx.core.elasticsearch.legacy.client.callAsCurrentUser, + esClient: ctx.core.elasticsearch.client.asCurrentUser, query: req.body, }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts index 12db6eeff8fcf..29f2f0cca82bc 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts @@ -27,6 +27,7 @@ import { GetMonitorStatusResult } from '../requests/get_monitor_status'; import { makePing } from '../../../common/runtime_types/ping'; import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability'; import type { UptimeRouter } from '../../types'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; /** * The alert takes some dependencies as parameters; these are things like @@ -63,7 +64,8 @@ const mockOptions = ( services = alertsMock.createAlertServices(), state = {} ): any => { - services.scopedClusterClient = jest.fn() as any; + services.scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + services.scopedClusterClient.asCurrentUser = (jest.fn() as unknown) as any; services.savedObjectsClient.get.mockResolvedValue({ id: '', diff --git a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts index 97e8b0614a354..654f99cb0265a 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/uptime_alert_wrapper.ts @@ -58,7 +58,10 @@ export const uptimeAlertWrapper = ( options.services.savedObjectsClient ); - const uptimeEsClient = createUptimeESClient({ esClient, savedObjectsClient }); + const uptimeEsClient = createUptimeESClient({ + esClient: esClient.asCurrentUser, + savedObjectsClient, + }); return uptimeAlert.executor({ options, dynamicSettings, uptimeEsClient, savedObjectsClient }); }, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 1847b3fbbce2a..59efbaf24d9e0 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -132,7 +132,7 @@ async function alwaysFiringExecutor(alertExecutorOptions: any) { } } - await services.scopedClusterClient.index({ + await services.scopedClusterClient.asCurrentUser.index({ index: params.index, refresh: 'wait_for', body: { @@ -212,7 +212,7 @@ function getNeverFiringAlertType() { defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', async executor({ services, params, state }) { - await services.callCluster('index', { + await services.scopedClusterClient.asCurrentUser.index({ index: params.index, refresh: 'wait_for', body: { @@ -252,7 +252,7 @@ function getFailingAlertType() { defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', async executor({ services, params, state }) { - await services.callCluster('index', { + await services.scopedClusterClient.asCurrentUser.index({ index: params.index, refresh: 'wait_for', body: { @@ -269,7 +269,6 @@ function getFailingAlertType() { } function getAuthorizationAlertType(core: CoreSetup) { - const clusterClient = core.elasticsearch.legacy.client; const paramsSchema = schema.object({ callClusterAuthorizationIndex: schema.string(), savedObjectsClientType: schema.string(), @@ -298,7 +297,7 @@ function getAuthorizationAlertType(core: CoreSetup) { let callClusterSuccess = false; let callClusterError; try { - await services.callCluster('index', { + await services.scopedClusterClient.asCurrentUser.index({ index: params.callClusterAuthorizationIndex, refresh: 'wait_for', body: { @@ -310,11 +309,11 @@ function getAuthorizationAlertType(core: CoreSetup) { callClusterError = e; } // Call scoped cluster - const scopedClusterClient = services.getLegacyScopedClusterClient(clusterClient); + const scopedClusterClient = services.scopedClusterClient; let callScopedClusterSuccess = false; let callScopedClusterError; try { - await scopedClusterClient.callAsCurrentUser('index', { + await scopedClusterClient.asCurrentUser.index({ index: params.callClusterAuthorizationIndex, refresh: 'wait_for', body: { @@ -338,7 +337,7 @@ function getAuthorizationAlertType(core: CoreSetup) { savedObjectsClientError = e; } // Save the result - await services.callCluster('index', { + await services.scopedClusterClient.asCurrentUser.index({ index: params.index, refresh: 'wait_for', body: { @@ -417,7 +416,7 @@ function getPatternFiringAlertType() { } if (params.reference) { - await services.scopedClusterClient.index({ + await services.scopedClusterClient.asCurrentUser.index({ index: ES_TEST_INDEX_NAME, refresh: 'wait_for', body: { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 05060a2fcf7a9..536c4cbbd710f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -533,11 +533,9 @@ instanceStateValue: true savedObjectsClientSuccess: false, callClusterError: { ...searchResult.hits.hits[0]._source.state.callClusterError, - statusCode: 403, }, callScopedClusterError: { ...searchResult.hits.hits[0]._source.state.callScopedClusterError, - statusCode: 403, }, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, From 2374af30117a258ceb64d7d772ee67486535e457 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 16 Mar 2021 12:32:02 -0700 Subject: [PATCH 31/44] Remove `string` as a valid registry var type value (#94174) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/fleet/common/types/models/epm.ts | 2 +- .../services/validate_package_policy.ts | 12 ++---------- .../server/services/epm/agent/agent.test.ts | 16 ++++++++-------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index b0649fb56d9b7..5ea997d217888 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -274,6 +274,7 @@ export interface RegistryElasticsearch { 'index_template.mappings'?: object; } +export type RegistryVarType = 'integer' | 'bool' | 'password' | 'text' | 'yaml' | 'string'; export enum RegistryVarsEntryKeys { name = 'name', title = 'title', @@ -286,7 +287,6 @@ export enum RegistryVarsEntryKeys { os = 'os', } -export type RegistryVarType = 'integer' | 'bool' | 'password' | 'text' | 'yaml' | 'string'; // EPR types this as `[]map[string]interface{}` // which means the official/possible type is Record // but we effectively only see this shape diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts index 1874a458d8541..e36a4b46039f4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts @@ -229,11 +229,7 @@ export const validatePackagePolicyConfig = ( }) ); } - if ( - (varDef.type === 'text' || varDef.type === 'string') && - parsedValue && - Array.isArray(parsedValue) - ) { + if (varDef.type === 'text' && parsedValue && Array.isArray(parsedValue)) { const invalidStrings = parsedValue.filter((cand) => /^[*&]/.test(cand)); // only show one error if multiple strings in array are invalid if (invalidStrings.length > 0) { @@ -247,11 +243,7 @@ export const validatePackagePolicyConfig = ( } } - if ( - (varDef.type === 'text' || varDef.type === 'string') && - parsedValue && - !Array.isArray(parsedValue) - ) { + if (varDef.type === 'text' && parsedValue && !Array.isArray(parsedValue)) { if (/^[*&]/.test(parsedValue)) { errors.push( i18n.translate('xpack.fleet.packagePolicyValidation.quoteStringErrorMessage', { diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts index a76a8b9672d21..dde9f1733dfe3 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.test.ts @@ -193,14 +193,14 @@ my-package: {{{ search }}} | streamstats`; const vars = { - asteriskOnly: { value: '"*"', type: 'string' }, - startsWithAsterisk: { value: '"*lala"', type: 'string' }, - numeric: { value: '100', type: 'string' }, - mixed: { value: '1s', type: 'string' }, - a: { value: '/opt/package/*', type: 'string' }, - b: { value: '/logs/my.log*', type: 'string' }, - c: { value: '/opt/*/package/', type: 'string' }, - d: { value: 'logs/*my.log', type: 'string' }, + asteriskOnly: { value: '"*"', type: 'text' }, + startsWithAsterisk: { value: '"*lala"', type: 'text' }, + numeric: { value: '100', type: 'text' }, + mixed: { value: '1s', type: 'text' }, + a: { value: '/opt/package/*', type: 'text' }, + b: { value: '/logs/my.log*', type: 'text' }, + c: { value: '/opt/*/package/', type: 'text' }, + d: { value: 'logs/*my.log', type: 'text' }, search: { value: 'search sourcetype="access*"', type: 'text' }, }; From 4914acc0d1c9b1e9f3d55b09ee2cd93f6d45810e Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 16 Mar 2021 19:54:56 +0000 Subject: [PATCH 32/44] [ML] Use indices options in anomaly detection job wizards (#91830) * [ML] WIP Datafeed preview refactor * adding indices options to ad job creation searches * update datafeed preview schema * updating types * recalculating wizard time range on JSON edit save * updating endpoint docs * fixing types * more type fixes * fixing missing runtime fields * using isPopulatedObject * adding indices options schema * fixing test * fixing schema Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../types/anomaly_detection_jobs/datafeed.ts | 2 +- x-pack/plugins/ml/common/types/job_service.ts | 19 ++- .../chart_loader.ts | 3 +- .../common/chart_loader/chart_loader.ts | 25 ++- .../new_job/common/chart_loader/searches.ts | 5 +- .../job_creator/advanced_job_creator.ts | 17 +- .../categorization_examples_loader.ts | 3 +- .../common/results_loader/results_loader.ts | 3 +- .../json_editor_flyout/json_editor_flyout.tsx | 18 +- .../estimate_bucket_span.ts | 4 +- .../metric_selection_summary.tsx | 3 +- .../multi_metric_view/metric_selection.tsx | 9 +- .../metric_selection_summary.tsx | 6 +- .../population_view/metric_selection.tsx | 9 +- .../metric_selection_summary.tsx | 9 +- .../single_metric_view/metric_selection.tsx | 3 +- .../metric_selection_summary.tsx | 3 +- .../components/time_range_step/time_range.tsx | 3 +- .../jobs/new_job/pages/new_job/page.tsx | 21 +-- .../application/services/job_service.js | 161 +----------------- .../services/ml_api_service/index.ts | 22 +-- .../services/ml_api_service/jobs.ts | 25 ++- .../results_service/results_service.d.ts | 4 +- .../results_service/results_service.js | 11 +- .../bucket_span_estimator.d.ts | 16 +- .../bucket_span_estimator.js | 32 +++- .../bucket_span_estimator.test.ts | 4 +- .../polled_data_checker.js | 4 +- .../single_series_checker.js | 14 +- .../models/fields_service/fields_service.ts | 8 +- .../ml/server/models/job_service/datafeeds.ts | 157 ++++++++++++++++- .../ml/server/models/job_service/index.ts | 4 +- .../ml/server/models/job_service/jobs.ts | 1 + .../models/job_service/model_snapshots.ts | 5 +- .../new_job/categorization/examples.ts | 11 +- .../models/job_service/new_job/line_chart.ts | 11 +- .../job_service/new_job/population_chart.ts | 11 +- x-pack/plugins/ml/server/routes/apidoc.json | 1 + .../apidoc_scripts/schema_extractor.test.ts | 2 +- .../ml/server/routes/fields_service.ts | 4 +- .../plugins/ml/server/routes/job_service.ts | 60 ++++++- .../server/routes/schemas/datafeeds_schema.ts | 26 ++- .../routes/schemas/fields_service_schema.ts | 3 + .../routes/schemas/job_service_schema.ts | 14 ++ .../routes/schemas/job_validation_schema.ts | 3 +- 45 files changed, 490 insertions(+), 289 deletions(-) diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts index 06938485649fb..ed9c9e7589749 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts @@ -45,7 +45,7 @@ export type Aggregation = Record< } >; -interface IndicesOptions { +export interface IndicesOptions { expand_wildcards?: 'all' | 'open' | 'closed' | 'hidden' | 'none'; ignore_unavailable?: boolean; allow_no_indices?: boolean; diff --git a/x-pack/plugins/ml/common/types/job_service.ts b/x-pack/plugins/ml/common/types/job_service.ts index aae0b9c9b209f..5209743f87b3c 100644 --- a/x-pack/plugins/ml/common/types/job_service.ts +++ b/x-pack/plugins/ml/common/types/job_service.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { Job, JobStats } from './anomaly_detection_jobs'; +import { Job, JobStats, IndicesOptions } from './anomaly_detection_jobs'; +import { RuntimeMappings } from './fields'; +import { ES_AGGREGATION } from '../constants/aggregation_types'; export interface MlJobsResponse { jobs: Job[]; @@ -23,3 +25,18 @@ export interface JobsExistResponse { isGroup: boolean; }; } + +export interface BucketSpanEstimatorData { + aggTypes: Array; + duration: { + start: number; + end: number; + }; + fields: Array; + index: string; + query: any; + splitField: string | undefined; + timeField: string | undefined; + runtimeMappings: RuntimeMappings | undefined; + indicesOptions: IndicesOptions | undefined; +} diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts index 020f0d015eafe..a3753c8f000ae 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/chart_loader.ts @@ -29,7 +29,8 @@ export function chartLoaderProvider(mlResultsService: MlResultsService) { job.data_description.time_field, job.data_counts.earliest_record_timestamp, job.data_counts.latest_record_timestamp, - intervalMs + intervalMs, + job.datafeed_config.indices_options ); if (resp.error !== undefined) { throw resp.error; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts index a36e52f4e863b..ddd2aa3619472 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts @@ -8,6 +8,7 @@ import memoizeOne from 'memoize-one'; import { isEqual } from 'lodash'; import { IndexPatternTitle } from '../../../../../../common/types/kibana'; +import { IndicesOptions } from '../../../../../../common/types/anomaly_detection_jobs'; import { Field, SplitField, @@ -56,7 +57,8 @@ export class ChartLoader { splitField: SplitField, splitFieldValue: SplitFieldValue, intervalMs: number, - runtimeMappings: RuntimeMappings | null + runtimeMappings: RuntimeMappings | null, + indicesOptions?: IndicesOptions ): Promise { if (this._timeFieldName !== '') { if (aggFieldPairsCanBeCharted(aggFieldPairs) === false) { @@ -77,7 +79,8 @@ export class ChartLoader { aggFieldPairNames, splitFieldName, splitFieldValue, - runtimeMappings ?? undefined + runtimeMappings ?? undefined, + indicesOptions ); return resp.results; @@ -91,7 +94,8 @@ export class ChartLoader { aggFieldPairs: AggFieldPair[], splitField: SplitField, intervalMs: number, - runtimeMappings: RuntimeMappings | null + runtimeMappings: RuntimeMappings | null, + indicesOptions?: IndicesOptions ): Promise { if (this._timeFieldName !== '') { if (aggFieldPairsCanBeCharted(aggFieldPairs) === false) { @@ -111,7 +115,8 @@ export class ChartLoader { this._query, aggFieldPairNames, splitFieldName, - runtimeMappings ?? undefined + runtimeMappings ?? undefined, + indicesOptions ); return resp.results; @@ -122,7 +127,8 @@ export class ChartLoader { async loadEventRateChart( start: number, end: number, - intervalMs: number + intervalMs: number, + indicesOptions?: IndicesOptions ): Promise { if (this._timeFieldName !== '') { const resp = await getEventRateData( @@ -131,7 +137,8 @@ export class ChartLoader { this._timeFieldName, start, end, - intervalMs * 3 + intervalMs * 3, + indicesOptions ); if (resp.error !== undefined) { throw resp.error; @@ -147,14 +154,16 @@ export class ChartLoader { async loadFieldExampleValues( field: Field, - runtimeMappings: RuntimeMappings | null + runtimeMappings: RuntimeMappings | null, + indicesOptions?: IndicesOptions ): Promise { const { results } = await getCategoryFields( this._indexPatternTitle, field.name, 10, this._query, - runtimeMappings ?? undefined + runtimeMappings ?? undefined, + indicesOptions ); return results; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts index 54917c4884f22..499e1639f1fde 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/searches.ts @@ -9,6 +9,7 @@ import { get } from 'lodash'; import { ml } from '../../../../services/ml_api_service'; import { RuntimeMappings } from '../../../../../../common/types/fields'; +import { IndicesOptions } from '../../../../../../common/types/anomaly_detection_jobs'; interface CategoryResults { success: boolean; @@ -20,7 +21,8 @@ export function getCategoryFields( fieldName: string, size: number, query: any, - runtimeMappings?: RuntimeMappings + runtimeMappings?: RuntimeMappings, + indicesOptions?: IndicesOptions ): Promise { return new Promise((resolve, reject) => { ml.esSearch({ @@ -38,6 +40,7 @@ export function getCategoryFields( }, ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, + ...(indicesOptions ?? {}), }) .then((resp: any) => { const catFields = get(resp, ['aggregations', 'catFields', 'buckets'], []); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index d03d67e058bfa..da5cfc53b7950 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -177,16 +177,13 @@ export class AdvancedJobCreator extends JobCreator { // load the start and end times for the selected index // and apply them to the job creator public async autoSetTimeRange() { - try { - const { start, end } = await ml.getTimeFieldRange({ - index: this._indexPatternTitle, - timeFieldName: this.timeFieldName, - query: this.query, - }); - this.setTimeRange(start.epoch, end.epoch); - } catch (error) { - throw Error(error); - } + const { start, end } = await ml.getTimeFieldRange({ + index: this._indexPatternTitle, + timeFieldName: this.timeFieldName, + query: this.query, + indicesOptions: this.datafeedConfig.indices_options, + }); + this.setTimeRange(start.epoch, end.epoch); } public cloneFromExistingJob(job: Job, datafeed: Datafeed) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts index 06d489ee5a437..641eda3dbf3e8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/categorization_examples_loader.ts @@ -51,7 +51,8 @@ export class CategorizationExamplesLoader { this._jobCreator.start, this._jobCreator.end, analyzer, - this._jobCreator.runtimeMappings ?? undefined + this._jobCreator.runtimeMappings ?? undefined, + this._jobCreator.datafeedConfig.indices_options ); return resp; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index c4365bd656f9e..a01581f7526c5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -256,7 +256,8 @@ export class ResultsLoader { if (this._jobCreator.splitField !== null) { const fieldValues = await this._chartLoader.loadFieldExampleValues( this._jobCreator.splitField, - this._jobCreator.runtimeMappings + this._jobCreator.runtimeMappings, + this._jobCreator.datafeedConfig.indices_options ); if (fieldValues.length > 0) { this._detectorSplitFieldFilters = { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index 39b03ac546081..b2e4a447e4c31 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -25,7 +25,9 @@ import { CombinedJob, Datafeed } from '../../../../../../../../common/types/anom import { ML_EDITOR_MODE, MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { isValidJson } from '../../../../../../../../common/util/validation_utils'; import { JobCreatorContext } from '../../job_creator_context'; +import { isAdvancedJobCreator } from '../../../../common/job_creator'; import { DatafeedPreview } from '../datafeed_preview_flyout'; +import { useToastNotificationService } from '../../../../../../services/toast_notification_service'; export enum EDITOR_MODE { HIDDEN, @@ -40,6 +42,7 @@ interface Props { } export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafeedEditorMode }) => { const { jobCreator, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext); + const { displayErrorToast } = useToastNotificationService(); const [showJsonFlyout, setShowJsonFlyout] = useState(false); const [showChangedIndicesWarning, setShowChangedIndicesWarning] = useState(false); @@ -120,10 +123,23 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee setSaveable(valid); } - function onSave() { + async function onSave() { const jobConfig = JSON.parse(jobConfigString); const datafeedConfig = JSON.parse(collapseLiteralStrings(datafeedConfigString)); jobCreator.cloneFromExistingJob(jobConfig, datafeedConfig); + if (isAdvancedJobCreator(jobCreator)) { + try { + await jobCreator.autoSetTimeRange(); + } catch (error) { + const title = i18n.translate( + 'xpack.ml.newJob.wizard.jsonFlyout.autoSetJobCreatorTimeRange.error', + { + defaultMessage: `Error retrieving beginning and end times of index`, + } + ); + displayErrorToast(error, title); + } + } jobCreatorUpdate(); setShowJsonFlyout(false); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index f0932b09af46b..85083146c1378 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -9,12 +9,13 @@ import { useContext, useState } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; import { EVENT_RATE_FIELD_ID } from '../../../../../../../../../common/types/fields'; +import { BucketSpanEstimatorData } from '../../../../../../../../../common/types/job_service'; import { isMultiMetricJobCreator, isPopulationJobCreator, isAdvancedJobCreator, } from '../../../../../common/job_creator'; -import { ml, BucketSpanEstimatorData } from '../../../../../../../services/ml_api_service'; +import { ml } from '../../../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../../../contexts/ml'; import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; @@ -41,6 +42,7 @@ export function useEstimateBucketSpan() { splitField: undefined, timeField: mlContext.currentIndexPattern.timeFieldName, runtimeMappings: jobCreator.runtimeMappings ?? undefined, + indicesOptions: jobCreator.datafeedConfig.indices_options, }; if ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx index f027372d01204..da9f306cf30e6 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection_summary.tsx @@ -54,7 +54,8 @@ export const CategorizationDetectorsSummary: FC = () => { const resp = await chartLoader.loadEventRateChart( jobCreator.start, jobCreator.end, - chartInterval.getInterval().asMilliseconds() + chartInterval.getInterval().asMilliseconds(), + jobCreator.datafeedConfig.indices_options ); setEventRateChartData(resp); } catch (error) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index 5bf4beacc1593..46eb4b88d0518 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -111,7 +111,11 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { useEffect(() => { if (splitField !== null) { chartLoader - .loadFieldExampleValues(splitField, jobCreator.runtimeMappings) + .loadFieldExampleValues( + splitField, + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options + ) .then(setFieldValues) .catch((error) => { getToastNotificationService().displayErrorToast(error); @@ -140,7 +144,8 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { jobCreator.splitField, fieldValues.length > 0 ? fieldValues[0] : null, cs.intervalMs, - jobCreator.runtimeMappings + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options ); setLineChartsData(resp); } catch (error) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx index 11f2f60e17d3d..a4c344d16482b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx @@ -43,7 +43,8 @@ export const MultiMetricDetectorsSummary: FC = () => { try { const tempFieldValues = await chartLoader.loadFieldExampleValues( jobCreator.splitField, - jobCreator.runtimeMappings + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options ); setFieldValues(tempFieldValues); } catch (error) { @@ -76,7 +77,8 @@ export const MultiMetricDetectorsSummary: FC = () => { jobCreator.splitField, fieldValues.length > 0 ? fieldValues[0] : null, cs.intervalMs, - jobCreator.runtimeMappings + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options ); setLineChartsData(resp); } catch (error) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index aba2acfa41a85..a7eaaff611183 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -160,7 +160,8 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { aggFieldPairList, jobCreator.splitField, cs.intervalMs, - jobCreator.runtimeMappings + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options ); setLineChartsData(resp); @@ -180,7 +181,11 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { (async (index: number, field: Field) => { return { index, - fields: await chartLoader.loadFieldExampleValues(field, jobCreator.runtimeMappings), + fields: await chartLoader.loadFieldExampleValues( + field, + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options + ), }; })(i, af.by.field) ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx index c615010891101..55a9d37d1115c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx @@ -78,7 +78,8 @@ export const PopulationDetectorsSummary: FC = () => { aggFieldPairList, jobCreator.splitField, cs.intervalMs, - jobCreator.runtimeMappings + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options ); setLineChartsData(resp); @@ -98,7 +99,11 @@ export const PopulationDetectorsSummary: FC = () => { (async (index: number, field: Field) => { return { index, - fields: await chartLoader.loadFieldExampleValues(field, jobCreator.runtimeMappings), + fields: await chartLoader.loadFieldExampleValues( + field, + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options + ), }; })(i, af.by.field) ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index f4a907dcc6a49..0e09a81908e83 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -93,7 +93,8 @@ export const SingleMetricDetectors: FC = ({ setIsValid }) => { null, null, cs.intervalMs, - jobCreator.runtimeMappings + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options ); if (resp[DTR_IDX] !== undefined) { setLineChartData(resp); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx index 4d8fc5ef76084..ced94b2095f72 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx @@ -59,7 +59,8 @@ export const SingleMetricDetectorsSummary: FC = () => { null, null, cs.intervalMs, - jobCreator.runtimeMappings + jobCreator.runtimeMappings, + jobCreator.datafeedConfig.indices_options ); if (resp[DTR_IDX] !== undefined) { setLineChartData(resp); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx index bed2d36524e36..d2cf6b7a00471 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx @@ -47,7 +47,8 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) const resp = await chartLoader.loadEventRateChart( jobCreator.start, jobCreator.end, - chartInterval.getInterval().asMilliseconds() + chartInterval.getInterval().asMilliseconds(), + jobCreator.datafeedConfig.indices_options ); setEventRateChartData(resp); } catch (error) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index c8dbb90804444..3a01ce8c70fc8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -20,7 +20,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Wizard } from './wizard'; import { WIZARD_STEPS } from '../components/step_types'; import { getJobCreatorTitle } from '../../common/job_creator/util/general'; -import { useMlKibana } from '../../../../contexts/kibana'; import { jobCreatorFactory, isAdvancedJobCreator, @@ -41,6 +40,7 @@ import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_se import { newJobCapsService } from '../../../../services/new_job_capabilities_service'; import { EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields'; import { getNewJobDefaults } from '../../../../services/ml_server_info'; +import { useToastNotificationService } from '../../../../services/toast_notification_service'; const PAGE_WIDTH = 1200; // document.querySelector('.single-metric-job-container').width(); const BAR_TARGET = PAGE_WIDTH > 2000 ? 1000 : PAGE_WIDTH / 2; @@ -52,15 +52,13 @@ export interface PageProps { } export const Page: FC = ({ existingJobsAndGroups, jobType }) => { - const { - services: { notifications }, - } = useMlKibana(); const mlContext = useMlContext(); const jobCreator = jobCreatorFactory(jobType)( mlContext.currentIndexPattern, mlContext.currentSavedSearch, mlContext.combinedQuery ); + const { displayErrorToast } = useToastNotificationService(); const { from, to } = getTimeFilterRange(); jobCreator.setTimeRange(from, to); @@ -154,17 +152,12 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { if (autoSetTimeRange && isAdvancedJobCreator(jobCreator)) { // for advanced jobs, load the full time range start and end times // so they can be used for job validation and bucket span estimation - try { - jobCreator.autoSetTimeRange(); - } catch (error) { - const { toasts } = notifications; - toasts.addDanger({ - title: i18n.translate('xpack.ml.newJob.wizard.autoSetJobCreatorTimeRange.error', { - defaultMessage: `Error retrieving beginning and end times of index`, - }), - text: error, + jobCreator.autoSetTimeRange().catch((error) => { + const title = i18n.translate('xpack.ml.newJob.wizard.autoSetJobCreatorTimeRange.error', { + defaultMessage: `Error retrieving beginning and end times of index`, }); - } + displayErrorToast(error, title); + }); } function initCategorizationSettings() { diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 33ccf81798353..2fa60b8db83a7 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -13,7 +13,6 @@ import { ml } from './ml_api_service'; import { getToastNotificationService } from '../services/toast_notification_service'; import { isWebUrl } from '../util/url_utils'; -import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; import { TIME_FORMAT } from '../../../common/constants/time_format'; import { parseInterval } from '../../../common/util/parse_interval'; import { validateTimeRange } from '../../../common/util/date_utils'; @@ -348,163 +347,9 @@ class JobService { return job; } - searchPreview(job) { - return new Promise((resolve, reject) => { - if (job.datafeed_config) { - // if query is set, add it to the search, otherwise use match_all - let query = { match_all: {} }; - if (job.datafeed_config.query) { - query = job.datafeed_config.query; - } - - // Get bucket span - // Get first doc time for datafeed - // Create a new query - must user query and must range query. - // Time range 'to' first doc time plus < 10 buckets - - // Do a preliminary search to get the date of the earliest doc matching the - // query in the datafeed. This will be used to apply a time range criteria - // on the datafeed search preview. - // This time filter is required for datafeed searches using aggregations to ensure - // the search does not create too many buckets (default 10000 max_bucket limit), - // but apply it to searches without aggregations too for consistency. - ml.getTimeFieldRange({ - index: job.datafeed_config.indices, - timeFieldName: job.data_description.time_field, - query, - }) - .then((timeRange) => { - const bucketSpan = parseInterval(job.analysis_config.bucket_span); - const earliestMs = timeRange.start.epoch; - const latestMs = +timeRange.start.epoch + 10 * bucketSpan.asMilliseconds(); - - const body = { - query: { - bool: { - must: [ - { - range: { - [job.data_description.time_field]: { - gte: earliestMs, - lt: latestMs, - format: 'epoch_millis', - }, - }, - }, - query, - ], - }, - }, - }; - - // if aggs or aggregations is set, add it to the search - const aggregations = job.datafeed_config.aggs || job.datafeed_config.aggregations; - if (aggregations && Object.keys(aggregations).length) { - body.size = 0; - body.aggregations = aggregations; - - // add script_fields if present - const scriptFields = job.datafeed_config.script_fields; - if (scriptFields && Object.keys(scriptFields).length) { - body.script_fields = scriptFields; - } - - // add runtime_mappings if present - const runtimeMappings = job.datafeed_config.runtime_mappings; - if (runtimeMappings && Object.keys(runtimeMappings).length) { - body.runtime_mappings = runtimeMappings; - } - } else { - // if aggregations is not set and retrieveWholeSource is not set, add all of the fields from the job - body.size = ML_DATA_PREVIEW_COUNT; - - // add script_fields if present - const scriptFields = job.datafeed_config.script_fields; - if (scriptFields && Object.keys(scriptFields).length) { - body.script_fields = scriptFields; - } - - // add runtime_mappings if present - const runtimeMappings = job.datafeed_config.runtime_mappings; - if (runtimeMappings && Object.keys(runtimeMappings).length) { - body.runtime_mappings = runtimeMappings; - } - - const fields = {}; - - // get fields from detectors - if (job.analysis_config.detectors) { - each(job.analysis_config.detectors, (dtr) => { - if (dtr.by_field_name) { - fields[dtr.by_field_name] = {}; - } - if (dtr.field_name) { - fields[dtr.field_name] = {}; - } - if (dtr.over_field_name) { - fields[dtr.over_field_name] = {}; - } - if (dtr.partition_field_name) { - fields[dtr.partition_field_name] = {}; - } - }); - } - - // get fields from influencers - if (job.analysis_config.influencers) { - each(job.analysis_config.influencers, (inf) => { - fields[inf] = {}; - }); - } - - // get fields from categorizationFieldName - if (job.analysis_config.categorization_field_name) { - fields[job.analysis_config.categorization_field_name] = {}; - } - - // get fields from summary_count_field_name - if (job.analysis_config.summary_count_field_name) { - fields[job.analysis_config.summary_count_field_name] = {}; - } - - // get fields from time_field - if (job.data_description.time_field) { - fields[job.data_description.time_field] = {}; - } - - // add runtime fields - if (runtimeMappings) { - Object.keys(runtimeMappings).forEach((fieldName) => { - fields[fieldName] = {}; - }); - } - - const fieldsList = Object.keys(fields); - if (fieldsList.length) { - body.fields = fieldsList; - body._source = false; - } - } - - const data = { - index: job.datafeed_config.indices, - body, - ...(job.datafeed_config.indices_options || {}), - }; - - ml.esSearch(data) - .then((resp) => { - resolve(resp); - }) - .catch((resp) => { - reject(resp); - }); - }) - .catch((resp) => { - reject(resp); - }); - } - }); + searchPreview(combinedJob) { + const { datafeed_config: datafeed, ...job } = combinedJob; + return ml.jobs.datafeedPreview(job, datafeed); } openJob(jobId) { diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 8d0ecddaa97b8..e6d0d93cade1f 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -24,7 +24,7 @@ import { import { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; -import { RuntimeMappings } from '../../../../common/types/fields'; +import { BucketSpanEstimatorData } from '../../../../common/types/job_service'; import { Job, JobStats, @@ -33,8 +33,8 @@ import { Detector, AnalysisConfig, ModelSnapshot, + IndicesOptions, } from '../../../../common/types/anomaly_detection_jobs'; -import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { FieldHistogramRequestConfig, FieldRequestConfig, @@ -53,20 +53,6 @@ export interface MlInfoResponse { cloudId?: string; } -export interface BucketSpanEstimatorData { - aggTypes: Array; - duration: { - start: number; - end: number; - }; - fields: Array; - index: string; - query: any; - splitField: string | undefined; - timeField: string | undefined; - runtimeMappings: RuntimeMappings | undefined; -} - export interface BucketSpanEstimatorResponse { name: string; ms: number; @@ -704,12 +690,14 @@ export function mlApiServicesProvider(httpService: HttpService) { index, timeFieldName, query, + indicesOptions, }: { index: string; timeFieldName?: string; query: any; + indicesOptions?: IndicesOptions; }) { - const body = JSON.stringify({ index, timeFieldName, query }); + const body = JSON.stringify({ index, timeFieldName, query, indicesOptions }); return httpService.http({ path: `${basePath()}/fields_service/time_field_range`, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index df72bd25c6bcd..811e9cab365d7 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -15,6 +15,7 @@ import type { CombinedJobWithStats, Job, Datafeed, + IndicesOptions, } from '../../../../common/types/anomaly_detection_jobs'; import type { JobMessage } from '../../../../common/types/audit_message'; import type { AggFieldNamePair, RuntimeMappings } from '../../../../common/types/fields'; @@ -189,7 +190,8 @@ export const jobsApiProvider = (httpService: HttpService) => ({ aggFieldNamePairs: AggFieldNamePair[], splitFieldName: string | null, splitFieldValue: string | null, - runtimeMappings?: RuntimeMappings + runtimeMappings?: RuntimeMappings, + indicesOptions?: IndicesOptions ) { const body = JSON.stringify({ indexPatternTitle, @@ -202,6 +204,7 @@ export const jobsApiProvider = (httpService: HttpService) => ({ splitFieldName, splitFieldValue, runtimeMappings, + indicesOptions, }); return httpService.http({ path: `${ML_BASE_PATH}/jobs/new_job_line_chart`, @@ -219,7 +222,8 @@ export const jobsApiProvider = (httpService: HttpService) => ({ query: any, aggFieldNamePairs: AggFieldNamePair[], splitFieldName: string, - runtimeMappings?: RuntimeMappings + runtimeMappings?: RuntimeMappings, + indicesOptions?: IndicesOptions ) { const body = JSON.stringify({ indexPatternTitle, @@ -231,6 +235,7 @@ export const jobsApiProvider = (httpService: HttpService) => ({ aggFieldNamePairs, splitFieldName, runtimeMappings, + indicesOptions, }); return httpService.http({ path: `${ML_BASE_PATH}/jobs/new_job_population_chart`, @@ -268,7 +273,8 @@ export const jobsApiProvider = (httpService: HttpService) => ({ start: number, end: number, analyzer: CategorizationAnalyzer, - runtimeMappings?: RuntimeMappings + runtimeMappings?: RuntimeMappings, + indicesOptions?: IndicesOptions ) { const body = JSON.stringify({ indexPatternTitle, @@ -280,6 +286,7 @@ export const jobsApiProvider = (httpService: HttpService) => ({ end, analyzer, runtimeMappings, + indicesOptions, }); return httpService.http<{ examples: CategoryFieldExample[]; @@ -322,4 +329,16 @@ export const jobsApiProvider = (httpService: HttpService) => ({ body, }); }, + + datafeedPreview(job: Job, datafeed: Datafeed) { + const body = JSON.stringify({ job, datafeed }); + return httpService.http<{ + total: number; + categories: Array<{ count?: number; category: Category }>; + }>({ + path: `${ML_BASE_PATH}/jobs/datafeed_preview`, + method: 'POST', + body, + }); + }, }); diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts index 90ab302458354..f9a2c1389c828 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IndicesOptions } from '../../../../common/types/anomaly_detection_jobs'; import { MlApiServices } from '../ml_api_service'; export function resultsServiceProvider( @@ -58,7 +59,8 @@ export function resultsServiceProvider( timeFieldName: string, earliestMs: number, latestMs: number, - intervalMs: number + intervalMs: number, + indicesOptions?: IndicesOptions ): Promise; getEventDistributionData( index: string, diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index 3390e62030dd6..502692da39c96 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -1052,7 +1052,15 @@ export function resultsServiceProvider(mlApiServices) { // Extra query object can be supplied, or pass null if no additional query. // Returned response contains a results property, which is an object // of document counts against time (epoch millis). - getEventRateData(index, query, timeFieldName, earliestMs, latestMs, intervalMs) { + getEventRateData( + index, + query, + timeFieldName, + earliestMs, + latestMs, + intervalMs, + indicesOptions + ) { return new Promise((resolve, reject) => { const obj = { success: true, results: {} }; @@ -1102,6 +1110,7 @@ export function resultsServiceProvider(mlApiServices) { }, }, }, + ...(indicesOptions ?? {}), }) .then((resp) => { const dataByTimeBucket = get(resp, ['aggregations', 'eventRate', 'buckets'], []); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts index 40a6bd1decd97..75983975f7acd 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts @@ -8,20 +8,8 @@ import { IScopedClusterClient } from 'kibana/server'; import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; import { RuntimeMappings } from '../../../common/types/fields'; - -export interface BucketSpanEstimatorData { - aggTypes: Array; - duration: { - start: number; - end: number; - }; - fields: Array; - index: string; - query: any; - splitField: string | undefined; - timeField: string | undefined; - runtimeMappings: RuntimeMappings | undefined; -} +import { IndicesOptions } from '../../../common/types/anomaly_detection_jobs'; +import { BucketSpanEstimatorData } from '../../../common/types/job_service'; export function estimateBucketSpanFactory({ asCurrentUser, diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js index 79f48645d52f2..29961918e7aba 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js @@ -20,7 +20,17 @@ export function estimateBucketSpanFactory(client) { class BucketSpanEstimator { constructor( - { index, timeField, aggTypes, fields, duration, query, splitField, runtimeMappings }, + { + index, + timeField, + aggTypes, + fields, + duration, + query, + splitField, + runtimeMappings, + indicesOptions, + }, splitFieldValues, maxBuckets ) { @@ -72,7 +82,8 @@ export function estimateBucketSpanFactory(client) { this.index, this.timeField, this.duration, - this.query + this.query, + indicesOptions ); if (this.aggTypes.length === this.fields.length) { @@ -89,7 +100,8 @@ export function estimateBucketSpanFactory(client) { this.duration, this.query, this.thresholds, - this.runtimeMappings + this.runtimeMappings, + indicesOptions ), result: null, }); @@ -112,7 +124,8 @@ export function estimateBucketSpanFactory(client) { this.duration, queryCopy, this.thresholds, - this.runtimeMappings + this.runtimeMappings, + indicesOptions ), result: null, }); @@ -246,7 +259,7 @@ export function estimateBucketSpanFactory(client) { } } - const getFieldCardinality = function (index, field, runtimeMappings) { + const getFieldCardinality = function (index, field, runtimeMappings, indicesOptions) { return new Promise((resolve, reject) => { asCurrentUser .search({ @@ -262,6 +275,7 @@ export function estimateBucketSpanFactory(client) { }, ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, + ...(indicesOptions ?? {}), }) .then(({ body }) => { const value = get(body, ['aggregations', 'field_count', 'value'], 0); @@ -273,13 +287,13 @@ export function estimateBucketSpanFactory(client) { }); }; - const getRandomFieldValues = function (index, field, query, runtimeMappings) { + const getRandomFieldValues = function (index, field, query, runtimeMappings, indicesOptions) { let fieldValues = []; return new Promise((resolve, reject) => { const NUM_PARTITIONS = 10; // use a partitioned search to load 10 random fields // load ten fields, to test that there are at least 10. - getFieldCardinality(index, field) + getFieldCardinality(index, field, runtimeMappings, indicesOptions) .then((value) => { const numPartitions = Math.floor(value / NUM_PARTITIONS) || 1; asCurrentUser @@ -301,6 +315,7 @@ export function estimateBucketSpanFactory(client) { }, ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, + ...(indicesOptions ?? {}), }) .then(({ body }) => { // eslint-disable-next-line camelcase @@ -390,7 +405,8 @@ export function estimateBucketSpanFactory(client) { formConfig.index, formConfig.splitField, formConfig.query, - formConfig.runtimeMappings + formConfig.runtimeMappings, + formConfig.indicesOptions ) .then((splitFieldValues) => { runEstimator(splitFieldValues); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts index aa576d1f69915..de5514ed1e18f 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts @@ -8,8 +8,9 @@ import { IScopedClusterClient } from 'kibana/server'; import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; +import { BucketSpanEstimatorData } from '../../../common/types/job_service'; -import { estimateBucketSpanFactory, BucketSpanEstimatorData } from './bucket_span_estimator'; +import { estimateBucketSpanFactory } from './bucket_span_estimator'; const callAs = { search: () => Promise.resolve({ body: {} }), @@ -36,6 +37,7 @@ const formConfig: BucketSpanEstimatorData = { splitField: undefined, timeField: undefined, runtimeMappings: undefined, + indicesOptions: undefined, }; describe('ML - BucketSpanEstimator', () => { diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js index 59da334a18393..8a40787f44490 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js @@ -15,11 +15,12 @@ import { get } from 'lodash'; export function polledDataCheckerFactory({ asCurrentUser }) { class PolledDataChecker { - constructor(index, timeField, duration, query) { + constructor(index, timeField, duration, query, indicesOptions) { this.index = index; this.timeField = timeField; this.duration = duration; this.query = query; + this.indicesOptions = indicesOptions; this.isPolled = false; this.minimumBucketSpan = 0; @@ -73,6 +74,7 @@ export function polledDataCheckerFactory({ asCurrentUser }) { index: this.index, size: 0, body: searchBody, + ...(this.indicesOptions ?? {}), }); return body; } diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js index 25c87c5c2acbf..f9f01070f2f82 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js @@ -18,7 +18,17 @@ export function singleSeriesCheckerFactory({ asCurrentUser }) { const REF_DATA_INTERVAL = { name: '1h', ms: 3600000 }; class SingleSeriesChecker { - constructor(index, timeField, aggType, field, duration, query, thresholds, runtimeMappings) { + constructor( + index, + timeField, + aggType, + field, + duration, + query, + thresholds, + runtimeMappings, + indicesOptions + ) { this.index = index; this.timeField = timeField; this.aggType = aggType; @@ -32,6 +42,7 @@ export function singleSeriesCheckerFactory({ asCurrentUser }) { created: false, }; this.runtimeMappings = runtimeMappings; + this.indicesOptions = indicesOptions; this.interval = null; } @@ -193,6 +204,7 @@ export function singleSeriesCheckerFactory({ asCurrentUser }) { index: this.index, size: 0, body: searchBody, + ...(this.indicesOptions ?? {}), }); return body; } diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index 56eddf9df2e04..1270cc6f08e23 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -13,7 +13,7 @@ import { initCardinalityFieldsCache } from './fields_aggs_cache'; import { AggCardinality } from '../../../common/types/fields'; import { isValidAggregationField } from '../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; -import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; +import { Datafeed, IndicesOptions } from '../../../common/types/anomaly_detection_jobs'; /** * Service for carrying out queries to obtain data @@ -183,6 +183,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { } = await asCurrentUser.search({ index, body, + ...(datafeedConfig?.indices_options ?? {}), }); if (!aggregations) { @@ -210,7 +211,8 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { async function getTimeFieldRange( index: string[] | string, timeFieldName: string, - query: any + query: any, + indicesOptions?: IndicesOptions ): Promise<{ success: boolean; start: { epoch: number; string: string }; @@ -238,6 +240,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { }, }, }, + ...(indicesOptions ?? {}), }); if (aggregations && aggregations.earliest && aggregations.latest) { @@ -394,6 +397,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { } = await asCurrentUser.search({ index, body, + ...(datafeedConfig?.indices_options ?? {}), }); if (!aggregations) { diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index 88c4659198727..cb651a0a410af 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -6,10 +6,15 @@ */ import { i18n } from '@kbn/i18n'; +import { IScopedClusterClient } from 'kibana/server'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; -import { Datafeed, DatafeedStats } from '../../../common/types/anomaly_detection_jobs'; +import { Datafeed, DatafeedStats, Job } from '../../../common/types/anomaly_detection_jobs'; +import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; +import { fieldsServiceProvider } from '../fields_service'; import type { MlClient } from '../../lib/ml_client'; +import { parseInterval } from '../../../common/util/parse_interval'; +import { isPopulatedObject } from '../../../common/util/object_utils'; export interface MlDatafeedsResponse { datafeeds: Datafeed[]; @@ -27,7 +32,7 @@ interface Results { }; } -export function datafeedsProvider(mlClient: MlClient) { +export function datafeedsProvider(client: IScopedClusterClient, mlClient: MlClient) { async function forceStartDatafeeds(datafeedIds: string[], start?: number, end?: number) { const jobIds = await getJobIdsByDatafeedId(); const doStartsCalled = datafeedIds.reduce((acc, cur) => { @@ -204,6 +209,153 @@ export function datafeedsProvider(mlClient: MlClient) { } } + async function datafeedPreview(job: Job, datafeed: Datafeed) { + let query: any = { match_all: {} }; + if (datafeed.query) { + query = datafeed.query; + } + const { getTimeFieldRange } = fieldsServiceProvider(client); + const { start } = await getTimeFieldRange( + datafeed.indices, + job.data_description.time_field, + query, + datafeed.indices_options + ); + + // Get bucket span + // Get first doc time for datafeed + // Create a new query - must user query and must range query. + // Time range 'to' first doc time plus < 10 buckets + + // Do a preliminary search to get the date of the earliest doc matching the + // query in the datafeed. This will be used to apply a time range criteria + // on the datafeed search preview. + // This time filter is required for datafeed searches using aggregations to ensure + // the search does not create too many buckets (default 10000 max_bucket limit), + // but apply it to searches without aggregations too for consistency. + const bucketSpan = parseInterval(job.analysis_config.bucket_span); + if (bucketSpan === null) { + return; + } + const earliestMs = start.epoch; + const latestMs = +start.epoch + 10 * bucketSpan.asMilliseconds(); + + const body: any = { + query: { + bool: { + must: [ + { + range: { + [job.data_description.time_field]: { + gte: earliestMs, + lt: latestMs, + format: 'epoch_millis', + }, + }, + }, + query, + ], + }, + }, + }; + + // if aggs or aggregations is set, add it to the search + const aggregations = datafeed.aggs ?? datafeed.aggregations; + if (isPopulatedObject(aggregations)) { + body.size = 0; + body.aggregations = aggregations; + + // add script_fields if present + const scriptFields = datafeed.script_fields; + if (isPopulatedObject(scriptFields)) { + body.script_fields = scriptFields; + } + + // add runtime_mappings if present + const runtimeMappings = datafeed.runtime_mappings; + if (isPopulatedObject(runtimeMappings)) { + body.runtime_mappings = runtimeMappings; + } + } else { + // if aggregations is not set and retrieveWholeSource is not set, add all of the fields from the job + body.size = ML_DATA_PREVIEW_COUNT; + + // add script_fields if present + const scriptFields = datafeed.script_fields; + if (isPopulatedObject(scriptFields)) { + body.script_fields = scriptFields; + } + + // add runtime_mappings if present + const runtimeMappings = datafeed.runtime_mappings; + if (isPopulatedObject(runtimeMappings)) { + body.runtime_mappings = runtimeMappings; + } + + const fields = new Set(); + + // get fields from detectors + if (job.analysis_config.detectors) { + job.analysis_config.detectors.forEach((dtr) => { + if (dtr.by_field_name) { + fields.add(dtr.by_field_name); + } + if (dtr.field_name) { + fields.add(dtr.field_name); + } + if (dtr.over_field_name) { + fields.add(dtr.over_field_name); + } + if (dtr.partition_field_name) { + fields.add(dtr.partition_field_name); + } + }); + } + + // get fields from influencers + if (job.analysis_config.influencers) { + job.analysis_config.influencers.forEach((inf) => { + fields.add(inf); + }); + } + + // get fields from categorizationFieldName + if (job.analysis_config.categorization_field_name) { + fields.add(job.analysis_config.categorization_field_name); + } + + // get fields from summary_count_field_name + if (job.analysis_config.summary_count_field_name) { + fields.add(job.analysis_config.summary_count_field_name); + } + + // get fields from time_field + if (job.data_description.time_field) { + fields.add(job.data_description.time_field); + } + + // add runtime fields + if (runtimeMappings) { + Object.keys(runtimeMappings).forEach((fieldName) => { + fields.add(fieldName); + }); + } + + const fieldsList = [...fields]; + if (fieldsList.length) { + body.fields = fieldsList; + body._source = false; + } + } + const data = { + index: datafeed.indices, + body, + ...(datafeed.indices_options ?? {}), + }; + + return (await client.asCurrentUser.search(data)).body; + } + return { forceStartDatafeeds, stopDatafeeds, @@ -211,5 +363,6 @@ export function datafeedsProvider(mlClient: MlClient) { getDatafeedIdsByJobId, getJobIdsByDatafeedId, getDatafeedByJobId, + datafeedPreview, }; } diff --git a/x-pack/plugins/ml/server/models/job_service/index.ts b/x-pack/plugins/ml/server/models/job_service/index.ts index e88ff8cb751aa..d36ec822c1314 100644 --- a/x-pack/plugins/ml/server/models/job_service/index.ts +++ b/x-pack/plugins/ml/server/models/job_service/index.ts @@ -16,12 +16,12 @@ import type { MlClient } from '../../lib/ml_client'; export function jobServiceProvider(client: IScopedClusterClient, mlClient: MlClient) { return { - ...datafeedsProvider(mlClient), + ...datafeedsProvider(client, mlClient), ...jobsProvider(client, mlClient), ...groupsProvider(mlClient), ...newJobCapsProvider(client), ...newJobChartsProvider(client), ...topCategoriesProvider(mlClient), - ...modelSnapshotProvider(mlClient), + ...modelSnapshotProvider(client, mlClient), }; } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index dc2c04540ef21..ac3e00a918da8 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -52,6 +52,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { const { asInternalUser } = client; const { forceDeleteDatafeed, getDatafeedIdsByJobId, getDatafeedByJobId } = datafeedsProvider( + client, mlClient ); const { getAuditMessagesSummary } = jobAuditMessagesProvider(client, mlClient); diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index 9f4607414c10a..f1f5d98b96a53 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -7,6 +7,7 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; +import { IScopedClusterClient } from 'kibana/server'; import { ModelSnapshot } from '../../../common/types/anomaly_detection_jobs'; import { datafeedsProvider } from './datafeeds'; import { FormCalendar, CalendarManager } from '../calendar'; @@ -20,8 +21,8 @@ export interface RevertModelSnapshotResponse { model: ModelSnapshot; } -export function modelSnapshotProvider(mlClient: MlClient) { - const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(mlClient); +export function modelSnapshotProvider(client: IScopedClusterClient, mlClient: MlClient) { + const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(client, mlClient); async function revertModelSnapshot( jobId: string, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts index 63df425791e85..37fa675362773 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -15,6 +15,7 @@ import { CategoryFieldExample, } from '../../../../../common/types/categories'; import { RuntimeMappings } from '../../../../../common/types/fields'; +import { IndicesOptions } from '../../../../../common/types/anomaly_detection_jobs'; import { ValidationResults } from './validation_results'; const CHUNK_SIZE = 100; @@ -34,7 +35,8 @@ export function categorizationExamplesProvider({ start: number, end: number, analyzer: CategorizationAnalyzer, - runtimeMappings: RuntimeMappings | undefined + runtimeMappings: RuntimeMappings | undefined, + indicesOptions: IndicesOptions | undefined ): Promise<{ examples: CategoryFieldExample[]; error?: any }> { if (timeField !== undefined) { const range = { @@ -69,6 +71,7 @@ export function categorizationExamplesProvider({ sort: ['_doc'], ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, + ...(indicesOptions ?? {}), }); // hit.fields can be undefined if value is originally null @@ -169,7 +172,8 @@ export function categorizationExamplesProvider({ start: number, end: number, analyzer: CategorizationAnalyzer, - runtimeMappings: RuntimeMappings | undefined + runtimeMappings: RuntimeMappings | undefined, + indicesOptions: IndicesOptions | undefined ) { const resp = await categorizationExamples( indexPatternTitle, @@ -180,7 +184,8 @@ export function categorizationExamplesProvider({ start, end, analyzer, - runtimeMappings + runtimeMappings, + indicesOptions ); const { examples } = resp; diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts index c83485211b455..e6a2432e28dc1 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -12,6 +12,7 @@ import { EVENT_RATE_FIELD_ID, RuntimeMappings, } from '../../../../common/types/fields'; +import { IndicesOptions } from '../../../../common/types/anomaly_detection_jobs'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; type DtrIndex = number; @@ -39,7 +40,8 @@ export function newJobLineChartProvider({ asCurrentUser }: IScopedClusterClient) aggFieldNamePairs: AggFieldNamePair[], splitFieldName: string | null, splitFieldValue: string | null, - runtimeMappings: RuntimeMappings | undefined + runtimeMappings: RuntimeMappings | undefined, + indicesOptions: IndicesOptions | undefined ) { const json: object = getSearchJsonFromConfig( indexPatternTitle, @@ -51,7 +53,8 @@ export function newJobLineChartProvider({ asCurrentUser }: IScopedClusterClient) aggFieldNamePairs, splitFieldName, splitFieldValue, - runtimeMappings + runtimeMappings, + indicesOptions ); const { body } = await asCurrentUser.search(json); @@ -110,7 +113,8 @@ function getSearchJsonFromConfig( aggFieldNamePairs: AggFieldNamePair[], splitFieldName: string | null, splitFieldValue: string | null, - runtimeMappings: RuntimeMappings | undefined + runtimeMappings: RuntimeMappings | undefined, + indicesOptions: IndicesOptions | undefined ): object { const json = { index: indexPatternTitle, @@ -134,6 +138,7 @@ function getSearchJsonFromConfig( }, ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, + ...(indicesOptions ?? {}), }; if (query.bool === undefined) { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts index 10f6d94e764ac..2385ffef67191 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -12,6 +12,7 @@ import { EVENT_RATE_FIELD_ID, RuntimeMappings, } from '../../../../common/types/fields'; +import { IndicesOptions } from '../../../../common/types/anomaly_detection_jobs'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; const OVER_FIELD_EXAMPLES_COUNT = 40; @@ -44,7 +45,8 @@ export function newJobPopulationChartProvider({ asCurrentUser }: IScopedClusterC query: object, aggFieldNamePairs: AggFieldNamePair[], splitFieldName: string | null, - runtimeMappings: RuntimeMappings | undefined + runtimeMappings: RuntimeMappings | undefined, + indicesOptions: IndicesOptions | undefined ) { const json: object = getPopulationSearchJsonFromConfig( indexPatternTitle, @@ -55,7 +57,8 @@ export function newJobPopulationChartProvider({ asCurrentUser }: IScopedClusterC query, aggFieldNamePairs, splitFieldName, - runtimeMappings + runtimeMappings, + indicesOptions ); const { body } = await asCurrentUser.search(json); @@ -138,7 +141,8 @@ function getPopulationSearchJsonFromConfig( query: any, aggFieldNamePairs: AggFieldNamePair[], splitFieldName: string | null, - runtimeMappings: RuntimeMappings | undefined + runtimeMappings: RuntimeMappings | undefined, + indicesOptions: IndicesOptions | undefined ): object { const json = { index: indexPatternTitle, @@ -162,6 +166,7 @@ function getPopulationSearchJsonFromConfig( }, ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, + ...(indicesOptions ?? {}), }; if (query.bool === undefined) { diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 33ae5b4f96829..1a10046380658 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -84,6 +84,7 @@ "GetLookBackProgress", "ValidateCategoryExamples", "TopCategories", + "DatafeedPreview", "UpdateGroups", "DeletingJobTasks", "DeleteJobs", diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.test.ts b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.test.ts index b7a706a9bbd0c..6c15e6c707a5a 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.test.ts +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/schema_extractor.test.ts @@ -133,7 +133,7 @@ describe('schema_extractor', () => { { name: 'expand_wildcards', documentation: '', - type: 'string[]', + type: '"all" | "open" | "closed" | "hidden" | "none"[]', }, { name: 'ignore_unavailable', diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 6e68a80354491..c087b86172fa9 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -22,8 +22,8 @@ function getCardinalityOfFields(client: IScopedClusterClient, payload: any) { function getTimeFieldRange(client: IScopedClusterClient, payload: any) { const fs = fieldsServiceProvider(client); - const { index, timeFieldName, query } = payload; - return fs.getTimeFieldRange(index, timeFieldName, query); + const { index, timeFieldName, query, indicesOptions } = payload; + return fs.getTimeFieldRange(index, timeFieldName, query, indicesOptions); } /** diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 1e028dfb20b4d..b3aa9f956895a 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -20,12 +20,14 @@ import { updateGroupsSchema, revertModelSnapshotSchema, jobsExistSchema, + datafeedPreviewSchema, } from './schemas/job_service_schema'; import { jobIdSchema } from './schemas/anomaly_detectors_schema'; import { jobServiceProvider } from '../models/job_service'; import { categorizationExamplesProvider } from '../models/job_service/new_job'; +import { getAuthorizationHeader } from '../lib/request_authorization'; /** * Routes for job service @@ -535,6 +537,7 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { splitFieldName, splitFieldValue, runtimeMappings, + indicesOptions, } = request.body; const { newJobLineChart } = jobServiceProvider(client, mlClient); @@ -548,7 +551,8 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { aggFieldNamePairs, splitFieldName, splitFieldValue, - runtimeMappings + runtimeMappings, + indicesOptions ); return response.ok({ @@ -591,6 +595,7 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { aggFieldNamePairs, splitFieldName, runtimeMappings, + indicesOptions, } = request.body; const { newJobPopulationChart } = jobServiceProvider(client, mlClient); @@ -603,7 +608,8 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { query, aggFieldNamePairs, splitFieldName, - runtimeMappings + runtimeMappings, + indicesOptions ); return response.ok({ @@ -710,6 +716,7 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { end, analyzer, runtimeMappings, + indicesOptions, } = request.body; const resp = await validateCategoryExamples( @@ -721,7 +728,8 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { start, end, analyzer, - runtimeMappings + runtimeMappings, + indicesOptions ); return response.ok({ @@ -767,6 +775,52 @@ export function jobServiceRoutes({ router, routeGuard }: RouteInitialization) { }) ); + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/datafeed_preview Get datafeed preview + * @apiName DatafeedPreview + * @apiDescription Returns a preview of the datafeed search + * + * @apiSchema (body) datafeedPreviewSchema + */ + router.post( + { + path: '/api/ml/jobs/datafeed_preview', + validate: { + body: datafeedPreviewSchema, + }, + options: { + tags: ['access:ml:canGetJobs'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { + try { + const { datafeedId, job, datafeed } = request.body; + + if (datafeedId !== undefined) { + const { body } = await mlClient.previewDatafeed( + { + datafeed_id: datafeedId, + }, + getAuthorizationHeader(request) + ); + return response.ok({ + body, + }); + } + + const { datafeedPreview } = jobServiceProvider(client, mlClient); + const body = await datafeedPreview(job, datafeed); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup JobService * diff --git a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts index d8d0cd659c2e6..e860be59e4eaf 100644 --- a/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/datafeeds_schema.ts @@ -13,6 +13,23 @@ export const startDatafeedSchema = schema.object({ timeout: schema.maybe(schema.any()), }); +export const indicesOptionsSchema = schema.object({ + expand_wildcards: schema.maybe( + schema.arrayOf( + schema.oneOf([ + schema.literal('all'), + schema.literal('open'), + schema.literal('closed'), + schema.literal('hidden'), + schema.literal('none'), + ]) + ) + ), + ignore_unavailable: schema.maybe(schema.boolean()), + allow_no_indices: schema.maybe(schema.boolean()), + ignore_throttled: schema.maybe(schema.boolean()), +}); + export const datafeedConfigSchema = schema.object({ datafeed_id: schema.maybe(schema.string()), feed_id: schema.maybe(schema.string()), @@ -35,14 +52,7 @@ export const datafeedConfigSchema = schema.object({ runtime_mappings: schema.maybe(schema.any()), scroll_size: schema.maybe(schema.number()), delayed_data_check_config: schema.maybe(schema.any()), - indices_options: schema.maybe( - schema.object({ - expand_wildcards: schema.maybe(schema.arrayOf(schema.string())), - ignore_unavailable: schema.maybe(schema.boolean()), - allow_no_indices: schema.maybe(schema.boolean()), - ignore_throttled: schema.maybe(schema.boolean()), - }) - ), + indices_options: indicesOptionsSchema, }); export const datafeedIdSchema = schema.object({ datafeedId: schema.string() }); diff --git a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts index 462fca17bda85..db827b26fe73a 100644 --- a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { indicesOptionsSchema } from './datafeeds_schema'; export const getCardinalityOfFieldsSchema = schema.object({ /** Index or indexes for which to return the time range. */ @@ -29,4 +30,6 @@ export const getTimeFieldRangeSchema = schema.object({ timeFieldName: schema.maybe(schema.string()), /** Query to match documents in the index(es). */ query: schema.maybe(schema.any()), + /** Additional search options. */ + indicesOptions: indicesOptionsSchema, }); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index 65955fbc47a37..8e160094c68eb 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -6,6 +6,8 @@ */ import { schema } from '@kbn/config-schema'; +import { anomalyDetectionJobSchema } from './anomaly_detectors_schema'; +import { datafeedConfigSchema, indicesOptionsSchema } from './datafeeds_schema'; export const categorizationFieldExamplesSchema = { indexPatternTitle: schema.string(), @@ -17,6 +19,7 @@ export const categorizationFieldExamplesSchema = { end: schema.number(), analyzer: schema.any(), runtimeMappings: schema.maybe(schema.any()), + indicesOptions: indicesOptionsSchema, }; export const chartSchema = { @@ -30,6 +33,7 @@ export const chartSchema = { splitFieldName: schema.maybe(schema.nullable(schema.string())), splitFieldValue: schema.maybe(schema.nullable(schema.string())), runtimeMappings: schema.maybe(schema.any()), + indicesOptions: indicesOptionsSchema, }; export const datafeedIdsSchema = schema.object({ @@ -92,6 +96,16 @@ export const revertModelSnapshotSchema = schema.object({ ), }); +export const datafeedPreviewSchema = schema.oneOf([ + schema.object({ + job: schema.maybe(schema.object(anomalyDetectionJobSchema)), + datafeed: schema.maybe(datafeedConfigSchema), + }), + schema.object({ + datafeedId: schema.maybe(schema.string()), + }), +]); + export const jobsExistSchema = schema.object({ jobIds: schema.arrayOf(schema.string()), allSpaces: schema.maybe(schema.boolean()), diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts index 8c054d54e0589..ad2bafdfb5dd1 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { analysisConfigSchema, anomalyDetectionJobSchema } from './anomaly_detectors_schema'; -import { datafeedConfigSchema } from './datafeeds_schema'; +import { datafeedConfigSchema, indicesOptionsSchema } from './datafeeds_schema'; export const estimateBucketSpanSchema = schema.object({ aggTypes: schema.arrayOf(schema.nullable(schema.string())), @@ -19,6 +19,7 @@ export const estimateBucketSpanSchema = schema.object({ splitField: schema.maybe(schema.string()), timeField: schema.maybe(schema.string()), runtimeMappings: schema.maybe(schema.any()), + indicesOptions: indicesOptionsSchema, }); export const modelMemoryLimitSchema = schema.object({ From 25baa3b5584a433b52b6f9b85cf93861d5739c52 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 16 Mar 2021 12:59:46 -0700 Subject: [PATCH 33/44] [Connectors UI] Make UI use new connector APIs (#94180) * [Connectors UI] Make UI use new connector APIs * fixed tests * fixed due to comment * fixed due to comment * fixed test * fixed test * fixed test * moved rewrite_request_case to the common folder * fixed due to comments --- x-pack/plugins/actions/common/index.ts | 2 + .../routes => common}/rewrite_request_case.ts | 0 .../actions/server/routes/connector_types.ts | 3 +- .../plugins/actions/server/routes/create.ts | 3 +- .../plugins/actions/server/routes/execute.ts | 3 +- x-pack/plugins/actions/server/routes/get.ts | 3 +- .../plugins/actions/server/routes/get_all.ts | 3 +- .../plugins/actions/server/routes/update.ts | 3 +- .../builtin_action_types/jira/api.test.ts | 8 +- .../builtin_action_types/jira/api.ts | 8 +- .../resilient/api.test.ts | 4 +- .../builtin_action_types/resilient/api.ts | 4 +- .../servicenow/api.test.ts | 2 +- .../builtin_action_types/servicenow/api.ts | 2 +- .../lib/action_connector_api.test.ts | 161 ------------------ .../application/lib/action_connector_api.ts | 83 --------- .../connector_types.test.ts | 49 ++++++ .../action_connector_api/connector_types.ts | 32 ++++ .../action_connector_api/connectors.test.ts | 27 +++ .../lib/action_connector_api/connectors.ts | 37 ++++ .../lib/action_connector_api/create.test.ts | 48 ++++++ .../lib/action_connector_api/create.ts | 43 +++++ .../lib/action_connector_api/delete.test.ts | 35 ++++ .../lib/action_connector_api/delete.ts | 28 +++ .../lib/action_connector_api/execute.test.ts | 42 +++++ .../lib/action_connector_api/execute.ts | 38 +++++ .../lib/action_connector_api/index.ts | 13 ++ .../lib/action_connector_api/update.test.ts | 49 ++++++ .../lib/action_connector_api/update.ts | 42 +++++ .../triggers_actions_ui/public/types.ts | 2 +- .../apps/triggers_actions_ui/alerts_list.ts | 2 +- .../apps/triggers_actions_ui/connectors.ts | 2 +- .../apps/triggers_actions_ui/details.ts | 4 +- .../apps/triggers_actions_ui/home_page.ts | 2 +- .../lib/get_test_data.ts | 2 +- 35 files changed, 512 insertions(+), 277 deletions(-) rename x-pack/plugins/actions/{server/routes => common}/rewrite_request_case.ts (100%) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/delete.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/delete.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/execute.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/execute.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts diff --git a/x-pack/plugins/actions/common/index.ts b/x-pack/plugins/actions/common/index.ts index f0e1439bce3e3..184ae9c226b8f 100644 --- a/x-pack/plugins/actions/common/index.ts +++ b/x-pack/plugins/actions/common/index.ts @@ -8,3 +8,5 @@ export * from './types'; export const BASE_ACTION_API_PATH = '/api/actions'; + +export * from './rewrite_request_case'; diff --git a/x-pack/plugins/actions/server/routes/rewrite_request_case.ts b/x-pack/plugins/actions/common/rewrite_request_case.ts similarity index 100% rename from x-pack/plugins/actions/server/routes/rewrite_request_case.ts rename to x-pack/plugins/actions/common/rewrite_request_case.ts diff --git a/x-pack/plugins/actions/server/routes/connector_types.ts b/x-pack/plugins/actions/server/routes/connector_types.ts index d686ddbdaee70..9f9ad5b2aea68 100644 --- a/x-pack/plugins/actions/server/routes/connector_types.ts +++ b/x-pack/plugins/actions/server/routes/connector_types.ts @@ -7,10 +7,9 @@ import { IRouter } from 'kibana/server'; import { ILicenseState } from '../lib'; -import { ActionType, BASE_ACTION_API_PATH } from '../../common'; +import { ActionType, BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common'; import { ActionsRequestHandlerContext } from '../types'; import { verifyAccessAndContext } from './verify_access_and_context'; -import { RewriteResponseCase } from './rewrite_request_case'; const rewriteBodyRes: RewriteResponseCase = (results) => { return results.map(({ enabledInConfig, enabledInLicense, minimumLicenseRequired, ...res }) => ({ diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index e1717891231db..c05f2180bd62b 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -9,9 +9,8 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; import { ActionResult, ActionsRequestHandlerContext } from '../types'; import { ILicenseState } from '../lib'; -import { BASE_ACTION_API_PATH } from '../../common'; +import { BASE_ACTION_API_PATH, RewriteRequestCase, RewriteResponseCase } from '../../common'; import { verifyAccessAndContext } from './verify_access_and_context'; -import { RewriteRequestCase, RewriteResponseCase } from './rewrite_request_case'; import { CreateOptions } from '../actions_client'; export const bodySchema = schema.object({ diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 0d1bee83ed047..377fe1215b3fb 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -10,10 +10,9 @@ import { IRouter } from 'kibana/server'; import { ILicenseState } from '../lib'; import { ActionTypeExecutorResult, ActionsRequestHandlerContext } from '../types'; -import { BASE_ACTION_API_PATH } from '../../common'; +import { BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common'; import { asHttpRequestExecutionSource } from '../lib/action_execution_source'; import { verifyAccessAndContext } from './verify_access_and_context'; -import { RewriteResponseCase } from './rewrite_request_case'; const paramSchema = schema.object({ id: schema.string(), diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index 63f89d6b3ca49..59766fc133ba6 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -8,10 +8,9 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; import { ILicenseState } from '../lib'; -import { BASE_ACTION_API_PATH } from '../../common'; +import { BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common'; import { ActionResult, ActionsRequestHandlerContext } from '../types'; import { verifyAccessAndContext } from './verify_access_and_context'; -import { RewriteResponseCase } from './rewrite_request_case'; const paramSchema = schema.object({ id: schema.string(), diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts index 32f48e32ab278..831722fd36eed 100644 --- a/x-pack/plugins/actions/server/routes/get_all.ts +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -7,10 +7,9 @@ import { IRouter } from 'kibana/server'; import { ILicenseState } from '../lib'; -import { BASE_ACTION_API_PATH } from '../../common'; +import { BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common'; import { ActionsRequestHandlerContext, FindActionResult } from '../types'; import { verifyAccessAndContext } from './verify_access_and_context'; -import { RewriteResponseCase } from './rewrite_request_case'; const rewriteBodyRes: RewriteResponseCase = (results) => { return results.map(({ actionTypeId, isPreconfigured, referencedByCount, ...res }) => ({ diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index af55fa32b76ca..d1758717e80f9 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -8,10 +8,9 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; import { ILicenseState } from '../lib'; -import { BASE_ACTION_API_PATH } from '../../common'; +import { BASE_ACTION_API_PATH, RewriteResponseCase } from '../../common'; import { ActionResult, ActionsRequestHandlerContext } from '../types'; import { verifyAccessAndContext } from './verify_access_and_context'; -import { RewriteResponseCase } from './rewrite_request_case'; const paramSchema = schema.object({ id: schema.string(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.test.ts index 3ad3c6ce02372..679bc3d53c40d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.test.ts @@ -102,7 +102,7 @@ describe('Jira API', () => { const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'test' }); expect(res).toEqual(issueTypesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}', signal: abortCtrl.signal, }); @@ -121,7 +121,7 @@ describe('Jira API', () => { }); expect(res).toEqual(fieldsResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}', signal: abortCtrl.signal, }); @@ -140,7 +140,7 @@ describe('Jira API', () => { }); expect(res).toEqual(issuesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}', signal: abortCtrl.signal, }); @@ -159,7 +159,7 @@ describe('Jira API', () => { }); expect(res).toEqual(issuesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts index 0184a1f0ca2e5..46ea9dea3aa56 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts @@ -17,7 +17,7 @@ export async function getIssueTypes({ signal: AbortSignal; connectorId: string; }): Promise> { - return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'issueTypes', subActionParams: {} }, }), @@ -36,7 +36,7 @@ export async function getFieldsByIssueType({ connectorId: string; id: string; }): Promise> { - return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'fieldsByIssueType', subActionParams: { id } }, }), @@ -55,7 +55,7 @@ export async function getIssues({ connectorId: string; title: string; }): Promise> { - return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'issues', subActionParams: { title } }, }), @@ -74,7 +74,7 @@ export async function getIssue({ connectorId: string; id: string; }): Promise> { - return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'issue', subActionParams: { id } }, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/api.test.ts index 68edff4dc3950..01208f93405d2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/api.test.ts @@ -61,7 +61,7 @@ describe('Resilient API', () => { }); expect(res).toEqual(incidentTypesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"incidentTypes","subActionParams":{}}}', signal: abortCtrl.signal, }); @@ -79,7 +79,7 @@ describe('Resilient API', () => { }); expect(res).toEqual(severityResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"severity","subActionParams":{}}}', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/api.ts index 46077fcaf6890..8ea3c3c63e50f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/api.ts @@ -17,7 +17,7 @@ export async function getIncidentTypes({ signal: AbortSignal; connectorId: string; }): Promise> { - return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'incidentTypes', subActionParams: {} }, }), @@ -34,7 +34,7 @@ export async function getSeverity({ signal: AbortSignal; connectorId: string; }): Promise> { - return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'severity', subActionParams: {} }, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.test.ts index 9d9b3c5e64909..5c814bbfd6450 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.test.ts @@ -61,7 +61,7 @@ describe('ServiceNow API', () => { }); expect(res).toEqual(choicesResponse); - expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}', signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts index 3e92515e49b49..bb90915591285 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts @@ -19,7 +19,7 @@ export async function getChoices({ connectorId: string; fields: string[]; }): Promise> { - return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'getChoices', subActionParams: { fields } }, }), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts deleted file mode 100644 index bf70e4c5f2408..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts +++ /dev/null @@ -1,161 +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 { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types'; -import { httpServiceMock } from '../../../../../../src/core/public/mocks'; -import { - createActionConnector, - deleteActions, - loadActionTypes, - loadAllActions, - updateActionConnector, - executeAction, -} from './action_connector_api'; - -const http = httpServiceMock.createStartContract(); - -beforeEach(() => jest.resetAllMocks()); - -describe('loadActionTypes', () => { - test('should call get types API', async () => { - const resolvedValue: ActionType[] = [ - { - id: 'test', - name: 'Test', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - ]; - http.get.mockResolvedValueOnce(resolvedValue); - - const result = await loadActionTypes({ http }); - expect(result).toEqual(resolvedValue); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/actions/list_action_types", - ] - `); - }); -}); - -describe('loadAllActions', () => { - test('should call getAll actions API', async () => { - http.get.mockResolvedValueOnce([]); - - const result = await loadAllActions({ http }); - expect(result).toEqual([]); - expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/actions", - ] - `); - }); -}); - -describe('createActionConnector', () => { - test('should call create action API', async () => { - const connector: ActionConnectorWithoutId<{}, {}> = { - actionTypeId: 'test', - isPreconfigured: false, - name: 'My test', - config: {}, - secrets: {}, - }; - const resolvedValue: ActionConnector = { ...connector, id: '123' }; - http.post.mockResolvedValueOnce(resolvedValue); - - const result = await createActionConnector({ http, connector }); - expect(result).toEqual(resolvedValue); - expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/actions/action", - Object { - "body": "{\\"actionTypeId\\":\\"test\\",\\"isPreconfigured\\":false,\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}", - }, - ] - `); - }); -}); - -describe('updateActionConnector', () => { - test('should call the update API', async () => { - const id = '123'; - const connector: ActionConnectorWithoutId<{}, {}> = { - actionTypeId: 'test', - isPreconfigured: false, - name: 'My test', - config: {}, - secrets: {}, - }; - const resolvedValue = { ...connector, id }; - http.put.mockResolvedValueOnce(resolvedValue); - - const result = await updateActionConnector({ http, connector, id }); - expect(result).toEqual(resolvedValue); - expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/actions/action/123", - Object { - "body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}", - }, - ] - `); - }); -}); - -describe('deleteActions', () => { - test('should call delete API per action', async () => { - const ids = ['1', '2', '3']; - - const result = await deleteActions({ ids, http }); - expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); - expect(http.delete.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "/api/actions/action/1", - ], - Array [ - "/api/actions/action/2", - ], - Array [ - "/api/actions/action/3", - ], - ] - `); - }); -}); - -describe('executeAction', () => { - test('should call execute API', async () => { - const id = '123'; - const params = { - stringParams: 'someString', - numericParams: 123, - }; - - http.post.mockResolvedValueOnce({ - actionId: id, - status: 'ok', - }); - - const result = await executeAction({ id, http, params }); - expect(result).toEqual({ - actionId: id, - status: 'ok', - }); - expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "/api/actions/action/123/_execute", - Object { - "body": "{\\"params\\":{\\"stringParams\\":\\"someString\\",\\"numericParams\\":123}}", - }, - ] - `); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts deleted file mode 100644 index 57fb079d97299..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts +++ /dev/null @@ -1,83 +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 { HttpSetup } from 'kibana/public'; -import { BASE_ACTION_API_PATH } from '../constants'; -import type { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types'; -import { ActionTypeExecutorResult } from '../../../../../plugins/actions/common'; - -export async function loadActionTypes({ http }: { http: HttpSetup }): Promise { - return await http.get(`${BASE_ACTION_API_PATH}/list_action_types`); -} - -export async function loadAllActions({ http }: { http: HttpSetup }): Promise { - return await http.get(`${BASE_ACTION_API_PATH}`); -} - -export async function createActionConnector({ - http, - connector, -}: { - http: HttpSetup; - connector: Omit; -}): Promise { - return await http.post(`${BASE_ACTION_API_PATH}/action`, { - body: JSON.stringify(connector), - }); -} - -export async function updateActionConnector({ - http, - connector, - id, -}: { - http: HttpSetup; - connector: Pick; - id: string; -}): Promise { - return await http.put(`${BASE_ACTION_API_PATH}/action/${id}`, { - body: JSON.stringify({ - name: connector.name, - config: connector.config, - secrets: connector.secrets, - }), - }); -} - -export async function deleteActions({ - ids, - http, -}: { - ids: string[]; - http: HttpSetup; -}): Promise<{ successes: string[]; errors: string[] }> { - const successes: string[] = []; - const errors: string[] = []; - await Promise.all(ids.map((id) => http.delete(`${BASE_ACTION_API_PATH}/action/${id}`))).then( - function (fulfilled) { - successes.push(...fulfilled); - }, - function (rejected) { - errors.push(...rejected); - } - ); - return { successes, errors }; -} - -export async function executeAction({ - id, - params, - http, -}: { - id: string; - http: HttpSetup; - params: Record; -}): Promise> { - return http.post(`${BASE_ACTION_API_PATH}/action/${id}/_execute`, { - body: JSON.stringify({ params }), - }); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.test.ts new file mode 100644 index 0000000000000..8815757df6af5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { ActionType } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadActionTypes } from './index'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('loadActionTypes', () => { + test('should call get types API', async () => { + const apiResponseValue = [ + { + id: 'test', + name: 'Test', + enabled: true, + enabled_in_config: true, + enabled_in_license: true, + minimum_license_required: 'basic', + }, + ]; + http.get.mockResolvedValueOnce(apiResponseValue); + + const resolvedValue: ActionType[] = [ + { + id: 'test', + name: 'Test', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]; + + const result = await loadActionTypes({ http }); + expect(result).toEqual(resolvedValue); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/actions/connector_types", + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.ts new file mode 100644 index 0000000000000..6f7e8b03658e0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connector_types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; + +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; +import { BASE_ACTION_API_PATH } from '../../constants'; +import type { ActionType } from '../../../types'; + +const rewriteResponseRes = (results: Array>): ActionType[] => { + return results.map((item) => rewriteBodyReq(item)); +}; + +const rewriteBodyReq: RewriteRequestCase = ({ + enabled_in_config: enabledInConfig, + enabled_in_license: enabledInLicense, + minimum_license_required: minimumLicenseRequired, + ...res +}: AsApiContract) => ({ + enabledInConfig, + enabledInLicense, + minimumLicenseRequired, + ...res, +}); + +export async function loadActionTypes({ http }: { http: HttpSetup }): Promise { + const res = await http.get(`${BASE_ACTION_API_PATH}/connector_types`); + return rewriteResponseRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.test.ts new file mode 100644 index 0000000000000..565cc0afebfea --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { loadAllActions } from './index'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('loadAllActions', () => { + test('should call getAll actions API', async () => { + http.get.mockResolvedValueOnce([]); + + const result = await loadAllActions({ http }); + expect(result).toEqual([]); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/actions/connectors", + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts new file mode 100644 index 0000000000000..cf424ea1e7317 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; +import { BASE_ACTION_API_PATH } from '../../constants'; +import type { ActionConnector, ActionConnectorProps } from '../../../types'; + +const rewriteResponseRes = ( + results: Array< + AsApiContract, Record>> + > +): Array, Record>> => { + return results.map((item) => transformConnector(item)); +}; + +const transformConnector: RewriteRequestCase< + ActionConnectorProps, Record> +> = ({ + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, + referenced_by_count: referencedByCount, + ...res +}) => ({ + actionTypeId, + isPreconfigured, + referencedByCount, + ...res, +}); + +export async function loadAllActions({ http }: { http: HttpSetup }): Promise { + const res = await http.get(`${BASE_ACTION_API_PATH}/connectors`); + return rewriteResponseRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts new file mode 100644 index 0000000000000..208970fbfc061 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionConnectorWithoutId } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { createActionConnector } from './index'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('createActionConnector', () => { + test('should call create action API', async () => { + const apiResponse = { + connector_type_id: 'test', + is_preconfigured: false, + name: 'My test', + config: {}, + secrets: {}, + id: '123', + }; + http.post.mockResolvedValueOnce(apiResponse); + + const connector: ActionConnectorWithoutId<{}, {}> = { + actionTypeId: 'test', + isPreconfigured: false, + name: 'My test', + config: {}, + secrets: {}, + }; + const resolvedValue = { ...connector, id: '123' }; + + const result = await createActionConnector({ http, connector }); + expect(result).toEqual(resolvedValue); + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/actions/connector", + Object { + "body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{},\\"connector_type_id\\":\\"test\\",\\"is_preconfigured\\":false}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts new file mode 100644 index 0000000000000..e6e74f3f3c059 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { RewriteRequestCase, RewriteResponseCase } from '../../../../../actions/common'; +import { BASE_ACTION_API_PATH } from '../../constants'; +import type { + ActionConnector, + ActionConnectorProps, + ActionConnectorWithoutId, +} from '../../../types'; + +const rewriteBodyRequest: RewriteResponseCase< + Omit +> = ({ actionTypeId, isPreconfigured, ...res }) => ({ + ...res, + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, +}); + +const rewriteBodyRes: RewriteRequestCase< + ActionConnectorProps, Record> +> = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, ...res }) => ({ + ...res, + actionTypeId, + isPreconfigured, +}); + +export async function createActionConnector({ + http, + connector, +}: { + http: HttpSetup; + connector: Omit; +}): Promise { + const res = await http.post(`${BASE_ACTION_API_PATH}/connector`, { + body: JSON.stringify(rewriteBodyRequest(connector)), + }); + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/delete.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/delete.test.ts new file mode 100644 index 0000000000000..bb00c8c30e4ed --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/delete.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { deleteActions } from './index'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('deleteActions', () => { + test('should call delete API per action', async () => { + const ids = ['1', '2', '3']; + + const result = await deleteActions({ ids, http }); + expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); + expect(http.delete.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/actions/connector/1", + ], + Array [ + "/api/actions/connector/2", + ], + Array [ + "/api/actions/connector/3", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/delete.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/delete.ts new file mode 100644 index 0000000000000..c9c25db676a06 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/delete.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { BASE_ACTION_API_PATH } from '../../constants'; + +export async function deleteActions({ + ids, + http, +}: { + ids: string[]; + http: HttpSetup; +}): Promise<{ successes: string[]; errors: string[] }> { + const successes: string[] = []; + const errors: string[] = []; + await Promise.all(ids.map((id) => http.delete(`${BASE_ACTION_API_PATH}/connector/${id}`))).then( + function (fulfilled) { + successes.push(...fulfilled); + }, + function (rejected) { + errors.push(...rejected); + } + ); + return { successes, errors }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/execute.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/execute.test.ts new file mode 100644 index 0000000000000..60cd3132aa756 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/execute.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { executeAction } from './index'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('executeAction', () => { + test('should call execute API', async () => { + const id = '123'; + const params = { + stringParams: 'someString', + numericParams: 123, + }; + + http.post.mockResolvedValueOnce({ + connector_id: id, + status: 'ok', + }); + + const result = await executeAction({ id, http, params }); + expect(result).toEqual({ + actionId: id, + status: 'ok', + }); + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/actions/connector/123/_execute", + Object { + "body": "{\\"params\\":{\\"stringParams\\":\\"someString\\",\\"numericParams\\":123}}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/execute.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/execute.ts new file mode 100644 index 0000000000000..638ceddb5652f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/execute.ts @@ -0,0 +1,38 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { + ActionTypeExecutorResult, + RewriteRequestCase, +} from '../../../../../../plugins/actions/common'; +import { BASE_ACTION_API_PATH } from '../../constants'; + +const rewriteBodyRes: RewriteRequestCase> = ({ + connector_id: actionId, + service_message: serviceMessage, + ...res +}) => ({ + ...res, + actionId, + serviceMessage, +}); + +export async function executeAction({ + id, + params, + http, +}: { + id: string; + http: HttpSetup; + params: Record; +}): Promise> { + const res = await http.post(`${BASE_ACTION_API_PATH}/connector/${id}/_execute`, { + body: JSON.stringify({ params }), + }); + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/index.ts new file mode 100644 index 0000000000000..7cc4f1df6a735 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { loadActionTypes } from './connector_types'; +export { loadAllActions } from './connectors'; +export { createActionConnector } from './create'; +export { deleteActions } from './delete'; +export { executeAction } from './execute'; +export { updateActionConnector } from './update'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.test.ts new file mode 100644 index 0000000000000..29e7a1e4bed3d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { ActionConnectorWithoutId } from '../../../types'; +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { updateActionConnector } from './index'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('updateActionConnector', () => { + test('should call the update API', async () => { + const id = '123'; + const apiResponse = { + connector_type_id: 'test', + is_preconfigured: false, + name: 'My test', + config: {}, + secrets: {}, + id, + }; + http.put.mockResolvedValueOnce(apiResponse); + + const connector: ActionConnectorWithoutId<{}, {}> = { + actionTypeId: 'test', + isPreconfigured: false, + name: 'My test', + config: {}, + secrets: {}, + }; + const resolvedValue = { ...connector, id }; + + const result = await updateActionConnector({ http, connector, id }); + expect(result).toEqual(resolvedValue); + expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/actions/connector/123", + Object { + "body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts new file mode 100644 index 0000000000000..18b8871ce25d1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { RewriteRequestCase } from '../../../../../actions/common'; +import { BASE_ACTION_API_PATH } from '../../constants'; +import type { + ActionConnector, + ActionConnectorProps, + ActionConnectorWithoutId, +} from '../../../types'; + +const rewriteBodyRes: RewriteRequestCase< + ActionConnectorProps, Record> +> = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, ...res }) => ({ + ...res, + actionTypeId, + isPreconfigured, +}); + +export async function updateActionConnector({ + http, + connector, + id, +}: { + http: HttpSetup; + connector: Pick; + id: string; +}): Promise { + const res = await http.put(`${BASE_ACTION_API_PATH}/connector/${id}`, { + body: JSON.stringify({ + name: connector.name, + config: connector.config, + secrets: connector.secrets, + }), + }); + + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 5c5dcf344b10b..cf2dda203bb2d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -121,7 +121,7 @@ export interface ConnectorValidationResult { secrets?: GenericValidationResult; } -interface ActionConnectorProps { +export interface ActionConnectorProps { secrets: Secrets; id: string; actionTypeId: string; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index e24d5a4ccf653..9287196a8bd78 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -42,7 +42,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function createAction(overwrites: Record = {}) { const { body: createdAction } = await supertest - .post(`/api/actions/action`) + .post(`/api/actions/connector`) .set('kbn-xsrf', 'foo') .send(getTestActionData(overwrites)) .expect(200); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index 7d43001bd0374..e40c821d98851 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -23,7 +23,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { const { body: createdAction } = await supertest - .post(`/api/actions/action`) + .post(`/api/actions/connector`) .set('kbn-xsrf', 'foo') .send(getTestActionData()) .expect(200); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index d27be915be512..5c4566121d478 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -27,7 +27,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function createActionManualCleanup(overwrites: Record = {}) { const { body: createdAction } = await supertest - .post(`/api/actions/action`) + .post(`/api/actions/connector`) .set('kbn-xsrf', 'foo') .send(getTestActionData(overwrites)) .expect(200); @@ -372,7 +372,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should show and update deleted connectors when there are no existing connectors of the same type', async () => { const action = await createActionManualCleanup({ name: `index-${testRunUuid}-${0}`, - actionTypeId: '.index', + connector_type_id: '.index', config: { index: `index-${testRunUuid}-${0}`, }, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 1b1288e4b4db8..4aeadf5f1ae8a 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -84,7 +84,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('navigates to an alert details page', async () => { const { body: createdAction } = await supertest - .post(`/api/actions/action`) + .post(`/api/actions/connector`) .set('kbn-xsrf', 'foo') .send(getTestActionData()) .expect(200); diff --git a/x-pack/test/functional_with_es_ssl/lib/get_test_data.ts b/x-pack/test/functional_with_es_ssl/lib/get_test_data.ts index a93c9987fd640..11ccd15571259 100644 --- a/x-pack/test/functional_with_es_ssl/lib/get_test_data.ts +++ b/x-pack/test/functional_with_es_ssl/lib/get_test_data.ts @@ -30,7 +30,7 @@ export function getTestAlertData(overwrites = {}) { export function getTestActionData(overwrites = {}) { return { name: `slack-${Date.now()}`, - actionTypeId: '.slack', + connector_type_id: '.slack', config: {}, secrets: { webhookUrl: 'https://test', From eb21f53df6b011d3a336c5b46992e0b0a45e9838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 16 Mar 2021 21:00:12 +0100 Subject: [PATCH 34/44] [chore] Remove the infra team from CODEOWNERS (#94740) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7995d5d5bba25..4f8d3fa4dc429 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -232,8 +232,8 @@ /x-pack/plugins/telemetry_collection_xpack/ @elastic/kibana-core /.telemetryrc.json @elastic/kibana-core /x-pack/.telemetryrc.json @elastic/kibana-core -src/plugins/telemetry/schema/ @elastic/kibana-core @elastic/kibana-telemetry @elastic/infra-telemetry -x-pack/plugins/telemetry_collection_xpack/schema/ @elastic/kibana-core @elastic/kibana-telemetry @elastic/infra-telemetry +/src/plugins/telemetry/schema/ @elastic/kibana-core @elastic/kibana-telemetry +/x-pack/plugins/telemetry_collection_xpack/schema/ @elastic/kibana-core @elastic/kibana-telemetry # Kibana Localization /src/dev/i18n/ @elastic/kibana-localization @elastic/kibana-core From 7e1f18f7f9b9f191c302f51b4fd67e14b9847e70 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 16 Mar 2021 16:29:36 -0400 Subject: [PATCH 35/44] [CI] Update Backport action inputs to match updated ones (#94721) --- .github/workflows/backport.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 79571d51659d6..9445d02265725 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -31,7 +31,9 @@ jobs: - name: Run Backport uses: ./actions/backport with: - branch: master github_token: ${{secrets.KIBANAMACHINE_TOKEN}} commit_user: kibanamachine commit_email: 42973632+kibanamachine@users.noreply.github.com + auto_merge: 'true' + auto_merge_method: 'squash' + manual_backport_command_template: 'node scripts/backport --pr %pullNumber%' From 4aa7036d77f7644471eda5d1073582bf2fb6bbb6 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 16 Mar 2021 16:06:21 -0500 Subject: [PATCH 36/44] [App Search] Role mappings migration part 2 (#94461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add engines mock and fix mock role mapping The original asRoleMapping was merely for a smoke test in the shared component. Refactored to work better in App Search component * Add RoleMappingsLogic * Fix test description Co-authored-by: Jason Stoltzfus * Fix test description Co-authored-by: Jason Stoltzfus * Add flash messages when creating, updating or deleting * Add path and resetState calls Refactoring the tests showed me that some parts of the state weren’t being reset. * Refactor handleAuthProviderChange logic I some how got the test coverage at 100% with my wrong way of doing tests before (scary). When I fixed it I noticed that noting I could do would trigger the fallback of just returning the `[ANY_AUTH_PROVIDER]` array. After talking with Constance, we could not come up with a way to trigger it either, given the conditions. She had suggested removing the first return statement but that caused an empty array being returned sometimes. Ultimately, I was able to get it working and covered with these changes. * Refactor tests per PR feedback The places where `role: 'superuser’` was deleted in the listeners was a side effect of using `setRoleMappingData` and not `mount` * Add back deleted assertion * Copy nit Co-authored-by: Constance Co-authored-by: Jason Stoltzfus Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Constance --- .../app_search/__mocks__/engines.mock.ts | 26 + .../applications/app_search/app_logic.ts | 2 +- .../components/role_mappings/constants.ts | 29 ++ .../role_mappings/role_mappings_logic.test.ts | 455 ++++++++++++++++++ .../role_mappings/role_mappings_logic.ts | 356 ++++++++++++++ .../shared/role_mapping/__mocks__/roles.ts | 8 +- .../role_mapping/role_mappings_table.test.tsx | 2 +- 7 files changed, 873 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engines.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engines.mock.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engines.mock.ts new file mode 100644 index 0000000000000..4076af058d6b7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/__mocks__/engines.mock.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EngineTypes } from '../components/engine/types'; + +export const defaultEngine = { + id: 'e1', + name: 'engine1', + type: EngineTypes.default, + language: null, + result_fields: {}, +}; + +export const indexedEngine = { + id: 'e2', + name: 'engine2', + type: EngineTypes.indexed, + language: null, + result_fields: {}, +}; + +export const engines = [defaultEngine, indexedEngine]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 44416b596e6ef..5f7dc683d93b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -13,7 +13,7 @@ import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; -interface AppValues { +export interface AppValues { ilmEnabled: boolean; configuredLimits: ConfiguredLimits; account: Account; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts index 74b8c6e640db1..6232ba0fb4668 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/constants.ts @@ -11,3 +11,32 @@ export const ROLE_MAPPINGS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.roleMappings.title', { defaultMessage: 'Role Mappings' } ); + +export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.deleteRoleMappingMessage', + { + defaultMessage: + 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.', + } +); + +export const ROLE_MAPPING_DELETED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingDeletedMessage', + { + defaultMessage: 'Successfully deleted role mapping', + } +); + +export const ROLE_MAPPING_CREATED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingCreatedMessage', + { + defaultMessage: 'Role mapping successfully created.', + } +); + +export const ROLE_MAPPING_UPDATED_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.roleMappingUpdatedMessage', + { + defaultMessage: 'Role mapping successfully updated.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts new file mode 100644 index 0000000000000..fa51c0036d0db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -0,0 +1,455 @@ +/* + * 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 { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; + +import { engines } from '../../__mocks__/engines.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; + +import { RoleMappingsLogic } from './role_mappings_logic'; + +describe('RoleMappingsLogic', () => { + const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; + const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers; + const { mount } = new LogicMounter(RoleMappingsLogic); + const DEFAULT_VALUES = { + attributes: [], + availableAuthProviders: [], + elasticsearchRoles: [], + roleMapping: null, + roleMappings: [], + roleType: 'owner', + attributeValue: '', + attributeName: 'username', + dataLoading: true, + hasAdvancedRoles: false, + multipleAuthProvidersConfig: false, + availableEngines: [], + selectedEngines: new Set(), + accessAllEngines: true, + selectedAuthProviders: [ANY_AUTH_PROVIDER], + }; + + const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [asRoleMapping] }; + const mappingServerProps = { + attributes: ['email', 'metadata', 'username', 'role'], + authProviders: [ANY_AUTH_PROVIDER], + availableEngines: engines, + elasticsearchRoles: [], + hasAdvancedRoles: false, + multipleAuthProvidersConfig: false, + roleMapping: asRoleMapping, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setRoleMappingsData', () => { + it('sets data based on server response from the `mappings` (plural) endpoint', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + + expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true); + }); + }); + + describe('setRoleMappingData', () => { + it('sets state based on server response from the `mapping` (singular) endpoint', () => { + RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + roleMapping: asRoleMapping, + dataLoading: false, + attributes: mappingServerProps.attributes, + availableAuthProviders: mappingServerProps.authProviders, + availableEngines: mappingServerProps.availableEngines, + accessAllEngines: true, + attributeName: 'role', + attributeValue: 'superuser', + elasticsearchRoles: mappingServerProps.elasticsearchRoles, + selectedEngines: new Set(engines.map((e) => e.name)), + }); + }); + + it('will remove all selected engines if no roleMapping was returned from the server', () => { + RoleMappingsLogic.actions.setRoleMappingData({ + ...mappingServerProps, + roleMapping: undefined, + }); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + selectedEngines: new Set(), + attributes: mappingServerProps.attributes, + availableAuthProviders: mappingServerProps.authProviders, + availableEngines: mappingServerProps.availableEngines, + }); + }); + }); + + it('handleRoleChange', () => { + RoleMappingsLogic.actions.handleRoleChange('dev'); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + roleType: 'dev', + accessAllEngines: false, + }); + }); + + describe('handleEngineSelectionChange', () => { + const engine = engines[0]; + const otherEngine = engines[1]; + const mountedValues = { + ...mappingServerProps, + roleMapping: { + ...asRoleMapping, + engines: [engine, otherEngine], + }, + selectedEngines: new Set([engine.name]), + }; + + beforeEach(() => { + mount(mountedValues); + }); + + it('handles adding an engine to selected engines', () => { + RoleMappingsLogic.actions.handleEngineSelectionChange(otherEngine.name, true); + + expect(RoleMappingsLogic.values.selectedEngines).toEqual( + new Set([engine.name, otherEngine.name]) + ); + }); + it('handles removing an engine from selected engines', () => { + RoleMappingsLogic.actions.handleEngineSelectionChange(otherEngine.name, false); + + expect(RoleMappingsLogic.values.selectedEngines).toEqual(new Set([engine.name])); + }); + }); + + it('handleAccessAllEnginesChange', () => { + RoleMappingsLogic.actions.handleAccessAllEnginesChange(); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + accessAllEngines: false, + }); + }); + + describe('handleAttributeSelectorChange', () => { + const elasticsearchRoles = ['foo', 'bar']; + + it('sets values correctly', () => { + mount({ + ...mappingServerProps, + elasticsearchRoles, + }); + RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + attributeValue: elasticsearchRoles[0], + roleMapping: asRoleMapping, + attributes: mappingServerProps.attributes, + availableEngines: mappingServerProps.availableEngines, + accessAllEngines: true, + attributeName: 'role', + elasticsearchRoles, + selectedEngines: new Set(), + }); + }); + + it('correctly handles "role" fallback', () => { + RoleMappingsLogic.actions.handleAttributeSelectorChange('username', elasticsearchRoles[0]); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + attributeValue: '', + }); + }); + }); + + it('handleAttributeValueChange', () => { + RoleMappingsLogic.actions.handleAttributeValueChange('changed_value'); + + expect(RoleMappingsLogic.values).toEqual({ + ...DEFAULT_VALUES, + attributeValue: 'changed_value', + }); + }); + + describe('handleAuthProviderChange', () => { + beforeEach(() => { + mount({ + ...mappingServerProps, + roleMapping: { + ...asRoleMapping, + authProvider: ['foo'], + }, + }); + }); + const providers = ['bar', 'baz']; + const providerWithAny = [ANY_AUTH_PROVIDER, providers[1]]; + it('handles empty state', () => { + RoleMappingsLogic.actions.handleAuthProviderChange([]); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([ANY_AUTH_PROVIDER]); + }); + + it('handles single value', () => { + RoleMappingsLogic.actions.handleAuthProviderChange([providers[0]]); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[0]]); + }); + + it('handles multiple values', () => { + RoleMappingsLogic.actions.handleAuthProviderChange(providers); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual(providers); + }); + + it('handles "any" auth in previous state', () => { + mount({ + ...mappingServerProps, + roleMapping: { + ...asRoleMapping, + authProvider: [ANY_AUTH_PROVIDER], + }, + }); + RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[1]]); + }); + }); + + it('resetState', () => { + mount(mappingsServerProps); + mount(mappingServerProps); + RoleMappingsLogic.actions.resetState(); + + expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('listeners', () => { + describe('initializeRoleMappings', () => { + it('calls API and sets values', async () => { + const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData'); + http.get.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.initializeRoleMappings(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings'); + await nextTick(); + expect(setRoleMappingsDataSpy).toHaveBeenCalledWith(mappingsServerProps); + }); + + it('handles error', async () => { + http.get.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.initializeRoleMappings(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('initializeRoleMapping', () => { + it('calls API and sets values for new mapping', async () => { + const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData'); + http.get.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.initializeRoleMapping(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/new'); + await nextTick(); + expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps); + }); + + it('calls API and sets values for existing mapping', async () => { + const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData'); + http.get.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.initializeRoleMapping('123'); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/123'); + await nextTick(); + expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps); + }); + + it('handles error', async () => { + http.get.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.initializeRoleMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + + it('redirects when there is a 404 status', async () => { + http.get.mockReturnValue(Promise.reject({ status: 404 })); + RoleMappingsLogic.actions.initializeRoleMapping(); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + }); + }); + + describe('handleResetMappings', () => { + const callback = jest.fn(); + it('calls API and executes callback', async () => { + http.post.mockReturnValue(Promise.resolve({})); + RoleMappingsLogic.actions.handleResetMappings(callback); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/role_mappings/reset'); + await nextTick(); + expect(callback).toHaveBeenCalled(); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.handleResetMappings(callback); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('handleSaveMapping', () => { + const body = { + roleType: 'owner', + accessAllEngines: true, + authProvider: [ANY_AUTH_PROVIDER], + rules: { + username: '', + }, + engines: [], + }; + + it('calls API and navigates when new mapping', async () => { + mount(mappingsServerProps); + + http.post.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.handleSaveMapping(); + + expect(http.post).toHaveBeenCalledWith('/api/app_search/role_mappings', { + body: JSON.stringify(body), + }); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + }); + + it('calls API and navigates when existing mapping', async () => { + mount(mappingServerProps); + + http.put.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.handleSaveMapping(); + + expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, { + body: JSON.stringify(body), + }); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + expect(setSuccessMessage).toHaveBeenCalled(); + }); + + it('sends array when "accessAllEngines" is false', () => { + const engine = engines[0]; + + mount({ + ...mappingServerProps, + accessAllEngines: false, + selectedEngines: new Set([engine.name]), + }); + + http.put.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.handleSaveMapping(); + + expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, { + body: JSON.stringify({ + ...body, + accessAllEngines: false, + engines: [engine.name], + }), + }); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.handleSaveMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('handleDeleteMapping', () => { + let confirmSpy: any; + + beforeEach(() => { + confirmSpy = jest.spyOn(window, 'confirm'); + confirmSpy.mockImplementation(jest.fn(() => true)); + }); + + afterEach(() => { + confirmSpy.mockRestore(); + }); + + it('returns when no mapping', () => { + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).not.toHaveBeenCalled(); + }); + + it('calls API and navigates', async () => { + mount(mappingServerProps); + http.delete.mockReturnValue(Promise.resolve({})); + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).toHaveBeenCalledWith( + `/api/app_search/role_mappings/${asRoleMapping.id}` + ); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + expect(setSuccessMessage).toHaveBeenCalled(); + }); + + it('handles error', async () => { + mount(mappingServerProps); + http.delete.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.handleDeleteMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + + it('will do nothing if not confirmed', () => { + mount(mappingServerProps); + jest.spyOn(window, 'confirm').mockReturnValueOnce(false); + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts new file mode 100644 index 0000000000000..f1b81a59779ac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts @@ -0,0 +1,356 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { + clearFlashMessages, + flashAPIErrors, + setSuccessMessage, +} from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; +import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { AttributeName } from '../../../shared/types'; +import { ROLE_MAPPINGS_PATH } from '../../routes'; +import { ASRoleMapping, RoleTypes } from '../../types'; +import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines'; +import { Engine } from '../engine/types'; + +import { + DELETE_ROLE_MAPPING_MESSAGE, + ROLE_MAPPING_DELETED_MESSAGE, + ROLE_MAPPING_CREATED_MESSAGE, + ROLE_MAPPING_UPDATED_MESSAGE, +} from './constants'; + +interface RoleMappingsServerDetails { + roleMappings: ASRoleMapping[]; + multipleAuthProvidersConfig: boolean; +} + +interface RoleMappingServerDetails { + attributes: string[]; + authProviders: string[]; + availableEngines: Engine[]; + elasticsearchRoles: string[]; + hasAdvancedRoles: boolean; + multipleAuthProvidersConfig: boolean; + roleMapping?: ASRoleMapping; +} + +const getFirstAttributeName = (roleMapping: ASRoleMapping) => + Object.entries(roleMapping.rules)[0][0] as AttributeName; +const getFirstAttributeValue = (roleMapping: ASRoleMapping) => + Object.entries(roleMapping.rules)[0][1] as AttributeName; + +export interface RoleMappingsActions { + handleAccessAllEnginesChange(): void; + handleAuthProviderChange(value: string[]): { value: string[] }; + handleAttributeSelectorChange( + value: AttributeName, + firstElasticsearchRole: string + ): { value: AttributeName; firstElasticsearchRole: string }; + handleAttributeValueChange(value: string): { value: string }; + handleDeleteMapping(): void; + handleEngineSelectionChange( + engineName: string, + selected: boolean + ): { + engineName: string; + selected: boolean; + }; + handleResetMappings(callback: () => void): Function; + handleRoleChange(roleType: RoleTypes): { roleType: RoleTypes }; + handleSaveMapping(): void; + initializeRoleMapping(roleId?: string): { roleId?: string }; + initializeRoleMappings(): void; + resetState(): void; + setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails; + setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; +} + +export interface RoleMappingsValues { + accessAllEngines: boolean; + attributeName: AttributeName; + attributeValue: string; + attributes: string[]; + availableAuthProviders: string[]; + availableEngines: Engine[]; + dataLoading: boolean; + elasticsearchRoles: string[]; + hasAdvancedRoles: boolean; + multipleAuthProvidersConfig: boolean; + roleMapping: ASRoleMapping | null; + roleMappings: ASRoleMapping[]; + roleType: RoleTypes; + selectedAuthProviders: string[]; + selectedEngines: Set; +} + +export const RoleMappingsLogic = kea>({ + path: ['enterprise_search', 'app_search', 'role_mappings'], + actions: { + setRoleMappingsData: (data: RoleMappingsServerDetails) => data, + setRoleMappingData: (data: RoleMappingServerDetails) => data, + handleAuthProviderChange: (value: string) => ({ value }), + handleRoleChange: (roleType: RoleTypes) => ({ roleType }), + handleEngineSelectionChange: (engineName: string, selected: boolean) => ({ + engineName, + selected, + }), + handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ + value, + firstElasticsearchRole, + }), + handleAttributeValueChange: (value: string) => ({ value }), + handleAccessAllEnginesChange: true, + resetState: true, + initializeRoleMappings: true, + initializeRoleMapping: (roleId) => ({ roleId }), + handleDeleteMapping: true, + handleResetMappings: (callback) => callback, + handleSaveMapping: true, + }, + reducers: { + dataLoading: [ + true, + { + setRoleMappingsData: () => false, + setRoleMappingData: () => false, + resetState: () => true, + }, + ], + roleMappings: [ + [], + { + setRoleMappingsData: (_, { roleMappings }) => roleMappings, + resetState: () => [], + }, + ], + multipleAuthProvidersConfig: [ + false, + { + setRoleMappingsData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, + setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, + resetState: () => false, + }, + ], + hasAdvancedRoles: [ + false, + { + setRoleMappingData: (_, { hasAdvancedRoles }) => hasAdvancedRoles, + }, + ], + availableEngines: [ + [], + { + setRoleMappingData: (_, { availableEngines }) => availableEngines, + resetState: () => [], + }, + ], + attributes: [ + [], + { + setRoleMappingData: (_, { attributes }) => attributes, + resetState: () => [], + }, + ], + elasticsearchRoles: [ + [], + { + setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles, + }, + ], + roleMapping: [ + null, + { + setRoleMappingData: (_, { roleMapping }) => roleMapping || null, + resetState: () => null, + }, + ], + roleType: [ + 'owner', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? (roleMapping.roleType as RoleTypes) : 'owner', + handleRoleChange: (_, { roleType }) => roleType, + }, + ], + accessAllEngines: [ + true, + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? roleMapping.accessAllEngines : true, + handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType), + handleAccessAllEnginesChange: (accessAllEngines) => !accessAllEngines, + }, + ], + attributeValue: [ + '', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? getFirstAttributeValue(roleMapping) : '', + handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) => + value === 'role' ? firstElasticsearchRole : '', + handleAttributeValueChange: (_, { value }) => value, + resetState: () => '', + }, + ], + attributeName: [ + 'username', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? getFirstAttributeName(roleMapping) : 'username', + handleAttributeSelectorChange: (_, { value }) => value, + resetState: () => 'username', + }, + ], + selectedEngines: [ + new Set(), + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? new Set(roleMapping.engines.map((engine) => engine.name)) : new Set(), + handleAccessAllEnginesChange: () => new Set(), + handleEngineSelectionChange: (engines, { engineName, selected }) => { + const newSelectedEngineNames = new Set(engines as Set); + if (selected) { + newSelectedEngineNames.add(engineName); + } else { + newSelectedEngineNames.delete(engineName); + } + + return newSelectedEngineNames; + }, + }, + ], + availableAuthProviders: [ + [], + { + setRoleMappingData: (_, { authProviders }) => authProviders, + }, + ], + selectedAuthProviders: [ + [ANY_AUTH_PROVIDER], + { + handleAuthProviderChange: (previous, { value }) => { + const previouslyContainedAny = previous.includes(ANY_AUTH_PROVIDER); + const newSelectionsContainAny = value.includes(ANY_AUTH_PROVIDER); + const hasItems = value.length > 0; + + if (value.length === 1) return value; + if (!newSelectionsContainAny && hasItems) return value; + if (previouslyContainedAny && hasItems) + return value.filter((v) => v !== ANY_AUTH_PROVIDER); + return [ANY_AUTH_PROVIDER]; + }, + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER], + }, + ], + }, + listeners: ({ actions, values }) => ({ + initializeRoleMappings: async () => { + const { http } = HttpLogic.values; + const route = '/api/app_search/role_mappings'; + + try { + const response = await http.get(route); + actions.setRoleMappingsData(response); + } catch (e) { + flashAPIErrors(e); + } + }, + initializeRoleMapping: async ({ roleId }) => { + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + const route = roleId + ? `/api/app_search/role_mappings/${roleId}` + : '/api/app_search/role_mappings/new'; + + try { + const response = await http.get(route); + actions.setRoleMappingData(response); + } catch (e) { + navigateToUrl(ROLE_MAPPINGS_PATH); + flashAPIErrors(e); + } + }, + handleDeleteMapping: async () => { + const { roleMapping } = values; + if (!roleMapping) return; + + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + const route = `/api/app_search/role_mappings/${roleMapping.id}`; + + if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) { + try { + await http.delete(route); + navigateToUrl(ROLE_MAPPINGS_PATH); + setSuccessMessage(ROLE_MAPPING_DELETED_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + } + }, + handleResetMappings: async (callback) => { + const { http } = HttpLogic.values; + try { + await http.post('/api/app_search/role_mappings/reset'); + actions.initializeRoleMappings(); + } catch (e) { + flashAPIErrors(e); + } finally { + callback(); + } + }, + handleSaveMapping: async () => { + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + + const { + attributeName, + attributeValue, + roleType, + roleMapping, + accessAllEngines, + selectedEngines, + selectedAuthProviders: authProvider, + } = values; + + const body = JSON.stringify({ + roleType, + accessAllEngines, + authProvider, + rules: { + [attributeName]: attributeValue, + }, + engines: accessAllEngines ? [] : Array.from(selectedEngines), + }); + + const request = !roleMapping + ? http.post('/api/app_search/role_mappings', { body }) + : http.put(`/api/app_search/role_mappings/${roleMapping.id}`, { body }); + + const SUCCESS_MESSAGE = !roleMapping + ? ROLE_MAPPING_CREATED_MESSAGE + : ROLE_MAPPING_UPDATED_MESSAGE; + + try { + await request; + navigateToUrl(ROLE_MAPPINGS_PATH); + setSuccessMessage(SUCCESS_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + }, + resetState: () => { + clearFlashMessages(); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts index 1576fa178cfa9..15dec753351ba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts @@ -5,19 +5,21 @@ * 2.0. */ +import { engines } from '../../../app_search/__mocks__/engines.mock'; + import { AttributeName } from '../../types'; export const asRoleMapping = { - id: null, + id: 'sdgfasdgadf123', attributeName: 'role' as AttributeName, - attributeValue: ['superuser'], + attributeValue: 'superuser', authProvider: ['*'], roleType: 'owner', rules: { role: 'superuser', }, accessAllEngines: true, - engines: [], + engines, toolTip: { content: 'Elasticsearch superusers will always be able to log in as the owner', }, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 9c47378302890..5589309d00ef8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -81,7 +81,7 @@ describe('RoleMappingsTable', () => { }); it('handles display when no items present', () => { - const noItemsRoleMapping = { ...asRoleMapping }; + const noItemsRoleMapping = { ...asRoleMapping, engines: [] }; noItemsRoleMapping.accessAllEngines = false; const wrapper = shallow( From 3afaebc49b3af141ff64b21586862bda3e2fc50d Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 16 Mar 2021 15:40:09 -0600 Subject: [PATCH 37/44] [Security Solutions] Remove commented out old linter rules (#94753) ## Summary Cleanup .. * Removes commented out old linter rules from 2+ years ago. The project is very large and a lot of people are in the code base now and the comments are not relevant. * Removes ts config optimize we don't use anymore * Removes old script for rules we don't use anymore with elastic searches. --- .eslintrc.js | 60 ------ .../scripts/convert_saved_search_to_rules.js | 177 ------------------ .../scripts/optimize_tsconfig.js | 14 -- .../scripts/optimize_tsconfig/README.md | 16 -- .../scripts/optimize_tsconfig/optimize.js | 77 -------- .../scripts/optimize_tsconfig/paths.js | 25 --- .../scripts/optimize_tsconfig/tsconfig.json | 15 -- .../scripts/optimize_tsconfig/unoptimize.js | 27 --- .../scripts/unoptimize_tsconfig.js | 14 -- .../scripts/convert_saved_search_to_rules.sh | 15 -- 10 files changed, 440 deletions(-) delete mode 100644 x-pack/plugins/security_solution/scripts/convert_saved_search_to_rules.js delete mode 100644 x-pack/plugins/security_solution/scripts/optimize_tsconfig.js delete mode 100644 x-pack/plugins/security_solution/scripts/optimize_tsconfig/README.md delete mode 100644 x-pack/plugins/security_solution/scripts/optimize_tsconfig/optimize.js delete mode 100644 x-pack/plugins/security_solution/scripts/optimize_tsconfig/paths.js delete mode 100644 x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json delete mode 100644 x-pack/plugins/security_solution/scripts/optimize_tsconfig/unoptimize.js delete mode 100644 x-pack/plugins/security_solution/scripts/unoptimize_tsconfig.js delete mode 100755 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh diff --git a/.eslintrc.js b/.eslintrc.js index 865bcc008afbc..1eb8faa2469c8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -829,48 +829,12 @@ module.exports = { // typescript only for front and back end files: ['x-pack/plugins/security_solution/**/*.{ts,tsx}'], rules: { - // This will be turned on after bug fixes are complete - // '@typescript-eslint/explicit-member-accessibility': 'warn', '@typescript-eslint/no-this-alias': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-useless-constructor': 'error', - // This will be turned on after bug fixes are complete - // '@typescript-eslint/no-object-literal-type-assertion': 'warn', '@typescript-eslint/unified-signatures': 'error', - - // eventually we want this to be a warn and then an error since this is a recommended linter rule - // for now, keeping it commented out to avoid too much IDE noise until the other linter issues - // are fixed in the next release or two - // '@typescript-eslint/explicit-function-return-type': 'warn', - - // these rules cannot be turned on and tested at the moment until this issue is resolved: - // https://github.com/prettier/prettier-eslint/issues/201 - // '@typescript-eslint/await-thenable': 'error', - // '@typescript-eslint/no-non-null-assertion': 'error' - // '@typescript-eslint/no-unnecessary-type-assertion': 'error', - // '@typescript-eslint/no-unused-vars': 'error', - // '@typescript-eslint/prefer-includes': 'error', - // '@typescript-eslint/prefer-string-starts-ends-with': 'error', - // '@typescript-eslint/promise-function-async': 'error', - // '@typescript-eslint/prefer-regexp-exec': 'error', - // '@typescript-eslint/promise-function-async': 'error', - // '@typescript-eslint/require-array-sort-compare': 'error', - // '@typescript-eslint/restrict-plus-operands': 'error', - // '@typescript-eslint/unbound-method': 'error', }, }, - // { - // // will introduced after the other warns are fixed - // // typescript and javascript for front end react performance - // files: ['x-pack/plugins/security_solution/public/**/!(*.test).{js,mjs,ts,tsx}'], - // plugins: ['react-perf'], - // rules: { - // // 'react-perf/jsx-no-new-object-as-prop': 'error', - // // 'react-perf/jsx-no-new-array-as-prop': 'error', - // // 'react-perf/jsx-no-new-function-as-prop': 'error', - // // 'react/jsx-no-bind': 'error', - // }, - // }, { // typescript and javascript for front and back end files: ['x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}'], @@ -883,21 +847,6 @@ module.exports = { 'array-callback-return': 'error', 'no-array-constructor': 'error', complexity: 'warn', - // This will be turned on after bug fixes are mostly completed - // 'consistent-return': 'warn', - // This will be turned on after bug fixes are mostly completed - // 'func-style': ['warn', 'expression'], - // These will be turned on after bug fixes are mostly completed and we can - // run a fix-lint - /* - 'import/order': [ - 'warn', - { - groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], - 'newlines-between': 'always', - }, - ], - */ 'node/no-deprecated-api': 'error', 'no-bitwise': 'error', 'no-continue': 'error', @@ -937,12 +886,8 @@ module.exports = { 'no-useless-catch': 'error', 'no-useless-concat': 'error', 'no-useless-computed-key': 'error', - // This will be turned on after bug fixes are mostly complete - // 'no-useless-escape': 'warn', 'no-useless-rename': 'error', 'no-useless-return': 'error', - // This will be turned on after bug fixers are mostly complete - // 'no-void': 'warn', 'one-var-declaration-per-line': 'error', 'prefer-object-spread': 'error', 'prefer-promise-reject-errors': 'error', @@ -958,9 +903,6 @@ module.exports = { 'react/no-danger-with-children': 'error', 'react/no-deprecated': 'error', 'react/no-did-mount-set-state': 'error', - // Re-enable once we have better options per this issue: - // https://github.com/airbnb/javascript/issues/1875 - // 'react/no-did-update-set-state': 'error', 'react/no-direct-mutation-state': 'error', 'react/no-find-dom-node': 'error', 'react/no-redundant-should-component-update': 'error', @@ -972,8 +914,6 @@ module.exports = { 'react/no-unsafe': 'error', 'react/no-unused-prop-types': 'error', 'react/no-unused-state': 'error', - // will introduced after the other warns are fixed - // 'react/sort-comp': 'error', 'react/void-dom-elements-no-children': 'error', 'react/jsx-no-comment-textnodes': 'error', 'react/jsx-no-literals': 'error', diff --git a/x-pack/plugins/security_solution/scripts/convert_saved_search_to_rules.js b/x-pack/plugins/security_solution/scripts/convert_saved_search_to_rules.js deleted file mode 100644 index 33968849fdb91..0000000000000 --- a/x-pack/plugins/security_solution/scripts/convert_saved_search_to_rules.js +++ /dev/null @@ -1,177 +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. - */ - -require('../../../../src/setup_node_env'); - -const fs = require('fs'); -const path = require('path'); -// eslint-disable-next-line import/no-extraneous-dependencies -const uuid = require('uuid'); - -/* - * This script is used to parse a set of saved searches on a file system - * and output rule data compatible json files. - * Example: - * node saved_query_to_rules.js ${HOME}/saved_searches ${HOME}/saved_rules - * - * After editing any changes in the files of ${HOME}/saved_rules/*.json - * you can then post the rules with a CURL post script such as: - * - * ./post_rule.sh ${HOME}/saved_rules/*.json - * - * Note: This script is recursive and but does not preserve folder structure - * when it outputs the saved rules. - */ - -// Defaults of the outputted rules since the saved KQL searches do not have -// this type of information. You usually will want to make any hand edits after -// doing a search to KQL conversion before posting it as a rule or checking it -// into another repository. -const INTERVAL = '5m'; -const SEVERITY = 'low'; -const TYPE = 'query'; -const FROM = 'now-6m'; -const TO = 'now'; -const IMMUTABLE = true; -const RISK_SCORE = 50; -const ENABLED = false; - -// For converting, if you want to use these instead of rely on the defaults then -// comment these in and use them for the script. Otherwise this is commented out -// so we can utilize the defaults of input and output which are based on saved objects -// of securitySolution:defaultIndex and your kibana.dev.yml setting of xpack.securitySolution.signalsIndex. If -// the setting of xpack.securitySolution.signalsIndex is not set it defaults to .siem-signals -// const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; -// const OUTPUT_INDEX = '.siem-signals-some-other-index'; - -const walk = (dir) => { - const list = fs.readdirSync(dir); - return list.reduce((accum, file) => { - const fileWithDir = `${dir}/${file}`; - const stat = fs.statSync(fileWithDir); - if (stat && stat.isDirectory()) { - return [...accum, ...walk(fileWithDir)]; - } else { - return [...accum, fileWithDir]; - } - }, []); -}; - -//clean up the file system characters -const cleanupFileName = (file) => { - const fileWithoutSpecialChars = file - .trim() - .replace(/\./g, '') - .replace(/\//g, '') - .replace(/\s+/g, '_') - .replace(/,/g, '') - .replace(/\[/g, '') - .replace(/\]/g, '') - .replace(/\(/g, '') - .replace(/\)/g, '') - .replace(/\@/g, '') - .replace(/\:/g, '') - .replace(/\+s/g, '') - .replace(/-/g, '') - .replace(/__/g, '_') - .toLowerCase(); - return path.basename( - fileWithoutSpecialChars.trim(), - path.extname(fileWithoutSpecialChars.trim()) - ); -}; - -async function main() { - if (process.argv.length !== 4) { - throw new Error( - 'usage: saved_query_to_rules [input directory with saved searches] [output directory]' - ); - } - - const files = process.argv[2]; - const outputDir = process.argv[3]; - - const savedSearchesJson = walk(files).filter((file) => { - return !path.basename(file).startsWith('.') && file.endsWith('.ndjson'); - }); - - const savedSearchesParsed = savedSearchesJson.reduce((accum, json) => { - const jsonFile = fs.readFileSync(json, 'utf8'); - const jsonLines = jsonFile.split(/\r{0,1}\n/); - const parsedLines = jsonLines.reduce((accum, line) => { - try { - const parsedLine = JSON.parse(line); - // don't try to parse out any exported count records - if (parsedLine.exportedCount != null) { - return accum; - } - parsedLine._file = parsedLine.attributes.title; - parsedLine.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.parse( - parsedLine.attributes.kibanaSavedObjectMeta.searchSourceJSON - ); - return [...accum, parsedLine]; - } catch (err) { - return accum; - } - }, []); - return [...accum, ...parsedLines]; - }, []); - - savedSearchesParsed.forEach( - ({ - _file, - attributes: { - description, - title, - kibanaSavedObjectMeta: { - searchSourceJSON: { - query: { query, language }, - filter, - }, - }, - }, - }) => { - const fileToWrite = cleanupFileName(_file); - - // remove meta value from the filter - const filterWithoutMeta = filter.map((filterValue) => { - filterValue.$state; - return filterValue; - }); - const outputMessage = { - description: description || title, - enabled: ENABLED, - filters: filterWithoutMeta, - from: FROM, - immutable: IMMUTABLE, - interval: INTERVAL, - language, - name: title, - query, - risk_score: RISK_SCORE, - rule_id: uuid.v4(), - severity: SEVERITY, - to: TO, - type: TYPE, - version: 1, - // comment these in if you want to use these for input output, otherwise - // with these two commented out, we will use the default saved objects from spaces. - // index: INDEX, - // output_index: OUTPUT_INDEX, - }; - - fs.writeFileSync( - `${outputDir}/${fileToWrite}.json`, - `${JSON.stringify(outputMessage, null, 2)}\n` - ); - } - ); -} - -if (require.main === module) { - main(); -} diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig.js b/x-pack/plugins/security_solution/scripts/optimize_tsconfig.js deleted file mode 100644 index e8fda71d8b7db..0000000000000 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig.js +++ /dev/null @@ -1,14 +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. - */ - -const { optimizeTsConfig } = require('./optimize_tsconfig/optimize'); - -optimizeTsConfig().catch((err) => { - console.error(err); - // eslint-disable-next-line no-process-exit - process.exit(1); -}); diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/README.md b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/README.md deleted file mode 100644 index b711b8bf1dbc2..0000000000000 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/README.md +++ /dev/null @@ -1,16 +0,0 @@ -Hard forked from here: -x-pack/plugins/apm/scripts/optimize-tsconfig.js - - -#### Optimizing TypeScript - -Kibana and X-Pack are very large TypeScript projects, and it comes at a cost. Editor responsiveness is not great, and the CLI type check for X-Pack takes about a minute. To get faster feedback, we create a smaller SIEM TypeScript project that only type checks the SIEM project and the files it uses. This optimization consists of creating a `tsconfig.json` in SIEM that includes the Kibana/X-Pack typings, and editing the Kibana/X-Pack configurations to not include any files, or removing the configurations altogether. The script configures git to ignore any changes in these files, and has an undo script as well. - -To run the optimization: - -`$ node x-pack/plugins/security_solution/scripts/optimize_tsconfig` - -To undo the optimization: - -`$ node x-pack/plugins/security_solution/scripts/unoptimize_tsconfig` - diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/optimize.js b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/optimize.js deleted file mode 100644 index 9bea8c93ed52c..0000000000000 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/optimize.js +++ /dev/null @@ -1,77 +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 import/no-extraneous-dependencies */ - -const fs = require('fs'); -const { promisify } = require('util'); -const path = require('path'); -const json5 = require('json5'); -const execa = require('execa'); - -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); - -const { xpackRoot, kibanaRoot, tsconfigTpl, filesToIgnore } = require('./paths'); -const { unoptimizeTsConfig } = require('./unoptimize'); - -function prepareParentTsConfigs() { - return Promise.all( - [path.resolve(xpackRoot, 'tsconfig.json'), path.resolve(kibanaRoot, 'tsconfig.json')].map( - async (filename) => { - const config = json5.parse(await readFile(filename, 'utf-8')); - - await writeFile( - filename, - JSON.stringify( - { - ...config, - include: [], - }, - null, - 2 - ), - { encoding: 'utf-8' } - ); - } - ) - ); -} - -async function addFilesToXpackTsConfig() { - const template = json5.parse(await readFile(tsconfigTpl, 'utf-8')); - const xpackTsConfig = path.join(xpackRoot, 'tsconfig.json'); - const config = json5.parse(await readFile(xpackTsConfig, 'utf-8')); - - await writeFile(xpackTsConfig, JSON.stringify({ ...config, ...template }, null, 2), { - encoding: 'utf-8', - }); -} - -async function setIgnoreChanges() { - for (const filename of filesToIgnore) { - await execa('git', ['update-index', '--skip-worktree', filename]); - } -} - -async function optimizeTsConfig() { - await unoptimizeTsConfig(); - - await prepareParentTsConfigs(); - - await addFilesToXpackTsConfig(); - - await setIgnoreChanges(); - // eslint-disable-next-line no-console - console.log( - 'Created an optimized tsconfig.json for SIEM. To undo these changes, run `./scripts/unoptimize_tsconfig.js`' - ); -} - -module.exports = { - optimizeTsConfig, -}; diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/paths.js b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/paths.js deleted file mode 100644 index ac32739627935..0000000000000 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/paths.js +++ /dev/null @@ -1,25 +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. - */ - -const path = require('path'); - -const xpackRoot = path.resolve(__dirname, '../../../..'); -const kibanaRoot = path.resolve(xpackRoot, '..'); - -const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); - -const filesToIgnore = [ - path.resolve(xpackRoot, 'tsconfig.json'), - path.resolve(kibanaRoot, 'tsconfig.json'), -]; - -module.exports = { - xpackRoot, - kibanaRoot, - tsconfigTpl, - filesToIgnore, -}; diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json deleted file mode 100644 index ac56a6af31c72..0000000000000 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "include": [ - "typings/**/*", - "plugins/lists/**/*", - "plugins/security_solution/**/*", - "plugins/apm/typings/numeral.d.ts", - "plugins/canvas/types/webpack.d.ts", - "plugins/triggers_actions_ui/**/*" - ], - "exclude": [ - "test/**/*", - "**/__fixtures__/**/*", - "plugins/security_solution/cypress/**/*" - ] -} diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/unoptimize.js b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/unoptimize.js deleted file mode 100644 index 58bd5d526a638..0000000000000 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/unoptimize.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable import/no-extraneous-dependencies */ - -const execa = require('execa'); - -const { filesToIgnore } = require('./paths'); - -async function unoptimizeTsConfig() { - for (const filename of filesToIgnore) { - await execa('git', ['update-index', '--no-skip-worktree', filename]); - await execa('git', ['checkout', filename]); - } -} - -module.exports = { - unoptimizeTsConfig: async () => { - await unoptimizeTsConfig(); - // eslint-disable-next-line no-console - console.log('Removed SIEM TypeScript optimizations'); - }, -}; diff --git a/x-pack/plugins/security_solution/scripts/unoptimize_tsconfig.js b/x-pack/plugins/security_solution/scripts/unoptimize_tsconfig.js deleted file mode 100644 index f7f6e7936fbbc..0000000000000 --- a/x-pack/plugins/security_solution/scripts/unoptimize_tsconfig.js +++ /dev/null @@ -1,14 +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. - */ - -const { unoptimizeTsConfig } = require('./optimize_tsconfig/unoptimize'); - -unoptimizeTsConfig().catch((err) => { - console.error(err); - // eslint-disable-next-line no-process-exit - process.exit(1); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh deleted file mode 100755 index 65f27647fd43a..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/convert_saved_search_to_rules.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# -# 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. -# - -set -e -./check_env_variables.sh - -OUTPUT=${2:-../rules/prepackaged_rules} - -node ../../../../scripts/convert_saved_search_to_rules.js $1 $OUTPUT From 769243cc98b92cd7f0fdcfb637a9778559c8cc0c Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 16 Mar 2021 17:49:24 -0400 Subject: [PATCH 38/44] [ML] Data Frame Analytics accessibility tests: fix flaky outlier creation test (#94735) * ensure callouts exist before moving to continue button * unskip ml accessibility suite --- x-pack/test/accessibility/apps/ml.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index 4689180c457a5..164c7032d9dd3 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -13,8 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const a11y = getService('a11y'); const ml = getService('ml'); - // Failing: See https://github.com/elastic/kibana/issues/94666 - describe.skip('ml', () => { + describe('ml', () => { const esArchiver = getService('esArchiver'); before(async () => { @@ -279,6 +278,7 @@ export default function ({ getService }: FtrProviderContext) { it('data frame analytics create job validation step for outlier job', async () => { await ml.dataFrameAnalyticsCreation.continueToValidationStep(); + await ml.dataFrameAnalyticsCreation.assertValidationCalloutsExists(); await a11y.testAppSnapshot(); }); From c497239d55574bb00fe1b2dff631a5288a5eb852 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 16 Mar 2021 16:05:30 -0600 Subject: [PATCH 39/44] [Security Solution] [Cases] Move create page components and dependencies to Cases (#94444) --- packages/kbn-optimizer/limits.yml | 1 + x-pack/plugins/cases/common/api/index.ts | 1 + x-pack/plugins/cases/common/constants.ts | 5 +- x-pack/plugins/cases/common/index.ts | 9 + x-pack/plugins/cases/kibana.json | 5 +- x-pack/plugins/cases/public/common/errors.ts | 39 + .../common/lib/kibana/__mocks__/index.ts | 30 + .../cases/public/common/lib/kibana/index.ts | 9 + .../common/lib/kibana/kibana_react.mock.ts | 35 + .../public/common/lib/kibana/kibana_react.ts | 16 + .../public/common/lib/kibana/services.ts | 42 ++ .../plugins/cases/public/common/mock/index.ts | 8 + .../public/common/mock/kibana_react.mock.ts | 23 + .../public/common/mock/test_providers.tsx | 58 ++ .../cases/public/common/shared_imports.ts | 33 + .../plugins/cases/public/common/test_utils.ts | 12 + .../cases/public/common/translations.ts | 252 +++++++ .../cases/public/components/__mock__/form.ts | 50 ++ .../configure_cases/__mock__/index.tsx | 60 ++ .../configure_cases/closure_options.test.tsx | 58 ++ .../configure_cases/closure_options.tsx | 54 ++ .../closure_options_radio.test.tsx | 77 ++ .../configure_cases/closure_options_radio.tsx | 60 ++ .../configure_cases/connectors.test.tsx | 115 +++ .../components/configure_cases/connectors.tsx | 119 +++ .../connectors_dropdown.test.tsx | 203 ++++++ .../configure_cases/connectors_dropdown.tsx | 121 ++++ .../configure_cases/field_mapping.test.tsx | 55 ++ .../configure_cases/field_mapping.tsx | 73 ++ .../field_mapping_row_static.tsx | 60 ++ .../components/configure_cases/index.test.tsx | 591 +++++++++++++++ .../components/configure_cases/index.tsx | 224 ++++++ .../configure_cases/mapping.test.tsx | 47 ++ .../components/configure_cases/mapping.tsx | 62 ++ .../configure_cases/translations.ts | 227 ++++++ .../components/configure_cases/utils.test.tsx | 64 ++ .../components/configure_cases/utils.ts | 80 ++ .../connector_selector/form.test.tsx | 67 ++ .../components/connector_selector/form.tsx | 70 ++ .../public/components/connectors/card.tsx | 71 ++ .../connectors/case/alert_fields.tsx | 106 +++ .../connectors/case/cases_dropdown.tsx | 73 ++ .../connectors/case/existing_case.tsx | 76 ++ .../components/connectors/case/index.ts | 42 ++ .../connectors/case/translations.ts | 109 +++ .../components/connectors/case/types.ts | 18 + .../public/components/connectors/config.ts | 39 + .../connectors/connectors_registry.ts | 49 ++ .../components/connectors/fields_form.tsx | 54 ++ .../public/components/connectors/index.ts | 57 ++ .../connectors/jira/__mocks__/api.ts | 45 ++ .../components/connectors/jira/api.test.ts | 160 ++++ .../public/components/connectors/jira/api.ts | 93 +++ .../connectors/jira/case_fields.test.tsx | 262 +++++++ .../connectors/jira/case_fields.tsx | 214 ++++++ .../components/connectors/jira/index.ts | 27 + .../connectors/jira/search_issues.tsx | 95 +++ .../connectors/jira/translations.ts | 68 ++ .../components/connectors/jira/types.ts | 22 + .../use_get_fields_by_issue_type.test.tsx | 105 +++ .../jira/use_get_fields_by_issue_type.tsx | 96 +++ .../jira/use_get_issue_types.test.tsx | 107 +++ .../connectors/jira/use_get_issue_types.tsx | 102 +++ .../connectors/jira/use_get_issues.test.tsx | 80 ++ .../connectors/jira/use_get_issues.tsx | 97 +++ .../jira/use_get_single_issue.test.tsx | 80 ++ .../connectors/jira/use_get_single_issue.tsx | 95 +++ .../public/components/connectors/mock.ts | 121 ++++ .../connectors/resilient/__mocks__/api.ts | 16 + .../components/connectors/resilient/api.ts | 42 ++ .../connectors/resilient/case_fields.test.tsx | 134 ++++ .../connectors/resilient/case_fields.tsx | 189 +++++ .../components/connectors/resilient/index.ts | 26 + .../connectors/resilient/translations.ts | 40 + .../components/connectors/resilient/types.ts | 9 + .../resilient/use_get_incident_types.test.tsx | 71 ++ .../resilient/use_get_incident_types.tsx | 94 +++ .../resilient/use_get_severity.test.tsx | 77 ++ .../connectors/resilient/use_get_severity.tsx | 91 +++ .../connectors/servicenow/__mocks__/api.ts | 19 + .../connectors/servicenow/api.test.ts | 40 + .../components/connectors/servicenow/api.ts | 31 + .../connectors/servicenow/helpers.ts | 12 + .../components/connectors/servicenow/index.ts | 32 + .../servicenow_itsm_case_fields.test.tsx | 164 +++++ .../servicenow_itsm_case_fields.tsx | 235 ++++++ .../servicenow_sir_case_fields.test.tsx | 221 ++++++ .../servicenow/servicenow_sir_case_fields.tsx | 282 ++++++++ .../connectors/servicenow/translations.ts | 75 ++ .../components/connectors/servicenow/types.ts | 15 + .../servicenow/use_get_choices.test.tsx | 144 ++++ .../connectors/servicenow/use_get_choices.tsx | 101 +++ .../public/components/connectors/types.ts | 53 ++ .../components/create/connector.test.tsx | 187 +++++ .../public/components/create/connector.tsx | 103 +++ .../components/create/description.test.tsx | 62 ++ .../public/components/create/description.tsx | 31 + .../public/components/create/flyout.test.tsx | 115 +++ .../cases/public/components/create/flyout.tsx | 71 ++ .../public/components/create/form.test.tsx | 109 +++ .../cases/public/components/create/form.tsx | 119 +++ .../components/create/form_context.test.tsx | 682 ++++++++++++++++++ .../public/components/create/form_context.tsx | 120 +++ .../public/components/create/index.test.tsx | 126 ++++ .../cases/public/components/create/index.tsx | 57 ++ .../cases/public/components/create/mock.ts | 101 +++ .../optional_field_label/index.test.tsx | 19 + .../create/optional_field_label/index.tsx | 17 + .../cases/public/components/create/schema.tsx | 58 ++ .../components/create/submit_button.test.tsx | 88 +++ .../components/create/submit_button.tsx | 31 + .../create/sync_alerts_toggle.test.tsx | 79 ++ .../components/create/sync_alerts_toggle.tsx | 38 + .../public/components/create/tags.test.tsx | 79 ++ .../cases/public/components/create/tags.tsx | 49 ++ .../public/components/create/title.test.tsx | 72 ++ .../cases/public/components/create/title.tsx | 33 + .../public/components/create/translations.ts | 26 + .../components/markdown_editor/editor.tsx | 66 ++ .../components/markdown_editor/eui_form.tsx | 66 ++ .../components/markdown_editor/index.tsx | 11 + .../markdown_editor/markdown_link.tsx | 35 + .../markdown_editor/renderer.test.tsx | 63 ++ .../components/markdown_editor/renderer.tsx | 41 ++ .../markdown_editor/translations.ts | 19 + .../components/markdown_editor/types.ts | 11 + .../public/components/status/button.test.tsx | 90 +++ .../cases/public/components/status/button.tsx | 52 ++ .../cases/public/components/status/config.ts | 82 +++ .../cases/public/components/status/index.ts | 11 + .../public/components/status/stats.test.tsx | 66 ++ .../cases/public/components/status/stats.tsx | 40 + .../public/components/status/status.test.tsx | 72 ++ .../cases/public/components/status/status.tsx | 43 ++ .../public/components/status/translations.ts | 69 ++ .../cases/public/components/status/types.ts | 43 ++ .../modal_all_errors.test.tsx.snap | 48 ++ .../public/components/toasters/errors.ts | 19 + .../public/components/toasters/index.test.tsx | 307 ++++++++ .../public/components/toasters/index.tsx | 136 ++++ .../toasters/modal_all_errors.test.tsx | 70 ++ .../components/toasters/modal_all_errors.tsx | 75 ++ .../components/toasters/translations.ts | 20 + .../public/components/toasters/utils.test.ts | 128 ++++ .../cases/public/components/toasters/utils.ts | 149 ++++ .../create_case_modal.test.tsx | 126 ++++ .../create_case_modal.tsx | 67 ++ .../use_create_case_modal/index.test.tsx | 151 ++++ .../use_create_case_modal/index.tsx | 60 ++ .../public/components/wrappers/index.tsx | 26 + .../cases/public/containers/__mocks__/api.ts | 114 +++ .../cases/public/containers/api.test.tsx | 465 ++++++++++++ x-pack/plugins/cases/public/containers/api.ts | 347 +++++++++ .../containers/configure/__mocks__/api.ts | 36 + .../public/containers/configure/api.test.ts | 154 ++++ .../cases/public/containers/configure/api.ts | 103 +++ .../cases/public/containers/configure/mock.ts | 160 ++++ .../containers/configure/translations.ts | 14 + .../public/containers/configure/types.ts | 46 ++ .../configure/use_action_types.test.tsx | 102 +++ .../containers/configure/use_action_types.tsx | 73 ++ .../configure/use_configure.test.tsx | 326 +++++++++ .../containers/configure/use_configure.tsx | 361 +++++++++ .../configure/use_connectors.test.tsx | 96 +++ .../containers/configure/use_connectors.tsx | 72 ++ .../cases/public/containers/constants.ts | 9 + .../plugins/cases/public/containers/mock.ts | 377 ++++++++++ .../cases/public/containers/translations.ts | 90 +++ .../plugins/cases/public/containers/types.ts | 175 +++++ .../public/containers/use_get_cases.test.tsx | 204 ++++++ .../cases/public/containers/use_get_cases.tsx | 255 +++++++ .../public/containers/use_get_tags.test.tsx | 89 +++ .../cases/public/containers/use_get_tags.tsx | 100 +++ .../public/containers/use_post_case.test.tsx | 114 +++ .../cases/public/containers/use_post_case.tsx | 91 +++ .../use_post_push_to_service.test.tsx | 101 +++ .../containers/use_post_push_to_service.tsx | 112 +++ .../cases/public/containers/utils.test.ts | 170 +++++ .../plugins/cases/public/containers/utils.ts | 150 ++++ .../plugins/cases/public/get_create_case.tsx | 19 + x-pack/plugins/cases/public/index.tsx | 20 + x-pack/plugins/cases/public/plugin.ts | 33 + x-pack/plugins/cases/public/types.ts | 29 + .../client/alerts/update_status.test.ts | 2 +- .../cases/server/client/cases/create.test.ts | 7 +- .../cases/server/client/cases/create.ts | 2 +- .../plugins/cases/server/client/cases/get.ts | 2 +- .../plugins/cases/server/client/cases/mock.ts | 2 +- .../plugins/cases/server/client/cases/push.ts | 2 +- .../cases/server/client/cases/types.ts | 2 +- .../cases/server/client/cases/update.test.ts | 2 +- .../cases/server/client/cases/update.ts | 2 +- .../cases/server/client/cases/utils.test.ts | 4 +- .../cases/server/client/cases/utils.ts | 4 +- x-pack/plugins/cases/server/client/client.ts | 2 +- .../cases/server/client/comments/add.test.ts | 2 +- .../cases/server/client/comments/add.ts | 4 +- .../client/configure/get_fields.test.ts | 2 +- .../server/client/configure/get_fields.ts | 2 +- .../client/configure/get_mappings.test.ts | 2 +- .../server/client/configure/get_mappings.ts | 2 +- .../cases/server/client/configure/mock.ts | 6 +- .../server/client/configure/utils.test.ts | 2 +- .../cases/server/client/configure/utils.ts | 6 +- x-pack/plugins/cases/server/client/types.ts | 2 +- .../cases/server/client/user_actions/get.ts | 2 +- .../server/common/models/commentable_case.ts | 2 +- .../plugins/cases/server/common/utils.test.ts | 2 +- x-pack/plugins/cases/server/common/utils.ts | 8 +- .../server/connectors/case/index.test.ts | 2 +- .../cases/server/connectors/case/index.ts | 7 +- .../cases/server/connectors/case/schema.ts | 2 +- .../cases/server/connectors/case/types.ts | 2 +- .../plugins/cases/server/connectors/index.ts | 2 +- .../jira/external_service_formatter.test.ts | 2 +- .../jira/external_service_formatter.ts | 2 +- .../external_service_formatter.test.ts | 2 +- .../resilient/external_service_formatter.ts | 2 +- .../connectors/servicenow/itsm_formatter.ts | 2 +- .../servicenow/itsm_formmater.test.ts | 2 +- .../servicenow/sir_formatter.test.ts | 2 +- .../connectors/servicenow/sir_formatter.ts | 2 +- .../plugins/cases/server/connectors/types.ts | 2 +- x-pack/plugins/cases/server/plugin.ts | 2 +- .../api/__fixtures__/mock_saved_objects.ts | 2 +- .../routes/api/__mocks__/request_responses.ts | 2 +- .../api/cases/comments/delete_all_comments.ts | 3 +- .../api/cases/comments/delete_comment.test.ts | 2 +- .../api/cases/comments/delete_comment.ts | 2 +- .../api/cases/comments/find_comments.ts | 4 +- .../api/cases/comments/get_all_comment.ts | 4 +- .../api/cases/comments/get_comment.test.ts | 2 +- .../routes/api/cases/comments/get_comment.ts | 4 +- .../api/cases/comments/patch_comment.test.ts | 4 +- .../api/cases/comments/patch_comment.ts | 4 +- .../api/cases/comments/post_comment.test.ts | 4 +- .../routes/api/cases/comments/post_comment.ts | 4 +- .../api/cases/configure/get_configure.test.ts | 3 +- .../api/cases/configure/get_configure.ts | 4 +- .../cases/configure/get_connectors.test.ts | 2 +- .../api/cases/configure/get_connectors.ts | 5 +- .../cases/configure/patch_configure.test.ts | 3 +- .../api/cases/configure/patch_configure.ts | 4 +- .../cases/configure/post_configure.test.ts | 3 +- .../api/cases/configure/post_configure.ts | 4 +- .../routes/api/cases/delete_cases.test.ts | 2 +- .../server/routes/api/cases/delete_cases.ts | 2 +- .../routes/api/cases/find_cases.test.ts | 2 +- .../server/routes/api/cases/find_cases.ts | 4 +- .../server/routes/api/cases/get_case.test.ts | 4 +- .../cases/server/routes/api/cases/get_case.ts | 2 +- .../server/routes/api/cases/helpers.test.ts | 2 +- .../cases/server/routes/api/cases/helpers.ts | 11 +- .../routes/api/cases/patch_cases.test.ts | 2 +- .../server/routes/api/cases/patch_cases.ts | 4 +- .../server/routes/api/cases/post_case.test.ts | 4 +- .../server/routes/api/cases/post_case.ts | 4 +- .../server/routes/api/cases/push_case.test.ts | 2 +- .../server/routes/api/cases/push_case.ts | 4 +- .../api/cases/reporters/get_reporters.ts | 4 +- .../api/cases/status/get_status.test.ts | 4 +- .../routes/api/cases/status/get_status.ts | 4 +- .../api/cases/sub_case/delete_sub_cases.ts | 2 +- .../api/cases/sub_case/find_sub_cases.ts | 4 +- .../routes/api/cases/sub_case/get_sub_case.ts | 4 +- .../api/cases/sub_case/patch_sub_cases.ts | 4 +- .../server/routes/api/cases/tags/get_tags.ts | 2 +- .../user_actions/get_all_user_actions.ts | 2 +- .../cases/server/routes/api/utils.test.ts | 2 +- .../plugins/cases/server/routes/api/utils.ts | 2 +- .../server/saved_object_types/migrations.ts | 2 +- .../cases/server/scripts/sub_cases/index.ts | 4 +- .../server/services/alerts/index.test.ts | 2 +- .../cases/server/services/alerts/index.ts | 2 +- .../cases/server/services/configure/index.ts | 2 +- .../services/connector_mappings/index.ts | 2 +- x-pack/plugins/cases/server/services/index.ts | 2 +- .../services/reporters/read_reporters.ts | 2 +- .../cases/server/services/tags/read_tags.ts | 2 +- .../server/services/user_actions/helpers.ts | 2 +- .../server/services/user_actions/index.ts | 2 +- .../security_solution/common/constants.ts | 9 + x-pack/plugins/security_solution/kibana.json | 1 + .../components/add_comment/index.test.tsx | 2 +- .../cases/components/add_comment/index.tsx | 4 +- .../cases/components/add_comment/schema.tsx | 2 +- .../cases/components/all_cases/actions.tsx | 2 +- .../cases/components/all_cases/columns.tsx | 2 +- .../components/all_cases/expanded_row.tsx | 2 +- .../cases/components/all_cases/helpers.ts | 2 +- .../cases/components/all_cases/index.test.tsx | 2 +- .../cases/components/all_cases/index.tsx | 2 +- .../all_cases/status_filter.test.tsx | 2 +- .../all_cases/table_filters.test.tsx | 2 +- .../components/all_cases/table_filters.tsx | 2 +- .../cases/components/bulk_actions/index.tsx | 2 +- .../case_action_bar/helpers.test.ts | 2 +- .../components/case_action_bar/helpers.ts | 2 +- .../components/case_action_bar/index.tsx | 2 +- .../status_context_menu.test.tsx | 2 +- .../case_action_bar/status_context_menu.tsx | 2 +- .../components/case_view/helpers.test.tsx | 2 +- .../cases/components/case_view/helpers.ts | 2 +- .../cases/components/case_view/index.test.tsx | 3 +- .../cases/components/case_view/index.tsx | 2 +- .../configure_cases/__mock__/index.tsx | 2 +- .../configure_cases/connectors.test.tsx | 2 +- .../components/configure_cases/connectors.tsx | 2 +- .../configure_cases/connectors_dropdown.tsx | 2 +- .../components/configure_cases/index.test.tsx | 2 +- .../components/configure_cases/index.tsx | 2 +- .../cases/components/configure_cases/utils.ts | 2 +- .../components/connector_selector/form.tsx | 2 +- .../cases/components/connectors/card.tsx | 2 +- .../connectors/case/alert_fields.tsx | 2 +- .../connectors/case/existing_case.tsx | 2 +- .../components/connectors/fields_form.tsx | 2 +- .../cases/components/connectors/index.ts | 2 +- .../connectors/jira/case_fields.tsx | 2 +- .../cases/components/connectors/jira/index.ts | 2 +- .../connectors/resilient/case_fields.tsx | 2 +- .../components/connectors/resilient/index.ts | 2 +- .../components/connectors/servicenow/index.ts | 5 +- .../servicenow_itsm_case_fields.tsx | 5 +- .../servicenow/servicenow_sir_case_fields.tsx | 5 +- .../cases/components/connectors/types.ts | 4 +- .../cases/components/create/connector.tsx | 2 +- .../components/create/form_context.test.tsx | 2 +- .../cases/components/create/form_context.tsx | 2 +- .../public/cases/components/create/index.tsx | 62 +- .../public/cases/components/create/mock.ts | 3 +- .../public/cases/components/create/schema.tsx | 2 +- .../cases/components/edit_connector/index.tsx | 3 +- .../cases/components/status/button.test.tsx | 2 +- .../public/cases/components/status/button.tsx | 2 +- .../public/cases/components/status/config.ts | 2 +- .../cases/components/status/stats.test.tsx | 2 +- .../public/cases/components/status/stats.tsx | 2 +- .../cases/components/status/status.test.tsx | 2 +- .../public/cases/components/status/types.ts | 2 +- .../timeline_actions/add_to_case_action.tsx | 2 +- .../use_all_cases_modal/all_cases_modal.tsx | 2 +- .../components/use_all_cases_modal/index.tsx | 2 +- .../create_case_modal.tsx | 2 +- .../use_create_case_modal/index.tsx | 2 +- .../use_push_to_service/index.test.tsx | 3 +- .../components/use_push_to_service/index.tsx | 2 +- .../user_action_tree/helpers.test.tsx | 2 +- .../components/user_action_tree/helpers.tsx | 2 +- .../components/user_action_tree/index.tsx | 2 +- .../user_action_alert_comment_event.test.tsx | 2 +- .../user_action_alert_comment_event.tsx | 2 +- .../public/cases/containers/api.ts | 32 +- .../public/cases/containers/configure/api.ts | 14 +- .../containers/configure/use_configure.tsx | 2 +- .../public/cases/containers/types.ts | 18 +- .../cases/containers/use_bulk_update_case.tsx | 2 +- .../public/cases/containers/utils.ts | 16 +- .../use_manage_case_action.tsx | 2 +- .../plugins/security_solution/public/index.ts | 4 +- .../public/timelines/containers/api.ts | 2 +- .../plugins/security_solution/public/types.ts | 2 + 362 files changed, 17515 insertions(+), 324 deletions(-) create mode 100644 x-pack/plugins/cases/common/index.ts create mode 100644 x-pack/plugins/cases/public/common/errors.ts create mode 100644 x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts create mode 100644 x-pack/plugins/cases/public/common/lib/kibana/index.ts create mode 100644 x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts create mode 100644 x-pack/plugins/cases/public/common/lib/kibana/kibana_react.ts create mode 100644 x-pack/plugins/cases/public/common/lib/kibana/services.ts create mode 100644 x-pack/plugins/cases/public/common/mock/index.ts create mode 100644 x-pack/plugins/cases/public/common/mock/kibana_react.mock.ts create mode 100644 x-pack/plugins/cases/public/common/mock/test_providers.tsx create mode 100644 x-pack/plugins/cases/public/common/shared_imports.ts create mode 100644 x-pack/plugins/cases/public/common/test_utils.ts create mode 100644 x-pack/plugins/cases/public/common/translations.ts create mode 100644 x-pack/plugins/cases/public/components/__mock__/form.ts create mode 100644 x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/closure_options.test.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.test.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/connectors.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/field_mapping.test.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/field_mapping_row_static.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/index.test.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/index.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/mapping.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/translations.ts create mode 100644 x-pack/plugins/cases/public/components/configure_cases/utils.test.tsx create mode 100644 x-pack/plugins/cases/public/components/configure_cases/utils.ts create mode 100644 x-pack/plugins/cases/public/components/connector_selector/form.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connector_selector/form.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/card.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/case/index.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/case/translations.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/case/types.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/config.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/connectors_registry.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/fields_form.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/index.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/__mocks__/api.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/api.test.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/api.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/index.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/translations.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/types.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/mock.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/__mocks__/api.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/api.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/case_fields.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/index.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/translations.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/types.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/__mocks__/api.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/api.test.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/api.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/helpers.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/index.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/translations.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/types.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/types.ts create mode 100644 x-pack/plugins/cases/public/components/create/connector.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/connector.tsx create mode 100644 x-pack/plugins/cases/public/components/create/description.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/description.tsx create mode 100644 x-pack/plugins/cases/public/components/create/flyout.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/flyout.tsx create mode 100644 x-pack/plugins/cases/public/components/create/form.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/form.tsx create mode 100644 x-pack/plugins/cases/public/components/create/form_context.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/form_context.tsx create mode 100644 x-pack/plugins/cases/public/components/create/index.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/index.tsx create mode 100644 x-pack/plugins/cases/public/components/create/mock.ts create mode 100644 x-pack/plugins/cases/public/components/create/optional_field_label/index.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/optional_field_label/index.tsx create mode 100644 x-pack/plugins/cases/public/components/create/schema.tsx create mode 100644 x-pack/plugins/cases/public/components/create/submit_button.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/submit_button.tsx create mode 100644 x-pack/plugins/cases/public/components/create/sync_alerts_toggle.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx create mode 100644 x-pack/plugins/cases/public/components/create/tags.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/tags.tsx create mode 100644 x-pack/plugins/cases/public/components/create/title.test.tsx create mode 100644 x-pack/plugins/cases/public/components/create/title.tsx create mode 100644 x-pack/plugins/cases/public/components/create/translations.ts create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/editor.tsx create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/index.tsx create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/markdown_link.tsx create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/translations.ts create mode 100644 x-pack/plugins/cases/public/components/markdown_editor/types.ts create mode 100644 x-pack/plugins/cases/public/components/status/button.test.tsx create mode 100644 x-pack/plugins/cases/public/components/status/button.tsx create mode 100644 x-pack/plugins/cases/public/components/status/config.ts create mode 100644 x-pack/plugins/cases/public/components/status/index.ts create mode 100644 x-pack/plugins/cases/public/components/status/stats.test.tsx create mode 100644 x-pack/plugins/cases/public/components/status/stats.tsx create mode 100644 x-pack/plugins/cases/public/components/status/status.test.tsx create mode 100644 x-pack/plugins/cases/public/components/status/status.tsx create mode 100644 x-pack/plugins/cases/public/components/status/translations.ts create mode 100644 x-pack/plugins/cases/public/components/status/types.ts create mode 100644 x-pack/plugins/cases/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap create mode 100644 x-pack/plugins/cases/public/components/toasters/errors.ts create mode 100644 x-pack/plugins/cases/public/components/toasters/index.test.tsx create mode 100644 x-pack/plugins/cases/public/components/toasters/index.tsx create mode 100644 x-pack/plugins/cases/public/components/toasters/modal_all_errors.test.tsx create mode 100644 x-pack/plugins/cases/public/components/toasters/modal_all_errors.tsx create mode 100644 x-pack/plugins/cases/public/components/toasters/translations.ts create mode 100644 x-pack/plugins/cases/public/components/toasters/utils.test.ts create mode 100644 x-pack/plugins/cases/public/components/toasters/utils.ts create mode 100644 x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx create mode 100644 x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx create mode 100644 x-pack/plugins/cases/public/components/use_create_case_modal/index.test.tsx create mode 100644 x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx create mode 100644 x-pack/plugins/cases/public/components/wrappers/index.tsx create mode 100644 x-pack/plugins/cases/public/containers/__mocks__/api.ts create mode 100644 x-pack/plugins/cases/public/containers/api.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/api.ts create mode 100644 x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts create mode 100644 x-pack/plugins/cases/public/containers/configure/api.test.ts create mode 100644 x-pack/plugins/cases/public/containers/configure/api.ts create mode 100644 x-pack/plugins/cases/public/containers/configure/mock.ts create mode 100644 x-pack/plugins/cases/public/containers/configure/translations.ts create mode 100644 x-pack/plugins/cases/public/containers/configure/types.ts create mode 100644 x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/configure/use_action_types.tsx create mode 100644 x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/configure/use_configure.tsx create mode 100644 x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/configure/use_connectors.tsx create mode 100644 x-pack/plugins/cases/public/containers/constants.ts create mode 100644 x-pack/plugins/cases/public/containers/mock.ts create mode 100644 x-pack/plugins/cases/public/containers/translations.ts create mode 100644 x-pack/plugins/cases/public/containers/types.ts create mode 100644 x-pack/plugins/cases/public/containers/use_get_cases.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_cases.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_tags.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_get_tags.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_post_case.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_post_case.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx create mode 100644 x-pack/plugins/cases/public/containers/use_post_push_to_service.tsx create mode 100644 x-pack/plugins/cases/public/containers/utils.test.ts create mode 100644 x-pack/plugins/cases/public/containers/utils.ts create mode 100644 x-pack/plugins/cases/public/get_create_case.tsx create mode 100644 x-pack/plugins/cases/public/index.tsx create mode 100644 x-pack/plugins/cases/public/plugin.ts create mode 100644 x-pack/plugins/cases/public/types.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index f93849e011d41..1af74aa3d8828 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -108,3 +108,4 @@ pageLoadAssetSize: fileUpload: 25664 banners: 17946 mapsEms: 26072 + cases: 102558 diff --git a/x-pack/plugins/cases/common/api/index.ts b/x-pack/plugins/cases/common/api/index.ts index 7780564089d3d..2ef03dd96e315 100644 --- a/x-pack/plugins/cases/common/api/index.ts +++ b/x-pack/plugins/cases/common/api/index.ts @@ -7,6 +7,7 @@ export * from './cases'; export * from './connectors'; +export * from './helpers'; export * from './runtime_types'; export * from './saved_object'; export * from './user'; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 1e7cff99a00bd..d779ccd0b7ab0 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { DEFAULT_MAX_SIGNALS } from '../../security_solution/common/constants'; - +// The DEFAULT_MAX_SIGNALS value should match the one in `x-pack/plugins/security_solution/common/constants.ts` +// If either changes, engineer should ensure both values are updated +const DEFAULT_MAX_SIGNALS = 100; export const APP_ID = 'cases'; /** diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts new file mode 100644 index 0000000000000..37c11172b50b2 --- /dev/null +++ b/x-pack/plugins/cases/common/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './constants'; +export * from './api'; diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 1aaf84decbe36..27b36d7e86e1f 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -2,12 +2,13 @@ "configPath": ["xpack", "cases"], "id": "cases", "kibanaVersion": "kibana", - "requiredPlugins": ["actions", "securitySolution"], + "extraPublicDirs": ["common"], + "requiredPlugins": ["actions", "esUiShared", "kibanaReact", "triggersActionsUi"], "optionalPlugins": [ "spaces", "security" ], "server": true, - "ui": false, + "ui": true, "version": "8.0.0" } diff --git a/x-pack/plugins/cases/public/common/errors.ts b/x-pack/plugins/cases/public/common/errors.ts new file mode 100644 index 0000000000000..6edef08c1f4b1 --- /dev/null +++ b/x-pack/plugins/cases/public/common/errors.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { has } from 'lodash/fp'; + +export interface AppError { + name: string; + message: string; + body: { + message: string; + }; +} + +export interface KibanaError extends AppError { + body: { + message: string; + statusCode: number; + }; +} + +export interface CasesAppError extends AppError { + body: { + message: string; + status_code: number; + }; +} + +export const isKibanaError = (error: unknown): error is KibanaError => + has('message', error) && has('body.message', error) && has('body.statusCode', error); + +export const isCasesAppError = (error: unknown): error is CasesAppError => + has('message', error) && has('body.message', error) && has('body.status_code', error); + +export const isAppError = (error: unknown): error is AppError => + isKibanaError(error) || isCasesAppError(error); diff --git a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts new file mode 100644 index 0000000000000..392b71befe2b4 --- /dev/null +++ b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks'; +import { + createKibanaContextProviderMock, + createStartServicesMock, + createWithKibanaMock, +} from '../kibana_react.mock'; + +export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; +export const useKibana = jest.fn().mockReturnValue({ + services: createStartServicesMock(), +}); + +export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http); +export const useTimeZone = jest.fn(); +export const useDateFormat = jest.fn(); +export const useBasePath = jest.fn(() => '/test/base/path'); +export const useToasts = jest + .fn() + .mockReturnValue(notificationServiceMock.createStartContract().toasts); +export const useCurrentUser = jest.fn(); +export const withKibana = jest.fn(createWithKibanaMock()); +export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); +export const useGetUserSavedObjectPermissions = jest.fn(); diff --git a/x-pack/plugins/cases/public/common/lib/kibana/index.ts b/x-pack/plugins/cases/public/common/lib/kibana/index.ts new file mode 100644 index 0000000000000..a7f3c1e70ced5 --- /dev/null +++ b/x-pack/plugins/cases/public/common/lib/kibana/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './kibana_react'; +export * from './services'; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts new file mode 100644 index 0000000000000..326163f6cdc03 --- /dev/null +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { RecursivePartial } from '@elastic/eui/src/components/common'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../../types'; +import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; + +export const createStartServicesMock = (): StartServices => + (coreMock.createStart() as unknown) as StartServices; + +export const createWithKibanaMock = () => { + const services = createStartServicesMock(); + + return (Component: unknown) => (props: unknown) => { + return React.createElement(Component as string, { ...(props as object), kibana: { services } }); + }; +}; + +export const createKibanaContextProviderMock = () => { + const services = createStartServicesMock(); + + return ({ children }: { children: React.ReactNode }) => + React.createElement(KibanaContextProvider, { services }, children); +}; + +export const getMockTheme = (partialTheme: RecursivePartial): EuiTheme => + partialTheme as EuiTheme; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.ts new file mode 100644 index 0000000000000..e23fad392040c --- /dev/null +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + KibanaContextProvider, + useKibana, +} from '../../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../../types'; + +const useTypedKibana = () => useKibana(); + +export { KibanaContextProvider, useTypedKibana as useKibana }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/services.ts b/x-pack/plugins/cases/public/common/lib/kibana/services.ts new file mode 100644 index 0000000000000..94487bd3ca5e9 --- /dev/null +++ b/x-pack/plugins/cases/public/common/lib/kibana/services.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from 'kibana/public'; + +type GlobalServices = Pick; + +export class KibanaServices { + private static kibanaVersion?: string; + private static services?: GlobalServices; + + public static init({ http, kibanaVersion }: GlobalServices & { kibanaVersion: string }) { + this.services = { http }; + this.kibanaVersion = kibanaVersion; + } + + public static get(): GlobalServices { + if (!this.services) { + this.throwUninitializedError(); + } + + return this.services; + } + + public static getKibanaVersion(): string { + if (!this.kibanaVersion) { + this.throwUninitializedError(); + } + + return this.kibanaVersion; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Kibana services not initialized - are you trying to import this module from outside of the Cases app?' + ); + } +} diff --git a/x-pack/plugins/cases/public/common/mock/index.ts b/x-pack/plugins/cases/public/common/mock/index.ts new file mode 100644 index 0000000000000..add4c1c206dd4 --- /dev/null +++ b/x-pack/plugins/cases/public/common/mock/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 * from './test_providers'; diff --git a/x-pack/plugins/cases/public/common/mock/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/mock/kibana_react.mock.ts new file mode 100644 index 0000000000000..274462aec575d --- /dev/null +++ b/x-pack/plugins/cases/public/common/mock/kibana_react.mock.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from 'kibana/public'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import React from 'react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public/context'; + +export const createStartServicesMock = (): CoreStart => { + const core = coreMock.createStart(); + return (core as unknown) as CoreStart; +}; +export const createKibanaContextProviderMock = () => { + const services = coreMock.createStart(); + + return ({ children }: { children: React.ReactNode }) => + React.createElement(KibanaContextProvider, { services }, children); +}; diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx new file mode 100644 index 0000000000000..4e40f3b3cb745 --- /dev/null +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { I18nProvider } from '@kbn/i18n/react'; +import React from 'react'; +import { BehaviorSubject } from 'rxjs'; +import { ThemeProvider } from 'styled-components'; +import { createKibanaContextProviderMock, createStartServicesMock } from './kibana_react.mock'; +import { FieldHook } from '../shared_imports'; + +interface Props { + children: React.ReactNode; +} + +export const kibanaObservable = new BehaviorSubject(createStartServicesMock()); + +window.scrollTo = jest.fn(); +const MockKibanaContextProvider = createKibanaContextProviderMock(); + +/** A utility for wrapping children in the providers required to run most tests */ +const TestProvidersComponent: React.FC = ({ children }) => ( + + + ({ eui: euiDarkVars, darkMode: true })}>{children} + + +); + +export const TestProviders = React.memo(TestProvidersComponent); + +export const useFormFieldMock = (options?: Partial>): FieldHook => { + return { + path: 'path', + type: 'type', + value: ('mockedValue' as unknown) as T, + isPristine: false, + isValidating: false, + isValidated: false, + isChangingValue: false, + errors: [], + isValid: true, + getErrorsMessages: jest.fn(), + onChange: jest.fn(), + setValue: jest.fn(), + setErrors: jest.fn(), + clearErrors: jest.fn(), + validate: jest.fn(), + reset: jest.fn(), + __isIncludedInOutput: true, + __serializeValue: jest.fn(), + ...options, + }; +}; diff --git a/x-pack/plugins/cases/public/common/shared_imports.ts b/x-pack/plugins/cases/public/common/shared_imports.ts new file mode 100644 index 0000000000000..675204076b02a --- /dev/null +++ b/x-pack/plugins/cases/public/common/shared_imports.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + getUseField, + getFieldValidityAndErrorMessage, + FieldHook, + FieldValidateResponse, + FIELD_TYPES, + Form, + FormData, + FormDataProvider, + FormHook, + FormSchema, + UseField, + UseMultiFields, + useForm, + useFormContext, + useFormData, + ValidationError, + ValidationFunc, + VALIDATION_TYPES, +} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { + Field, + SelectField, +} from '../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/plugins/cases/public/common/test_utils.ts b/x-pack/plugins/cases/public/common/test_utils.ts new file mode 100644 index 0000000000000..f6ccf28bcb643 --- /dev/null +++ b/x-pack/plugins/cases/public/common/test_utils.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Convenience utility to remove text appended to links by EUI + */ +export const removeExternalLinkText = (str: string) => + str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts new file mode 100644 index 0000000000000..881acb9d4c90e --- /dev/null +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -0,0 +1,252 @@ +/* + * 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'; + +export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.cases.caseSavedObjectNoPermissionsTitle', + { + defaultMessage: 'Kibana feature privileges required', + } +); + +export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.cases.caseSavedObjectNoPermissionsMessage', + { + defaultMessage: + 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + } +); + +export const BACK_TO_ALL = i18n.translate('xpack.cases.caseView.backLabel', { + defaultMessage: 'Back to cases', +}); + +export const CANCEL = i18n.translate('xpack.cases.caseView.cancel', { + defaultMessage: 'Cancel', +}); + +export const DELETE_CASE = i18n.translate('xpack.cases.confirmDeleteCase.deleteCase', { + defaultMessage: 'Delete case', +}); + +export const DELETE_CASES = i18n.translate('xpack.cases.confirmDeleteCase.deleteCases', { + defaultMessage: 'Delete cases', +}); + +export const NAME = i18n.translate('xpack.cases.caseView.name', { + defaultMessage: 'Name', +}); + +export const OPENED_ON = i18n.translate('xpack.cases.caseView.openedOn', { + defaultMessage: 'Opened on', +}); + +export const CLOSED_ON = i18n.translate('xpack.cases.caseView.closedOn', { + defaultMessage: 'Closed on', +}); + +export const REPORTER = i18n.translate('xpack.cases.caseView.reporterLabel', { + defaultMessage: 'Reporter', +}); + +export const PARTICIPANTS = i18n.translate('xpack.cases.caseView.particpantsLabel', { + defaultMessage: 'Participants', +}); + +export const CREATE_BC_TITLE = i18n.translate('xpack.cases.caseView.breadcrumb', { + defaultMessage: 'Create', +}); + +export const CREATE_TITLE = i18n.translate('xpack.cases.caseView.create', { + defaultMessage: 'Create new case', +}); + +export const DESCRIPTION = i18n.translate('xpack.cases.caseView.description', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.cases.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const COMMENT_REQUIRED = i18n.translate('xpack.cases.caseView.commentFieldRequiredError', { + defaultMessage: 'A comment is required.', +}); + +export const REQUIRED_FIELD = i18n.translate('xpack.cases.caseView.fieldRequiredError', { + defaultMessage: 'Required field', +}); + +export const EDIT = i18n.translate('xpack.cases.caseView.edit', { + defaultMessage: 'Edit', +}); + +export const OPTIONAL = i18n.translate('xpack.cases.caseView.optional', { + defaultMessage: 'Optional', +}); + +export const PAGE_TITLE = i18n.translate('xpack.cases.pageTitle', { + defaultMessage: 'Cases', +}); + +export const CREATE_CASE = i18n.translate('xpack.cases.caseView.createCase', { + defaultMessage: 'Create case', +}); + +export const CLOSE_CASE = i18n.translate('xpack.cases.caseView.closeCase', { + defaultMessage: 'Close case', +}); + +export const MARK_CASE_IN_PROGRESS = i18n.translate('xpack.cases.caseView.markInProgress', { + defaultMessage: 'Mark in progress', +}); + +export const REOPEN_CASE = i18n.translate('xpack.cases.caseView.reopenCase', { + defaultMessage: 'Reopen case', +}); + +export const OPEN_CASE = i18n.translate('xpack.cases.caseView.openCase', { + defaultMessage: 'Open case', +}); + +export const CASE_NAME = i18n.translate('xpack.cases.caseView.caseName', { + defaultMessage: 'Case name', +}); + +export const TO = i18n.translate('xpack.cases.caseView.to', { + defaultMessage: 'to', +}); + +export const TAGS = i18n.translate('xpack.cases.caseView.tags', { + defaultMessage: 'Tags', +}); + +export const ACTIONS = i18n.translate('xpack.cases.allCases.actions', { + defaultMessage: 'Actions', +}); + +export const NO_TAGS_AVAILABLE = i18n.translate('xpack.cases.allCases.noTagsAvailable', { + defaultMessage: 'No tags available', +}); + +export const NO_REPORTERS_AVAILABLE = i18n.translate('xpack.cases.caseView.noReportersAvailable', { + defaultMessage: 'No reporters available.', +}); + +export const COMMENTS = i18n.translate('xpack.cases.allCases.comments', { + defaultMessage: 'Comments', +}); + +export const TAGS_HELP = i18n.translate('xpack.cases.createCase.fieldTagsHelpText', { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', +}); + +export const NO_TAGS = i18n.translate('xpack.cases.caseView.noTags', { + defaultMessage: 'No tags are currently assigned to this case.', +}); + +export const TITLE_REQUIRED = i18n.translate('xpack.cases.createCase.titleFieldRequiredError', { + defaultMessage: 'A title is required.', +}); + +export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate('xpack.cases.configureCases.headerTitle', { + defaultMessage: 'Configure cases', +}); + +export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.cases.configureCasesButton', { + defaultMessage: 'Edit external connection', +}); + +export const ADD_COMMENT = i18n.translate('xpack.cases.caseView.comment.addComment', { + defaultMessage: 'Add comment', +}); + +export const ADD_COMMENT_HELP_TEXT = i18n.translate( + 'xpack.cases.caseView.comment.addCommentHelpText', + { + defaultMessage: 'Add a new comment...', + } +); + +export const SAVE = i18n.translate('xpack.cases.caseView.description.save', { + defaultMessage: 'Save', +}); + +export const GO_TO_DOCUMENTATION = i18n.translate('xpack.cases.caseView.goToDocumentationButton', { + defaultMessage: 'View documentation', +}); + +export const CONNECTORS = i18n.translate('xpack.cases.caseView.connectors', { + defaultMessage: 'External Incident Management System', +}); + +export const EDIT_CONNECTOR = i18n.translate('xpack.cases.caseView.editConnector', { + defaultMessage: 'Change external incident management system', +}); + +export const NO_CONNECTOR = i18n.translate('xpack.cases.common.noConnector', { + defaultMessage: 'No connector selected', +}); + +export const UNKNOWN = i18n.translate('xpack.cases.caseView.unknown', { + defaultMessage: 'Unknown', +}); + +export const MARKED_CASE_AS = i18n.translate('xpack.cases.caseView.markedCaseAs', { + defaultMessage: 'marked case as', +}); + +export const OPEN_CASES = i18n.translate('xpack.cases.caseTable.openCases', { + defaultMessage: 'Open cases', +}); + +export const CLOSED_CASES = i18n.translate('xpack.cases.caseTable.closedCases', { + defaultMessage: 'Closed cases', +}); + +export const IN_PROGRESS_CASES = i18n.translate('xpack.cases.caseTable.inProgressCases', { + defaultMessage: 'In progress cases', +}); + +export const SYNC_ALERTS_SWITCH_LABEL_ON = i18n.translate( + 'xpack.cases.settings.syncAlertsSwitchLabelOn', + { + defaultMessage: 'On', + } +); + +export const SYNC_ALERTS_SWITCH_LABEL_OFF = i18n.translate( + 'xpack.cases.settings.syncAlertsSwitchLabelOff', + { + defaultMessage: 'Off', + } +); + +export const SYNC_ALERTS_HELP = i18n.translate('xpack.cases.components.create.syncAlertHelpText', { + defaultMessage: + 'Enabling this option will sync the status of alerts in this case with the case status.', +}); + +export const ALERT = i18n.translate('xpack.cases.common.alertLabel', { + defaultMessage: 'Alert', +}); + +export const ALERT_ADDED_TO_CASE = i18n.translate('xpack.cases.common.alertAddedToCase', { + defaultMessage: 'added to case', +}); + +export const SELECTABLE_MESSAGE_COLLECTIONS = i18n.translate( + 'xpack.cases.common.allCases.table.selectableMessageCollections', + { + defaultMessage: 'Cases with sub-cases cannot be selected', + } +); diff --git a/x-pack/plugins/cases/public/components/__mock__/form.ts b/x-pack/plugins/cases/public/components/__mock__/form.ts new file mode 100644 index 0000000000000..6d3e8353e630a --- /dev/null +++ b/x-pack/plugins/cases/public/components/__mock__/form.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useForm } from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { useFormData } from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; + +jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'); +jest.mock( + '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' +); + +export const mockFormHook = { + isSubmitted: false, + isSubmitting: false, + isValid: true, + submit: jest.fn(), + subscribe: jest.fn(), + setFieldValue: jest.fn(), + setFieldErrors: jest.fn(), + getFields: jest.fn(), + getFormData: jest.fn(), + /* Returns a list of all errors in the form */ + getErrors: jest.fn(), + reset: jest.fn(), + __options: {}, + __formData$: {}, + __addField: jest.fn(), + __removeField: jest.fn(), + __validateFields: jest.fn(), + __updateFormDataAt: jest.fn(), + __readFieldConfigFromSchema: jest.fn(), + __getFieldDefaultValue: jest.fn(), +}; + +export const getFormMock = (sampleData: any) => ({ + ...mockFormHook, + submit: () => + Promise.resolve({ + data: sampleData, + isValid: true, + }), + getFormData: () => sampleData, +}); + +export const useFormMock = useForm as jest.Mock; +export const useFormDataMock = useFormData as jest.Mock; diff --git a/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx new file mode 100644 index 0000000000000..e3abbeadd2d3c --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorTypes } from '../../../../common'; +import { ActionConnector } from '../../../containers/configure/types'; +import { UseConnectorsResponse } from '../../../containers/configure/use_connectors'; +import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; +import { UseActionTypesResponse } from '../../../containers/configure/use_action_types'; +import { connectorsMock, actionTypesMock } from '../../../containers/configure/mock'; +export { mappings } from '../../../containers/configure/mock'; +export const connectors: ActionConnector[] = connectorsMock; + +export const searchURL = + '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; + +export const useCaseConfigureResponse: ReturnUseCaseConfigure = { + closureType: 'close-by-user', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + currentConfiguration: { + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + closureType: 'close-by-user', + }, + firstLoad: false, + loading: false, + mappings: [], + persistCaseConfigure: jest.fn(), + persistLoading: false, + refetchCaseConfigure: jest.fn(), + setClosureType: jest.fn(), + setConnector: jest.fn(), + setCurrentConfiguration: jest.fn(), + setMappings: jest.fn(), + version: '', +}; + +export const useConnectorsResponse: UseConnectorsResponse = { + loading: false, + connectors, + refetchConnectors: jest.fn(), +}; + +export const useActionTypesResponse: UseActionTypesResponse = { + loading: false, + actionTypes: actionTypesMock, + refetchActionTypes: jest.fn(), +}; diff --git a/x-pack/plugins/cases/public/components/configure_cases/closure_options.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/closure_options.test.tsx new file mode 100644 index 0000000000000..56123a934d51f --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/closure_options.test.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { ClosureOptions, ClosureOptionsProps } from './closure_options'; +import { TestProviders } from '../../common/mock'; +import { ClosureOptionsRadio } from './closure_options_radio'; + +describe('ClosureOptions', () => { + let wrapper: ReactWrapper; + const onChangeClosureType = jest.fn(); + const props: ClosureOptionsProps = { + disabled: false, + closureTypeSelected: 'close-by-user', + onChangeClosureType, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows the closure options form group', () => { + expect( + wrapper.find('[data-test-subj="case-closure-options-form-group"]').first().exists() + ).toBe(true); + }); + + test('it shows the closure options form row', () => { + expect(wrapper.find('[data-test-subj="case-closure-options-form-row"]').first().exists()).toBe( + true + ); + }); + + test('it shows closure options', () => { + expect(wrapper.find('[data-test-subj="case-closure-options-radio"]').first().exists()).toBe( + true + ); + }); + + test('it pass the correct props to child', () => { + const closureOptionsRadioComponent = wrapper.find(ClosureOptionsRadio); + expect(closureOptionsRadioComponent.props().disabled).toEqual(false); + expect(closureOptionsRadioComponent.props().closureTypeSelected).toEqual('close-by-user'); + expect(closureOptionsRadioComponent.props().onChangeClosureType).toEqual(onChangeClosureType); + }); + + test('the closure type is changed successfully', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + + expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx b/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx new file mode 100644 index 0000000000000..ba892116320ce --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +import { ClosureType } from '../../containers/configure/types'; +import { ClosureOptionsRadio } from './closure_options_radio'; +import * as i18n from './translations'; + +export interface ClosureOptionsProps { + closureTypeSelected: ClosureType; + disabled: boolean; + onChangeClosureType: (newClosureType: ClosureType) => void; +} + +const ClosureOptionsComponent: React.FC = ({ + closureTypeSelected, + disabled, + onChangeClosureType, +}) => { + return ( + {i18n.CASE_CLOSURE_OPTIONS_TITLE}

} + description={ + <> +

{i18n.CASE_CLOSURE_OPTIONS_DESC}

+

{i18n.CASE_COLSURE_OPTIONS_SUB_CASES}

+ + } + data-test-subj="case-closure-options-form-group" + > + + + + + ); +}; + +export const ClosureOptions = React.memo(ClosureOptionsComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.test.tsx new file mode 100644 index 0000000000000..b9885b4e07d48 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { ReactWrapper, mount } from 'enzyme'; + +import { ClosureOptionsRadio, ClosureOptionsRadioComponentProps } from './closure_options_radio'; +import { TestProviders } from '../../common/mock'; + +describe('ClosureOptionsRadio', () => { + let wrapper: ReactWrapper; + const onChangeClosureType = jest.fn(); + const props: ClosureOptionsRadioComponentProps = { + disabled: false, + closureTypeSelected: 'close-by-user', + onChangeClosureType, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect(wrapper.find('[data-test-subj="closure-options-radio-group"]').first().exists()).toBe( + true + ); + }); + + test('it shows the correct number of radio buttons', () => { + expect(wrapper.find('input[name="closure_options"]')).toHaveLength(2); + }); + + test('it renders close by user radio button', () => { + expect(wrapper.find('input[id="close-by-user"]').exists()).toBeTruthy(); + }); + + test('it renders close by pushing radio button', () => { + expect(wrapper.find('input[id="close-by-pushing"]').exists()).toBeTruthy(); + }); + + test('it disables the close by user radio button', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('input[id="close-by-user"]').prop('disabled')).toEqual(true); + }); + + test('it disables correctly the close by pushing radio button', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('input[id="close-by-pushing"]').prop('disabled')).toEqual(true); + }); + + test('it selects the correct radio button', () => { + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + expect(newWrapper.find('input[id="close-by-pushing"]').prop('checked')).toEqual(true); + }); + + test('it calls the onChangeClosureType function', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + expect(onChangeClosureType).toHaveBeenCalled(); + expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.tsx b/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.tsx new file mode 100644 index 0000000000000..cb6fa0953a796 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/closure_options_radio.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode, useCallback } from 'react'; +import { EuiRadioGroup } from '@elastic/eui'; + +import { ClosureType } from '../../containers/configure/types'; +import * as i18n from './translations'; + +interface ClosureRadios { + id: ClosureType; + label: ReactNode; +} + +const radios: ClosureRadios[] = [ + { + id: 'close-by-user', + label: i18n.CASE_CLOSURE_OPTIONS_MANUAL, + }, + { + id: 'close-by-pushing', + label: i18n.CASE_CLOSURE_OPTIONS_NEW_INCIDENT, + }, +]; + +export interface ClosureOptionsRadioComponentProps { + closureTypeSelected: ClosureType; + disabled: boolean; + onChangeClosureType: (newClosureType: ClosureType) => void; +} + +const ClosureOptionsRadioComponent: React.FC = ({ + closureTypeSelected, + disabled, + onChangeClosureType, +}) => { + const onChangeLocal = useCallback( + (id: string) => { + onChangeClosureType(id as ClosureType); + }, + [onChangeClosureType] + ); + + return ( + + ); +}; + +export const ClosureOptionsRadio = React.memo(ClosureOptionsRadioComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx new file mode 100644 index 0000000000000..d5b9a885f2c6d --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.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 { mount, ReactWrapper } from 'enzyme'; + +import { Connectors, Props } from './connectors'; +import { TestProviders } from '../../common/mock'; +import { ConnectorsDropdown } from './connectors_dropdown'; +import { connectors } from './__mock__'; +import { ConnectorTypes } from '../../../common'; + +describe('Connectors', () => { + let wrapper: ReactWrapper; + const onChangeConnector = jest.fn(); + const handleShowEditFlyout = jest.fn(); + + const props: Props = { + connectors, + disabled: false, + handleShowEditFlyout, + isLoading: false, + mappings: [], + onChangeConnector, + selectedConnector: { id: 'none', type: ConnectorTypes.none }, + updateConnectorDisabled: false, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows the connectors from group', () => { + expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').first().exists()).toBe( + true + ); + }); + + test('it shows the connectors form row', () => { + expect(wrapper.find('[data-test-subj="case-connectors-form-row"]').first().exists()).toBe(true); + }); + + test('it shows the connectors dropdown', () => { + expect(wrapper.find('[data-test-subj="case-connectors-dropdown"]').first().exists()).toBe(true); + }); + + test('it pass the correct props to child', () => { + const connectorsDropdownProps = wrapper.find(ConnectorsDropdown).props(); + expect(connectorsDropdownProps).toMatchObject({ + disabled: false, + isLoading: false, + connectors, + selectedConnector: 'none', + onChange: props.onChangeConnector, + }); + }); + + test('the connector is changed successfully', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); + + expect(onChangeConnector).toHaveBeenCalled(); + expect(onChangeConnector).toHaveBeenCalledWith('resilient-2'); + }); + + test('the connector is changed successfully to none', () => { + onChangeConnector.mockClear(); + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + newWrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + newWrapper.find('button[data-test-subj="dropdown-connector-no-connector"]').simulate('click'); + + expect(onChangeConnector).toHaveBeenCalled(); + expect(onChangeConnector).toHaveBeenCalledWith('none'); + }); + + test('it shows the add connector button', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').exists() + ).toBeTruthy(); + }); + + test('the text of the update button is shown correctly', () => { + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .text() + ).toBe('Update My Connector'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx new file mode 100644 index 0000000000000..45be02e05e1f0 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -0,0 +1,119 @@ +/* + * 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 { + EuiDescribedFormGroup, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiLink, +} from '@elastic/eui'; + +import styled from 'styled-components'; + +import { ConnectorsDropdown } from './connectors_dropdown'; +import * as i18n from './translations'; + +import { ActionConnector, CaseConnectorMapping } from '../../containers/configure/types'; +import { Mapping } from './mapping'; +import { ConnectorTypes } from '../../../common'; + +const EuiFormRowExtended = styled(EuiFormRow)` + .euiFormRow__labelWrapper { + .euiFormRow__label { + width: 100%; + } + } +`; + +export interface Props { + connectors: ActionConnector[]; + disabled: boolean; + handleShowEditFlyout: () => void; + isLoading: boolean; + mappings: CaseConnectorMapping[]; + onChangeConnector: (id: string) => void; + selectedConnector: { id: string; type: string }; + updateConnectorDisabled: boolean; +} +const ConnectorsComponent: React.FC = ({ + connectors, + disabled, + handleShowEditFlyout, + isLoading, + mappings, + onChangeConnector, + selectedConnector, + updateConnectorDisabled, +}) => { + const connectorsName = useMemo( + () => connectors.find((c) => c.id === selectedConnector.id)?.name ?? 'none', + [connectors, selectedConnector.id] + ); + + const dropDownLabel = useMemo( + () => ( + + {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} + + {connectorsName !== 'none' && ( + + {i18n.UPDATE_SELECTED_CONNECTOR(connectorsName)} + + )} + + + ), + [connectorsName, handleShowEditFlyout, updateConnectorDisabled] + ); + return ( + <> + {i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}} + description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC} + data-test-subj="case-connectors-form-group" + > + + + + + + {selectedConnector.type !== ConnectorTypes.none ? ( + + + + ) : null} + + + + + ); +}; + +export const Connectors = React.memo(ConnectorsComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx new file mode 100644 index 0000000000000..5149052d9a4bf --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -0,0 +1,203 @@ +/* + * 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, ReactWrapper } from 'enzyme'; +import { EuiSuperSelect } from '@elastic/eui'; + +import { ConnectorsDropdown, Props } from './connectors_dropdown'; +import { TestProviders } from '../../common/mock'; +import { connectors } from './__mock__'; + +describe('ConnectorsDropdown', () => { + let wrapper: ReactWrapper; + const props: Props = { + disabled: false, + connectors, + isLoading: false, + onChange: jest.fn(), + selectedConnector: 'none', + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().exists()).toBe(true); + }); + + test('it formats the connectors correctly', () => { + const selectProps = wrapper.find(EuiSuperSelect).props(); + + expect(selectProps.options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "dropdown-connector-no-connector", + "inputDisplay": + + + + + + No connector selected + + + , + "value": "none", + }, + Object { + "data-test-subj": "dropdown-connector-servicenow-1", + "inputDisplay": + + + + + + My Connector + + + , + "value": "servicenow-1", + }, + Object { + "data-test-subj": "dropdown-connector-resilient-2", + "inputDisplay": + + + + + + My Connector 2 + + + , + "value": "resilient-2", + }, + Object { + "data-test-subj": "dropdown-connector-jira-1", + "inputDisplay": + + + + + + Jira + + + , + "value": "jira-1", + }, + Object { + "data-test-subj": "dropdown-connector-servicenow-sir", + "inputDisplay": + + + + + + My Connector SIR + + + , + "value": "servicenow-sir", + }, + ] + `); + }); + + test('it disables the dropdown', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled') + ).toEqual(true); + }); + + test('it loading correctly', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading') + ).toEqual(true); + }); + + test('it selects the correct connector', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('button span:not([data-euiicon-type])').text()).toEqual('My Connector'); + }); + + test('if the props hideConnectorServiceNowSir is true, the connector should not be part of the list of options ', () => { + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + const selectProps = newWrapper.find(EuiSuperSelect).props(); + const options = selectProps.options as Array<{ 'data-test-subj': string }>; + expect( + options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-1') + ).toBeTruthy(); + expect( + options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-sir') + ).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx new file mode 100644 index 0000000000000..21ef5c490b17a --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -0,0 +1,121 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import styled from 'styled-components'; + +import { ConnectorTypes } from '../../../common'; +import { ActionConnector } from '../../containers/configure/types'; +import { connectorsConfiguration } from '../connectors'; +import * as i18n from './translations'; + +export interface Props { + connectors: ActionConnector[]; + disabled: boolean; + isLoading: boolean; + onChange: (id: string) => void; + selectedConnector: string; + appendAddConnectorButton?: boolean; + hideConnectorServiceNowSir?: boolean; +} + +const ICON_SIZE = 'm'; + +const EuiIconExtended = styled(EuiIcon)` + margin-right: 13px; + margin-bottom: 0 !important; +`; + +const noConnectorOption = { + value: 'none', + inputDisplay: ( + + + + + + {i18n.NO_CONNECTOR} + + + ), + 'data-test-subj': 'dropdown-connector-no-connector', +}; + +const addNewConnector = { + value: 'add-connector', + inputDisplay: ( + + {i18n.ADD_NEW_CONNECTOR} + + ), + 'data-test-subj': 'dropdown-connector-add-connector', +}; + +const ConnectorsDropdownComponent: React.FC = ({ + connectors, + disabled, + isLoading, + onChange, + selectedConnector, + appendAddConnectorButton = false, + hideConnectorServiceNowSir = false, +}) => { + const connectorsAsOptions = useMemo(() => { + const connectorsFormatted = connectors.reduce( + (acc, connector) => { + if (hideConnectorServiceNowSir && connector.actionTypeId === ConnectorTypes.serviceNowSIR) { + return acc; + } + + return [ + ...acc, + { + value: connector.id, + inputDisplay: ( + + + + + + {connector.name} + + + ), + 'data-test-subj': `dropdown-connector-${connector.id}`, + }, + ]; + }, + [noConnectorOption] + ); + + if (appendAddConnectorButton) { + return [...connectorsFormatted, addNewConnector]; + } + + return connectorsFormatted; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connectors]); + + return ( + + ); +}; + +export const ConnectorsDropdown = React.memo(ConnectorsDropdownComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/field_mapping.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.test.tsx new file mode 100644 index 0000000000000..8c2a66ad7ee53 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { FieldMapping, FieldMappingProps } from './field_mapping'; +import { mappings } from './__mock__'; +import { TestProviders } from '../../common/mock'; +import { FieldMappingRowStatic } from './field_mapping_row_static'; + +describe('FieldMappingRow', () => { + let wrapper: ReactWrapper; + const props: FieldMappingProps = { + isLoading: false, + mappings, + connectorActionTypeId: '.servicenow', + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + test('it renders', () => { + expect( + wrapper.find('[data-test-subj="case-configure-field-mappings-row-wrapper"]').first().exists() + ).toBe(true); + + expect(wrapper.find(FieldMappingRowStatic).length).toEqual(3); + }); + + test('it does not render without mappings', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect( + newWrapper + .find('[data-test-subj="case-configure-field-mappings-row-wrapper"]') + .first() + .exists() + ).toBe(false); + }); + + test('it pass the corrects props to mapping row', () => { + const rows = wrapper.find(FieldMappingRowStatic); + rows.forEach((row, index) => { + expect(row.prop('casesField')).toEqual(mappings[index].source); + expect(row.prop('selectedActionType')).toEqual(mappings[index].actionType); + expect(row.prop('selectedThirdParty')).toEqual(mappings[index].target); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.tsx new file mode 100644 index 0000000000000..ef7e8ecda0c87 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/field_mapping.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 React, { useMemo } from 'react'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import styled from 'styled-components'; + +import { FieldMappingRowStatic } from './field_mapping_row_static'; +import * as i18n from './translations'; + +import { CaseConnectorMapping } from '../../containers/configure/types'; +import { connectorsConfiguration } from '../connectors'; + +const FieldRowWrapper = styled.div` + margin: 10px 0; + font-size: 14px; +`; + +export interface FieldMappingProps { + connectorActionTypeId: string; + isLoading: boolean; + mappings: CaseConnectorMapping[]; +} + +const FieldMappingComponent: React.FC = ({ + connectorActionTypeId, + isLoading, + mappings, +}) => { + const selectedConnector = useMemo( + () => connectorsConfiguration[connectorActionTypeId] ?? { fields: {} }, + [connectorActionTypeId] + ); + return mappings.length ? ( + + + {' '} + + + {i18n.FIELD_MAPPING_FIRST_COL} + + + + {i18n.FIELD_MAPPING_SECOND_COL(selectedConnector.name)} + + + + {i18n.FIELD_MAPPING_THIRD_COL} + + + + + + {mappings.map((item) => ( + + ))} + + + + ) : null; +}; + +export const FieldMapping = React.memo(FieldMappingComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/field_mapping_row_static.tsx b/x-pack/plugins/cases/public/components/configure_cases/field_mapping_row_static.tsx new file mode 100644 index 0000000000000..52672197ecb55 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/field_mapping_row_static.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiCode, EuiFlexItem, EuiFlexGroup, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; + +import { capitalize } from 'lodash/fp'; +import { CaseField, ActionType, ThirdPartyField } from '../../containers/configure/types'; + +export interface RowProps { + isLoading: boolean; + casesField: CaseField; + selectedActionType: ActionType; + selectedThirdParty: ThirdPartyField; +} + +const FieldMappingRowComponent: React.FC = ({ + isLoading, + casesField, + selectedActionType, + selectedThirdParty, +}) => { + const selectedActionTypeCapitalized = useMemo(() => capitalize(selectedActionType), [ + selectedActionType, + ]); + return ( + + + + + {casesField} + + + + + + + + + + {isLoading ? ( + + ) : ( + {selectedThirdParty} + )} + + + + + {isLoading ? : selectedActionTypeCapitalized} + + + ); +}; + +export const FieldMappingRowStatic = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx new file mode 100644 index 0000000000000..898d6cde19a77 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -0,0 +1,591 @@ +/* + * 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 { ReactWrapper, mount } from 'enzyme'; + +import { ConfigureCases } from '.'; +import { TestProviders } from '../../common/mock'; +import { Connectors } from './connectors'; +import { ClosureOptions } from './closure_options'; +import { + ActionConnector, + ConnectorAddFlyout, + ConnectorEditFlyout, + TriggersAndActionsUIPublicPluginStart, +} from '../../../../triggers_actions_ui/public'; +import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; + +import { useKibana } from '../../common/lib/kibana'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useActionTypes } from '../../containers/configure/use_action_types'; + +import { + connectors, + searchURL, + useCaseConfigureResponse, + useConnectorsResponse, + useActionTypesResponse, +} from './__mock__'; +import { ConnectorTypes } from '../../../common'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_configure'); +jest.mock('../../containers/configure/use_action_types'); + +const useKibanaMock = useKibana as jest.Mocked; +const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; +const useGetUrlSearchMock = jest.fn(); +const useActionTypesMock = useActionTypes as jest.Mock; + +describe('ConfigureCases', () => { + beforeEach(() => { + useKibanaMock().services.triggersActionsUi = ({ + actionTypeRegistry: actionTypeRegistryMock.create(), + getAddConnectorFlyout: jest.fn().mockImplementation(() => ( + {}} + actionTypeRegistry={actionTypeRegistryMock.create()} + actionTypes={[ + { + id: '.servicenow', + name: 'servicenow', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold', + }, + { + id: '.jira', + name: 'jira', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold', + }, + { + id: '.resilient', + name: 'resilient', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold', + }, + ]} + /> + )), + getEditConnectorFlyout: jest + .fn() + .mockImplementation(() => ( + {}} + actionTypeRegistry={actionTypeRegistryMock.create()} + initialConnector={connectors[1] as ActionConnector} + /> + )), + } as unknown) as TriggersAndActionsUIPublicPluginStart; + + useActionTypesMock.mockImplementation(() => useActionTypesResponse); + }); + + describe('rendering', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders the Connectors', () => { + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').exists()).toBeTruthy(); + }); + + test('it renders the ClosureType', () => { + expect(wrapper.find('[data-test-subj="closure-options-radio-group"]').exists()).toBeTruthy(); + }); + + test('it does NOT render the ConnectorAddFlyout', () => { + // Components from triggersActionsUi do not have a data-test-subj + expect(wrapper.find(ConnectorAddFlyout).exists()).toBeFalsy(); + }); + + test('it does NOT render the ConnectorEditFlyout', () => { + // Components from triggersActionsUi do not have a data-test-subj + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); + }); + + test('it does NOT render the EuiCallOut', () => { + expect( + wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() + ).toBeFalsy(); + }); + }); + + describe('Unhappy path', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + closureType: 'close-by-user', + connector: { + id: 'not-id', + name: 'unchanged', + type: ConnectorTypes.none, + fields: null, + }, + currentConfiguration: { + connector: { + id: 'not-id', + name: 'unchanged', + type: ConnectorTypes.none, + fields: null, + }, + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows the warning callout when configuration is invalid', () => { + expect( + wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() + ).toBeTruthy(); + }); + + test('it hides the update connector button when the connectorId is invalid', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .exists() + ).toBeFalsy(); + }); + }); + + describe('Happy path', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mappings: [], + closureType: 'close-by-user', + connector: { + id: 'servicenow-1', + name: 'unchanged', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + currentConfiguration: { + connector: { + id: 'servicenow-1', + name: 'unchanged', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders with correct props', () => { + // Connector + expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); + expect(wrapper.find(Connectors).prop('disabled')).toBe(false); + expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); + expect(wrapper.find(Connectors).prop('selectedConnector').id).toBe('servicenow-1'); + + // ClosureOptions + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); + expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); + + // Flyouts + expect(wrapper.find(ConnectorAddFlyout).exists()).toBe(false); + expect(wrapper.find(ConnectorEditFlyout).exists()).toBe(false); + }); + + test('it disables correctly when the user cannot crud', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe( + true + ); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); + + // Two closure options + expect( + newWrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .first() + .prop('disabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .at(1) + .prop('disabled') + ).toBe(true); + }); + }); + + describe('loading connectors', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: null, + closureType: 'close-by-user', + connector: { + id: 'resilient-2', + name: 'unchanged', + type: ConnectorTypes.resilient, + fields: null, + }, + currentConfiguration: { + connector: { + id: 'servicenow-1', + name: 'unchanged', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + closureType: 'close-by-user', + }, + })); + + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it disables correctly Connector when loading connectors', () => { + expect( + wrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled') + ).toBeTruthy(); + }); + + test('it pass the correct value to isLoading attribute on Connector', () => { + expect(wrapper.find(Connectors).prop('isLoading')).toBe(true); + }); + + test('it disables correctly ClosureOptions when loading connectors', () => { + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it hides the update connector button when loading the connectors', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); + }); + + test('it shows isLoading when loading action types', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: false, + })); + + useActionTypesMock.mockImplementation(() => ({ ...useActionTypesResponse, loading: true })); + + wrapper = mount(, { wrappingComponent: TestProviders }); + expect(wrapper.find(Connectors).prop('isLoading')).toBe(true); + }); + }); + + describe('saving configuration', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + connector: { + id: 'servicenow-1', + name: 'SN', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + persistLoading: true, + })); + + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it disables correctly Connector when saving configuration', () => { + expect(wrapper.find(Connectors).prop('disabled')).toBe(true); + }); + + test('it disables correctly ClosureOptions when saving configuration', () => { + expect( + wrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .first() + .prop('disabled') + ).toBe(true); + + expect( + wrapper.find('[data-test-subj="closure-options-radio-group"] input').at(1).prop('disabled') + ).toBe(true); + }); + + test('it disables the update connector button when saving the configuration', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); + }); + }); + + describe('loading configuration', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + loading: true, + })); + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + })); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it hides the update connector button when loading the configuration', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .exists() + ).toBeFalsy(); + }); + }); + + describe('connectors', () => { + let wrapper: ReactWrapper; + let persistCaseConfigure: jest.Mock; + + beforeEach(() => { + persistCaseConfigure = jest.fn(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: null, + closureType: 'close-by-user', + connector: { + id: 'resilient-2', + name: 'My connector', + type: ConnectorTypes.resilient, + fields: null, + }, + currentConfiguration: { + connector: { + id: 'My connector', + name: 'My connector', + type: ConnectorTypes.jira, + fields: null, + }, + closureType: 'close-by-user', + }, + persistCaseConfigure, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it submits the configuration correctly when changing connector', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); + wrapper.update(); + + expect(persistCaseConfigure).toHaveBeenCalled(); + expect(persistCaseConfigure).toHaveBeenCalledWith({ + connector: { + id: 'resilient-2', + name: 'My Connector 2', + type: ConnectorTypes.resilient, + fields: null, + }, + closureType: 'close-by-user', + }); + }); + + test('the text of the update button is changed successfully', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + connector: { + id: 'servicenow-1', + name: 'My connector', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + connector: { + id: 'resilient-2', + name: 'My connector 2', + type: ConnectorTypes.resilient, + fields: null, + }, + })); + + wrapper = mount(, { wrappingComponent: TestProviders }); + + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .text() + ).toBe('Update My Connector 2'); + }); + }); +}); + +describe('closure options', () => { + let wrapper: ReactWrapper; + let persistCaseConfigure: jest.Mock; + + beforeEach(() => { + persistCaseConfigure = jest.fn(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: null, + closureType: 'close-by-user', + connector: { + id: 'servicenow-1', + name: 'My connector', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + currentConfiguration: { + connector: { + id: 'My connector', + name: 'My connector', + type: ConnectorTypes.jira, + fields: null, + }, + closureType: 'close-by-user', + }, + persistCaseConfigure, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it submits the configuration correctly when changing closure type', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect(persistCaseConfigure).toHaveBeenCalled(); + expect(persistCaseConfigure).toHaveBeenCalledWith({ + connector: { + id: 'servicenow-1', + name: 'My connector', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + closureType: 'close-by-pushing', + }); + }); +}); + +describe('user interactions', () => { + beforeEach(() => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: null, + closureType: 'close-by-user', + connector: { + id: 'resilient-2', + name: 'unchanged', + type: ConnectorTypes.resilient, + fields: null, + }, + currentConfiguration: { + connector: { + id: 'resilient-2', + name: 'unchanged', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useGetUrlSearchMock.mockImplementation(() => searchURL); + }); + + test('it show the add flyout when pressing the add connector button', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorAddFlyout).exists()).toBe(true); + expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ + expect.objectContaining({ + id: '.servicenow', + }), + expect.objectContaining({ + id: '.jira', + }), + expect.objectContaining({ + id: '.resilient', + }), + ]); + }); + + test('it show the edit flyout when pressing the update connector button', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorEditFlyout).exists()).toBe(true); + expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[1]); + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx new file mode 100644 index 0000000000000..3e352f119e840 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -0,0 +1,224 @@ +/* + * 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, { useCallback, useEffect, useMemo, useState } from 'react'; +import styled, { css } from 'styled-components'; + +import { EuiCallOut } from '@elastic/eui'; + +import { SUPPORTED_CONNECTORS } from '../../../common'; +import { useKibana } from '../../common/lib/kibana'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useActionTypes } from '../../containers/configure/use_action_types'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; + +import { ClosureType } from '../../containers/configure/types'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionConnectorTableItem } from '../../../../triggers_actions_ui/public/types'; + +import { SectionWrapper } from '../wrappers'; +import { Connectors } from './connectors'; +import { ClosureOptions } from './closure_options'; +import { + getConnectorById, + getNoneConnector, + normalizeActionConnector, + normalizeCaseConnector, +} from './utils'; +import * as i18n from './translations'; + +const FormWrapper = styled.div` + ${({ theme }) => css` + & > * { + margin-top 40px; + } + + & > :first-child { + margin-top: 0; + } + + padding-top: ${theme.eui.paddingSizes.xl}; + padding-bottom: ${theme.eui.paddingSizes.xl}; + .euiFlyout { + z-index: ${theme.eui.euiZNavigation + 1}; + } + `} +`; + +interface ConfigureCasesComponentProps { + userCanCrud: boolean; +} + +const ConfigureCasesComponent: React.FC = ({ userCanCrud }) => { + const { triggersActionsUi } = useKibana().services; + + const [connectorIsValid, setConnectorIsValid] = useState(true); + const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); + const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); + const [editedConnectorItem, setEditedConnectorItem] = useState( + null + ); + + const { + connector, + closureType, + loading: loadingCaseConfigure, + mappings, + persistLoading, + persistCaseConfigure, + refetchCaseConfigure, + setConnector, + setClosureType, + } = useCaseConfigure(); + + const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); + const { loading: isLoadingActionTypes, actionTypes, refetchActionTypes } = useActionTypes(); + const supportedActionTypes = useMemo( + () => actionTypes.filter((actionType) => SUPPORTED_CONNECTORS.includes(actionType.id)), + [actionTypes] + ); + + const onConnectorUpdate = useCallback(async () => { + refetchConnectors(); + refetchActionTypes(); + refetchCaseConfigure(); + }, [refetchActionTypes, refetchCaseConfigure, refetchConnectors]); + + const isLoadingAny = + isLoadingConnectors || persistLoading || loadingCaseConfigure || isLoadingActionTypes; + const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connector.id === 'none'; + const onClickUpdateConnector = useCallback(() => { + setEditFlyoutVisibility(true); + }, []); + + const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [ + setAddFlyoutVisibility, + ]); + + const onCloseEditFlyout = useCallback(() => setEditFlyoutVisibility(false), []); + + const onChangeConnector = useCallback( + (id: string) => { + if (id === 'add-connector') { + setAddFlyoutVisibility(true); + return; + } + + const actionConnector = getConnectorById(id, connectors); + const caseConnector = + actionConnector != null ? normalizeActionConnector(actionConnector) : getNoneConnector(); + + setConnector(caseConnector); + persistCaseConfigure({ + connector: caseConnector, + closureType, + }); + }, + [connectors, closureType, persistCaseConfigure, setConnector] + ); + + const onChangeClosureType = useCallback( + (type: ClosureType) => { + setClosureType(type); + persistCaseConfigure({ + connector, + closureType: type, + }); + }, + [connector, persistCaseConfigure, setClosureType] + ); + + useEffect(() => { + if ( + !isLoadingConnectors && + connector.id !== 'none' && + !connectors.some((c) => c.id === connector.id) + ) { + setConnectorIsValid(false); + } else if ( + !isLoadingConnectors && + (connector.id === 'none' || connectors.some((c) => c.id === connector.id)) + ) { + setConnectorIsValid(true); + } + }, [connectors, connector, isLoadingConnectors]); + + useEffect(() => { + if (!isLoadingConnectors && connector.id !== 'none') { + setEditedConnectorItem( + normalizeCaseConnector(connectors, connector) as ActionConnectorTableItem + ); + } + }, [connectors, connector, isLoadingConnectors]); + + const ConnectorAddFlyout = useMemo( + () => + triggersActionsUi.getAddConnectorFlyout({ + consumer: 'case', + onClose: onCloseAddFlyout, + actionTypes: supportedActionTypes, + reloadConnectors: onConnectorUpdate, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [supportedActionTypes] + ); + + const ConnectorEditFlyout = useMemo( + () => + editedConnectorItem && editFlyoutVisible + ? triggersActionsUi.getEditConnectorFlyout({ + initialConnector: editedConnectorItem, + consumer: 'case', + onClose: onCloseEditFlyout, + reloadConnectors: onConnectorUpdate, + }) + : null, + // eslint-disable-next-line react-hooks/exhaustive-deps + [connector.id, editFlyoutVisible] + ); + + return ( + + {!connectorIsValid && ( + + + {i18n.WARNING_NO_CONNECTOR_MESSAGE} + + + )} + + + + + + + {addFlyoutVisible && ConnectorAddFlyout} + {ConnectorEditFlyout} + + ); +}; + +export const ConfigureCases = React.memo(ConfigureCasesComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.test.tsx new file mode 100644 index 0000000000000..75b2410dde957 --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.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. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { TestProviders } from '../../common/mock'; +import { Mapping, MappingProps } from './mapping'; +import { mappings } from './__mock__'; + +describe('Mapping', () => { + const props: MappingProps = { + connectorActionTypeId: '.servicenow', + isLoading: false, + mappings, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + test('it shows mapping form group', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + expect(wrapper.find('[data-test-subj="static-mappings"]').first().exists()).toBe(true); + }); + + test('correctly maps fields', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + expect(wrapper.find('[data-test-subj="field-mapping-source"] code').first().text()).toBe( + 'title' + ); + expect(wrapper.find('[data-test-subj="field-mapping-target"] code').first().text()).toBe( + 'short_description' + ); + }); + test('displays connection warning when isLoading: false and mappings: []', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="field-mapping-desc"]').first().text()).toBe( + 'Field mappings require an established connection to ServiceNow ITSM. Please check your connection credentials.' + ); + }); +}); diff --git a/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx b/x-pack/plugins/cases/public/components/configure_cases/mapping.tsx new file mode 100644 index 0000000000000..5ec6a33f48b6a --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/mapping.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 React, { useMemo } from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTextColor } from '@elastic/eui'; + +import { TextColor } from '@elastic/eui/src/components/text/text_color'; +import * as i18n from './translations'; + +import { FieldMapping } from './field_mapping'; +import { CaseConnectorMapping } from '../../containers/configure/types'; +import { connectorsConfiguration } from '../connectors'; + +export interface MappingProps { + connectorActionTypeId: string; + isLoading: boolean; + mappings: CaseConnectorMapping[]; +} + +const MappingComponent: React.FC = ({ + connectorActionTypeId, + isLoading, + mappings, +}) => { + const selectedConnector = useMemo(() => connectorsConfiguration[connectorActionTypeId], [ + connectorActionTypeId, + ]); + const fieldMappingDesc: { desc: string; color: TextColor } = useMemo( + () => + mappings.length > 0 || isLoading + ? { desc: i18n.FIELD_MAPPING_DESC(selectedConnector.name), color: 'subdued' } + : { desc: i18n.FIELD_MAPPING_DESC_ERR(selectedConnector.name), color: 'danger' }, + [isLoading, mappings.length, selectedConnector.name] + ); + return ( + + + +

{i18n.FIELD_MAPPING_TITLE(selectedConnector.name)}

+ + {fieldMappingDesc.desc} + +
+
+ + + +
+ ); +}; + +export const Mapping = React.memo(MappingComponent); diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts new file mode 100644 index 0000000000000..2fb2133ba470c --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -0,0 +1,227 @@ +/* + * 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'; + +export * from '../../common/translations'; + +export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( + 'xpack.cases.configureCases.incidentManagementSystemTitle', + { + defaultMessage: 'Connect to external incident management system', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( + 'xpack.cases.configureCases.incidentManagementSystemDesc', + { + defaultMessage: + 'You may optionally connect cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( + 'xpack.cases.configureCases.incidentManagementSystemLabel', + { + defaultMessage: 'Incident management system', + } +); + +export const ADD_NEW_CONNECTOR = i18n.translate('xpack.cases.configureCases.addNewConnector', { + defaultMessage: 'Add new connector', +}); + +export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( + 'xpack.cases.configureCases.caseClosureOptionsTitle', + { + defaultMessage: 'Case Closures', + } +); + +export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( + 'xpack.cases.configureCases.caseClosureOptionsDesc', + { + defaultMessage: + 'Define how you wish cases to be closed. Automated case closures require an established connection to an external incident management system.', + } +); + +export const CASE_COLSURE_OPTIONS_SUB_CASES = i18n.translate( + 'xpack.cases.configureCases.caseClosureOptionsSubCases', + { + defaultMessage: 'Automated closures of sub-cases is not currently supported.', + } +); + +export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate( + 'xpack.cases.configureCases.caseClosureOptionsLabel', + { + defaultMessage: 'Case closure options', + } +); + +export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate( + 'xpack.cases.configureCases.caseClosureOptionsManual', + { + defaultMessage: 'Manually close cases', + } +); + +export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate( + 'xpack.cases.configureCases.caseClosureOptionsNewIncident', + { + defaultMessage: 'Automatically close cases when pushing new incident to external system', + } +); + +export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( + 'xpack.cases.configureCases.caseClosureOptionsClosedIncident', + { + defaultMessage: 'Automatically close cases when incident is closed in external system', + } +); +export const FIELD_MAPPING_TITLE = (thirdPartyName: string): string => { + return i18n.translate('xpack.cases.configureCases.fieldMappingTitle', { + values: { thirdPartyName }, + defaultMessage: '{ thirdPartyName } field mappings', + }); +}; + +export const FIELD_MAPPING_DESC = (thirdPartyName: string): string => { + return i18n.translate('xpack.cases.configureCases.fieldMappingDesc', { + values: { thirdPartyName }, + defaultMessage: + 'Map Case fields to { thirdPartyName } fields when pushing data to { thirdPartyName }. Field mappings require an established connection to { thirdPartyName }.', + }); +}; + +export const FIELD_MAPPING_DESC_ERR = (thirdPartyName: string): string => { + return i18n.translate('xpack.cases.configureCases.fieldMappingDescErr', { + values: { thirdPartyName }, + defaultMessage: + 'Field mappings require an established connection to { thirdPartyName }. Please check your connection credentials.', + }); +}; +export const EDIT_FIELD_MAPPING_TITLE = (thirdPartyName: string): string => { + return i18n.translate('xpack.cases.configureCases.editFieldMappingTitle', { + values: { thirdPartyName }, + defaultMessage: 'Edit { thirdPartyName } field mappings', + }); +}; + +export const FIELD_MAPPING_FIRST_COL = i18n.translate( + 'xpack.cases.configureCases.fieldMappingFirstCol', + { + defaultMessage: 'Kibana case field', + } +); + +export const FIELD_MAPPING_SECOND_COL = (thirdPartyName: string): string => { + return i18n.translate('xpack.cases.configureCases.fieldMappingSecondCol', { + values: { thirdPartyName }, + defaultMessage: '{ thirdPartyName } field', + }); +}; + +export const FIELD_MAPPING_THIRD_COL = i18n.translate( + 'xpack.cases.configureCases.fieldMappingThirdCol', + { + defaultMessage: 'On edit and update', + } +); + +export const FIELD_MAPPING_EDIT_NOTHING = i18n.translate( + 'xpack.cases.configureCases.fieldMappingEditNothing', + { + defaultMessage: 'Nothing', + } +); + +export const FIELD_MAPPING_EDIT_OVERWRITE = i18n.translate( + 'xpack.cases.configureCases.fieldMappingEditOverwrite', + { + defaultMessage: 'Overwrite', + } +); + +export const FIELD_MAPPING_EDIT_APPEND = i18n.translate( + 'xpack.cases.configureCases.fieldMappingEditAppend', + { + defaultMessage: 'Append', + } +); + +export const CANCEL = i18n.translate('xpack.cases.configureCases.cancelButton', { + defaultMessage: 'Cancel', +}); + +export const SAVE = i18n.translate('xpack.cases.configureCases.saveButton', { + defaultMessage: 'Save', +}); + +export const SAVE_CLOSE = i18n.translate('xpack.cases.configureCases.saveAndCloseButton', { + defaultMessage: 'Save & close', +}); + +export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( + 'xpack.cases.configureCases.warningTitle', + { + defaultMessage: 'Warning', + } +); + +export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( + 'xpack.cases.configureCases.warningMessage', + { + defaultMessage: + 'The selected connector has been deleted. Either select a different connector or create a new one.', + } +); + +export const MAPPING_FIELD_NOT_MAPPED = i18n.translate( + 'xpack.cases.configureCases.mappingFieldNotMapped', + { + defaultMessage: 'Not mapped', + } +); + +export const COMMENT = i18n.translate('xpack.cases.configureCases.commentMapping', { + defaultMessage: 'Comments', +}); + +export const NO_FIELDS_ERROR = (connectorName: string): string => { + return i18n.translate('xpack.cases.configureCases.noFieldsError', { + values: { connectorName }, + defaultMessage: + 'No { connectorName } fields found. Please check your { connectorName } connector settings or your { connectorName } instance settings to resolve.', + }); +}; + +export const BLANK_MAPPINGS = (connectorName: string): string => { + return i18n.translate('xpack.cases.configureCases.blankMappings', { + values: { connectorName }, + defaultMessage: 'At least one field needs to be mapped to { connectorName }', + }); +}; + +export const REQUIRED_MAPPINGS = (connectorName: string, fields: string): string => { + return i18n.translate('xpack.cases.configureCases.requiredMappings', { + values: { connectorName, fields }, + defaultMessage: + 'At least one Case field needs to be mapped to the following required { connectorName } fields: { fields }', + }); +}; +export const UPDATE_FIELD_MAPPINGS = i18n.translate('xpack.cases.configureCases.updateConnector', { + defaultMessage: 'Update field mappings', +}); + +export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => { + return i18n.translate('xpack.cases.configureCases.updateSelectedConnector', { + values: { connectorName }, + defaultMessage: 'Update { connectorName }', + }); +}; diff --git a/x-pack/plugins/cases/public/components/configure_cases/utils.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/utils.test.tsx new file mode 100644 index 0000000000000..45bb7f1f5136d --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/utils.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mappings } from './__mock__'; +import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; +import { CaseConnectorMapping } from '../../containers/configure/types'; + +describe('FieldMappingRow', () => { + test('it should change the action type', () => { + const newMapping = setActionTypeToMapping('title', 'nothing', mappings); + expect(newMapping[0].actionType).toBe('nothing'); + }); + + test('it should not change other fields', () => { + const [newTitle, description, comments] = setActionTypeToMapping('title', 'nothing', mappings); + expect(newTitle).not.toEqual(mappings[0]); + expect(description).toEqual(mappings[1]); + expect(comments).toEqual(mappings[2]); + }); + + test('it should return a new array when changing action type', () => { + const newMapping = setActionTypeToMapping('title', 'nothing', mappings); + expect(newMapping).not.toBe(mappings); + }); + + test('it should change the third party', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mappings); + expect(newMapping[0].target).toBe('description'); + }); + + test('it should not change other fields when there is not a conflict', () => { + const tempMapping: CaseConnectorMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ]; + + const [newTitle, comments] = setThirdPartyToMapping('title', 'description', tempMapping); + + expect(newTitle).not.toEqual(mappings[0]); + expect(comments).toEqual(tempMapping[1]); + }); + + test('it should return a new array when changing third party', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mappings); + expect(newMapping).not.toBe(mappings); + }); + + test('it should change the target of the conflicting third party field to not_mapped', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mappings); + expect(newMapping[1].target).toBe('not_mapped'); + }); +}); diff --git a/x-pack/plugins/cases/public/components/configure_cases/utils.ts b/x-pack/plugins/cases/public/components/configure_cases/utils.ts new file mode 100644 index 0000000000000..ade1a5e0c2bba --- /dev/null +++ b/x-pack/plugins/cases/public/components/configure_cases/utils.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConnectorTypeFields, ConnectorTypes } from '../../../common'; +import { + CaseField, + ActionType, + ThirdPartyField, + ActionConnector, + CaseConnector, + CaseConnectorMapping, +} from '../../containers/configure/types'; + +export const setActionTypeToMapping = ( + caseField: CaseField, + newActionType: ActionType, + mapping: CaseConnectorMapping[] +): CaseConnectorMapping[] => { + const findItemIndex = mapping.findIndex((item) => item.source === caseField); + + if (findItemIndex >= 0) { + return [ + ...mapping.slice(0, findItemIndex), + { ...mapping[findItemIndex], actionType: newActionType }, + ...mapping.slice(findItemIndex + 1), + ]; + } + + return [...mapping]; +}; + +export const setThirdPartyToMapping = ( + caseField: CaseField, + newThirdPartyField: ThirdPartyField, + mapping: CaseConnectorMapping[] +): CaseConnectorMapping[] => + mapping.map((item) => { + if (item.source !== caseField && item.target === newThirdPartyField) { + return { ...item, target: 'not_mapped' }; + } else if (item.source === caseField) { + return { ...item, target: newThirdPartyField }; + } + return item; + }); + +export const getNoneConnector = (): CaseConnector => ({ + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, +}); + +export const getConnectorById = ( + id: string, + connectors: ActionConnector[] +): ActionConnector | null => connectors.find((c) => c.id === id) ?? null; + +export const normalizeActionConnector = ( + actionConnector: ActionConnector, + fields: CaseConnector['fields'] = null +): CaseConnector => { + const caseConnectorFieldsType = { + type: actionConnector.actionTypeId, + fields, + } as ConnectorTypeFields; + return { + id: actionConnector.id, + name: actionConnector.name, + ...caseConnectorFieldsType, + }; +}; + +export const normalizeCaseConnector = ( + connectors: ActionConnector[], + caseConnector: CaseConnector +): ActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null; diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx new file mode 100644 index 0000000000000..ec136989dd937 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connector_selector/form.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { UseField, Form, useForm, FormHook } from '../../common/shared_imports'; +import { ConnectorSelector } from './form'; +import { connectorsMock } from '../../containers/mock'; +import { getFormMock } from '../__mock__/form'; + +jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'); + +const useFormMock = useForm as jest.Mock; + +describe('ConnectorSelector', () => { + const formHookMock = getFormMock({ connectorId: connectorsMock[0].id }); + + beforeEach(() => { + jest.resetAllMocks(); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + }); + + it('it should render', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); + }); + + it('it should not render when is not in edit mode', async () => { + const wrapper = mount( +
+ + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx new file mode 100644 index 0000000000000..210334e93adb8 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -0,0 +1,70 @@ +/* + * 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, { useCallback } from 'react'; +import { isEmpty } from 'lodash/fp'; +import { EuiFormRow } from '@elastic/eui'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; +import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; +import { ActionConnector } from '../../../common'; + +interface ConnectorSelectorProps { + connectors: ActionConnector[]; + dataTestSubj: string; + disabled: boolean; + field: FieldHook; + idAria: string; + isEdit: boolean; + isLoading: boolean; + handleChange?: (newValue: string) => void; + hideConnectorServiceNowSir?: boolean; +} +export const ConnectorSelector = ({ + connectors, + dataTestSubj, + disabled = false, + field, + idAria, + isEdit = true, + isLoading = false, + handleChange, + hideConnectorServiceNowSir = false, +}: ConnectorSelectorProps) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const onChange = useCallback( + (val: string) => { + if (handleChange) { + handleChange(val); + } + field.setValue(val); + }, + [handleChange, field] + ); + + return isEdit ? ( + + + + ) : null; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx new file mode 100644 index 0000000000000..82a508ccf3432 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import styled from 'styled-components'; + +import { connectorsConfiguration } from '.'; +import { ConnectorTypes } from '../../../common'; + +interface ConnectorCardProps { + connectorType: ConnectorTypes; + title: string; + listItems: Array<{ title: string; description: React.ReactNode }>; + isLoading: boolean; +} + +const StyledText = styled.span` + span { + display: block; + } +`; + +const ConnectorCardDisplay: React.FC = ({ + connectorType, + title, + listItems, + isLoading, +}) => { + const description = useMemo( + () => ( + + {listItems.length > 0 && + listItems.map((item, i) => ( + + {`${item.title}: `} + {item.description} + + ))} + + ), + [listItems] + ); + const icon = useMemo( + () => , + [connectorType] + ); + return ( + <> + {isLoading && } + {!isLoading && ( + + )} + + ); +}; + +export const ConnectorCard = memo(ConnectorCardDisplay); diff --git a/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx b/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx new file mode 100644 index 0000000000000..10955db69461c --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +import { ActionParamsProps } from '../../../../../triggers_actions_ui/public/types'; +import { CommentType } from '../../../../common'; + +import { CaseActionParams } from './types'; +import { ExistingCase } from './existing_case'; + +import * as i18n from './translations'; + +const Container = styled.div` + ${({ theme }) => ` + padding: ${theme.eui?.euiSizeS ?? '8px'} ${theme.eui?.euiSizeL ?? '24px'} ${ + theme.eui?.euiSizeL ?? '24px' + } ${theme.eui?.euiSizeL ?? '24px'}; + `} +`; + +const defaultAlertComment = { + type: CommentType.generatedAlert, + alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{signal.rule.id}}", "ruleName": "{{signal.rule.name}}"}__SEPARATOR__{{/context.alerts}}]`, +}; + +const CaseParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + actionConnector, +}) => { + const { caseId = null, comment = defaultAlertComment } = actionParams.subActionParams ?? {}; + + const [selectedCase, setSelectedCase] = useState(null); + + const editSubActionProperty = useCallback( + (key: string, value: unknown) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }, + // edit action causes re-renders + // eslint-disable-next-line react-hooks/exhaustive-deps + [actionParams.subActionParams, index] + ); + + const onCaseChanged = useCallback( + (id: string) => { + setSelectedCase(id); + editSubActionProperty('caseId', id); + }, + [editSubActionProperty] + ); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'addComment', index); + } + + if (!actionParams.subActionParams?.caseId) { + editSubActionProperty('caseId', caseId); + } + + if (!actionParams.subActionParams?.comment) { + editSubActionProperty('comment', comment); + } + + if (caseId != null) { + setSelectedCase((prevCaseId) => (prevCaseId !== caseId ? caseId : prevCaseId)); + } + + // editAction creates an infinity loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + actionConnector, + index, + actionParams.subActionParams?.caseId, + actionParams.subActionParams?.comment, + caseId, + comment, + actionParams.subAction, + ]); + + return ( + + + + +

{i18n.CASE_CONNECTOR_CALL_OUT_MSG}

+
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { CaseParamsFields as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx b/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx new file mode 100644 index 0000000000000..3f3c7d4931192 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.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 { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; +import React, { memo, useMemo, useCallback } from 'react'; +import { Case } from '../../../containers/types'; + +import * as i18n from './translations'; + +interface CaseDropdownProps { + isLoading: boolean; + cases: Case[]; + selectedCase?: string; + onCaseChanged: (id: string) => void; +} + +export const ADD_CASE_BUTTON_ID = 'add-case'; + +const addNewCase = { + value: ADD_CASE_BUTTON_ID, + inputDisplay: ( + + {i18n.CASE_CONNECTOR_ADD_NEW_CASE} + + ), + 'data-test-subj': 'dropdown-connector-add-connector', +}; + +const CasesDropdownComponent: React.FC = ({ + isLoading, + cases, + selectedCase, + onCaseChanged, +}) => { + const caseOptions: Array> = useMemo( + () => + cases.reduce>>( + (acc, theCase) => [ + ...acc, + { + value: theCase.id, + inputDisplay: {theCase.title}, + 'data-test-subj': `case-connector-cases-dropdown-${theCase.id}`, + }, + ], + [] + ), + [cases] + ); + + const options = useMemo(() => [...caseOptions, addNewCase], [caseOptions]); + const onChange = useCallback((id: string) => onCaseChanged(id), [onCaseChanged]); + + return ( + + + + ); +}; + +export const CasesDropdown = memo(CasesDropdownComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx b/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx new file mode 100644 index 0000000000000..22798843dd856 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx @@ -0,0 +1,76 @@ +/* + * 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 { CaseType } from '../../../../common'; +import { + useGetCases, + DEFAULT_QUERY_PARAMS, + DEFAULT_FILTER_OPTIONS, +} from '../../../containers/use_get_cases'; +import { useCreateCaseModal } from '../../use_create_case_modal'; +import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown'; + +interface ExistingCaseProps { + selectedCase: string | null; + onCaseChanged: (id: string) => void; +} + +const ExistingCaseComponent: React.FC = ({ onCaseChanged, selectedCase }) => { + const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(DEFAULT_QUERY_PARAMS, { + ...DEFAULT_FILTER_OPTIONS, + onlyCollectionType: true, + }); + + const onCaseCreated = useCallback( + (newCase) => { + refetchCases(); + onCaseChanged(newCase.id); + }, + [onCaseChanged, refetchCases] + ); + + const { modal, openModal } = useCreateCaseModal({ + onCaseCreated, + caseType: CaseType.collection, + // FUTURE DEVELOPER + // We are making the assumption that this component is only used in rules creation + // that's why we want to hide ServiceNow SIR + hideConnectorServiceNowSir: true, + }); + + const onChange = useCallback( + (id: string) => { + if (id === ADD_CASE_BUTTON_ID) { + openModal(); + return; + } + + onCaseChanged(id); + }, + [onCaseChanged, openModal] + ); + + const isCasesLoading = useMemo( + () => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'), + [isLoadingCases] + ); + + return ( + <> + + {modal} + + ); +}; + +export const ExistingCase = memo(ExistingCaseComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/case/index.ts b/x-pack/plugins/cases/public/components/connectors/case/index.ts new file mode 100644 index 0000000000000..c2cf4980da7ec --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/case/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionTypeModel } from '../../../../../triggers_actions_ui/public/types'; +import { CaseActionParams } from './types'; +import * as i18n from './translations'; + +interface ValidationResult { + errors: { + caseId: string[]; + }; +} + +const validateParams = (actionParams: CaseActionParams) => { + const validationResult: ValidationResult = { errors: { caseId: [] } }; + + if (actionParams.subActionParams && !actionParams.subActionParams.caseId) { + validationResult.errors.caseId.push(i18n.CASE_CONNECTOR_CASE_REQUIRED); + } + + return validationResult; +}; + +export function getActionType(): ActionTypeModel { + return { + id: '.case', + iconClass: 'securityAnalyticsApp', + selectMessage: i18n.CASE_CONNECTOR_DESC, + actionTypeTitle: i18n.CASE_CONNECTOR_TITLE, + validateConnector: () => ({ config: { errors: {} }, secrets: { errors: {} } }), + validateParams, + actionConnectorFields: null, + actionParamsFields: lazy(() => import('./alert_fields')), + }; +} diff --git a/x-pack/plugins/cases/public/components/connectors/case/translations.ts b/x-pack/plugins/cases/public/components/connectors/case/translations.ts new file mode 100644 index 0000000000000..8304aaef5765c --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/case/translations.ts @@ -0,0 +1,109 @@ +/* + * 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'; + +export * from '../../../common/translations'; + +export const CASE_CONNECTOR_DESC = i18n.translate( + 'xpack.cases.components.connectors.cases.selectMessageText', + { + defaultMessage: 'Create or update a case.', + } +); + +export const CASE_CONNECTOR_TITLE = i18n.translate( + 'xpack.cases.components.connectors.cases.actionTypeTitle', + { + defaultMessage: 'Cases', + } +); + +export const CASE_CONNECTOR_COMMENT_LABEL = i18n.translate( + 'xpack.cases.components.connectors.cases.commentLabel', + { + defaultMessage: 'Comment', + } +); + +export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate( + 'xpack.cases.components.connectors.cases.commentRequired', + { + defaultMessage: 'Comment is required.', + } +); + +export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate( + 'xpack.cases.components.connectors.cases.casesDropdownRowLabel', + { + defaultMessage: 'Case allowing sub-cases', + } +); + +export const CASE_CONNECTOR_CASES_DROPDOWN_PLACEHOLDER = i18n.translate( + 'xpack.cases.components.connectors.cases.casesDropdownPlaceholder', + { + defaultMessage: 'Select case', + } +); + +export const CASE_CONNECTOR_CASES_OPTION_NEW_CASE = i18n.translate( + 'xpack.cases.components.connectors.cases.optionAddNewCase', + { + defaultMessage: 'Add to a new case', + } +); + +export const CASE_CONNECTOR_CASES_OPTION_EXISTING_CASE = i18n.translate( + 'xpack.cases.components.connectors.cases.optionAddToExistingCase', + { + defaultMessage: 'Add to existing case', + } +); + +export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate( + 'xpack.cases.components.connectors.cases.caseRequired', + { + defaultMessage: 'You must select a case.', + } +); + +export const CASE_CONNECTOR_CALL_OUT_TITLE = i18n.translate( + 'xpack.cases.components.connectors.cases.callOutTitle', + { + defaultMessage: 'Generated alerts will be attached to sub-cases', + } +); + +export const CASE_CONNECTOR_CALL_OUT_MSG = i18n.translate( + 'xpack.cases.components.connectors.cases.callOutMsg', + { + defaultMessage: + 'A case can contain multiple sub-cases to allow grouping of generated alerts. Sub-cases will give more granular control over the status of these generated alerts and prevents having too many alerts attached to one case.', + } +); + +export const CASE_CONNECTOR_ADD_NEW_CASE = i18n.translate( + 'xpack.cases.components.connectors.cases.addNewCaseOption', + { + defaultMessage: 'Add new case', + } +); + +export const CREATE_CASE = i18n.translate( + 'xpack.cases.components.connectors.cases.createCaseLabel', + { + defaultMessage: 'Create case', + } +); + +export const CONNECTED_CASE = i18n.translate( + 'xpack.cases.components.connectors.cases.connectedCaseLabel', + { + defaultMessage: 'Connected case', + } +); diff --git a/x-pack/plugins/cases/public/components/connectors/case/types.ts b/x-pack/plugins/cases/public/components/connectors/case/types.ts new file mode 100644 index 0000000000000..aec9e09ea198c --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/case/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface CaseActionParams { + subAction: string; + subActionParams: { + caseId: string; + comment: { + alertId: string; + index: string; + type: 'alert'; + }; + }; +} diff --git a/x-pack/plugins/cases/public/components/connectors/config.ts b/x-pack/plugins/cases/public/components/connectors/config.ts new file mode 100644 index 0000000000000..e8d87511c7e17 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/config.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getResilientActionType, + getServiceNowITSMActionType, + getServiceNowSIRActionType, + getJiraActionType, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../triggers_actions_ui/public/common'; +import { ConnectorConfiguration } from './types'; + +const resilient = getResilientActionType(); +const serviceNowITSM = getServiceNowITSMActionType(); +const serviceNowSIR = getServiceNowSIRActionType(); +const jira = getJiraActionType(); + +export const connectorsConfiguration: Record = { + '.servicenow': { + name: serviceNowITSM.actionTypeTitle ?? '', + logo: serviceNowITSM.iconClass, + }, + '.servicenow-sir': { + name: serviceNowSIR.actionTypeTitle ?? '', + logo: serviceNowSIR.iconClass, + }, + '.jira': { + name: jira.actionTypeTitle ?? '', + logo: jira.iconClass, + }, + '.resilient': { + name: resilient.actionTypeTitle ?? '', + logo: resilient.iconClass, + }, +}; diff --git a/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts b/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts new file mode 100644 index 0000000000000..2e02cb290c3c8 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts @@ -0,0 +1,49 @@ +/* + * 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 { CaseConnector, CaseConnectorsRegistry } from './types'; + +export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => { + const connectors: Map> = new Map(); + + const registry: CaseConnectorsRegistry = { + has: (id: string) => connectors.has(id), + register: (connector: CaseConnector) => { + if (connectors.has(connector.id)) { + throw new Error( + i18n.translate('xpack.cases.connecors.register.duplicateCaseConnectorErrorMessage', { + defaultMessage: 'Object type "{id}" is already registered.', + values: { + id: connector.id, + }, + }) + ); + } + + connectors.set(connector.id, connector); + }, + get: (id: string): CaseConnector => { + if (!connectors.has(id)) { + throw new Error( + i18n.translate('xpack.cases.connecors.get.missingCaseConnectorErrorMessage', { + defaultMessage: 'Object type "{id}" is not registered.', + values: { + id, + }, + }) + ); + } + return connectors.get(id)!; + }, + list: () => { + return Array.from(connectors).map(([id, connector]) => connector); + }, + }; + + return registry; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx new file mode 100644 index 0000000000000..d71da6f87689d --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, Suspense } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +import { CaseActionConnector, ConnectorFieldsProps } from './types'; +import { getCaseConnectors } from '.'; +import { ConnectorTypeFields } from '../../../common'; + +interface Props extends Omit, 'connector'> { + connector: CaseActionConnector | null; +} + +const ConnectorFieldsFormComponent: React.FC = ({ connector, isEdit, onChange, fields }) => { + const { caseConnectorsRegistry } = getCaseConnectors(); + + if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') { + return null; + } + + const { fieldsComponent: FieldsComponent } = caseConnectorsRegistry.get(connector.actionTypeId); + + return ( + <> + {FieldsComponent != null ? ( + + + + +
+ } + > +
+ +
+ + ) : null} + + ); +}; + +export const ConnectorFieldsForm = memo(ConnectorFieldsFormComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts new file mode 100644 index 0000000000000..7444c403a3b60 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseConnectorsRegistry } from './types'; +import { createCaseConnectorsRegistry } from './connectors_registry'; +import { getCaseConnector as getJiraCaseConnector } from './jira'; +import { getCaseConnector as getResilientCaseConnector } from './resilient'; +import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; +import { + JiraFieldsType, + ServiceNowITSMFieldsType, + ServiceNowSIRFieldsType, + ResilientFieldsType, +} from '../../../common'; + +export { getActionType as getCaseConnectorUI } from './case'; + +export * from './config'; +export * from './types'; + +interface GetCaseConnectorsReturn { + caseConnectorsRegistry: CaseConnectorsRegistry; +} + +class CaseConnectors { + private caseConnectorsRegistry: CaseConnectorsRegistry; + + constructor() { + this.caseConnectorsRegistry = createCaseConnectorsRegistry(); + this.init(); + } + + private init() { + this.caseConnectorsRegistry.register(getJiraCaseConnector()); + this.caseConnectorsRegistry.register(getResilientCaseConnector()); + this.caseConnectorsRegistry.register( + getServiceNowITSMCaseConnector() + ); + this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector()); + } + + registry(): CaseConnectorsRegistry { + return this.caseConnectorsRegistry; + } +} + +const caseConnectors = new CaseConnectors(); + +export const getCaseConnectors = (): GetCaseConnectorsReturn => { + return { + caseConnectorsRegistry: caseConnectors.registry(), + }; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/__mocks__/api.ts b/x-pack/plugins/cases/public/components/connectors/jira/__mocks__/api.ts new file mode 100644 index 0000000000000..3a7b51545dfca --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/__mocks__/api.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetIssueTypesProps, GetFieldsByIssueTypeProps, GetIssueTypeProps } from '../api'; +import { IssueTypes, Fields, Issues, Issue } from '../types'; +import { issues } from '../../mock'; + +const issueTypes = [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, +]; + +const fieldsByIssueType = { + summary: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, +}; + +export const getIssue = async (props: GetIssueTypeProps): Promise<{ data: Issue }> => + Promise.resolve({ data: issues[0] }); +export const getIssues = async (props: GetIssueTypesProps): Promise<{ data: Issues }> => + Promise.resolve({ data: issues }); +export const getIssueTypes = async (props: GetIssueTypesProps): Promise<{ data: IssueTypes }> => + Promise.resolve({ data: issueTypes }); + +export const getFieldsByIssueType = async ( + props: GetFieldsByIssueTypeProps +): Promise<{ data: Fields }> => Promise.resolve({ data: fieldsByIssueType }); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/api.test.ts b/x-pack/plugins/cases/public/components/connectors/jira/api.test.ts new file mode 100644 index 0000000000000..bbab8a14b5ed9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/api.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { getIssueTypes, getFieldsByIssueType, getIssues, getIssue } from './api'; + +const issueTypesResponse = { + data: { + projects: [ + { + issuetypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], + }, + ], + }, +}; + +const fieldsResponse = { + data: { + projects: [ + { + issuetypes: [ + { + id: '10006', + name: 'Task', + fields: { + summary: { fieldId: 'summary' }, + priority: { + fieldId: 'priority', + allowedValues: [ + { + name: 'Highest', + id: '1', + }, + { + name: 'High', + id: '2', + }, + { + name: 'Medium', + id: '3', + }, + { + name: 'Low', + id: '4', + }, + { + name: 'Lowest', + id: '5', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + }, + }, + ], + }, + ], + }, +}; + +const issueResponse = { + id: '10267', + key: 'RJ-107', + fields: { summary: 'Test title' }, +}; + +const issuesResponse = [issueResponse]; + +describe('Jira API', () => { + const http = httpServiceMock.createStartContract(); + + beforeEach(() => jest.resetAllMocks()); + + describe('getIssueTypes', () => { + test('should call get issue types API', async () => { + const abortCtrl = new AbortController(); + http.post.mockResolvedValueOnce(issueTypesResponse); + const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'test' }); + + expect(res).toEqual(issueTypesResponse); + expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}', + signal: abortCtrl.signal, + }); + }); + }); + + describe('getFieldsByIssueType', () => { + test('should call get fields API', async () => { + const abortCtrl = new AbortController(); + http.post.mockResolvedValueOnce(fieldsResponse); + const res = await getFieldsByIssueType({ + http, + signal: abortCtrl.signal, + connectorId: 'test', + id: '10006', + }); + + expect(res).toEqual(fieldsResponse); + expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}', + signal: abortCtrl.signal, + }); + }); + }); + + describe('getIssues', () => { + test('should call get fields API', async () => { + const abortCtrl = new AbortController(); + http.post.mockResolvedValueOnce(issuesResponse); + const res = await getIssues({ + http, + signal: abortCtrl.signal, + connectorId: 'test', + title: 'test issue', + }); + + expect(res).toEqual(issuesResponse); + expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}', + signal: abortCtrl.signal, + }); + }); + }); + + describe('getIssue', () => { + test('should call get fields API', async () => { + const abortCtrl = new AbortController(); + http.post.mockResolvedValueOnce(issuesResponse); + const res = await getIssue({ + http, + signal: abortCtrl.signal, + connectorId: 'test', + id: 'RJ-107', + }); + + expect(res).toEqual(issuesResponse); + expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}', + signal: abortCtrl.signal, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/api.ts b/x-pack/plugins/cases/public/components/connectors/jira/api.ts new file mode 100644 index 0000000000000..dff3e3a5b41ab --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/api.ts @@ -0,0 +1,93 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { ActionTypeExecutorResult } from '../../../../../actions/common'; +import { IssueTypes, Fields, Issues, Issue } from './types'; + +export const BASE_ACTION_API_PATH = '/api/actions'; + +export interface GetIssueTypesProps { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; +} + +export async function getIssueTypes({ http, signal, connectorId }: GetIssueTypesProps) { + return http.post>( + `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, + { + body: JSON.stringify({ + params: { subAction: 'issueTypes', subActionParams: {} }, + }), + signal, + } + ); +} + +export interface GetFieldsByIssueTypeProps { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; + id: string; +} + +export async function getFieldsByIssueType({ + http, + signal, + connectorId, + id, +}: GetFieldsByIssueTypeProps): Promise> { + return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'fieldsByIssueType', subActionParams: { id } }, + }), + signal, + }); +} + +export interface GetIssuesTypeProps { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; + title: string; +} + +export async function getIssues({ + http, + signal, + connectorId, + title, +}: GetIssuesTypeProps): Promise> { + return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'issues', subActionParams: { title } }, + }), + signal, + }); +} + +export interface GetIssueTypeProps { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; + id: string; +} + +export async function getIssue({ + http, + signal, + connectorId, + id, +}: GetIssueTypeProps): Promise> { + return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'issue', subActionParams: { id } }, + }), + signal, + }); +} diff --git a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx new file mode 100644 index 0000000000000..38a1e30616200 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.test.tsx @@ -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 React from 'react'; +import { mount } from 'enzyme'; +import { omit } from 'lodash/fp'; + +import { connector, issues } from '../mock'; +import { useGetIssueTypes } from './use_get_issue_types'; +import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; +import Fields from './case_fields'; +import { waitFor } from '@testing-library/dom'; +import { useGetSingleIssue } from './use_get_single_issue'; +import { useGetIssues } from './use_get_issues'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +jest.mock('./use_get_issue_types'); +jest.mock('./use_get_fields_by_issue_type'); +jest.mock('./use_get_single_issue'); +jest.mock('./use_get_issues'); +jest.mock('../../../common/lib/kibana'); +const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; +const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; +const useGetSingleIssueMock = useGetSingleIssue as jest.Mock; +const useGetIssuesMock = useGetIssues as jest.Mock; + +describe('Jira Fields', () => { + const useGetIssueTypesResponse = { + isLoading: false, + issueTypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], + }; + + const useGetFieldsByIssueTypeResponse = { + isLoading: false, + fields: { + summary: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + { + name: 'Low', + id: '2', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + }, + }; + + const useGetSingleIssueResponse = { + isLoading: false, + issue: { title: 'Parent Task', key: 'parentId' }, + }; + + const fields = { + issueType: '10006', + priority: 'High', + parent: null, + }; + + const useGetIssuesResponse = { + isLoading: false, + issues, + }; + + const onChange = jest.fn(); + + beforeEach(() => { + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + useGetSingleIssueMock.mockReturnValue(useGetSingleIssueResponse); + jest.clearAllMocks(); + }); + + test('all params fields are rendered - isEdit: true', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('value')).toStrictEqual( + '10006' + ); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('value')).toStrictEqual( + 'High' + ); + expect(wrapper.find('[data-test-subj="search-parent-issues"]').first().exists()).toBeFalsy(); + }); + + test('all params fields are rendered - isEdit: false', () => { + const wrapper = mount( + + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( + 'Issue type: Task' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( + 'Parent issue: Parent Task' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( + 'Priority: High' + ); + }); + + test('it sets parent correctly', async () => { + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + fields: { + ...useGetFieldsByIssueTypeResponse.fields, + parent: {}, + }, + }); + useGetIssuesMock.mockReturnValue(useGetIssuesResponse); + const wrapper = mount(); + + await waitFor(() => + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'parentId', value: 'parentId' }]) + ); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + issueType: '10006', + parent: 'parentId', + priority: 'High', + }); + }); + test('it searches parent correctly', async () => { + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + fields: { + ...useGetFieldsByIssueTypeResponse.fields, + parent: {}, + }, + }); + useGetSingleIssueMock.mockReturnValue({ useGetSingleIssueResponse, issue: null }); + useGetIssuesMock.mockReturnValue(useGetIssuesResponse); + const wrapper = mount(); + + await waitFor(() => + ((wrapper.find(EuiComboBox).props() as unknown) as { + onSearchChange: (a: string) => void; + }).onSearchChange('womanId') + ); + wrapper.update(); + expect(useGetIssuesMock.mock.calls[2][0].query).toEqual('womanId'); + }); + + test('it disabled the fields when loading issue types', () => { + useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true }); + + const wrapper = mount(); + + expect( + wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled') + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy(); + }); + + test('it disabled the fields when loading fields', () => { + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + isLoading: true, + }); + + const wrapper = mount(); + + expect( + wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled') + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy(); + }); + + test('it hides the priority if not supported', () => { + const response = omit('fields.priority', useGetFieldsByIssueTypeResponse); + + useGetFieldsByIssueTypeMock.mockReturnValue(response); + + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().exists()).toBeFalsy(); + }); + + test('it sets issue type correctly', () => { + const wrapper = mount(); + + wrapper + .find('select[data-test-subj="issueTypeSelect"]') + .first() + .simulate('change', { + target: { value: '10007' }, + }); + + expect(onChange).toHaveBeenCalledWith({ issueType: '10007', parent: null, priority: null }); + }); + + test('it sets issue type when it comes as null', () => { + const wrapper = mount( + + ); + expect(wrapper.find('select[data-test-subj="issueTypeSelect"]').first().props().value).toEqual( + '10006' + ); + }); + + test('it sets issue type when it comes as unknown value', () => { + const wrapper = mount( + + ); + expect(wrapper.find('select[data-test-subj="issueTypeSelect"]').first().props().value).toEqual( + '10006' + ); + }); + + test('it sets priority correctly', () => { + const wrapper = mount(); + + wrapper + .find('select[data-test-subj="prioritySelect"]') + .first() + .simulate('change', { + target: { value: '2' }, + }); + + expect(onChange).toHaveBeenCalledWith({ issueType: '10006', parent: null, priority: '2' }); + }); + + test('it resets priority when changing issue type', () => { + const wrapper = mount(); + wrapper + .find('select[data-test-subj="issueTypeSelect"]') + .first() + .simulate('change', { + target: { value: '10007' }, + }); + + expect(onChange).toBeCalledWith({ issueType: '10007', parent: null, priority: null }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx new file mode 100644 index 0000000000000..6aff81f380015 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx @@ -0,0 +1,214 @@ +/* + * 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, { useCallback, useMemo, useEffect, useRef } from 'react'; +import { map } from 'lodash/fp'; +import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import * as i18n from './translations'; + +import { ConnectorTypes, JiraFieldsType } from '../../../../common'; +import { useKibana } from '../../../common/lib/kibana'; +import { ConnectorFieldsProps } from '../types'; +import { useGetIssueTypes } from './use_get_issue_types'; +import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; +import { SearchIssues } from './search_issues'; +import { ConnectorCard } from '../card'; + +const JiraFieldsComponent: React.FunctionComponent> = ({ + connector, + fields, + isEdit = true, + onChange, +}) => { + const init = useRef(true); + const { issueType = null, priority = null, parent = null } = fields ?? {}; + const { http, notifications } = useKibana().services; + + const handleIssueType = useCallback( + (issueTypeSelectOptions: Array<{ value: string; text: string }>) => { + if (issueType == null && issueTypeSelectOptions.length > 0) { + // if there is no issue type set in the edit view, set it to default + if (isEdit) { + onChange({ + issueType: issueTypeSelectOptions[0].value, + parent, + priority, + }); + } + } + }, + [isEdit, issueType, onChange, parent, priority] + ); + const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({ + connector, + http, + toastNotifications: notifications.toasts, + handleIssueType, + }); + + const issueTypesSelectOptions = useMemo( + () => + issueTypes.map((type) => ({ + text: type.name ?? '', + value: type.id ?? '', + })), + [issueTypes] + ); + + const currentIssueType = useMemo(() => { + if (!issueType && issueTypesSelectOptions.length > 0) { + return issueTypesSelectOptions[0].value; + } else if ( + issueTypesSelectOptions.length > 0 && + !issueTypesSelectOptions.some(({ value }) => value === issueType) + ) { + return issueTypesSelectOptions[0].value; + } + return issueType; + }, [issueType, issueTypesSelectOptions]); + + const { isLoading: isLoadingFields, fields: fieldsByIssueType } = useGetFieldsByIssueType({ + connector, + http, + issueType: currentIssueType, + toastNotifications: notifications.toasts, + }); + + const hasPriority = useMemo(() => fieldsByIssueType.priority != null, [fieldsByIssueType]); + + const hasParent = useMemo(() => fieldsByIssueType.parent != null, [fieldsByIssueType]); + + const prioritiesSelectOptions = useMemo(() => { + const priorities = fieldsByIssueType.priority?.allowedValues ?? []; + return map( + (p) => ({ + text: p.name, + value: p.name, + }), + priorities + ); + }, [fieldsByIssueType]); + + const listItems = useMemo( + () => [ + ...(issueType != null && issueType.length > 0 + ? [ + { + title: i18n.ISSUE_TYPE, + description: issueTypes.find((issue) => issue.id === issueType)?.name ?? '', + }, + ] + : []), + ...(parent != null && parent.length > 0 + ? [ + { + title: i18n.PARENT_ISSUE, + description: parent, + }, + ] + : []), + ...(priority != null && priority.length > 0 + ? [ + { + title: i18n.PRIORITY, + description: priority, + }, + ] + : []), + ], + [issueType, issueTypes, parent, priority] + ); + + const onFieldChange = useCallback( + (key, value) => { + if (key === 'issueType') { + return onChange({ ...fields, issueType: value, priority: null, parent: null }); + } + return onChange({ + ...fields, + issueType: currentIssueType, + parent, + priority, + [key]: value, + }); + }, + [currentIssueType, fields, onChange, parent, priority] + ); + + // Set field at initialization + useEffect(() => { + if (init.current) { + init.current = false; + onChange({ issueType, priority, parent }); + } + }, [issueType, onChange, parent, priority]); + + return isEdit ? ( +
+ + onFieldChange('issueType', e.target.value)} + options={issueTypesSelectOptions} + value={currentIssueType ?? ''} + /> + + + <> + {hasParent && ( + <> + + + + onFieldChange('parent', parentIssueKey)} + selectedValue={parent} + /> + + + + + + )} + {hasPriority && ( + <> + + + + onFieldChange('priority', e.target.value)} + options={prioritiesSelectOptions} + value={priority ?? ''} + /> + + + + + )} + +
+ ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { JiraFieldsComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/index.ts b/x-pack/plugins/cases/public/components/connectors/jira/index.ts new file mode 100644 index 0000000000000..ea408a1bd6664 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { JiraFieldsType } from '../../../../common'; +import * as i18n from './translations'; + +export * from './types'; + +export const getCaseConnector = (): CaseConnector => { + return { + id: '.jira', + fieldsComponent: lazy(() => import('./case_fields')), + }; +}; + +export const fieldLabels = { + issueType: i18n.ISSUE_TYPE, + priority: i18n.PRIORITY, + parent: i18n.PARENT_ISSUE, +}; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx b/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx new file mode 100644 index 0000000000000..9270abed0881f --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx @@ -0,0 +1,95 @@ +/* + * 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, useEffect, useCallback, useState, memo } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { useKibana } from '../../../common/lib/kibana'; +import { ActionConnector } from '../../../containers/types'; +import { useGetIssues } from './use_get_issues'; +import { useGetSingleIssue } from './use_get_single_issue'; +import * as i18n from './translations'; + +interface Props { + selectedValue: string | null; + actionConnector?: ActionConnector; + onChange: (parentIssueKey: string) => void; +} + +const SearchIssuesComponent: React.FC = ({ selectedValue, actionConnector, onChange }) => { + const [query, setQuery] = useState(null); + const [selectedOptions, setSelectedOptions] = useState>>( + [] + ); + const [options, setOptions] = useState>>([]); + const { http, notifications } = useKibana().services; + + const { isLoading: isLoadingIssues, issues } = useGetIssues({ + http, + toastNotifications: notifications.toasts, + actionConnector, + query, + }); + + const { isLoading: isLoadingSingleIssue, issue: singleIssue } = useGetSingleIssue({ + http, + toastNotifications: notifications.toasts, + actionConnector, + id: selectedValue, + }); + + useEffect(() => setOptions(issues.map((issue) => ({ label: issue.title, value: issue.key }))), [ + issues, + ]); + + useEffect(() => { + if (isLoadingSingleIssue || singleIssue == null) { + return; + } + + const singleIssueAsOptions = [{ label: singleIssue.title, value: singleIssue.key }]; + setOptions(singleIssueAsOptions); + setSelectedOptions(singleIssueAsOptions); + }, [singleIssue, isLoadingSingleIssue]); + + const onSearchChange = useCallback((searchVal: string) => { + setQuery(searchVal); + }, []); + + const onChangeComboBox = useCallback( + (changedOptions) => { + setSelectedOptions(changedOptions); + onChange(changedOptions[0].value); + }, + [onChange] + ); + + const inputPlaceholder = useMemo( + (): string => + isLoadingIssues || isLoadingSingleIssue + ? i18n.SEARCH_ISSUES_LOADING + : i18n.SEARCH_ISSUES_PLACEHOLDER, + [isLoadingIssues, isLoadingSingleIssue] + ); + + return ( + + ); +}; + +export const SearchIssues = memo(SearchIssuesComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/translations.ts b/x-pack/plugins/cases/public/components/connectors/jira/translations.ts new file mode 100644 index 0000000000000..88dd7d0c7c27b --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/translations.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ISSUE_TYPES_API_ERROR = i18n.translate( + 'xpack.cases.connectors.jira.unableToGetIssueTypesMessage', + { + defaultMessage: 'Unable to get issue types', + } +); + +export const FIELDS_API_ERROR = i18n.translate( + 'xpack.cases.connectors.jira.unableToGetFieldsMessage', + { + defaultMessage: 'Unable to get connectors', + } +); + +export const ISSUES_API_ERROR = i18n.translate( + 'xpack.cases.connectors.jira.unableToGetIssuesMessage', + { + defaultMessage: 'Unable to get issues', + } +); + +export const GET_ISSUE_API_ERROR = (id: string) => + i18n.translate('xpack.cases.connectors.jira.unableToGetIssueMessage', { + defaultMessage: 'Unable to get issue with id {id}', + values: { id }, + }); + +export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( + 'xpack.cases.connectors.jira.searchIssuesComboBoxAriaLabel', + { + defaultMessage: 'Type to search', + } +); + +export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( + 'xpack.cases.connectors.jira.searchIssuesComboBoxPlaceholder', + { + defaultMessage: 'Type to search', + } +); + +export const SEARCH_ISSUES_LOADING = i18n.translate( + 'xpack.cases.connectors.jira.searchIssuesLoading', + { + defaultMessage: 'Loading...', + } +); + +export const PRIORITY = i18n.translate('xpack.cases.connectors.jira.prioritySelectFieldLabel', { + defaultMessage: 'Priority', +}); + +export const ISSUE_TYPE = i18n.translate('xpack.cases.connectors.jira.issueTypesSelectFieldLabel', { + defaultMessage: 'Issue type', +}); + +export const PARENT_ISSUE = i18n.translate('xpack.cases.connectors.jira.parentIssueSearchLabel', { + defaultMessage: 'Parent issue', +}); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/types.ts b/x-pack/plugins/cases/public/components/connectors/jira/types.ts new file mode 100644 index 0000000000000..76c08a852c679 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type IssueTypes = Array<{ id: string; name: string }>; +export interface Fields { + [key: string]: { + allowedValues: Array<{ name: string; id: string }> | []; + defaultValue: { name: string; id: string } | {}; + }; +} + +export interface Issue { + id: string; + key: string; + title: string; +} + +export type Issues = Issue[]; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.test.tsx new file mode 100644 index 0000000000000..b4c2c848d79ed --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.test.tsx @@ -0,0 +1,105 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../common/lib/kibana'; +import { connector } from '../mock'; +import { useGetFieldsByIssueType, UseGetFieldsByIssueType } from './use_get_fields_by_issue_type'; +import * as api from './api'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useGetFieldsByIssueType', () => { + const { http, notifications } = useKibanaMock().services; + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetFieldsByIssueType({ http, toastNotifications: notifications.toasts, issueType: null }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ isLoading: true, fields: {} }); + }); + }); + + test('does not fetch when issueType is not provided', async () => { + const spyOnGetFieldsByIssueType = jest.spyOn(api, 'getFieldsByIssueType'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetFieldsByIssueType({ + http, + toastNotifications: notifications.toasts, + connector, + issueType: null, + }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetFieldsByIssueType).not.toHaveBeenCalled(); + expect(result.current).toEqual({ isLoading: false, fields: {} }); + }); + }); + + test('fetch fields', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetFieldsByIssueType({ + http, + toastNotifications: notifications.toasts, + connector, + issueType: 'Task', + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + fields: { + summary: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + }, + }); + }); + }); + + test('unhappy path', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getFieldsByIssueType'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetFieldsByIssueType({ + http, + toastNotifications: notifications.toasts, + connector, + issueType: null, + }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ isLoading: false, fields: {} }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx new file mode 100644 index 0000000000000..03000e8916617 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.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 { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getFieldsByIssueType } from './api'; +import { Fields } from './types'; +import * as i18n from './translations'; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + issueType: string | null; + connector?: ActionConnector; +} + +export interface UseGetFieldsByIssueType { + fields: Fields; + isLoading: boolean; +} + +export const useGetFieldsByIssueType = ({ + http, + toastNotifications, + connector, + issueType, +}: Props): UseGetFieldsByIssueType => { + const [isLoading, setIsLoading] = useState(true); + const [fields, setFields] = useState({}); + const didCancel = useRef(false); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + const fetchData = async () => { + if (!connector || !issueType) { + setIsLoading(false); + return; + } + + try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + + const res = await getFieldsByIssueType({ + http, + signal: abortCtrl.current.signal, + connectorId: connector.id, + id: issueType, + }); + + if (!didCancel.current) { + setIsLoading(false); + setFields(res.data ?? {}); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.FIELDS_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel.current) { + setIsLoading(false); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.FIELDS_API_ERROR, + text: error.message, + }); + } + } + } + }; + + didCancel.current = false; + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; + }, [http, connector, issueType, toastNotifications]); + + return { + isLoading, + fields, + }; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.test.tsx new file mode 100644 index 0000000000000..6c1a9b5fcab08 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../common/lib/kibana'; +import { connector } from '../mock'; +import { useGetIssueTypes, UseGetIssueTypes } from './use_get_issue_types'; +import * as api from './api'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useGetIssueTypes', () => { + const { http, notifications } = useKibanaMock().services; + const handleIssueType = jest.fn(); + + beforeEach(() => jest.clearAllMocks()); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssueTypes({ http, toastNotifications: notifications.toasts, handleIssueType }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ isLoading: true, issueTypes: [] }); + }); + }); + + test('fetch issue types', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssueTypes({ + http, + toastNotifications: notifications.toasts, + connector, + handleIssueType, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + issueTypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], + }); + }); + }); + + test('handleIssueType is called', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useGetIssueTypes({ + http, + toastNotifications: notifications.toasts, + connector, + handleIssueType, + }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(handleIssueType).toHaveBeenCalledWith([ + { text: 'Task', value: '10006' }, + { text: 'Bug', value: '10007' }, + ]); + }); + }); + + test('unhappy path', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssueTypes'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssueTypes({ + http, + toastNotifications: notifications.toasts, + connector, + handleIssueType, + }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ isLoading: false, issueTypes: [] }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx new file mode 100644 index 0000000000000..3c35d315a2bcd --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getIssueTypes } from './api'; +import { IssueTypes } from './types'; +import * as i18n from './translations'; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + connector?: ActionConnector; + handleIssueType: (options: Array<{ value: string; text: string }>) => void; +} + +export interface UseGetIssueTypes { + issueTypes: IssueTypes; + isLoading: boolean; +} + +export const useGetIssueTypes = ({ + http, + connector, + toastNotifications, + handleIssueType, +}: Props): UseGetIssueTypes => { + const [isLoading, setIsLoading] = useState(true); + const [issueTypes, setIssueTypes] = useState([]); + const didCancel = useRef(false); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + const fetchData = async () => { + if (!connector) { + setIsLoading(false); + return; + } + + try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + + const res = await getIssueTypes({ + http, + signal: abortCtrl.current.signal, + connectorId: connector.id, + }); + + if (!didCancel.current) { + setIsLoading(false); + const asOptions = (res.data ?? []).map((type) => ({ + text: type.name ?? '', + value: type.id ?? '', + })); + setIssueTypes(res.data ?? []); + handleIssueType(asOptions); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.ISSUE_TYPES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel.current) { + setIsLoading(false); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.ISSUE_TYPES_API_ERROR, + text: error.message, + }); + } + } + } + }; + + didCancel.current = false; + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; + // handleIssueType unmounts the component at init causing the request to be aborted + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [http, connector, toastNotifications]); + + return { + issueTypes, + isLoading, + }; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.test.tsx new file mode 100644 index 0000000000000..2308fe604e710 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../common/lib/kibana'; +import { connector as actionConnector, issues } from '../mock'; +import { useGetIssues, UseGetIssues } from './use_get_issues'; +import * as api from './api'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useGetIssues', () => { + const { http, notifications } = useKibanaMock().services; + beforeEach(() => jest.clearAllMocks()); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssues({ + http, + toastNotifications: notifications.toasts, + actionConnector, + query: null, + }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ isLoading: false, issues: [] }); + }); + }); + + test('fetch issues', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssues({ + http, + toastNotifications: notifications.toasts, + actionConnector, + query: 'Task', + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + issues, + }); + }); + }); + + test('unhappy path', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssues'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIssues({ + http, + toastNotifications: notifications.toasts, + actionConnector, + query: 'oh no', + }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ isLoading: false, issues: [] }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.tsx new file mode 100644 index 0000000000000..b44b0558f1536 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.tsx @@ -0,0 +1,97 @@ +/* + * 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 { isEmpty, debounce } from 'lodash/fp'; +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getIssues } from './api'; +import { Issues } from './types'; +import * as i18n from './translations'; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + actionConnector?: ActionConnector; + query: string | null; +} + +export interface UseGetIssues { + issues: Issues; + isLoading: boolean; +} + +export const useGetIssues = ({ + http, + actionConnector, + toastNotifications, + query, +}: Props): UseGetIssues => { + const [isLoading, setIsLoading] = useState(false); + const [issues, setIssues] = useState([]); + const didCancel = useRef(false); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + const fetchData = debounce(500, async () => { + if (!actionConnector || isEmpty(query)) { + setIsLoading(false); + return; + } + + try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + + const res = await getIssues({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + title: query ?? '', + }); + + if (!didCancel.current) { + setIsLoading(false); + setIssues(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.ISSUES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel.current) { + setIsLoading(false); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.ISSUES_API_ERROR, + text: error.message, + }); + } + } + } + }); + + didCancel.current = false; + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; + }, [http, actionConnector, toastNotifications, query]); + + return { + issues, + isLoading, + }; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.test.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.test.tsx new file mode 100644 index 0000000000000..28949b456ecdd --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../common/lib/kibana'; +import { connector as actionConnector, issues } from '../mock'; +import { useGetSingleIssue, UseGetSingleIssue } from './use_get_single_issue'; +import * as api from './api'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useGetSingleIssue', () => { + const { http, notifications } = useKibanaMock().services; + beforeEach(() => jest.clearAllMocks()); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSingleIssue({ + http, + toastNotifications: notifications.toasts, + actionConnector, + id: null, + }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ isLoading: false, issue: null }); + }); + }); + + test('fetch issues', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSingleIssue({ + http, + toastNotifications: notifications.toasts, + actionConnector, + id: '123', + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + issue: issues[0], + }); + }); + }); + + test('unhappy path', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssue'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSingleIssue({ + http, + toastNotifications: notifications.toasts, + actionConnector, + id: '123', + }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ isLoading: false, issue: null }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.tsx new file mode 100644 index 0000000000000..6c70286426168 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.tsx @@ -0,0 +1,95 @@ +/* + * 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 { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getIssue } from './api'; +import { Issue } from './types'; +import * as i18n from './translations'; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + id: string | null; + actionConnector?: ActionConnector; +} + +export interface UseGetSingleIssue { + issue: Issue | null; + isLoading: boolean; +} + +export const useGetSingleIssue = ({ + http, + toastNotifications, + actionConnector, + id, +}: Props): UseGetSingleIssue => { + const [isLoading, setIsLoading] = useState(false); + const [issue, setIssue] = useState(null); + const didCancel = useRef(false); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + const fetchData = async () => { + if (!actionConnector || !id) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + try { + const res = await getIssue({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + id, + }); + + if (!didCancel.current) { + setIsLoading(false); + setIssue(res.data ?? null); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.GET_ISSUE_API_ERROR(id), + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel.current) { + setIsLoading(false); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.GET_ISSUE_API_ERROR(id), + text: error.message, + }); + } + } + } + }; + + didCancel.current = false; + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; + }, [http, actionConnector, id, toastNotifications]); + + return { + isLoading, + issue, + }; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/mock.ts b/x-pack/plugins/cases/public/components/connectors/mock.ts new file mode 100644 index 0000000000000..f5429fa2396aa --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/mock.ts @@ -0,0 +1,121 @@ +/* + * 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 const connector = { + id: '123', + name: 'My connector', + actionTypeId: '.jira', + config: {}, + isPreconfigured: false, +}; + +export const issues = [ + { id: 'personId', title: 'Person Task', key: 'personKey' }, + { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, + { id: 'manId', title: 'Man Task', key: 'manKey' }, + { id: 'cameraId', title: 'Camera Task', key: 'cameraKey' }, + { id: 'tvId', title: 'TV Task', key: 'tvKey' }, +]; + +export const choices = [ + { + dependent_value: '', + label: 'Priviledge Escalation', + value: 'Priviledge Escalation', + element: 'category', + }, + { + dependent_value: '', + label: 'Criminal activity/investigation', + value: 'Criminal activity/investigation', + element: 'category', + }, + { + dependent_value: '', + label: 'Denial of Service', + value: 'Denial of Service', + element: 'category', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound or outbound', + value: '12', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Single or distributed (DoS or DDoS)', + value: '26', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound DDos', + value: 'inbound_ddos', + element: 'subcategory', + }, + { + dependent_value: '', + label: 'Software', + value: 'software', + element: 'category', + }, + { + dependent_value: 'software', + label: 'Operation System', + value: 'os', + element: 'subcategory', + }, + ...['severity', 'urgency', 'impact', 'priority'] + .map((element) => [ + { + dependent_value: '', + label: '1 - Critical', + value: '1', + element, + }, + { + dependent_value: '', + label: '2 - High', + value: '2', + element, + }, + { + dependent_value: '', + label: '3 - Moderate', + value: '3', + element, + }, + { + dependent_value: '', + label: '4 - Low', + value: '4', + element, + }, + ]) + .flat(), +]; + +export const severity = [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, +]; + +export const incidentTypes = [ + { id: 17, name: 'Communication error (fax; email)' }, + { id: 1001, name: 'Custom type' }, +]; diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/__mocks__/api.ts b/x-pack/plugins/cases/public/components/connectors/resilient/__mocks__/api.ts new file mode 100644 index 0000000000000..c27248288907d --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/__mocks__/api.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { incidentTypes, severity } from '../../mock'; +import { Props } from '../api'; +import { ResilientIncidentTypes, ResilientSeverity } from '../types'; + +export const getIncidentTypes = async (props: Props): Promise<{ data: ResilientIncidentTypes }> => + Promise.resolve({ data: incidentTypes }); + +export const getSeverity = async (props: Props): Promise<{ data: ResilientSeverity }> => + Promise.resolve({ data: severity }); diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/api.ts b/x-pack/plugins/cases/public/components/connectors/resilient/api.ts new file mode 100644 index 0000000000000..5fec83f303950 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/api.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from 'kibana/public'; +import { ActionTypeExecutorResult } from '../../../../../actions/common'; +import { ResilientIncidentTypes, ResilientSeverity } from './types'; + +export const BASE_ACTION_API_PATH = '/api/actions'; + +export interface Props { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; +} + +export async function getIncidentTypes({ http, signal, connectorId }: Props) { + return http.post>( + `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, + { + body: JSON.stringify({ + params: { subAction: 'incidentTypes', subActionParams: {} }, + }), + signal, + } + ); +} + +export async function getSeverity({ http, signal, connectorId }: Props) { + return http.post>( + `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, + { + body: JSON.stringify({ + params: { subAction: 'severity', subActionParams: {} }, + }), + signal, + } + ); +} diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.test.tsx new file mode 100644 index 0000000000000..dda6ba5de95cc --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { waitFor } from '@testing-library/react'; + +import { connector } from '../mock'; +import { useGetIncidentTypes } from './use_get_incident_types'; +import { useGetSeverity } from './use_get_severity'; +import Fields from './case_fields'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./use_get_incident_types'); +jest.mock('./use_get_severity'); + +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; + +describe('ResilientParamsFields renders', () => { + const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], + }; + + const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], + }; + + const fields = { + severityCode: '6', + incidentTypes: ['19'], + }; + + const onChange = jest.fn(); + + beforeEach(() => { + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + jest.clearAllMocks(); + }); + + test('all params fields are rendered', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('options')).toEqual( + [ + { label: 'Malware', value: '19' }, + { label: 'Denial of Service', value: '21' }, + ] + ); + + expect( + wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('selectedOptions') + ).toEqual([{ label: 'Malware', value: '19' }]); + + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( + '6' + ); + }); + + test('it disabled the fields when loading incident types', () => { + useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true }); + + const wrapper = mount(); + + expect( + wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isDisabled') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading severity', () => { + useGetSeverityMock.mockReturnValue({ + ...useGetSeverityResponse, + isLoading: true, + }); + + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('disabled')).toBeTruthy(); + }); + + test('it sets issue type correctly', async () => { + const wrapper = mount(); + + await waitFor(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ value: '19', label: 'Denial of Service' }]); + }); + + expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '6' }); + }); + + test('it sets severity correctly', async () => { + const wrapper = mount(); + + wrapper + .find('select[data-test-subj="severitySelect"]') + .first() + .simulate('change', { + target: { value: '4' }, + }); + + expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '4' }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx new file mode 100644 index 0000000000000..e1eeb13bf684c --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx @@ -0,0 +1,189 @@ +/* + * 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, useCallback, useEffect, useRef } from 'react'; +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiFormRow, + EuiSelect, + EuiSelectOption, + EuiSpacer, +} from '@elastic/eui'; + +import { useKibana } from '../../../common/lib/kibana'; +import { ConnectorFieldsProps } from '../types'; +import { useGetIncidentTypes } from './use_get_incident_types'; +import { useGetSeverity } from './use_get_severity'; + +import * as i18n from './translations'; +import { ConnectorTypes, ResilientFieldsType } from '../../../../common'; +import { ConnectorCard } from '../card'; + +const ResilientFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); + const { incidentTypes = null, severityCode = null } = fields ?? {}; + + const { http, notifications } = useKibana().services; + + const { + isLoading: isLoadingIncidentTypes, + incidentTypes: allIncidentTypes, + } = useGetIncidentTypes({ + http, + toastNotifications: notifications.toasts, + connector, + }); + + const { isLoading: isLoadingSeverity, severity } = useGetSeverity({ + http, + toastNotifications: notifications.toasts, + connector, + }); + + const severitySelectOptions: EuiSelectOption[] = useMemo( + () => + severity.map((s) => ({ + value: s.id.toString(), + text: s.name, + })), + [severity] + ); + + const incidentTypesComboBoxOptions: Array> = useMemo( + () => + allIncidentTypes + ? allIncidentTypes.map((type: { id: number; name: string }) => ({ + label: type.name, + value: type.id.toString(), + })) + : [], + [allIncidentTypes] + ); + const listItems = useMemo( + () => [ + ...(incidentTypes != null && incidentTypes.length > 0 + ? [ + { + title: i18n.INCIDENT_TYPES_LABEL, + description: allIncidentTypes + .filter((type) => incidentTypes.includes(type.id.toString())) + .map((type) => type.name) + .join(', '), + }, + ] + : []), + ...(severityCode != null && severityCode.length > 0 + ? [ + { + title: i18n.SEVERITY_LABEL, + description: + severity.find((severityObj) => severityObj.id.toString() === severityCode)?.name ?? + '', + }, + ] + : []), + ], + [incidentTypes, severityCode, allIncidentTypes, severity] + ); + + const onFieldChange = useCallback( + (key, value) => { + onChange({ + ...fields, + incidentTypes, + severityCode, + [key]: value, + }); + }, + [incidentTypes, severityCode, onChange, fields] + ); + + const selectedIncidentTypesComboBoxOptionsMemo = useMemo(() => { + const allIncidentTypesAsObject = allIncidentTypes.reduce( + (acc, type) => ({ ...acc, [type.id.toString()]: type.name }), + {} as Record + ); + return incidentTypes + ? incidentTypes + .map((type) => ({ + label: allIncidentTypesAsObject[type.toString()], + value: type.toString(), + })) + .filter((type) => type.label != null) + : []; + }, [allIncidentTypes, incidentTypes]); + + const onIncidentChange = useCallback( + (selectedOptions: Array<{ label: string; value?: string }>) => { + onFieldChange( + 'incidentTypes', + selectedOptions.map((selectedOption) => selectedOption.value ?? selectedOption.label) + ); + }, + [onFieldChange] + ); + + const onIncidentBlur = useCallback(() => { + if (!incidentTypes) { + onFieldChange('incidentTypes', []); + } + }, [incidentTypes, onFieldChange]); + + // Set field at initialization + useEffect(() => { + if (init.current) { + init.current = false; + onChange({ incidentTypes, severityCode }); + } + }, [incidentTypes, onChange, severityCode]); + + return isEdit ? ( + + + + + + + onFieldChange('severityCode', e.target.value)} + options={severitySelectOptions} + value={severityCode ?? undefined} + /> + + + + ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ResilientFieldsComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts new file mode 100644 index 0000000000000..c8e7ad9a063cb --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { ResilientFieldsType } from '../../../../common'; +import * as i18n from './translations'; + +export * from './types'; + +export const getCaseConnector = (): CaseConnector => { + return { + id: '.resilient', + fieldsComponent: lazy(() => import('./case_fields')), + }; +}; + +export const fieldLabels = { + incidentTypes: i18n.INCIDENT_TYPES_LABEL, + severityCode: i18n.SEVERITY_LABEL, +}; diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/translations.ts b/x-pack/plugins/cases/public/components/connectors/resilient/translations.ts new file mode 100644 index 0000000000000..1b63a5098e92a --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/translations.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const INCIDENT_TYPES_API_ERROR = i18n.translate( + 'xpack.cases.connectors.resilient.unableToGetIncidentTypesMessage', + { + defaultMessage: 'Unable to get incident types', + } +); + +export const SEVERITY_API_ERROR = i18n.translate( + 'xpack.cases.connectors.resilient.unableToGetSeverityMessage', + { + defaultMessage: 'Unable to get severity', + } +); + +export const INCIDENT_TYPES_PLACEHOLDER = i18n.translate( + 'xpack.cases.connectors.resilient.incidentTypesPlaceholder', + { + defaultMessage: 'Choose types', + } +); + +export const INCIDENT_TYPES_LABEL = i18n.translate( + 'xpack.cases.connectors.resilient.incidentTypesLabel', + { + defaultMessage: 'Incident Types', + } +); + +export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.resilient.severityLabel', { + defaultMessage: 'Severity', +}); diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/types.ts b/x-pack/plugins/cases/public/components/connectors/resilient/types.ts new file mode 100644 index 0000000000000..06506d2c0d2f9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/types.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type ResilientIncidentTypes = Array<{ id: number; name: string }>; +export type ResilientSeverity = ResilientIncidentTypes; diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.test.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.test.tsx new file mode 100644 index 0000000000000..59c1f8e9b40d0 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../common/lib/kibana'; +import { connector } from '../mock'; +import { useGetIncidentTypes, UseGetIncidentTypes } from './use_get_incident_types'; +import * as api from './api'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useGetIncidentTypes', () => { + const { http, notifications } = useKibanaMock().services; + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIncidentTypes({ http, toastNotifications: notifications.toasts }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ isLoading: true, incidentTypes: [] }); + }); + }); + + test('fetch incident types', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIncidentTypes({ + http, + toastNotifications: notifications.toasts, + connector, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + incidentTypes: [ + { id: 17, name: 'Communication error (fax; email)' }, + { id: 1001, name: 'Custom type' }, + ], + }); + }); + }); + + test('unhappy path', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getIncidentTypes'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetIncidentTypes({ http, toastNotifications: notifications.toasts, connector }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ isLoading: false, incidentTypes: [] }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.tsx new file mode 100644 index 0000000000000..34cbb0a69b0f4 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getIncidentTypes } from './api'; +import * as i18n from './translations'; + +type IncidentTypes = Array<{ id: number; name: string }>; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + connector?: ActionConnector; +} + +export interface UseGetIncidentTypes { + incidentTypes: IncidentTypes; + isLoading: boolean; +} + +export const useGetIncidentTypes = ({ + http, + toastNotifications, + connector, +}: Props): UseGetIncidentTypes => { + const [isLoading, setIsLoading] = useState(true); + const [incidentTypes, setIncidentTypes] = useState([]); + const didCancel = useRef(false); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + const fetchData = async () => { + if (!connector) { + setIsLoading(false); + return; + } + + try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + + const res = await getIncidentTypes({ + http, + signal: abortCtrl.current.signal, + connectorId: connector.id, + }); + + if (!didCancel.current) { + setIsLoading(false); + setIncidentTypes(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.INCIDENT_TYPES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel.current) { + setIsLoading(false); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.INCIDENT_TYPES_API_ERROR, + text: error.message, + }); + } + } + } + }; + + didCancel.current = false; + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; + }, [http, connector, toastNotifications]); + + return { + incidentTypes, + isLoading, + }; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.test.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.test.tsx new file mode 100644 index 0000000000000..f646dd7e8f7c2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../common/lib/kibana'; +import { connector } from '../mock'; +import { useGetSeverity, UseGetSeverity } from './use_get_severity'; +import * as api from './api'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./api'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useGetSeverity', () => { + const { http, notifications } = useKibanaMock().services; + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSeverity({ http, toastNotifications: notifications.toasts }) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ isLoading: true, severity: [] }); + }); + }); + + test('fetch severity', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSeverity({ http, toastNotifications: notifications.toasts, connector }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], + }); + }); + }); + + test('unhappy path', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getSeverity'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetSeverity({ http, toastNotifications: notifications.toasts, connector }) + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ isLoading: false, severity: [] }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.tsx new file mode 100644 index 0000000000000..5b44c6b4a32b2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getSeverity } from './api'; +import * as i18n from './translations'; + +type Severity = Array<{ id: number; name: string }>; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + connector?: ActionConnector; +} + +export interface UseGetSeverity { + severity: Severity; + isLoading: boolean; +} + +export const useGetSeverity = ({ http, toastNotifications, connector }: Props): UseGetSeverity => { + const [isLoading, setIsLoading] = useState(true); + const [severity, setSeverity] = useState([]); + const abortCtrl = useRef(new AbortController()); + const didCancel = useRef(false); + + useEffect(() => { + const fetchData = async () => { + if (!connector) { + setIsLoading(false); + return; + } + + try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + + const res = await getSeverity({ + http, + signal: abortCtrl.current.signal, + connectorId: connector.id, + }); + + if (!didCancel.current) { + setIsLoading(false); + setSeverity(res.data ?? []); + + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.SEVERITY_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel.current) { + setIsLoading(false); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.SEVERITY_API_ERROR, + text: error.message, + }); + } + } + } + }; + + didCancel.current = false; + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; + }, [http, connector, toastNotifications]); + + return { + severity, + isLoading, + }; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/__mocks__/api.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/__mocks__/api.ts new file mode 100644 index 0000000000000..215e3d6f92e6d --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/__mocks__/api.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { choices } from '../../mock'; +import { GetChoicesProps } from '../api'; +import { Choice } from '../types'; + +export const choicesResponse = { + status: 'ok', + data: choices, +}; + +export const getChoices = async ( + props: GetChoicesProps +): Promise<{ status: string; data: Choice[] }> => Promise.resolve(choicesResponse); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/api.test.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/api.test.ts new file mode 100644 index 0000000000000..461823036ed21 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/api.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { getChoices } from './api'; +import { choices } from '../mock'; + +const choicesResponse = { + status: 'ok', + data: choices, +}; + +describe('ServiceNow API', () => { + const http = httpServiceMock.createStartContract(); + + beforeEach(() => jest.resetAllMocks()); + + describe('getChoices', () => { + test('should call get choices API', async () => { + const abortCtrl = new AbortController(); + http.post.mockResolvedValueOnce(choicesResponse); + const res = await getChoices({ + http, + signal: abortCtrl.signal, + connectorId: 'test', + fields: ['priority'], + }); + + expect(res).toEqual(choicesResponse); + expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}', + signal: abortCtrl.signal, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/api.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/api.ts new file mode 100644 index 0000000000000..e68eb18860ae3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/api.ts @@ -0,0 +1,31 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import { ActionTypeExecutorResult } from '../../../../../actions/common'; +import { Choice } from './types'; + +export const BASE_ACTION_API_PATH = '/api/actions'; + +export interface GetChoicesProps { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; + fields: string[]; +} + +export async function getChoices({ http, signal, connectorId, fields }: GetChoicesProps) { + return http.post>( + `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, + { + body: JSON.stringify({ + params: { subAction: 'getChoices', subActionParams: { fields } }, + }), + signal, + } + ); +} diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/helpers.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/helpers.ts new file mode 100644 index 0000000000000..314d224491128 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/helpers.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSelectOption } from '@elastic/eui'; +import { Choice } from './types'; + +export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => + choices.map((choice) => ({ value: choice.value, text: choice.label })); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts new file mode 100644 index 0000000000000..a6f0795fe4d8f --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType } from '../../../../common'; +import * as i18n from './translations'; + +export const getServiceNowITSMCaseConnector = (): CaseConnector => { + return { + id: '.servicenow', + fieldsComponent: lazy(() => import('./servicenow_itsm_case_fields')), + }; +}; + +export const getServiceNowSIRCaseConnector = (): CaseConnector => { + return { + id: '.servicenow-sir', + fieldsComponent: lazy(() => import('./servicenow_sir_case_fields')), + }; +}; + +export const serviceNowITSMFieldLabels = { + impact: i18n.IMPACT, + severity: i18n.SEVERITY, + urgency: i18n.URGENCY, +}; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx new file mode 100644 index 0000000000000..9688ca191d672 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -0,0 +1,164 @@ +/* + * 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 { waitFor, act } from '@testing-library/react'; +import { EuiSelect } from '@elastic/eui'; +import { mount } from 'enzyme'; + +import { connector, choices as mockChoices } from '../mock'; +import { Choice } from './types'; +import Fields from './servicenow_itsm_case_fields'; + +let onChoicesSuccess = (c: Choice[]) => {}; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./use_get_choices', () => ({ + useGetChoices: (args: { onSuccess: () => void }) => { + onChoicesSuccess = args.onSuccess; + return { isLoading: false, choices: mockChoices }; + }, +})); + +describe('ServiceNowITSM Fields', () => { + const fields = { + severity: '1', + urgency: '2', + impact: '3', + category: 'software', + subcategory: 'os', + }; + const onChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('all params fields are rendered - isEdit: true', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy(); + }); + + it('all params fields are rendered - isEdit: false', () => { + const wrapper = mount( + + ); + act(() => { + onChoicesSuccess(mockChoices); + }); + + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( + 'Urgency: 2 - High' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( + 'Severity: 1 - Critical' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( + 'Impact: 3 - Moderate' + ); + }); + + test('it transforms the categories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([ + { value: 'Priviledge Escalation', text: 'Priviledge Escalation' }, + { + value: 'Criminal activity/investigation', + text: 'Criminal activity/investigation', + }, + { value: 'Denial of Service', text: 'Denial of Service' }, + { + value: 'software', + text: 'Software', + }, + ]); + }); + + test('it transforms the subcategories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([ + { + text: 'Operation System', + value: 'os', + }, + ]); + }); + + it('it transforms the options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + const testers = ['severity', 'urgency', 'impact']; + testers.forEach((subj) => + expect(wrapper.find(`[data-test-subj="${subj}Select"]`).first().prop('options')).toEqual([ + { value: '1', text: '1 - Critical' }, + { value: '2', text: '2 - High' }, + { value: '3', text: '3 - Moderate' }, + { value: '4', text: '4 - Low' }, + ]) + ); + }); + + describe('onChange calls', () => { + const wrapper = mount(); + + expect(onChange).toHaveBeenCalledWith(fields); + + const testers = ['severity', 'urgency', 'impact', 'subcategory']; + testers.forEach((subj) => + test(`${subj.toUpperCase()}`, async () => { + await waitFor(() => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="${subj}Select"]`)!; + select.prop('onChange')!({ + target: { + value: '9', + }, + } as React.ChangeEvent); + }); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + [subj]: '9', + }); + }) + ); + + test('it should set subcategory to null when changing category', async () => { + await waitFor(() => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="categorySelect"]`)!; + select.prop('onChange')!({ + target: { + value: 'network', + }, + } as React.ChangeEvent); + }); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + subcategory: null, + category: 'network', + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx new file mode 100644 index 0000000000000..710e230958354 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -0,0 +1,235 @@ +/* + * 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, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +import { ConnectorFieldsProps } from '../types'; +import { ConnectorTypes, ServiceNowITSMFieldsType } from '../../../../common'; +import { useKibana } from '../../../common/lib/kibana'; +import { ConnectorCard } from '../card'; +import { useGetChoices } from './use_get_choices'; +import { Fields, Choice } from './types'; +import { choicesToEuiOptions } from './helpers'; + +const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; +const defaultFields: Fields = { + urgency: [], + severity: [], + impact: [], + category: [], + subcategory: [], +}; + +const ServiceNowITSMFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); + const { severity = null, urgency = null, impact = null, category = null, subcategory = null } = + fields ?? {}; + const { http, notifications } = useKibana().services; + const [choices, setChoices] = useState(defaultFields); + + const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); + const urgencyOptions = useMemo(() => choicesToEuiOptions(choices.urgency), [choices.urgency]); + const severityOptions = useMemo(() => choicesToEuiOptions(choices.severity), [choices.severity]); + const impactOptions = useMemo(() => choicesToEuiOptions(choices.impact), [choices.impact]); + + const subcategoryOptions = useMemo( + () => + choicesToEuiOptions( + choices.subcategory.filter((choice) => choice.dependent_value === category) + ), + [choices.subcategory, category] + ); + + const listItems = useMemo( + () => [ + ...(urgency != null && urgency.length > 0 + ? [ + { + title: i18n.URGENCY, + description: urgencyOptions.find((option) => `${option.value}` === urgency)?.text, + }, + ] + : []), + ...(severity != null && severity.length > 0 + ? [ + { + title: i18n.SEVERITY, + description: severityOptions.find((option) => `${option.value}` === severity)?.text, + }, + ] + : []), + ...(impact != null && impact.length > 0 + ? [ + { + title: i18n.IMPACT, + description: impactOptions.find((option) => `${option.value}` === impact)?.text, + }, + ] + : []), + ...(category != null && category.length > 0 + ? [ + { + title: i18n.CATEGORY, + description: categoryOptions.find((option) => `${option.value}` === category)?.text, + }, + ] + : []), + ...(subcategory != null && subcategory.length > 0 + ? [ + { + title: i18n.SUBCATEGORY, + description: subcategoryOptions.find((option) => `${option.value}` === subcategory) + ?.text, + }, + ] + : []), + ], + [ + category, + categoryOptions, + impact, + impactOptions, + severity, + severityOptions, + subcategory, + subcategoryOptions, + urgency, + urgencyOptions, + ] + ); + + const onChoicesSuccess = (values: Choice[]) => { + setChoices( + values.reduce( + (acc, value) => ({ + ...acc, + [value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value], + }), + defaultFields + ) + ); + }; + + const { isLoading: isLoadingChoices } = useGetChoices({ + http, + toastNotifications: notifications.toasts, + connector, + fields: useGetChoicesFields, + onSuccess: onChoicesSuccess, + }); + + const onChangeCb = useCallback( + ( + key: keyof ServiceNowITSMFieldsType, + value: ServiceNowITSMFieldsType[keyof ServiceNowITSMFieldsType] + ) => { + onChange({ ...fields, [key]: value }); + }, + [fields, onChange] + ); + + // Set field at initialization + useEffect(() => { + if (init.current) { + init.current = false; + onChange({ urgency, severity, impact, category, subcategory }); + } + }, [category, impact, onChange, severity, subcategory, urgency]); + + return isEdit ? ( +
+ + onChangeCb('urgency', e.target.value)} + /> + + + + + + onChangeCb('severity', e.target.value)} + /> + + + + + onChangeCb('impact', e.target.value)} + /> + + + + + + + onChange({ ...fields, category: e.target.value, subcategory: null })} + /> + + + + + onChangeCb('subcategory', e.target.value)} + /> + + + +
+ ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowITSMFieldsComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx new file mode 100644 index 0000000000000..4a5b34cd3c3cb --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx @@ -0,0 +1,221 @@ +/* + * 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 { waitFor, act } from '@testing-library/react'; +import { EuiSelect } from '@elastic/eui'; + +import { connector, choices as mockChoices } from '../mock'; +import { Choice } from './types'; +import Fields from './servicenow_sir_case_fields'; + +let onChoicesSuccess = (c: Choice[]) => {}; + +jest.mock('../../../common/lib/kibana'); +jest.mock('./use_get_choices', () => ({ + useGetChoices: (args: { onSuccess: () => void }) => { + onChoicesSuccess = args.onSuccess; + return { isLoading: false, mockChoices }; + }, +})); + +describe('ServiceNowSIR Fields', () => { + const fields = { + destIp: true, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + priority: '1', + category: 'Denial of Service', + subcategory: '26', + }; + const onChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('all params fields are rendered - isEdit: true', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="destIpCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sourceIpCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="malwareUrlCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="malwareHashCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy(); + }); + + test('all params fields are rendered - isEdit: false', () => { + const wrapper = mount( + + ); + act(() => { + onChoicesSuccess(mockChoices); + }); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( + 'Destination IP: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( + 'Source IP: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( + 'Malware URL: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(3).text()).toEqual( + 'Malware Hash: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(4).text()).toEqual( + 'Priority: 1 - Critical' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(5).text()).toEqual( + 'Category: Denial of Service' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(6).text()).toEqual( + 'Subcategory: Single or distributed (DoS or DDoS)' + ); + }); + + test('it transforms the categories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([ + { value: 'Priviledge Escalation', text: 'Priviledge Escalation' }, + { + value: 'Criminal activity/investigation', + text: 'Criminal activity/investigation', + }, + { value: 'Denial of Service', text: 'Denial of Service' }, + { + text: 'Software', + value: 'software', + }, + ]); + }); + + test('it transforms the subcategories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([ + { + text: 'Inbound or outbound', + value: '12', + }, + { + text: 'Single or distributed (DoS or DDoS)', + value: '26', + }, + { + text: 'Inbound DDos', + value: 'inbound_ddos', + }, + ]); + }); + + test('it transforms the priorities to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('options')).toEqual([ + { + text: '1 - Critical', + value: '1', + }, + { + text: '2 - High', + value: '2', + }, + { + text: '3 - Moderate', + value: '3', + }, + { + text: '4 - Low', + value: '4', + }, + ]); + }); + + describe('onChange calls', () => { + const wrapper = mount(); + + act(() => { + onChoicesSuccess(mockChoices); + }); + wrapper.update(); + + expect(onChange).toHaveBeenCalledWith(fields); + + const checkbox = ['destIp', 'sourceIp', 'malwareHash', 'malwareUrl']; + checkbox.forEach((subj) => + test(`${subj.toUpperCase()}`, async () => { + await waitFor(() => { + wrapper + .find(`[data-test-subj="${subj}Checkbox"] input`) + .first() + .simulate('change', { target: { checked: false } }); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + [subj]: false, + }); + }); + }) + ); + + const testers = ['priority', 'subcategory']; + testers.forEach((subj) => + test(`${subj.toUpperCase()}`, async () => { + await waitFor(() => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="${subj}Select"]`)!; + select.prop('onChange')!({ + target: { + value: '9', + }, + } as React.ChangeEvent); + }); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + [subj]: '9', + }); + }) + ); + + test('it should set subcategory to null when changing category', async () => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="categorySelect"]`)!; + select.prop('onChange')!({ + target: { + value: 'network', + }, + } as React.ChangeEvent); + + wrapper.update(); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith({ + ...fields, + subcategory: null, + category: 'network', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx new file mode 100644 index 0000000000000..1f9a7cf7acd64 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -0,0 +1,282 @@ +/* + * 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, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; +import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; + +import { ConnectorTypes, ServiceNowSIRFieldsType } from '../../../../common'; +import { useKibana } from '../../../common/lib/kibana'; +import { ConnectorFieldsProps } from '../types'; +import { ConnectorCard } from '../card'; +import { useGetChoices } from './use_get_choices'; +import { Choice, Fields } from './types'; +import { choicesToEuiOptions } from './helpers'; + +import * as i18n from './translations'; + +const useGetChoicesFields = ['category', 'subcategory', 'priority']; +const defaultFields: Fields = { + category: [], + subcategory: [], + priority: [], +}; + +const ServiceNowSIRFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); + const { + category = null, + destIp = true, + malwareHash = true, + malwareUrl = true, + priority = null, + sourceIp = true, + subcategory = null, + } = fields ?? {}; + + const { http, notifications } = useKibana().services; + + const [choices, setChoices] = useState(defaultFields); + + const onChangeCb = useCallback( + ( + key: keyof ServiceNowSIRFieldsType, + value: ServiceNowSIRFieldsType[keyof ServiceNowSIRFieldsType] + ) => { + onChange({ ...fields, [key]: value }); + }, + [fields, onChange] + ); + + const onChoicesSuccess = (values: Choice[]) => { + setChoices( + values.reduce( + (acc, value) => ({ + ...acc, + [value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value], + }), + defaultFields + ) + ); + }; + + const { isLoading: isLoadingChoices } = useGetChoices({ + http, + toastNotifications: notifications.toasts, + connector, + fields: useGetChoicesFields, + onSuccess: onChoicesSuccess, + }); + + const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); + const priorityOptions = useMemo(() => choicesToEuiOptions(choices.priority), [choices.priority]); + + const subcategoryOptions = useMemo( + () => + choicesToEuiOptions( + choices.subcategory.filter((choice) => choice.dependent_value === category) + ), + [choices.subcategory, category] + ); + + const listItems = useMemo( + () => [ + ...(destIp != null && destIp + ? [ + { + title: i18n.DEST_IP, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(sourceIp != null && sourceIp + ? [ + { + title: i18n.SOURCE_IP, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(malwareUrl != null && malwareUrl + ? [ + { + title: i18n.MALWARE_URL, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(malwareHash != null && malwareHash + ? [ + { + title: i18n.MALWARE_HASH, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(priority != null && priority.length > 0 + ? [ + { + title: i18n.PRIORITY, + description: priorityOptions.find((option) => `${option.value}` === priority)?.text, + }, + ] + : []), + ...(category != null && category.length > 0 + ? [ + { + title: i18n.CATEGORY, + description: categoryOptions.find((option) => `${option.value}` === category)?.text, + }, + ] + : []), + ...(subcategory != null && subcategory.length > 0 + ? [ + { + title: i18n.SUBCATEGORY, + description: subcategoryOptions.find((option) => `${option.value}` === subcategory) + ?.text, + }, + ] + : []), + ], + [ + category, + categoryOptions, + destIp, + malwareHash, + malwareUrl, + priority, + priorityOptions, + sourceIp, + subcategory, + subcategoryOptions, + ] + ); + + // Set field at initialization + useEffect(() => { + if (init.current) { + init.current = false; + onChange({ category, destIp, malwareHash, malwareUrl, priority, sourceIp, subcategory }); + } + }, [category, destIp, malwareHash, malwareUrl, onChange, priority, sourceIp, subcategory]); + + return isEdit ? ( +
+ + + + <> + + + onChangeCb('destIp', e.target.checked)} + /> + + + onChangeCb('sourceIp', e.target.checked)} + /> + + + + + onChangeCb('malwareUrl', e.target.checked)} + /> + + + onChangeCb('malwareHash', e.target.checked)} + /> + + + + + + + + + + onChangeCb('priority', e.target.value)} + /> + + + + + + + onChange({ ...fields, category: e.target.value, subcategory: null })} + /> + + + + + onChangeCb('subcategory', e.target.value)} + /> + + + +
+ ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowSIRFieldsComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/translations.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/translations.ts new file mode 100644 index 0000000000000..fc48ecf17f2c6 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/translations.ts @@ -0,0 +1,75 @@ +/* + * 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'; + +export const URGENCY = i18n.translate('xpack.cases.connectors.serviceNow.urgencySelectFieldLabel', { + defaultMessage: 'Urgency', +}); + +export const SEVERITY = i18n.translate( + 'xpack.cases.connectors.serviceNow.severitySelectFieldLabel', + { + defaultMessage: 'Severity', + } +); + +export const IMPACT = i18n.translate('xpack.cases.connectors.serviceNow.impactSelectFieldLabel', { + defaultMessage: 'Impact', +}); + +export const CHOICES_API_ERROR = i18n.translate( + 'xpack.cases.connectors.serviceNow.unableToGetChoicesMessage', + { + defaultMessage: 'Unable to get choices', + } +); + +export const MALWARE_URL = i18n.translate('xpack.cases.connectors.serviceNow.malwareURLTitle', { + defaultMessage: 'Malware URL', +}); + +export const MALWARE_HASH = i18n.translate('xpack.cases.connectors.serviceNow.malwareHashTitle', { + defaultMessage: 'Malware Hash', +}); + +export const CATEGORY = i18n.translate('xpack.cases.connectors.serviceNow.categoryTitle', { + defaultMessage: 'Category', +}); + +export const SUBCATEGORY = i18n.translate('xpack.cases.connectors.serviceNow.subcategoryTitle', { + defaultMessage: 'Subcategory', +}); + +export const SOURCE_IP = i18n.translate('xpack.cases.connectors.serviceNow.sourceIPTitle', { + defaultMessage: 'Source IP', +}); + +export const DEST_IP = i18n.translate('xpack.cases.connectors.serviceNow.destinationIPTitle', { + defaultMessage: 'Destination IP', +}); + +export const PRIORITY = i18n.translate( + 'xpack.cases.connectors.serviceNow.prioritySelectFieldTitle', + { + defaultMessage: 'Priority', + } +); + +export const ALERT_FIELDS_LABEL = i18n.translate( + 'xpack.cases.connectors.serviceNow.alertFieldsTitle', + { + defaultMessage: 'Select Observables to push', + } +); + +export const ALERT_FIELD_ENABLED_TEXT = i18n.translate( + 'xpack.cases.connectors.serviceNow.alertFieldEnabledText', + { + defaultMessage: 'Yes', + } +); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/types.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/types.ts new file mode 100644 index 0000000000000..fd1af62f7bb2a --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Choice { + value: string; + label: string; + dependent_value: string; + element: string; +} + +export type Fields = Record; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx new file mode 100644 index 0000000000000..ed4577dd0114b --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx @@ -0,0 +1,144 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../common/lib/kibana'; +import { ActionConnector } from '../../../containers/types'; +import { choices } from '../mock'; +import { useGetChoices, UseGetChoices, UseGetChoicesProps } from './use_get_choices'; +import * as api from './api'; + +jest.mock('./api'); +jest.mock('../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked; +const onSuccess = jest.fn(); +const fields = ['priority']; + +const connector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + name: 'ServiceNow', + isPreconfigured: false, + config: { + apiUrl: 'https://dev94428.service-now.com/', + }, +} as ActionConnector; + +describe('useGetChoices', () => { + const { services } = useKibanaMock(); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('init', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + isLoading: false, + choices, + }); + }); + + it('returns an empty array when connector is not presented', async () => { + const { result } = renderHook(() => + useGetChoices({ + http: services.http, + connector: undefined, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + expect(result.current).toEqual({ + isLoading: false, + choices: [], + }); + }); + + it('it calls onSuccess', async () => { + const { waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(onSuccess).toHaveBeenCalledWith(choices); + }); + + it('it displays an error when service fails', async () => { + const spyOnGetChoices = jest.spyOn(api, 'getChoices'); + spyOnGetChoices.mockResolvedValue( + Promise.resolve({ + actionId: 'test', + status: 'error', + serviceMessage: 'An error occurred', + }) + ); + + const { waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }); + }); + + it('it displays an error when http throws an error', async () => { + const spyOnGetChoices = jest.spyOn(api, 'getChoices'); + spyOnGetChoices.mockImplementation(() => { + throw new Error('An error occurred'); + }); + + renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.tsx new file mode 100644 index 0000000000000..a979f96d84ab2 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getChoices } from './api'; +import { Choice } from './types'; +import * as i18n from './translations'; + +export interface UseGetChoicesProps { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + connector?: ActionConnector; + fields: string[]; + onSuccess?: (choices: Choice[]) => void; +} + +export interface UseGetChoices { + choices: Choice[]; + isLoading: boolean; +} + +export const useGetChoices = ({ + http, + connector, + toastNotifications, + fields, + onSuccess, +}: UseGetChoicesProps): UseGetChoices => { + const [isLoading, setIsLoading] = useState(false); + const [choices, setChoices] = useState([]); + const didCancel = useRef(false); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + const fetchData = async () => { + if (!connector) { + setIsLoading(false); + return; + } + + try { + abortCtrl.current = new AbortController(); + setIsLoading(true); + + const res = await getChoices({ + http, + signal: abortCtrl.current.signal, + connectorId: connector.id, + fields, + }); + + if (!didCancel.current) { + setIsLoading(false); + setChoices(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.CHOICES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } else if (onSuccess) { + onSuccess(res.data ?? []); + } + } + } catch (error) { + if (!didCancel.current) { + setIsLoading(false); + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.CHOICES_API_ERROR, + text: error.message, + }); + } + } + } + }; + + didCancel.current = false; + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [http, connector, toastNotifications, fields]); + + return { + choices, + isLoading, + }; +}; diff --git a/x-pack/plugins/cases/public/components/connectors/types.ts b/x-pack/plugins/cases/public/components/connectors/types.ts new file mode 100644 index 0000000000000..fc2f66d331700 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/types.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + ActionType as ThirdPartySupportedActions, + CaseField, + ActionConnector, + ConnectorTypeFields, +} from '../../../common'; + +export { ThirdPartyField as AllThirdPartyFields } from '../../../common'; +export type CaseActionConnector = ActionConnector; + +export interface ThirdPartyField { + label: string; + validSourceFields: CaseField[]; + defaultSourceField: CaseField; + defaultActionType: ThirdPartySupportedActions; +} + +export interface ConnectorConfiguration { + name: string; + logo: string; +} + +export interface CaseConnector { + id: string; + fieldsComponent: React.LazyExoticComponent< + React.ComponentType> + > | null; +} + +export interface CaseConnectorsRegistry { + has: (id: string) => boolean; + register: ( + connector: CaseConnector + ) => void; + get: (id: string) => CaseConnector; + list: () => CaseConnector[]; +} + +export interface ConnectorFieldsProps { + isEdit?: boolean; + connector: CaseActionConnector; + fields: TFields; + onChange: (fields: TFields) => void; +} diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx new file mode 100644 index 0000000000000..db9e5ffac1533 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx @@ -0,0 +1,187 @@ +/* + * 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 { act, waitFor } from '@testing-library/react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { useForm, Form, FormHook } from '../../common/shared_imports'; +import { connectorsMock } from '../../containers/mock'; +import { Connector } from './connector'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; +import { useGetSeverity } from '../connectors/resilient/use_get_severity'; +import { useGetChoices } from '../connectors/servicenow/use_get_choices'; +import { incidentTypes, severity, choices } from '../connectors/mock'; +import { schema, FormProps } from './schema'; + +jest.mock('../../common/lib/kibana', () => { + return { + useKibana: () => ({ + services: { + notifications: {}, + http: {}, + }, + }), + }; +}); +jest.mock('../../containers/configure/use_connectors'); +jest.mock('../connectors/resilient/use_get_incident_types'); +jest.mock('../connectors/resilient/use_get_severity'); +jest.mock('../connectors/servicenow/use_get_choices'); + +const useConnectorsMock = useConnectors as jest.Mock; +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; +const useGetChoicesMock = useGetChoices as jest.Mock; + +const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes, +}; + +const useGetSeverityResponse = { + isLoading: false, + severity, +}; + +const useGetChoicesResponse = { + isLoading: false, + choices, +}; + +describe('Connector', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm({ + defaultValue: { connectorId: connectorsMock[0].id, fields: null }, + schema: { + connectorId: schema.connectorId, + fields: schema.fields, + }, + }); + + globalForm = form; + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + useGetChoicesMock.mockReturnValue(useGetChoicesResponse); + }); + + it('it renders', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeTruthy(); + + await waitFor(() => { + expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe( + 'My Connector' + ); + }); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy(); + }); + }); + + it('it is loading when fetching connectors', async () => { + useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock }); + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading') + ).toEqual(true); + }); + + it('it is disabled when fetching connectors', async () => { + useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock }); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual( + true + ); + }); + + it('it is disabled and loading when passing loading as true', async () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading') + ).toEqual(true); + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual( + true + ); + }); + + it(`it should change connector`, async () => { + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); + wrapper.update(); + }); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); + }); + + act(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ value: '19', label: 'Denial of Service' }]); + }); + + act(() => { + wrapper + .find('select[data-test-subj="severitySelect"]') + .first() + .simulate('change', { + target: { value: '4' }, + }); + }); + + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ + connectorId: 'resilient-2', + fields: { incidentTypes: ['19'], severityCode: '4' }, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx new file mode 100644 index 0000000000000..9b6063a7bf9b9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/connector.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, { memo, useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { ConnectorTypes } from '../../../common'; +import { UseField, useFormData, FieldHook, useFormContext } from '../../common/shared_imports'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { ConnectorSelector } from '../connector_selector/form'; +import { ConnectorFieldsForm } from '../connectors/fields_form'; +import { ActionConnector } from '../../containers/types'; +import { getConnectorById } from '../configure_cases/utils'; +import { FormProps } from './schema'; + +interface Props { + isLoading: boolean; + hideConnectorServiceNowSir?: boolean; +} + +interface ConnectorsFieldProps { + connectors: ActionConnector[]; + field: FieldHook; + isEdit: boolean; + hideConnectorServiceNowSir?: boolean; +} + +const ConnectorFields = ({ + connectors, + isEdit, + field, + hideConnectorServiceNowSir = false, +}: ConnectorsFieldProps) => { + const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); + const { setValue } = field; + let connector = getConnectorById(connectorId, connectors) ?? null; + if ( + connector && + hideConnectorServiceNowSir && + connector.actionTypeId === ConnectorTypes.serviceNowSIR + ) { + connector = null; + } + return ( + + ); +}; + +const ConnectorComponent: React.FC = ({ hideConnectorServiceNowSir = false, isLoading }) => { + const { getFields } = useFormContext(); + const { loading: isLoadingConnectors, connectors } = useConnectors(); + const handleConnectorChange = useCallback( + (newConnector) => { + const { fields } = getFields(); + fields.setValue(null); + }, + [getFields] + ); + + return ( + + + + + + + + + ); +}; + +ConnectorComponent.displayName = 'ConnectorComponent'; + +export const Connector = memo(ConnectorComponent); diff --git a/x-pack/plugins/cases/public/components/create/description.test.tsx b/x-pack/plugins/cases/public/components/create/description.test.tsx new file mode 100644 index 0000000000000..fcd1f82d64a53 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/description.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 React from 'react'; +import { mount } from 'enzyme'; +import { act } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../common/shared_imports'; +import { Description } from './description'; +import { schema, FormProps } from './schema'; + +describe('Description', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm({ + defaultValue: { description: 'My description' }, + schema: { + description: schema.description, + }, + }); + + globalForm = form; + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy(); + }); + + it('it changes the description', async () => { + const wrapper = mount( + + + + ); + + await act(async () => { + wrapper + .find(`[data-test-subj="caseDescription"] textarea`) + .first() + .simulate('change', { target: { value: 'My new description' } }); + }); + + expect(globalForm.getFormData()).toEqual({ description: 'My new description' }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx new file mode 100644 index 0000000000000..0a7102cff1ad5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -0,0 +1,31 @@ +/* + * 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 { MarkdownEditorForm } from '../markdown_editor'; +import { UseField } from '../../common/shared_imports'; +interface Props { + isLoading: boolean; +} + +export const fieldName = 'description'; + +const DescriptionComponent: React.FC = ({ isLoading }) => ( + +); + +DescriptionComponent.displayName = 'DescriptionComponent'; + +export const Description = memo(DescriptionComponent); diff --git a/x-pack/plugins/cases/public/components/create/flyout.test.tsx b/x-pack/plugins/cases/public/components/create/flyout.test.tsx new file mode 100644 index 0000000000000..5187029ab60c7 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/flyout.test.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, { ReactNode } from 'react'; +import { mount } from 'enzyme'; + +import { CreateCaseFlyout } from './flyout'; +import { TestProviders } from '../../common/mock'; + +jest.mock('../create/form_context', () => { + return { + FormContext: ({ + children, + onSuccess, + }: { + children: ReactNode; + onSuccess: ({ id }: { id: string }) => Promise; + }) => { + return ( + <> + + {children} + + ); + }, + }; +}); + +jest.mock('../create/form', () => { + return { + CreateCaseForm: () => { + return <>{'form'}; + }, + }; +}); + +jest.mock('../create/submit_button', () => { + return { + SubmitCaseButton: () => { + return <>{'Submit'}; + }, + }; +}); + +const onCloseFlyout = jest.fn(); +const onSuccess = jest.fn(); +const defaultProps = { + onCloseFlyout, + onSuccess, +}; + +describe('CreateCaseFlyout', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy(); + }); + + it('Closing modal calls onCloseCaseModal', () => { + const wrapper = mount( + + + + ); + + wrapper.find('.euiFlyout__closeButton').first().simulate('click'); + expect(onCloseFlyout).toBeCalled(); + }); + + it('pass the correct props to FormContext component', () => { + const wrapper = mount( + + + + ); + + const props = wrapper.find('FormContext').props(); + expect(props).toEqual( + expect.objectContaining({ + onSuccess, + }) + ); + }); + + it('onSuccess called when creating a case', () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click'); + expect(onSuccess).toHaveBeenCalledWith({ id: 'case-id' }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout.tsx new file mode 100644 index 0000000000000..8ed09865e9eab --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/flyout.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; + +import { FormContext } from '../create/form_context'; +import { CreateCaseForm } from '../create/form'; +import { SubmitCaseButton } from '../create/submit_button'; +import { Case } from '../../containers/types'; +import * as i18n from '../../common/translations'; + +export interface CreateCaseModalProps { + onCloseFlyout: () => void; + onSuccess: (theCase: Case) => Promise; + afterCaseCreated?: (theCase: Case) => Promise; +} + +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui.euiSize}; + text-align: right; + `} +`; + +const StyledFlyout = styled(EuiFlyout)` + ${({ theme }) => ` + z-index: ${theme.eui.euiZModal}; + `} +`; + +// Adding bottom padding because timeline's +// bottom bar gonna hide the submit button. +const FormWrapper = styled.div` + padding-bottom: 50px; +`; + +const CreateCaseFlyoutComponent: React.FC = ({ + onSuccess, + afterCaseCreated, + onCloseFlyout, +}) => { + return ( + + + +

{i18n.CREATE_TITLE}

+
+
+ + + + + + + + + + +
+ ); +}; + +export const CreateCaseFlyout = memo(CreateCaseFlyoutComponent); + +CreateCaseFlyout.displayName = 'CreateCaseFlyout'; diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx new file mode 100644 index 0000000000000..9e59924bdf483 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -0,0 +1,109 @@ +/* + * 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 { act, waitFor } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../common/shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { connectorsMock } from '../../containers/mock'; +import { schema, FormProps } from './schema'; +import { CreateCaseForm } from './form'; + +jest.mock('../../containers/use_get_tags'); +jest.mock('../../containers/configure/use_connectors'); +const useGetTagsMock = useGetTags as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; + +const initialCaseValue: FormProps = { + description: '', + tags: [], + title: '', + connectorId: 'none', + fields: null, + syncAlerts: true, +}; + +describe('CreateCaseForm', () => { + let globalForm: FormHook; + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + }); + + globalForm = form; + + return
{children}
; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useGetTagsMock.mockReturnValue({ tags: ['test'] }); + useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + }); + + it('it renders with steps', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeTruthy(); + }); + + it('it renders without steps', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeFalsy(); + }); + + it('it renders all form fields', async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseTags"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseSyncAlerts"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); + }); + + it('should render spinner when loading', async () => { + const wrapper = mount( + + + + ); + + await act(async () => { + globalForm.setFieldValue('title', 'title'); + globalForm.setFieldValue('description', 'description'); + globalForm.submit(); + // For some weird reason this is needed to pass the test. + // It does not do anything useful + await wrapper.find(`[data-test-subj="caseTitle"]`); + await wrapper.update(); + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists() + ).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx new file mode 100644 index 0000000000000..a81ecf32576a9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -0,0 +1,119 @@ +/* + * 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 { EuiLoadingSpinner, EuiSteps } from '@elastic/eui'; +import styled, { css } from 'styled-components'; + +import { useFormContext } from '../../common/shared_imports'; + +import { Title } from './title'; +import { Description } from './description'; +import { Tags } from './tags'; +import { Connector } from './connector'; +import * as i18n from './translations'; +import { SyncAlertsToggle } from './sync_alerts_toggle'; + +interface ContainerProps { + big?: boolean; +} + +const Container = styled.div.attrs((props) => props)` + ${({ big, theme }) => css` + margin-top: ${big ? theme.eui?.euiSizeXL ?? '32px' : theme.eui?.euiSize ?? '16px'}; + `} +`; + +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; + z-index: 99; +`; + +interface Props { + hideConnectorServiceNowSir?: boolean; + withSteps?: boolean; +} + +export const CreateCaseForm: React.FC = React.memo( + ({ hideConnectorServiceNowSir = false, withSteps = true }) => { + const { isSubmitting } = useFormContext(); + + const firstStep = useMemo( + () => ({ + title: i18n.STEP_ONE_TITLE, + children: ( + <> + + <Container> + <Tags isLoading={isSubmitting} /> + </Container> + <Container big> + <Description isLoading={isSubmitting} /> + </Container> + </> + ), + }), + [isSubmitting] + ); + + const secondStep = useMemo( + () => ({ + title: i18n.STEP_TWO_TITLE, + children: ( + <Container> + <SyncAlertsToggle isLoading={isSubmitting} /> + </Container> + ), + }), + [isSubmitting] + ); + + const thirdStep = useMemo( + () => ({ + title: i18n.STEP_THREE_TITLE, + children: ( + <Container> + <Connector + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + isLoading={isSubmitting} + /> + </Container> + ), + }), + [hideConnectorServiceNowSir, isSubmitting] + ); + + const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [ + firstStep, + secondStep, + thirdStep, + ]); + + return ( + <> + {isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} + {withSteps ? ( + <EuiSteps + headingElement="h2" + steps={allSteps} + data-test-subj={'case-creation-form-steps'} + /> + ) : ( + <> + {firstStep.children} + {secondStep.children} + {thirdStep.children} + </> + )} + </> + ); + } +); + +CreateCaseForm.displayName = 'CreateCaseForm'; diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx new file mode 100644 index 0000000000000..207ff6207e09d --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -0,0 +1,682 @@ +/* + * 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, ReactWrapper } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { ConnectorTypes } from '../../../common'; +import { TestProviders } from '../../common/mock'; +import { usePostCase } from '../../containers/use_post_case'; +import { useGetTags } from '../../containers/use_get_tags'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { connectorsMock } from '../../containers/configure/mock'; +import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; +import { useGetSeverity } from '../connectors/resilient/use_get_severity'; +import { useGetIssueTypes } from '../connectors/jira/use_get_issue_types'; +import { useGetChoices } from '../connectors/servicenow/use_get_choices'; +import { useGetFieldsByIssueType } from '../connectors/jira/use_get_fields_by_issue_type'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; +import { + sampleConnectorData, + sampleData, + sampleTags, + useGetIncidentTypesResponse, + useGetSeverityResponse, + useGetIssueTypesResponse, + useGetFieldsByIssueTypeResponse, + useGetChoicesResponse, +} from './mock'; +import { FormContext } from './form_context'; +import { CreateCaseForm } from './form'; +import { SubmitCaseButton } from './submit_button'; +import { usePostPushToService } from '../../containers/use_post_push_to_service'; + +const sampleId = 'case-id'; + +jest.mock('../../containers/use_post_case'); +jest.mock('../../containers/use_post_push_to_service'); +jest.mock('../../containers/use_get_tags'); +jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_configure'); +jest.mock('../connectors/resilient/use_get_incident_types'); +jest.mock('../connectors/resilient/use_get_severity'); +jest.mock('../connectors/jira/use_get_issue_types'); +jest.mock('../connectors/jira/use_get_fields_by_issue_type'); +jest.mock('../connectors/jira/use_get_single_issue'); +jest.mock('../connectors/jira/use_get_issues'); +jest.mock('../connectors/servicenow/use_get_choices'); + +const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; +const usePostCaseMock = usePostCase as jest.Mock; +const usePostPushToServiceMock = usePostPushToService as jest.Mock; +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; +const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; +const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; +const useGetChoicesMock = useGetChoices as jest.Mock; +const postCase = jest.fn(); +const pushCaseToExternalService = jest.fn(); + +const defaultPostCase = { + isLoading: false, + isError: false, + postCase, +}; + +const defaultPostPushToService = { + isLoading: false, + isError: false, + pushCaseToExternalService, +}; + +const fillForm = (wrapper: ReactWrapper) => { + wrapper + .find(`[data-test-subj="caseTitle"] input`) + .first() + .simulate('change', { target: { value: sampleData.title } }); + + wrapper + .find(`[data-test-subj="caseDescription"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.description } }); + + act(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange(sampleTags.map((tag) => ({ label: tag }))); + }); +}; + +describe('Create case', () => { + const fetchTags = jest.fn(); + const onFormSubmitSuccess = jest.fn(); + const afterCaseCreated = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + postCase.mockResolvedValue({ + id: sampleId, + ...sampleData, + }); + usePostCaseMock.mockImplementation(() => defaultPostCase); + usePostPushToServiceMock.mockImplementation(() => defaultPostPushToService); + useConnectorsMock.mockReturnValue(sampleConnectorData); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + useGetChoicesMock.mockReturnValue(useGetChoicesResponse); + + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); + }); + + describe('Step 1 - Case Fields', () => { + it('it renders', async () => { + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseDescription"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseTags"]`).first().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseConnectors"]`).first().exists()).toBeTruthy(); + expect( + wrapper.find(`[data-test-subj="case-creation-form-steps"]`).first().exists() + ).toBeTruthy(); + }); + + it('should post case on submit click', async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => expect(postCase).toBeCalledWith(sampleData)); + }); + + it('should toggle sync settings', async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + wrapper.find('[data-test-subj="caseSyncAlerts"] button').first().simulate('click'); + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await waitFor(() => + expect(postCase).toBeCalledWith({ ...sampleData, settings: { syncAlerts: false } }) + ); + }); + + it('it should select the default connector set in the configuration', async () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + connector: { + id: 'servicenow-1', + name: 'SN', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + persistLoading: false, + })); + + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + + await waitFor(() => + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + fields: { + impact: null, + severity: null, + urgency: null, + category: null, + subcategory: null, + }, + id: 'servicenow-1', + name: 'My Connector', + type: '.servicenow', + }, + }) + ); + }); + + it('it should default to none if the default connector does not exist in connectors', async () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + connector: { + id: 'not-exist', + name: 'SN', + type: ConnectorTypes.serviceNowITSM, + fields: null, + }, + persistLoading: false, + })); + + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => { + expect(postCase).toBeCalledWith(sampleData); + expect(pushCaseToExternalService).not.toHaveBeenCalled(); + }); + }); + }); + + describe('Step 2 - Connector Fields', () => { + it(`it should submit and push to Jira connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + }); + + wrapper + .find('select[data-test-subj="issueTypeSelect"]') + .first() + .simulate('change', { + target: { value: '10007' }, + }); + + wrapper + .find('select[data-test-subj="prioritySelect"]') + .first() + .simulate('change', { + target: { value: '2' }, + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await waitFor(() => { + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'jira-1', + name: 'Jira', + type: '.jira', + fields: { issueType: '10007', parent: null, priority: '2' }, + }, + }); + expect(pushCaseToExternalService).toHaveBeenCalledWith({ + caseId: sampleId, + connector: { + id: 'jira-1', + name: 'Jira', + type: '.jira', + fields: { issueType: '10007', parent: null, priority: '2' }, + }, + }); + expect(onFormSubmitSuccess).toHaveBeenCalledWith({ + id: sampleId, + ...sampleData, + }); + }); + }); + + it(`it should submit and push to resilient connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); + }); + + act(() => { + ((wrapper.find(EuiComboBox).at(1).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ value: '19', label: 'Denial of Service' }]); + }); + + wrapper + .find('select[data-test-subj="severitySelect"]') + .first() + .simulate('change', { + target: { value: '4' }, + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await waitFor(() => { + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'resilient-2', + name: 'My Connector 2', + type: '.resilient', + fields: { incidentTypes: ['19'], severityCode: '4' }, + }, + }); + + expect(pushCaseToExternalService).toHaveBeenCalledWith({ + caseId: sampleId, + connector: { + id: 'resilient-2', + name: 'My Connector 2', + type: '.resilient', + fields: { incidentTypes: ['19'], severityCode: '4' }, + }, + }); + + expect(onFormSubmitSuccess).toHaveBeenCalledWith({ + id: sampleId, + ...sampleData, + }); + }); + }); + + it(`it should submit and push to servicenow itsm connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy(); + }); + + ['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => { + wrapper + .find(`select[data-test-subj="${subj}"]`) + .first() + .simulate('change', { + target: { value: '2' }, + }); + }); + + wrapper + .find('select[data-test-subj="categorySelect"]') + .first() + .simulate('change', { + target: { value: 'software' }, + }); + + wrapper + .find('select[data-test-subj="subcategorySelect"]') + .first() + .simulate('change', { + target: { value: 'os' }, + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await waitFor(() => { + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'servicenow-1', + name: 'My Connector', + type: '.servicenow', + fields: { + impact: '2', + severity: '2', + urgency: '2', + category: 'software', + subcategory: 'os', + }, + }, + }); + + expect(pushCaseToExternalService).toHaveBeenCalledWith({ + caseId: sampleId, + connector: { + id: 'servicenow-1', + name: 'My Connector', + type: '.servicenow', + fields: { + impact: '2', + severity: '2', + urgency: '2', + category: 'software', + subcategory: 'os', + }, + }, + }); + + expect(onFormSubmitSuccess).toHaveBeenCalledWith({ + id: sampleId, + ...sampleData, + }); + }); + }); + + it(`it should submit and push to servicenow sir connector`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-sir"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-sir"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn-sir"]`).exists()).toBeTruthy(); + }); + + wrapper + .find('[data-test-subj="destIpCheckbox"] input') + .first() + .simulate('change', { target: { checked: false } }); + + wrapper + .find('select[data-test-subj="prioritySelect"]') + .first() + .simulate('change', { + target: { value: '1' }, + }); + + wrapper + .find('select[data-test-subj="categorySelect"]') + .first() + .simulate('change', { + target: { value: 'Denial of Service' }, + }); + + wrapper + .find('select[data-test-subj="subcategorySelect"]') + .first() + .simulate('change', { + target: { value: '26' }, + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await waitFor(() => { + expect(postCase).toBeCalledWith({ + ...sampleData, + connector: { + id: 'servicenow-sir', + name: 'My Connector SIR', + type: '.servicenow-sir', + fields: { + destIp: false, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + priority: '1', + category: 'Denial of Service', + subcategory: '26', + }, + }, + }); + + expect(pushCaseToExternalService).toHaveBeenCalledWith({ + caseId: sampleId, + connector: { + id: 'servicenow-sir', + name: 'My Connector SIR', + type: '.servicenow-sir', + fields: { + destIp: false, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + priority: '1', + category: 'Denial of Service', + subcategory: '26', + }, + }, + }); + + expect(onFormSubmitSuccess).toHaveBeenCalledWith({ + id: sampleId, + ...sampleData, + }); + }); + }); + }); + + it(`it should call afterCaseCreated`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => { + expect(afterCaseCreated).toHaveBeenCalledWith({ + id: sampleId, + ...sampleData, + }); + }); + }); + + it(`it should call callbacks in correct order`, async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + <TestProviders> + <FormContext onSuccess={onFormSubmitSuccess} afterCaseCreated={afterCaseCreated}> + <CreateCaseForm /> + <SubmitCaseButton /> + </FormContext> + </TestProviders> + ); + + fillForm(wrapper); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); + }); + + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => { + expect(postCase).toHaveBeenCalled(); + expect(afterCaseCreated).toHaveBeenCalled(); + expect(pushCaseToExternalService).toHaveBeenCalled(); + expect(onFormSubmitSuccess).toHaveBeenCalled(); + }); + + const postCaseOrder = postCase.mock.invocationCallOrder[0]; + const afterCaseOrder = afterCaseCreated.mock.invocationCallOrder[0]; + const pushCaseToExternalServiceOrder = pushCaseToExternalService.mock.invocationCallOrder[0]; + const onFormSubmitSuccessOrder = onFormSubmitSuccess.mock.invocationCallOrder[0]; + + expect( + postCaseOrder < afterCaseOrder && + postCaseOrder < pushCaseToExternalServiceOrder && + postCaseOrder < onFormSubmitSuccessOrder + ).toBe(true); + + expect( + afterCaseOrder < pushCaseToExternalServiceOrder && afterCaseOrder < onFormSubmitSuccessOrder + ).toBe(true); + + expect(pushCaseToExternalServiceOrder < onFormSubmitSuccessOrder).toBe(true); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx new file mode 100644 index 0000000000000..e84f451ab4215 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -0,0 +1,120 @@ +/* + * 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, { useCallback, useEffect, useMemo } from 'react'; +import { schema, FormProps } from './schema'; +import { Form, useForm } from '../../common/shared_imports'; +import { + getConnectorById, + getNoneConnector, + normalizeActionConnector, +} from '../configure_cases/utils'; +import { usePostCase } from '../../containers/use_post_case'; +import { usePostPushToService } from '../../containers/use_post_push_to_service'; + +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { Case } from '../../containers/types'; +import { CaseType, ConnectorTypes } from '../../../common'; + +const initialCaseValue: FormProps = { + description: '', + tags: [], + title: '', + connectorId: 'none', + fields: null, + syncAlerts: true, +}; + +interface Props { + afterCaseCreated?: (theCase: Case) => Promise<void>; + caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; + onSuccess?: (theCase: Case) => Promise<void>; +} + +export const FormContext: React.FC<Props> = ({ + afterCaseCreated, + caseType = CaseType.individual, + children, + hideConnectorServiceNowSir, + onSuccess, +}) => { + const { connectors } = useConnectors(); + const { connector: configurationConnector } = useCaseConfigure(); + const { postCase } = usePostCase(); + const { pushCaseToExternalService } = usePostPushToService(); + + const connectorId = useMemo(() => { + if ( + hideConnectorServiceNowSir && + configurationConnector.type === ConnectorTypes.serviceNowSIR + ) { + return 'none'; + } + return connectors.some((connector) => connector.id === configurationConnector.id) + ? configurationConnector.id + : 'none'; + }, [ + configurationConnector.id, + configurationConnector.type, + connectors, + hideConnectorServiceNowSir, + ]); + + const submitCase = useCallback( + async ( + { connectorId: dataConnectorId, fields, syncAlerts, ...dataWithoutConnectorId }, + isValid + ) => { + if (isValid) { + const caseConnector = getConnectorById(dataConnectorId, connectors); + + const connectorToUpdate = caseConnector + ? normalizeActionConnector(caseConnector, fields) + : getNoneConnector(); + + const updatedCase = await postCase({ + ...dataWithoutConnectorId, + type: caseType, + connector: connectorToUpdate, + settings: { syncAlerts }, + }); + + if (afterCaseCreated && updatedCase) { + await afterCaseCreated(updatedCase); + } + + if (updatedCase?.id && dataConnectorId !== 'none') { + await pushCaseToExternalService({ + caseId: updatedCase.id, + connector: connectorToUpdate, + }); + } + + if (onSuccess && updatedCase) { + await onSuccess(updatedCase); + } + } + }, + [caseType, connectors, postCase, onSuccess, pushCaseToExternalService, afterCaseCreated] + ); + + const { form } = useForm<FormProps>({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + onSubmit: submitCase, + }); + const { setFieldValue } = form; + // Set the selected connector to the configuration connector + useEffect(() => setFieldValue('connectorId', connectorId), [connectorId, setFieldValue]); + + return <Form form={form}>{children}</Form>; +}; + +FormContext.displayName = 'FormContext'; diff --git a/x-pack/plugins/cases/public/components/create/index.test.tsx b/x-pack/plugins/cases/public/components/create/index.test.tsx new file mode 100644 index 0000000000000..e82af8edc6337 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/index.test.tsx @@ -0,0 +1,126 @@ +/* + * 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, ReactWrapper } from 'enzyme'; +import { act, waitFor } from '@testing-library/react'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { TestProviders } from '../../common/mock'; +import { useGetTags } from '../../containers/use_get_tags'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; +import { useGetSeverity } from '../connectors/resilient/use_get_severity'; +import { useGetIssueTypes } from '../connectors/jira/use_get_issue_types'; +import { useGetFieldsByIssueType } from '../connectors/jira/use_get_fields_by_issue_type'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; +import { + sampleConnectorData, + sampleData, + sampleTags, + useGetIncidentTypesResponse, + useGetSeverityResponse, + useGetIssueTypesResponse, + useGetFieldsByIssueTypeResponse, +} from './mock'; +import { CreateCase } from '.'; + +jest.mock('../../containers/api'); +jest.mock('../../containers/use_get_tags'); +jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_configure'); +jest.mock('../connectors/resilient/use_get_incident_types'); +jest.mock('../connectors/resilient/use_get_severity'); +jest.mock('../connectors/jira/use_get_issue_types'); +jest.mock('../connectors/jira/use_get_fields_by_issue_type'); +jest.mock('../connectors/jira/use_get_single_issue'); +jest.mock('../connectors/jira/use_get_issues'); + +const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; +const useGetTagsMock = useGetTags as jest.Mock; +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; +const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; +const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; +const fetchTags = jest.fn(); + +const fillForm = (wrapper: ReactWrapper) => { + wrapper + .find(`[data-test-subj="caseTitle"] input`) + .first() + .simulate('change', { target: { value: sampleData.title } }); + + wrapper + .find(`[data-test-subj="caseDescription"] textarea`) + .first() + .simulate('change', { target: { value: sampleData.description } }); + + act(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange(sampleTags.map((tag) => ({ label: tag }))); + }); +}; + +const defaultProps = { + onCancel: jest.fn(), + onSuccess: jest.fn(), +}; + +describe('CreateCase case', () => { + beforeEach(() => { + jest.resetAllMocks(); + useConnectorsMock.mockReturnValue(sampleConnectorData); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + useGetTagsMock.mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); + }); + + it('it renders', async () => { + const wrapper = mount( + <TestProviders> + <CreateCase {...defaultProps} /> + </TestProviders> + ); + + expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="create-case-cancel"]`).exists()).toBeTruthy(); + }); + + it('should call cancel on cancel click', async () => { + const wrapper = mount( + <TestProviders> + <CreateCase {...defaultProps} /> + </TestProviders> + ); + + wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click'); + expect(defaultProps.onCancel).toHaveBeenCalled(); + }); + + it('should redirect to new case when posting the case', async () => { + const wrapper = mount( + <TestProviders> + <CreateCase {...defaultProps} /> + </TestProviders> + ); + + fillForm(wrapper); + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + await waitFor(() => { + expect(defaultProps.onSuccess).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx new file mode 100644 index 0000000000000..192effb6adb24 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { Field, getUseField } from '../../common/shared_imports'; +import * as i18n from './translations'; +import { CreateCaseForm } from './form'; +import { FormContext } from './form_context'; +import { SubmitCaseButton } from './submit_button'; +import { Case } from '../../containers/types'; + +export const CommonUseField = getUseField({ component: Field }); + +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui.euiSize}; + `} +`; + +export interface CreateCaseProps { + afterCaseCreated?: (theCase: Case) => Promise<void>; + onCancel: () => void; + onSuccess: (theCase: Case) => Promise<void>; +} + +export const CreateCase = ({ afterCaseCreated, onCancel, onSuccess }: CreateCaseProps) => ( + <FormContext afterCaseCreated={afterCaseCreated} onSuccess={onSuccess}> + <CreateCaseForm /> + <Container> + <EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="xs" responsive={false}> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="create-case-cancel" + size="s" + onClick={onCancel} + iconType="cross" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <SubmitCaseButton /> + </EuiFlexItem> + </EuiFlexGroup> + </Container> + </FormContext> +); + +// eslint-disable-next-line import/no-default-export +export { CreateCase as default }; diff --git a/x-pack/plugins/cases/public/components/create/mock.ts b/x-pack/plugins/cases/public/components/create/mock.ts new file mode 100644 index 0000000000000..eb40fa097d3cc --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/mock.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasePostRequest, CaseType, ConnectorTypes } from '../../../common'; +import { choices } from '../connectors/mock'; + +export const sampleTags = ['coke', 'pepsi']; +export const sampleData: CasePostRequest = { + description: 'what a great description', + tags: sampleTags, + title: 'what a cool title', + type: CaseType.individual, + connector: { + fields: null, + id: 'none', + name: 'none', + type: ConnectorTypes.none, + }, + settings: { + syncAlerts: true, + }, +}; + +export const sampleConnectorData = { loading: false, connectors: [] }; + +export const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], +}; + +export const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], +}; + +export const useGetIssueTypesResponse = { + isLoading: false, + issueTypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], +}; + +export const useGetFieldsByIssueTypeResponse = { + isLoading: false, + fields: { + summary: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + { + name: 'Low', + id: '2', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + }, +}; + +export const useGetChoicesResponse = { + isLoading: false, + choices, +}; diff --git a/x-pack/plugins/cases/public/components/create/optional_field_label/index.test.tsx b/x-pack/plugins/cases/public/components/create/optional_field_label/index.test.tsx new file mode 100644 index 0000000000000..4b6d5f90513ef --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/optional_field_label/index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; + +import { OptionalFieldLabel } from '.'; + +describe('OptionalFieldLabel', () => { + it('it renders correctly', async () => { + const wrapper = mount(OptionalFieldLabel); + expect(wrapper.find('[data-test-subj="form-optional-field-label"]').first().text()).toBe( + 'Optional' + ); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/optional_field_label/index.tsx b/x-pack/plugins/cases/public/components/create/optional_field_label/index.tsx new file mode 100644 index 0000000000000..ea994b2219961 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/optional_field_label/index.tsx @@ -0,0 +1,17 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../../../common/translations'; + +export const OptionalFieldLabel = ( + <EuiText color="subdued" size="xs" data-test-subj="form-optional-field-label"> + {i18n.OPTIONAL} + </EuiText> +); diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx new file mode 100644 index 0000000000000..7ca1e2e061545 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasePostRequest, ConnectorTypeFields } from '../../../common'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../common/shared_imports'; +import * as i18n from './translations'; + +import { OptionalFieldLabel } from './optional_field_label'; +const { emptyField } = fieldValidators; + +export const schemaTags = { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.TAGS, + helpText: i18n.TAGS_HELP, + labelAppend: OptionalFieldLabel, +}; + +export type FormProps = Omit<CasePostRequest, 'connector' | 'settings'> & { + connectorId: string; + fields: ConnectorTypeFields['fields']; + syncAlerts: boolean; +}; + +export const schema: FormSchema<FormProps> = { + title: { + type: FIELD_TYPES.TEXT, + label: i18n.NAME, + validations: [ + { + validator: emptyField(i18n.TITLE_REQUIRED), + }, + ], + }, + description: { + label: i18n.DESCRIPTION, + validations: [ + { + validator: emptyField(i18n.DESCRIPTION_REQUIRED), + }, + ], + }, + tags: schemaTags, + connectorId: { + type: FIELD_TYPES.SUPER_SELECT, + label: i18n.CONNECTORS, + defaultValue: 'none', + }, + fields: {}, + syncAlerts: { + helpText: i18n.SYNC_ALERTS_HELP, + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + }, +}; diff --git a/x-pack/plugins/cases/public/components/create/submit_button.test.tsx b/x-pack/plugins/cases/public/components/create/submit_button.test.tsx new file mode 100644 index 0000000000000..dd67c8170dc3f --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/submit_button.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { act, waitFor } from '@testing-library/react'; + +import { useForm, Form } from '../../common/shared_imports'; +import { SubmitCaseButton } from './submit_button'; +import { schema, FormProps } from './schema'; + +describe('SubmitCaseButton', () => { + const onSubmit = jest.fn(); + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<FormProps>({ + defaultValue: { title: 'My title' }, + schema: { + title: schema.title, + }, + onSubmit, + }); + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy(); + }); + + it('it submits', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await act(async () => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + }); + + await waitFor(() => expect(onSubmit).toBeCalled()); + }); + + it('it disables when submitting', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + expect( + wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isDisabled') + ).toBeTruthy(); + }); + }); + + it('it is loading when submitting', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SubmitCaseButton /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + expect( + wrapper.find(`[data-test-subj="create-case-submit"]`).first().prop('isLoading') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/submit_button.tsx b/x-pack/plugins/cases/public/components/create/submit_button.tsx new file mode 100644 index 0000000000000..b5e58517e6ec1 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/submit_button.tsx @@ -0,0 +1,31 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; + +import { useFormContext } from '../../common/shared_imports'; +import * as i18n from './translations'; + +const SubmitCaseButtonComponent: React.FC = () => { + const { submit, isSubmitting } = useFormContext(); + + return ( + <EuiButton + data-test-subj="create-case-submit" + fill + iconType="plusInCircle" + isDisabled={isSubmitting} + isLoading={isSubmitting} + onClick={submit} + > + {i18n.CREATE_CASE} + </EuiButton> + ); +}; + +export const SubmitCaseButton = memo(SubmitCaseButtonComponent); diff --git a/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.test.tsx b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.test.tsx new file mode 100644 index 0000000000000..b4a37f0abb518 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { waitFor } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../common/shared_imports'; +import { SyncAlertsToggle } from './sync_alerts_toggle'; +import { schema, FormProps } from './schema'; + +describe('SyncAlertsToggle', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<FormProps>({ + defaultValue: { syncAlerts: true }, + schema: { + syncAlerts: schema.syncAlerts, + }, + }); + + globalForm = form; + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SyncAlertsToggle isLoading={false} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="caseSyncAlerts"]`).exists()).toBeTruthy(); + }); + + it('it toggles the switch', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SyncAlertsToggle isLoading={false} /> + </MockHookWrapperComponent> + ); + + wrapper.find('[data-test-subj="caseSyncAlerts"] button').first().simulate('click'); + + await waitFor(() => { + expect(globalForm.getFormData()).toEqual({ syncAlerts: false }); + }); + }); + + it('it shows the correct labels', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <SyncAlertsToggle isLoading={false} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="caseSyncAlerts"] .euiSwitch__label`).first().text()).toBe( + 'On' + ); + + wrapper.find('[data-test-subj="caseSyncAlerts"] button').first().simulate('click'); + + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="caseSyncAlerts"] .euiSwitch__label`).first().text() + ).toBe('Off'); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx new file mode 100644 index 0000000000000..bed8e6d18f5e3 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/sync_alerts_toggle.tsx @@ -0,0 +1,38 @@ +/* + * 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 { Field, getUseField, useFormData } from '../../common/shared_imports'; +import * as i18n from './translations'; + +const CommonUseField = getUseField({ component: Field }); + +interface Props { + isLoading: boolean; +} + +const SyncAlertsToggleComponent: React.FC<Props> = ({ isLoading }) => { + const [{ syncAlerts }] = useFormData({ watch: ['syncAlerts'] }); + return ( + <CommonUseField + path="syncAlerts" + componentProps={{ + idAria: 'caseSyncAlerts', + 'data-test-subj': 'caseSyncAlerts', + label: i18n.SYNC_ALERTS_LABEL, + euiFieldProps: { + disabled: isLoading, + label: syncAlerts ? i18n.SYNC_ALERTS_SWITCH_LABEL_ON : i18n.SYNC_ALERTS_SWITCH_LABEL_OFF, + }, + }} + /> + ); +}; + +SyncAlertsToggleComponent.displayName = 'SyncAlertsToggleComponent'; + +export const SyncAlertsToggle = memo(SyncAlertsToggleComponent); diff --git a/x-pack/plugins/cases/public/components/create/tags.test.tsx b/x-pack/plugins/cases/public/components/create/tags.test.tsx new file mode 100644 index 0000000000000..2eddb83dcac29 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/tags.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { waitFor } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../common/shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; +import { Tags } from './tags'; +import { schema, FormProps } from './schema'; + +jest.mock('../../containers/use_get_tags'); +const useGetTagsMock = useGetTags as jest.Mock; + +describe('Tags', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<FormProps>({ + defaultValue: { tags: [] }, + schema: { + tags: schema.tags, + }, + }); + + globalForm = form; + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + useGetTagsMock.mockReturnValue({ tags: ['test'] }); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={false} /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="caseTags"]`).exists()).toBeTruthy(); + }); + }); + + it('it disables the input when loading', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={true} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(EuiComboBox).prop('disabled')).toBeTruthy(); + }); + + it('it changes the tags', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Tags isLoading={false} /> + </MockHookWrapperComponent> + ); + + await waitFor(() => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange(['test', 'case'].map((tag) => ({ label: tag }))); + }); + + expect(globalForm.getFormData()).toEqual({ tags: ['test', 'case'] }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/tags.tsx b/x-pack/plugins/cases/public/components/create/tags.tsx new file mode 100644 index 0000000000000..ac0b67529e15a --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/tags.tsx @@ -0,0 +1,49 @@ +/* + * 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 } from 'react'; + +import { Field, getUseField } from '../../common/shared_imports'; +import { useGetTags } from '../../containers/use_get_tags'; + +const CommonUseField = getUseField({ component: Field }); + +interface Props { + isLoading: boolean; +} + +const TagsComponent: React.FC<Props> = ({ isLoading }) => { + const { tags: tagOptions, isLoading: isLoadingTags } = useGetTags(); + const options = useMemo( + () => + tagOptions.map((label) => ({ + label, + })), + [tagOptions] + ); + + return ( + <CommonUseField + path="tags" + componentProps={{ + idAria: 'caseTags', + 'data-test-subj': 'caseTags', + euiFieldProps: { + fullWidth: true, + placeholder: '', + disabled: isLoading || isLoadingTags, + options, + noSuggestions: false, + }, + }} + /> + ); +}; + +TagsComponent.displayName = 'TagsComponent'; + +export const Tags = memo(TagsComponent); diff --git a/x-pack/plugins/cases/public/components/create/title.test.tsx b/x-pack/plugins/cases/public/components/create/title.test.tsx new file mode 100644 index 0000000000000..a41d5afbb4038 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/title.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { act } from '@testing-library/react'; + +import { useForm, Form, FormHook } from '../../common/shared_imports'; +import { Title } from './title'; +import { schema, FormProps } from './schema'; + +describe('Title', () => { + let globalForm: FormHook; + + const MockHookWrapperComponent: React.FC = ({ children }) => { + const { form } = useForm<FormProps>({ + defaultValue: { title: 'My title' }, + schema: { + title: schema.title, + }, + }); + + globalForm = form; + + return <Form form={form}>{children}</Form>; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('it renders', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={false} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"]`).exists()).toBeTruthy(); + }); + + it('it disables the input when loading', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={true} /> + </MockHookWrapperComponent> + ); + + expect(wrapper.find(`[data-test-subj="caseTitle"] input`).prop('disabled')).toBeTruthy(); + }); + + it('it changes the title', async () => { + const wrapper = mount( + <MockHookWrapperComponent> + <Title isLoading={false} /> + </MockHookWrapperComponent> + ); + + await act(async () => { + wrapper + .find(`[data-test-subj="caseTitle"] input`) + .first() + .simulate('change', { target: { value: 'My new title' } }); + }); + + expect(globalForm.getFormData()).toEqual({ title: 'My new title' }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/create/title.tsx b/x-pack/plugins/cases/public/components/create/title.tsx new file mode 100644 index 0000000000000..cc51a805b5c38 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/title.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { Field, getUseField } from '../../common/shared_imports'; + +const CommonUseField = getUseField({ component: Field }); + +interface Props { + isLoading: boolean; +} + +const TitleComponent: React.FC<Props> = ({ isLoading }) => ( + <CommonUseField + path="title" + componentProps={{ + idAria: 'caseTitle', + 'data-test-subj': 'caseTitle', + euiFieldProps: { + fullWidth: true, + disabled: isLoading, + }, + }} + /> +); + +TitleComponent.displayName = 'TitleComponent'; + +export const Title = memo(TitleComponent); diff --git a/x-pack/plugins/cases/public/components/create/translations.ts b/x-pack/plugins/cases/public/components/create/translations.ts new file mode 100644 index 0000000000000..7e0f7e5a6b9d5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/create/translations.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../common/translations'; + +export const STEP_ONE_TITLE = i18n.translate('xpack.cases.create.stepOneTitle', { + defaultMessage: 'Case fields', +}); + +export const STEP_TWO_TITLE = i18n.translate('xpack.cases.create.stepTwoTitle', { + defaultMessage: 'Case settings', +}); + +export const STEP_THREE_TITLE = i18n.translate('xpack.cases.create.stepThreeTitle', { + defaultMessage: 'External Connector Fields', +}); + +export const SYNC_ALERTS_LABEL = i18n.translate('xpack.cases.create.syncAlertsLabel', { + defaultMessage: 'Sync alert status with case status', +}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx new file mode 100644 index 0000000000000..a7d37fdda3085 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useEffect, useState, useCallback } from 'react'; +import { + EuiMarkdownEditor, + getDefaultEuiMarkdownParsingPlugins, + getDefaultEuiMarkdownProcessingPlugins, + getDefaultEuiMarkdownUiPlugins, +} from '@elastic/eui'; + +interface MarkdownEditorProps { + onChange: (content: string) => void; + value: string; + ariaLabel: string; + editorId?: string; + dataTestSubj?: string; + height?: number; +} + +// create plugin stuff here +export const { uiPlugins, parsingPlugins, processingPlugins } = { + uiPlugins: getDefaultEuiMarkdownUiPlugins(), + parsingPlugins: getDefaultEuiMarkdownParsingPlugins(), + processingPlugins: getDefaultEuiMarkdownProcessingPlugins(), +}; +const MarkdownEditorComponent: React.FC<MarkdownEditorProps> = ({ + onChange, + value, + ariaLabel, + editorId, + dataTestSubj, + height, +}) => { + const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); + const onParse = useCallback((err, { messages }) => { + setMarkdownErrorMessages(err ? [err] : messages); + }, []); + + useEffect( + () => document.querySelector<HTMLElement>('textarea.euiMarkdownEditorTextArea')?.focus(), + [] + ); + + return ( + <EuiMarkdownEditor + aria-label={ariaLabel} + editorId={editorId} + onChange={onChange} + value={value} + uiPlugins={uiPlugins} + parsingPluginList={parsingPlugins} + processingPluginList={processingPlugins} + onParse={onParse} + errors={markdownErrorMessages} + data-test-subj={dataTestSubj} + height={height} + /> + ); +}; + +export const MarkdownEditor = memo(MarkdownEditorComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx new file mode 100644 index 0000000000000..858e79ff65baf --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { EuiMarkdownEditorProps, EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; + +import { MarkdownEditor } from './editor'; + +type MarkdownEditorFormProps = EuiMarkdownEditorProps & { + id: string; + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled?: boolean; + bottomRightContent?: React.ReactNode; +}; + +const BottomContentWrapper = styled(EuiFlexGroup)` + ${({ theme }) => ` + padding: ${theme.eui.ruleMargins.marginSmall} 0; + `} +`; + +export const MarkdownEditorForm: React.FC<MarkdownEditorFormProps> = ({ + id, + field, + dataTestSubj, + idAria, + bottomRightContent, +}) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + <EuiFormRow + data-test-subj={dataTestSubj} + describedByIds={idAria ? [idAria] : undefined} + error={errorMessage} + fullWidth + helpText={field.helpText} + isInvalid={isInvalid} + label={field.label} + labelAppend={field.labelAppend} + > + <> + <MarkdownEditor + ariaLabel={idAria} + editorId={id} + onChange={field.setValue} + value={field.value as string} + data-test-subj={`${dataTestSubj}-markdown-editor`} + /> + {bottomRightContent && ( + <BottomContentWrapper justifyContent={'flexEnd'}> + <EuiFlexItem grow={false}>{bottomRightContent}</EuiFlexItem> + </BottomContentWrapper> + )} + </> + </EuiFormRow> + ); +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/index.tsx b/x-pack/plugins/cases/public/components/markdown_editor/index.tsx new file mode 100644 index 0000000000000..e77a36d48f7d9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/index.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './types'; +export * from './renderer'; +export * from './editor'; +export * from './eui_form'; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/markdown_link.tsx b/x-pack/plugins/cases/public/components/markdown_editor/markdown_link.tsx new file mode 100644 index 0000000000000..7cc8a07c8c04e --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/markdown_link.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiLink, EuiLinkAnchorProps, EuiToolTip } from '@elastic/eui'; + +type MarkdownLinkProps = { disableLinks?: boolean } & EuiLinkAnchorProps; + +/** prevents search engine manipulation by noting the linked document is not trusted or endorsed by us */ +const REL_NOFOLLOW = 'nofollow'; + +const MarkdownLinkComponent: React.FC<MarkdownLinkProps> = ({ + disableLinks, + href, + target, + children, + ...props +}) => ( + <EuiToolTip content={href}> + <EuiLink + href={disableLinks ? undefined : href} + data-test-subj="markdown-link" + rel={`${REL_NOFOLLOW}`} + target="_blank" + > + {children} + </EuiLink> + </EuiToolTip> +); + +export const MarkdownLink = memo(MarkdownLinkComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx new file mode 100644 index 0000000000000..5d299529561ba --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.test.tsx @@ -0,0 +1,63 @@ +/* + * 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 { removeExternalLinkText } from '../../common/test_utils'; +import { MarkdownRenderer } from './renderer'; + +describe('Markdown', () => { + describe('markdown links', () => { + const markdownWithLink = 'A link to an external site [External Site](https://google.com)'; + + test('it renders the expected link text', () => { + const wrapper = mount(<MarkdownRenderer>{markdownWithLink}</MarkdownRenderer>); + + expect( + removeExternalLinkText(wrapper.find('[data-test-subj="markdown-link"]').first().text()) + ).toEqual('External Site'); + }); + + test('it renders the expected href', () => { + const wrapper = mount(<MarkdownRenderer>{markdownWithLink}</MarkdownRenderer>); + + expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty( + 'href', + 'https://google.com/' + ); + }); + + test('it does NOT render the href if links are disabled', () => { + const wrapper = mount( + <MarkdownRenderer disableLinks={true}>{markdownWithLink}</MarkdownRenderer> + ); + + expect( + wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode() + ).not.toHaveProperty('href'); + }); + + test('it opens links in a new tab via target="_blank"', () => { + const wrapper = mount(<MarkdownRenderer>{markdownWithLink}</MarkdownRenderer>); + + expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty( + 'target', + '_blank' + ); + }); + + test('it sets the link `rel` attribute to `noopener` to prevent the new page from accessing `window.opener`, `nofollow` to note the link is not endorsed by us, and noreferrer to prevent the browser from sending the current address', () => { + const wrapper = mount(<MarkdownRenderer>{markdownWithLink}</MarkdownRenderer>); + + expect(wrapper.find('[data-test-subj="markdown-link"]').first().getDOMNode()).toHaveProperty( + 'rel', + 'nofollow noopener noreferrer' + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx new file mode 100644 index 0000000000000..c321c794c1e77 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx @@ -0,0 +1,41 @@ +/* + * 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 } from 'react'; +import { cloneDeep } from 'lodash/fp'; +import { EuiMarkdownFormat, EuiLinkAnchorProps } from '@elastic/eui'; + +import { parsingPlugins, processingPlugins } from './'; +import { MarkdownLink } from './markdown_link'; + +interface Props { + children: string; + disableLinks?: boolean; +} + +const MarkdownRendererComponent: React.FC<Props> = ({ children, disableLinks }) => { + const MarkdownLinkProcessingComponent: React.FC<EuiLinkAnchorProps> = useMemo( + () => (props) => <MarkdownLink {...props} disableLinks={disableLinks} />, + [disableLinks] + ); + + // Deep clone of the processing plugins to prevent affecting the markdown editor. + const processingPluginList = cloneDeep(processingPlugins); + // This line of code is TS-compatible and it will break if [1][1] change in the future. + processingPluginList[1][1].components.a = MarkdownLinkProcessingComponent; + + return ( + <EuiMarkdownFormat + parsingPluginList={parsingPlugins} + processingPluginList={processingPluginList} + > + {children} + </EuiMarkdownFormat> + ); +}; + +export const MarkdownRenderer = memo(MarkdownRendererComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/translations.ts b/x-pack/plugins/cases/public/components/markdown_editor/translations.ts new file mode 100644 index 0000000000000..365738f53ef8a --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/translations.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const MARKDOWN_SYNTAX_HELP = i18n.translate('xpack.cases.markdownEditor.markdownInputHelp', { + defaultMessage: 'Markdown syntax help', +}); + +export const MARKDOWN = i18n.translate('xpack.cases.markdownEditor.markdown', { + defaultMessage: 'Markdown', +}); +export const PREVIEW = i18n.translate('xpack.cases.markdownEditor.preview', { + defaultMessage: 'Preview', +}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/types.ts b/x-pack/plugins/cases/public/components/markdown_editor/types.ts new file mode 100644 index 0000000000000..8a30a4a143f54 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface CursorPosition { + start: number; + end: number; +} diff --git a/x-pack/plugins/cases/public/components/status/button.test.tsx b/x-pack/plugins/cases/public/components/status/button.test.tsx new file mode 100644 index 0000000000000..a4d4a53ff4a62 --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/button.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { CaseStatuses } from '../../../common'; +import { StatusActionButton } from './button'; + +describe('StatusActionButton', () => { + const onStatusChanged = jest.fn(); + const defaultProps = { + status: CaseStatuses.open, + disabled: false, + isLoading: false, + onStatusChanged, + }; + + it('it renders', async () => { + const wrapper = mount(<StatusActionButton {...defaultProps} />); + + expect(wrapper.find(`[data-test-subj="case-view-status-action-button"]`).exists()).toBeTruthy(); + }); + + describe('Button icons', () => { + it('it renders the correct button icon: status open', () => { + const wrapper = mount(<StatusActionButton {...defaultProps} />); + + expect( + wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') + ).toBe('folderExclamation'); + }); + + it('it renders the correct button icon: status in-progress', () => { + const wrapper = mount( + <StatusActionButton {...defaultProps} status={CaseStatuses['in-progress']} /> + ); + + expect( + wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') + ).toBe('folderCheck'); + }); + + it('it renders the correct button icon: status closed', () => { + const wrapper = mount(<StatusActionButton {...defaultProps} status={CaseStatuses.closed} />); + + expect( + wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') + ).toBe('folderOpen'); + }); + }); + + describe('Status rotation', () => { + it('rotates correctly to in-progress when status is open', () => { + const wrapper = mount(<StatusActionButton {...defaultProps} />); + + wrapper + .find(`button[data-test-subj="case-view-status-action-button"]`) + .first() + .simulate('click'); + expect(onStatusChanged).toHaveBeenCalledWith('in-progress'); + }); + + it('rotates correctly to closed when status is in-progress', () => { + const wrapper = mount( + <StatusActionButton {...defaultProps} status={CaseStatuses['in-progress']} /> + ); + + wrapper + .find(`button[data-test-subj="case-view-status-action-button"]`) + .first() + .simulate('click'); + expect(onStatusChanged).toHaveBeenCalledWith('closed'); + }); + + it('rotates correctly to open when status is closed', () => { + const wrapper = mount(<StatusActionButton {...defaultProps} status={CaseStatuses.closed} />); + + wrapper + .find(`button[data-test-subj="case-view-status-action-button"]`) + .first() + .simulate('click'); + expect(onStatusChanged).toHaveBeenCalledWith('open'); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/status/button.tsx b/x-pack/plugins/cases/public/components/status/button.tsx new file mode 100644 index 0000000000000..623afeb43c596 --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/button.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; + +import { CaseStatuses, caseStatuses } from '../../../common'; +import { statuses } from './config'; + +interface Props { + status: CaseStatuses; + disabled: boolean; + isLoading: boolean; + onStatusChanged: (status: CaseStatuses) => void; +} + +// Rotate over the statuses. open -> in-progress -> closes -> open... +const getNextItem = (item: number) => (item + 1) % caseStatuses.length; + +const StatusActionButtonComponent: React.FC<Props> = ({ + status, + onStatusChanged, + disabled, + isLoading, +}) => { + const indexOfCurrentStatus = useMemo( + () => caseStatuses.findIndex((caseStatus) => caseStatus === status), + [status] + ); + const nextStatusIndex = useMemo(() => getNextItem(indexOfCurrentStatus), [indexOfCurrentStatus]); + + const onClick = useCallback(() => { + onStatusChanged(caseStatuses[nextStatusIndex]); + }, [nextStatusIndex, onStatusChanged]); + + return ( + <EuiButton + data-test-subj="case-view-status-action-button" + iconType={statuses[caseStatuses[nextStatusIndex]].icon} + isDisabled={disabled} + isLoading={isLoading} + onClick={onClick} + > + {statuses[caseStatuses[nextStatusIndex]].button.label} + </EuiButton> + ); +}; +export const StatusActionButton = memo(StatusActionButtonComponent); diff --git a/x-pack/plugins/cases/public/components/status/config.ts b/x-pack/plugins/cases/public/components/status/config.ts new file mode 100644 index 0000000000000..e85d429067724 --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/config.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { CaseStatuses } from '../../../common'; +import * as i18n from './translations'; +import { AllCaseStatus, Statuses, StatusAll } from './types'; + +export const allCaseStatus: AllCaseStatus = { + [StatusAll]: { color: 'hollow', label: i18n.ALL }, +}; + +export const statuses: Statuses = { + [CaseStatuses.open]: { + color: 'primary', + label: i18n.OPEN, + icon: 'folderOpen' as const, + actions: { + bulk: { + title: i18n.BULK_ACTION_OPEN_SELECTED, + }, + single: { + title: i18n.OPEN_CASE, + }, + }, + actionBar: { + title: i18n.CASE_OPENED, + }, + button: { + label: i18n.REOPEN_CASE, + }, + stats: { + title: i18n.OPEN_CASES, + }, + }, + [CaseStatuses['in-progress']]: { + color: 'warning', + label: i18n.IN_PROGRESS, + icon: 'folderExclamation' as const, + actions: { + bulk: { + title: i18n.BULK_ACTION_MARK_IN_PROGRESS, + }, + single: { + title: i18n.MARK_CASE_IN_PROGRESS, + }, + }, + actionBar: { + title: i18n.CASE_IN_PROGRESS, + }, + button: { + label: i18n.MARK_CASE_IN_PROGRESS, + }, + stats: { + title: i18n.IN_PROGRESS_CASES, + }, + }, + [CaseStatuses.closed]: { + color: 'default', + label: i18n.CLOSED, + icon: 'folderCheck' as const, + actions: { + bulk: { + title: i18n.BULK_ACTION_CLOSE_SELECTED, + }, + single: { + title: i18n.CLOSE_CASE, + }, + }, + actionBar: { + title: i18n.CASE_CLOSED, + }, + button: { + label: i18n.CLOSE_CASE, + }, + stats: { + title: i18n.CLOSED_CASES, + }, + }, +}; diff --git a/x-pack/plugins/cases/public/components/status/index.ts b/x-pack/plugins/cases/public/components/status/index.ts new file mode 100644 index 0000000000000..94d7cb6a31830 --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './status'; +export * from './config'; +export * from './stats'; +export * from './types'; diff --git a/x-pack/plugins/cases/public/components/status/stats.test.tsx b/x-pack/plugins/cases/public/components/status/stats.test.tsx new file mode 100644 index 0000000000000..b2da828da77b0 --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/stats.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CaseStatuses } from '../../../common'; +import { Stats } from './stats'; + +describe('Stats', () => { + const defaultProps = { + caseStatus: CaseStatuses.open, + caseCount: 2, + isLoading: false, + dataTestSubj: 'test-stats', + }; + it('it renders', async () => { + const wrapper = mount(<Stats {...defaultProps} />); + + expect(wrapper.find(`[data-test-subj="test-stats"]`).exists()).toBeTruthy(); + }); + + it('shows the count', async () => { + const wrapper = mount(<Stats {...defaultProps} />); + + expect( + wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__description`).first().text() + ).toBe('2'); + }); + + it('shows the loading spinner', async () => { + const wrapper = mount(<Stats {...defaultProps} isLoading={true} />); + + expect(wrapper.find(`[data-test-subj="test-stats-loading-spinner"]`).exists()).toBeTruthy(); + }); + + describe('Status title', () => { + it('shows the correct title for status open', async () => { + const wrapper = mount(<Stats {...defaultProps} />); + + expect( + wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text() + ).toBe('Open cases'); + }); + + it('shows the correct title for status in-progress', async () => { + const wrapper = mount(<Stats {...defaultProps} caseStatus={CaseStatuses['in-progress']} />); + + expect( + wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text() + ).toBe('In progress cases'); + }); + + it('shows the correct title for status closed', async () => { + const wrapper = mount(<Stats {...defaultProps} caseStatus={CaseStatuses.closed} />); + + expect( + wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text() + ).toBe('Closed cases'); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/status/stats.tsx b/x-pack/plugins/cases/public/components/status/stats.tsx new file mode 100644 index 0000000000000..071ea43746fdc --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/stats.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui'; +import { CaseStatuses } from '../../../common'; +import { statuses } from './config'; + +export interface Props { + caseCount: number | null; + caseStatus: CaseStatuses; + isLoading: boolean; + dataTestSubj?: string; +} + +const StatsComponent: React.FC<Props> = ({ caseCount, caseStatus, isLoading, dataTestSubj }) => { + const statusStats = useMemo( + () => [ + { + title: statuses[caseStatus].stats.title, + description: isLoading ? ( + <EuiLoadingSpinner data-test-subj={`${dataTestSubj}-loading-spinner`} /> + ) : ( + caseCount ?? 'N/A' + ), + }, + ], + [caseCount, caseStatus, dataTestSubj, isLoading] + ); + return ( + <EuiDescriptionList data-test-subj={dataTestSubj} textStyle="reverse" listItems={statusStats} /> + ); +}; + +StatsComponent.displayName = 'StatsComponent'; +export const Stats = memo(StatsComponent); diff --git a/x-pack/plugins/cases/public/components/status/status.test.tsx b/x-pack/plugins/cases/public/components/status/status.test.tsx new file mode 100644 index 0000000000000..7cddbf5ca4a1d --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/status.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CaseStatuses } from '../../../common'; +import { Status } from './status'; + +describe('Stats', () => { + const onClick = jest.fn(); + + it('it renders', async () => { + const wrapper = mount(<Status type={CaseStatuses.open} withArrow={false} onClick={onClick} />); + + expect(wrapper.find(`[data-test-subj="status-badge-open"]`).exists()).toBeTruthy(); + expect( + wrapper.find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`).exists() + ).toBeFalsy(); + }); + + it('it renders with arrow', async () => { + const wrapper = mount(<Status type={CaseStatuses.open} withArrow={true} onClick={onClick} />); + + expect( + wrapper.find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`).exists() + ).toBeTruthy(); + }); + + it('it calls onClick when pressing the badge', async () => { + const wrapper = mount(<Status type={CaseStatuses.open} withArrow={true} onClick={onClick} />); + + wrapper.find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`).simulate('click'); + expect(onClick).toHaveBeenCalled(); + }); + + describe('Colors', () => { + it('shows the correct color when status is open', async () => { + const wrapper = mount( + <Status type={CaseStatuses.open} withArrow={false} onClick={onClick} /> + ); + + expect(wrapper.find(`[data-test-subj="status-badge-open"]`).first().prop('color')).toBe( + 'primary' + ); + }); + + it('shows the correct color when status is in-progress', async () => { + const wrapper = mount( + <Status type={CaseStatuses['in-progress']} withArrow={false} onClick={onClick} /> + ); + + expect( + wrapper.find(`[data-test-subj="status-badge-in-progress"]`).first().prop('color') + ).toBe('warning'); + }); + + it('shows the correct color when status is closed', async () => { + const wrapper = mount( + <Status type={CaseStatuses.closed} withArrow={false} onClick={onClick} /> + ); + + expect(wrapper.find(`[data-test-subj="status-badge-closed"]`).first().prop('color')).toBe( + 'default' + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/status/status.tsx b/x-pack/plugins/cases/public/components/status/status.tsx new file mode 100644 index 0000000000000..de4c979daf4c1 --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/status.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { noop } from 'lodash/fp'; +import { EuiBadge } from '@elastic/eui'; + +import { allCaseStatus, statuses } from './config'; +import { CaseStatusWithAllStatus, StatusAll } from './types'; +import * as i18n from './translations'; + +interface Props { + type: CaseStatusWithAllStatus; + withArrow?: boolean; + onClick?: () => void; +} + +const StatusComponent: React.FC<Props> = ({ type, withArrow = false, onClick = noop }) => { + const props = useMemo( + () => ({ + color: type === StatusAll ? allCaseStatus[StatusAll].color : statuses[type].color, + ...(withArrow ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}), + }), + [withArrow, type] + ); + + return ( + <EuiBadge + {...props} + iconOnClick={onClick} + iconOnClickAriaLabel={i18n.STATUS_ICON_ARIA} + data-test-subj={`status-badge-${type}`} + > + {type === StatusAll ? allCaseStatus[StatusAll].label : statuses[type].label} + </EuiBadge> + ); +}; + +export const Status = memo(StatusComponent); diff --git a/x-pack/plugins/cases/public/components/status/translations.ts b/x-pack/plugins/cases/public/components/status/translations.ts new file mode 100644 index 0000000000000..b3eadfd681ba5 --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/translations.ts @@ -0,0 +1,69 @@ +/* + * 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'; +export * from '../../common/translations'; + +export const ALL = i18n.translate('xpack.cases.status.all', { + defaultMessage: 'All', +}); + +export const OPEN = i18n.translate('xpack.cases.status.open', { + defaultMessage: 'Open', +}); + +export const IN_PROGRESS = i18n.translate('xpack.cases.status.inProgress', { + defaultMessage: 'In progress', +}); + +export const CLOSED = i18n.translate('xpack.cases.status.closed', { + defaultMessage: 'Closed', +}); + +export const STATUS_ICON_ARIA = i18n.translate('xpack.cases.status.iconAria', { + defaultMessage: 'Change status', +}); + +export const CASE_OPENED = i18n.translate('xpack.cases.caseView.caseOpened', { + defaultMessage: 'Case opened', +}); + +export const CASE_IN_PROGRESS = i18n.translate('xpack.cases.caseView.caseInProgress', { + defaultMessage: 'Case in progress', +}); + +export const CASE_CLOSED = i18n.translate('xpack.cases.caseView.caseClosed', { + defaultMessage: 'Case closed', +}); + +export const BULK_ACTION_CLOSE_SELECTED = i18n.translate( + 'xpack.cases.caseTable.bulkActions.closeSelectedTitle', + { + defaultMessage: 'Close selected', + } +); + +export const BULK_ACTION_OPEN_SELECTED = i18n.translate( + 'xpack.cases.caseTable.bulkActions.openSelectedTitle', + { + defaultMessage: 'Open selected', + } +); + +export const BULK_ACTION_DELETE_SELECTED = i18n.translate( + 'xpack.cases.caseTable.bulkActions.deleteSelectedTitle', + { + defaultMessage: 'Delete selected', + } +); + +export const BULK_ACTION_MARK_IN_PROGRESS = i18n.translate( + 'xpack.cases.caseTable.bulkActions.markInProgressTitle', + { + defaultMessage: 'Mark in progress', + } +); diff --git a/x-pack/plugins/cases/public/components/status/types.ts b/x-pack/plugins/cases/public/components/status/types.ts new file mode 100644 index 0000000000000..674838067b0ac --- /dev/null +++ b/x-pack/plugins/cases/public/components/status/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import { CaseStatuses } from '../../../common'; + +export const StatusAll = 'all' as const; +type StatusAllType = typeof StatusAll; + +export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType; + +export type AllCaseStatus = Record<StatusAllType, { color: string; label: string }>; + +export type Statuses = Record< + CaseStatuses, + { + color: string; + label: string; + icon: EuiIconType; + actions: { + bulk: { + title: string; + }; + single: { + title: string; + description?: string; + }; + }; + actionBar: { + title: string; + }; + button: { + label: string; + }; + stats: { + title: string; + }; + } +>; diff --git a/x-pack/plugins/cases/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/plugins/cases/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap new file mode 100644 index 0000000000000..5e008e28073de --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = ` +<EuiModal + onClose={[Function]} +> + <EuiModalHeader> + <EuiModalHeaderTitle> + Your visualization has error(s) + </EuiModalHeaderTitle> + </EuiModalHeader> + <EuiModalBody> + <EuiCallOut + color="danger" + iconType="alert" + size="s" + title="Test & Test" + /> + <EuiSpacer + size="s" + /> + <EuiAccordion + arrowDisplay="left" + buttonContent="Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt u ..." + data-test-subj="modal-all-errors-accordion" + id="accordion1" + initialIsOpen={true} + isLoading={false} + isLoadingMessage={false} + key="id-super-id-0" + paddingSize="none" + > + <MyEuiCodeBlock> + Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + </MyEuiCodeBlock> + </EuiAccordion> + </EuiModalBody> + <EuiModalFooter> + <EuiButton + data-test-subj="modal-all-errors-close" + fill={true} + onClick={[Function]} + > + Close + </EuiButton> + </EuiModalFooter> +</EuiModal> +`; diff --git a/x-pack/plugins/cases/public/components/toasters/errors.ts b/x-pack/plugins/cases/public/components/toasters/errors.ts new file mode 100644 index 0000000000000..0a672aeee8b7c --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/errors.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class ToasterError extends Error { + public readonly messages: string[]; + + constructor(messages: string[]) { + super(messages[0]); + this.name = 'ToasterError'; + this.messages = messages; + } +} + +export const isToasterError = (error: unknown): error is ToasterError => + error instanceof ToasterError; diff --git a/x-pack/plugins/cases/public/components/toasters/index.test.tsx b/x-pack/plugins/cases/public/components/toasters/index.test.tsx new file mode 100644 index 0000000000000..1d78570e18a59 --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/index.test.tsx @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { set } from '@elastic/safer-lodash-set/fp'; +import { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import React, { useEffect } from 'react'; + +import { + AppToast, + useStateToaster, + ManageGlobalToaster, + GlobalToaster, + displayErrorToast, +} from '.'; + +jest.mock('uuid', () => { + return { + v1: jest.fn(() => '27261ae0-0bbb-11ea-b0ea-db767b07ea47'), + v4: jest.fn(() => '9e1f72a9-7c73-4b7f-a562-09940f7daf4a'), + }; +}); + +const mockToast: AppToast = { + color: 'danger', + id: 'id-super-id', + iconType: 'alert', + title: 'Test & Test', + toastLifeTimeMs: 100, + text: + 'Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', +}; + +describe('Toaster', () => { + describe('Manage Global Toaster Reducer', () => { + test('we can add a toast in the reducer', () => { + const AddToaster = () => { + const [{ toasts }, dispatch] = useStateToaster(); + return ( + <> + <button + data-test-subj="add-toast" + type="button" + onClick={() => dispatch({ type: 'addToaster', toast: mockToast })} + /> + {toasts.map((toast) => ( + <span + data-test-subj={`add-toaster-${toast.id}`} + key={`add-toaster-${toast.id}`} + >{`${toast.title} ${toast.text}`}</span> + ))} + </> + ); + }; + const wrapper = mount( + <ManageGlobalToaster> + <AddToaster /> + </ManageGlobalToaster> + ); + wrapper.find('[data-test-subj="add-toast"]').simulate('click'); + expect(wrapper.find('[data-test-subj="add-toaster-id-super-id"]').exists()).toBe(true); + }); + test('we can delete a toast in the reducer', () => { + const DeleteToaster = () => { + const [{ toasts }, dispatch] = useStateToaster(); + useEffect(() => { + if (toasts.length === 0) { + dispatch({ type: 'addToaster', toast: mockToast }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( + <> + <button + data-test-subj="delete-toast" + type="button" + onClick={() => dispatch({ type: 'deleteToaster', id: mockToast.id })} + /> + {toasts.map((toast) => ( + <span + data-test-subj={`delete-toaster-${toast.id}`} + key={`delete-toaster-${toast.id}`} + >{`${toast.title} ${toast.text}`}</span> + ))} + </> + ); + }; + const wrapper = mount( + <ManageGlobalToaster> + <DeleteToaster /> + </ManageGlobalToaster> + ); + + expect(wrapper.find('[data-test-subj="delete-toaster-id-super-id"]').exists()).toBe(true); + wrapper.find('[data-test-subj="delete-toast"]').simulate('click'); + expect(wrapper.find('[data-test-subj="delete-toaster-id-super-id"]').exists()).toBe(false); + }); + }); + + describe('Global Toaster', () => { + test('Render a basic toaster', () => { + const AddToaster = () => { + const [{ toasts }, dispatch] = useStateToaster(); + return ( + <> + <button + data-test-subj="add-toast" + type="button" + onClick={() => dispatch({ type: 'addToaster', toast: mockToast })} + /> + {toasts.map((toast) => ( + <span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span> + ))} + </> + ); + }; + const wrapper = mount( + <ManageGlobalToaster> + <AddToaster /> + <GlobalToaster /> + </ManageGlobalToaster> + ); + wrapper.find('[data-test-subj="add-toast"]').simulate('click'); + + expect(wrapper.find('.euiGlobalToastList').exists()).toBe(true); + expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test'); + }); + + test('Render an error toaster', () => { + let mockErrorToast: AppToast = cloneDeep(mockToast); + mockErrorToast.title = 'Test & Test ERROR'; + mockErrorToast = set('errors', [mockErrorToast.text], mockErrorToast); + + const AddToaster = () => { + const [{ toasts }, dispatch] = useStateToaster(); + return ( + <> + <button + data-test-subj="add-toast" + type="button" + onClick={() => dispatch({ type: 'addToaster', toast: mockErrorToast })} + /> + {toasts.map((toast) => ( + <span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span> + ))} + </> + ); + }; + const wrapper = mount( + <ManageGlobalToaster> + <AddToaster /> + <GlobalToaster /> + </ManageGlobalToaster> + ); + wrapper.find('[data-test-subj="add-toast"]').simulate('click'); + + expect(wrapper.find('.euiGlobalToastList').exists()).toBe(true); + expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test ERROR'); + expect(wrapper.find('button[data-test-subj="toaster-show-all-error-modal"]').exists()).toBe( + true + ); + }); + + test('Only show one toast at the time', () => { + const mockOneMoreToast: AppToast = cloneDeep(mockToast); + mockOneMoreToast.id = 'id-super-id-II'; + mockOneMoreToast.title = 'Test & Test II'; + + const AddToaster = () => { + const [{ toasts }, dispatch] = useStateToaster(); + return ( + <> + <button + data-test-subj="add-toast" + type="button" + onClick={() => { + dispatch({ type: 'addToaster', toast: mockToast }); + dispatch({ type: 'addToaster', toast: mockOneMoreToast }); + }} + /> + <button + data-test-subj="delete-toast" + type="button" + onClick={() => { + dispatch({ type: 'deleteToaster', id: mockToast.id }); + }} + /> + {toasts.map((toast) => ( + <span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span> + ))} + </> + ); + }; + const wrapper = mount( + <ManageGlobalToaster> + <AddToaster /> + <GlobalToaster /> + </ManageGlobalToaster> + ); + wrapper.find('[data-test-subj="add-toast"]').simulate('click'); + + expect(wrapper.find('button[data-test-subj="toastCloseButton"]').length).toBe(1); + expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test'); + wrapper.find('button[data-test-subj="delete-toast"]').simulate('click'); + expect(wrapper.find('.euiToast').length).toBe(1); + expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test II'); + }); + + test('Do not show anymore toaster when modal error is open', () => { + let mockErrorToast: AppToast = cloneDeep(mockToast); + mockErrorToast.id = 'id-super-id-error'; + mockErrorToast = set('errors', [mockErrorToast.text], mockErrorToast); + + const AddToaster = () => { + const [{ toasts }, dispatch] = useStateToaster(); + return ( + <> + <button + data-test-subj="add-toast" + type="button" + onClick={() => { + dispatch({ type: 'addToaster', toast: mockErrorToast }); + dispatch({ type: 'addToaster', toast: mockToast }); + }} + /> + {toasts.map((toast) => ( + <span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span> + ))} + </> + ); + }; + const wrapper = mount( + <ManageGlobalToaster> + <AddToaster /> + <GlobalToaster /> + </ManageGlobalToaster> + ); + wrapper.find('[data-test-subj="add-toast"]').simulate('click'); + wrapper.find('button[data-test-subj="toaster-show-all-error-modal"]').simulate('click'); + + expect(wrapper.find('.euiToast').length).toBe(0); + }); + + test('Show new toaster when modal error is closing', () => { + let mockErrorToast: AppToast = cloneDeep(mockToast); + mockErrorToast.title = 'Test & Test II'; + mockErrorToast.id = 'id-super-id-error'; + mockErrorToast = set('errors', [mockErrorToast.text], mockErrorToast); + + const AddToaster = () => { + const [{ toasts }, dispatch] = useStateToaster(); + return ( + <> + <button + data-test-subj="add-toast" + type="button" + onClick={() => { + dispatch({ type: 'addToaster', toast: mockErrorToast }); + dispatch({ type: 'addToaster', toast: mockToast }); + }} + /> + {toasts.map((toast) => ( + <span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span> + ))} + </> + ); + }; + const wrapper = mount( + <ManageGlobalToaster> + <AddToaster /> + <GlobalToaster /> + </ManageGlobalToaster> + ); + wrapper.find('[data-test-subj="add-toast"]').simulate('click'); + expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test II'); + + wrapper.find('button[data-test-subj="toaster-show-all-error-modal"]').simulate('click'); + expect(wrapper.find('.euiToast').length).toBe(0); + + wrapper.find('button[data-test-subj="modal-all-errors-close"]').simulate('click'); + expect(wrapper.find('.euiToast').length).toBe(1); + expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test'); + }); + }); + + describe('displayErrorToast', () => { + test('dispatches toast with correct title and message', () => { + const mockErrorToast = { + toast: { + color: 'danger', + errors: ['message'], + iconType: 'alert', + id: '9e1f72a9-7c73-4b7f-a562-09940f7daf4a', + title: 'Title', + }, + type: 'addToaster', + }; + const dispatchToasterMock = jest.fn(); + displayErrorToast('Title', ['message'], dispatchToasterMock); + expect(dispatchToasterMock.mock.calls[0][0]).toEqual(mockErrorToast); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/toasters/index.tsx b/x-pack/plugins/cases/public/components/toasters/index.tsx new file mode 100644 index 0000000000000..ea17b03082751 --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/index.tsx @@ -0,0 +1,136 @@ +/* + * 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, EuiGlobalToastList, EuiGlobalToastListToast as Toast } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { createContext, Dispatch, useContext, useReducer, useState } from 'react'; +import styled from 'styled-components'; + +import { ModalAllErrors } from './modal_all_errors'; +import * as i18n from './translations'; + +export * from './utils'; +export * from './errors'; + +export interface AppToast extends Toast { + errors?: string[]; +} + +interface ToastState { + toasts: AppToast[]; +} + +const initialToasterState: ToastState = { + toasts: [], +}; + +export type ActionToaster = + | { type: 'addToaster'; toast: AppToast } + | { type: 'deleteToaster'; id: string } + | { type: 'toggleWaitToShowNextToast' }; + +export const StateToasterContext = createContext<[ToastState, Dispatch<ActionToaster>]>([ + initialToasterState, + () => noop, +]); + +export const useStateToaster = () => useContext(StateToasterContext); + +interface ManageGlobalToasterProps { + children: React.ReactNode; +} + +export const ManageGlobalToaster = ({ children }: ManageGlobalToasterProps) => { + const reducerToaster = (state: ToastState, action: ActionToaster) => { + switch (action.type) { + case 'addToaster': + return { ...state, toasts: [...state.toasts, action.toast] }; + case 'deleteToaster': + return { ...state, toasts: state.toasts.filter((msg) => msg.id !== action.id) }; + default: + return state; + } + }; + + return ( + <StateToasterContext.Provider value={useReducer(reducerToaster, initialToasterState)}> + {children} + </StateToasterContext.Provider> + ); +}; + +const GlobalToasterListContainer = styled.div` + position: absolute; + right: 0; + bottom: 0; +`; + +interface GlobalToasterProps { + toastLifeTimeMs?: number; +} + +export const GlobalToaster = ({ toastLifeTimeMs = 5000 }: GlobalToasterProps) => { + const [{ toasts }, dispatch] = useStateToaster(); + const [isShowing, setIsShowing] = useState(false); + const [toastInModal, setToastInModal] = useState<AppToast | null>(null); + + const toggle = (toast: AppToast) => { + if (isShowing) { + dispatch({ type: 'deleteToaster', id: toast.id }); + setToastInModal(null); + } else { + setToastInModal(toast); + } + setIsShowing(!isShowing); + }; + + return ( + <> + {toasts.length > 0 && !isShowing && ( + <GlobalToasterListContainer> + <EuiGlobalToastList + toasts={[formatToErrorToastIfNeeded(toasts[0], toggle)]} + dismissToast={({ id }) => { + dispatch({ type: 'deleteToaster', id }); + }} + toastLifeTimeMs={toastLifeTimeMs} + /> + </GlobalToasterListContainer> + )} + {toastInModal != null && ( + <ModalAllErrors isShowing={isShowing} toast={toastInModal} toggle={toggle} /> + )} + </> + ); +}; + +const formatToErrorToastIfNeeded = ( + toast: AppToast, + toggle: (toast: AppToast) => void +): AppToast => { + if (toast != null && toast.errors != null && toast.errors.length > 0) { + toast.text = ( + <ErrorToastContainer> + <EuiButton + data-test-subj="toaster-show-all-error-modal" + size="s" + color="danger" + onClick={() => toast != null && toggle(toast)} + > + {i18n.SEE_ALL_ERRORS} + </EuiButton> + </ErrorToastContainer> + ); + } + return toast; +}; + +const ErrorToastContainer = styled.div` + text-align: right; +`; + +ErrorToastContainer.displayName = 'ErrorToastContainer'; diff --git a/x-pack/plugins/cases/public/components/toasters/modal_all_errors.test.tsx b/x-pack/plugins/cases/public/components/toasters/modal_all_errors.test.tsx new file mode 100644 index 0000000000000..7ec0553591103 --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/modal_all_errors.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 { ModalAllErrors } from './modal_all_errors'; +import { AppToast } from '.'; +import { cloneDeep } from 'lodash/fp'; + +const mockToast: AppToast = { + color: 'danger', + id: 'id-super-id', + iconType: 'alert', + title: 'Test & Test', + errors: [ + 'Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + ], +}; + +describe('Modal all errors', () => { + const toggle = jest.fn(); + describe('rendering', () => { + test('it renders the default all errors modal when isShowing is positive', () => { + const wrapper = shallow( + <ModalAllErrors isShowing={true} toast={mockToast} toggle={toggle} /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders null when isShowing is negative', () => { + const wrapper = shallow( + <ModalAllErrors isShowing={false} toast={mockToast} toggle={toggle} /> + ); + expect(wrapper.html()).toEqual(null); + }); + + test('it renders multiple errors in modal', () => { + const mockToastWithTwoError = cloneDeep(mockToast); + mockToastWithTwoError.errors = [ + 'Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + 'Error 2, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + 'Error 3, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + ]; + const wrapper = shallow( + <ModalAllErrors isShowing={true} toast={mockToastWithTwoError} toggle={toggle} /> + ); + expect(wrapper.find('[data-test-subj="modal-all-errors-accordion"]').length).toBe( + mockToastWithTwoError.errors.length + ); + }); + }); + + describe('events', () => { + test('Make sure that toggle function has been called when you click on the close button', () => { + const wrapper = shallow( + <ModalAllErrors isShowing={true} toast={mockToast} toggle={toggle} /> + ); + + wrapper.find('[data-test-subj="modal-all-errors-close"]').simulate('click'); + wrapper.update(); + expect(toggle).toHaveBeenCalledWith(mockToast); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/toasters/modal_all_errors.tsx b/x-pack/plugins/cases/public/components/toasters/modal_all_errors.tsx new file mode 100644 index 0000000000000..0a78139f5fe3a --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/modal_all_errors.tsx @@ -0,0 +1,75 @@ +/* + * 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, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiCallOut, + EuiSpacer, + EuiCodeBlock, + EuiModalFooter, + EuiAccordion, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { AppToast } from '.'; +import * as i18n from './translations'; + +interface FullErrorProps { + isShowing: boolean; + toast: AppToast; + toggle: (toast: AppToast) => void; +} + +const ModalAllErrorsComponent: React.FC<FullErrorProps> = ({ isShowing, toast, toggle }) => { + const handleClose = useCallback(() => toggle(toast), [toggle, toast]); + + if (!isShowing || toast == null) return null; + + return ( + <EuiModal onClose={handleClose}> + <EuiModalHeader> + <EuiModalHeaderTitle>{i18n.TITLE_ERROR_MODAL}</EuiModalHeaderTitle> + </EuiModalHeader> + + <EuiModalBody> + <EuiCallOut title={toast.title} color="danger" size="s" iconType="alert" /> + <EuiSpacer size="s" /> + {toast.errors != null && + toast.errors.map((error, index) => ( + <EuiAccordion + key={`${toast.id}-${index}`} + id="accordion1" + initialIsOpen={index === 0 ? true : false} + buttonContent={error.length > 100 ? `${error.substring(0, 100)} ...` : error} + data-test-subj="modal-all-errors-accordion" + > + <MyEuiCodeBlock>{error}</MyEuiCodeBlock> + </EuiAccordion> + ))} + </EuiModalBody> + + <EuiModalFooter> + <EuiButton onClick={handleClose} fill data-test-subj="modal-all-errors-close"> + {i18n.CLOSE_ERROR_MODAL} + </EuiButton> + </EuiModalFooter> + </EuiModal> + ); +}; + +export const ModalAllErrors = React.memo(ModalAllErrorsComponent); + +const MyEuiCodeBlock = styled(EuiCodeBlock)` + margin-top: 4px; +`; + +MyEuiCodeBlock.displayName = 'MyEuiCodeBlock'; diff --git a/x-pack/plugins/cases/public/components/toasters/translations.ts b/x-pack/plugins/cases/public/components/toasters/translations.ts new file mode 100644 index 0000000000000..cf7fac462a122 --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/translations.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const SEE_ALL_ERRORS = i18n.translate('xpack.cases.modalAllErrors.seeAllErrors.button', { + defaultMessage: 'See the full error(s)', +}); + +export const TITLE_ERROR_MODAL = i18n.translate('xpack.cases.modalAllErrors.title', { + defaultMessage: 'Your visualization has error(s)', +}); + +export const CLOSE_ERROR_MODAL = i18n.translate('xpack.cases.modalAllErrors.close.button', { + defaultMessage: 'Close', +}); diff --git a/x-pack/plugins/cases/public/components/toasters/utils.test.ts b/x-pack/plugins/cases/public/components/toasters/utils.test.ts new file mode 100644 index 0000000000000..34871b2e68efa --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/utils.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { errorToToaster } from './utils'; +import { ToasterError } from './errors'; + +const ApiError = class extends Error { + public body: {} = {}; +}; + +describe('error_to_toaster', () => { + let dispatchToaster = jest.fn(); + + beforeEach(() => { + dispatchToaster = jest.fn(); + }); + + describe('#errorToToaster', () => { + test('dispatches an error toast given a ToasterError with multiple error messages', () => { + const error = new ToasterError(['some error 1', 'some error 2']); + errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); + expect(dispatchToaster.mock.calls[0]).toEqual([ + { + toast: { + color: 'danger', + errors: ['some error 1', 'some error 2'], + iconType: 'alert', + id: 'some-made-up-id', + title: 'some title', + }, + type: 'addToaster', + }, + ]); + }); + + test('dispatches an error toast given a ToasterError with a single error message', () => { + const error = new ToasterError(['some error 1']); + errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); + expect(dispatchToaster.mock.calls[0]).toEqual([ + { + toast: { + color: 'danger', + errors: ['some error 1'], + iconType: 'alert', + id: 'some-made-up-id', + title: 'some title', + }, + type: 'addToaster', + }, + ]); + }); + + test('dispatches an error toast given an ApiError with a message', () => { + const error = new ApiError('Internal Server Error'); + error.body = { message: 'something bad happened', status_code: 500 }; + + errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); + expect(dispatchToaster.mock.calls[0]).toEqual([ + { + toast: { + color: 'danger', + errors: ['something bad happened'], + iconType: 'alert', + id: 'some-made-up-id', + title: 'some title', + }, + type: 'addToaster', + }, + ]); + }); + + test('dispatches an error toast given an ApiError with no message', () => { + const error = new ApiError('Internal Server Error'); + + errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); + expect(dispatchToaster.mock.calls[0]).toEqual([ + { + toast: { + color: 'danger', + errors: ['Internal Server Error'], + iconType: 'alert', + id: 'some-made-up-id', + title: 'some title', + }, + type: 'addToaster', + }, + ]); + }); + + test('dispatches an error toast given a standard Error', () => { + const error = new Error('some error 1'); + errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); + expect(dispatchToaster.mock.calls[0]).toEqual([ + { + toast: { + color: 'danger', + errors: ['some error 1'], + iconType: 'alert', + id: 'some-made-up-id', + title: 'some title', + }, + type: 'addToaster', + }, + ]); + }); + + test('adds a generic Network Error given a non Error object such as a string', () => { + const error = 'terrible string'; + errorToToaster({ id: 'some-made-up-id', title: 'some title', error, dispatchToaster }); + expect(dispatchToaster.mock.calls[0]).toEqual([ + { + toast: { + color: 'danger', + errors: ['Network Error'], + iconType: 'alert', + id: 'some-made-up-id', + title: 'some title', + }, + type: 'addToaster', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/toasters/utils.ts b/x-pack/plugins/cases/public/components/toasters/utils.ts new file mode 100644 index 0000000000000..0575c40107668 --- /dev/null +++ b/x-pack/plugins/cases/public/components/toasters/utils.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type React from 'react'; +import uuid from 'uuid'; +import { isError } from 'lodash/fp'; + +import { AppToast, ActionToaster } from './'; +import { isToasterError } from './errors'; +import { isAppError } from '../../common/errors'; + +/** + * Displays an error toast for the provided title and message + * + * @param errorTitle Title of error to display in toaster and modal + * @param errorMessages Message to display in error modal when clicked + * @param dispatchToaster provided by useStateToaster() + */ +export const displayErrorToast = ( + errorTitle: string, + errorMessages: string[], + dispatchToaster: React.Dispatch<ActionToaster>, + id: string = uuid.v4() +): void => { + const toast: AppToast = { + id, + title: errorTitle, + color: 'danger', + iconType: 'alert', + errors: errorMessages, + }; + dispatchToaster({ + type: 'addToaster', + toast, + }); +}; + +/** + * Displays a warning toast for the provided title and message + * + * @param title warning message to display in toaster and modal + * @param dispatchToaster provided by useStateToaster() + * @param id unique ID if necessary + */ +export const displayWarningToast = ( + title: string, + dispatchToaster: React.Dispatch<ActionToaster>, + id: string = uuid.v4() +): void => { + const toast: AppToast = { + id, + title, + color: 'warning', + iconType: 'help', + }; + dispatchToaster({ + type: 'addToaster', + toast, + }); +}; + +/** + * Displays a success toast for the provided title and message + * + * @param title success message to display in toaster and modal + * @param dispatchToaster provided by useStateToaster() + */ +export const displaySuccessToast = ( + title: string, + dispatchToaster: React.Dispatch<ActionToaster>, + id: string = uuid.v4() +): void => { + const toast: AppToast = { + id, + title, + color: 'success', + iconType: 'check', + }; + dispatchToaster({ + type: 'addToaster', + toast, + }); +}; + +export type ErrorToToasterArgs = Partial<AppToast> & { + error: unknown; + dispatchToaster: React.Dispatch<ActionToaster>; +}; + +/** + * Displays an error toast with messages parsed from the error + * + * @param title error message to display in toaster and modal + * @param error the error from which messages will be parsed + * @param dispatchToaster provided by useStateToaster() + */ +export const errorToToaster = ({ + id = uuid.v4(), + title, + error, + color = 'danger', + iconType = 'alert', + dispatchToaster, +}: ErrorToToasterArgs) => { + let toast: AppToast; + + if (isToasterError(error)) { + toast = { + id, + title, + color, + iconType, + errors: error.messages, + }; + } else if (isAppError(error)) { + toast = { + id, + title, + color, + iconType, + errors: [error.body.message], + }; + } else if (isError(error)) { + toast = { + id, + title, + color, + iconType, + errors: [error.message], + }; + } else { + toast = { + id, + title, + color, + iconType, + errors: ['Network Error'], + }; + } + + dispatchToaster({ + type: 'addToaster', + toast, + }); +}; diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx new file mode 100644 index 0000000000000..fcdc2f8e58774 --- /dev/null +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx @@ -0,0 +1,126 @@ +/* + * 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, { ReactNode } from 'react'; +import { mount } from 'enzyme'; + +import { CreateCaseModal } from './create_case_modal'; +import { TestProviders } from '../../common/mock'; + +jest.mock('../create/form_context', () => { + return { + FormContext: ({ + children, + onSuccess, + }: { + children: ReactNode; + onSuccess: ({ id }: { id: string }) => Promise<void>; + }) => { + return ( + <> + <button + type="button" + data-test-subj="form-context-on-success" + onClick={async () => { + await onSuccess({ id: 'case-id' }); + }} + > + {'submit'} + </button> + {children} + </> + ); + }, + }; +}); + +jest.mock('../create/form', () => { + return { + CreateCaseForm: () => { + return <>{'form'}</>; + }, + }; +}); + +jest.mock('../create/submit_button', () => { + return { + SubmitCaseButton: () => { + return <>{'Submit'}</>; + }, + }; +}); + +const onCloseCaseModal = jest.fn(); +const onSuccess = jest.fn(); +const defaultProps = { + isModalOpen: true, + onCloseCaseModal, + onSuccess, +}; + +describe('CreateCaseModal', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders', () => { + const wrapper = mount( + <TestProviders> + <CreateCaseModal {...defaultProps} /> + </TestProviders> + ); + + expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy(); + }); + + it('it does not render the modal isModalOpen=false ', () => { + const wrapper = mount( + <TestProviders> + <CreateCaseModal {...defaultProps} isModalOpen={false} /> + </TestProviders> + ); + + expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeFalsy(); + }); + + it('Closing modal calls onCloseCaseModal', () => { + const wrapper = mount( + <TestProviders> + <CreateCaseModal {...defaultProps} /> + </TestProviders> + ); + + wrapper.find('.euiModal__closeIcon').first().simulate('click'); + expect(onCloseCaseModal).toBeCalled(); + }); + + it('pass the correct props to FormContext component', () => { + const wrapper = mount( + <TestProviders> + <CreateCaseModal {...defaultProps} /> + </TestProviders> + ); + + const props = wrapper.find('FormContext').props(); + expect(props).toEqual( + expect.objectContaining({ + onSuccess, + }) + ); + }); + + it('onSuccess called when creating a case', () => { + const wrapper = mount( + <TestProviders> + <CreateCaseModal {...defaultProps} /> + </TestProviders> + ); + + wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click'); + expect(onSuccess).toHaveBeenCalledWith({ id: 'case-id' }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx new file mode 100644 index 0000000000000..fc397b24e7046 --- /dev/null +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; + +import { FormContext } from '../create/form_context'; +import { CreateCaseForm } from '../create/form'; +import { SubmitCaseButton } from '../create/submit_button'; +import { Case } from '../../containers/types'; +import * as i18n from '../../common/translations'; +import { CaseType } from '../../../common'; + +export interface CreateCaseModalProps { + isModalOpen: boolean; + onCloseCaseModal: () => void; + onSuccess: (theCase: Case) => Promise<void>; + caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; +} + +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui.euiSize}; + text-align: right; + `} +`; + +const CreateModalComponent: React.FC<CreateCaseModalProps> = ({ + isModalOpen, + onCloseCaseModal, + onSuccess, + caseType = CaseType.individual, + hideConnectorServiceNowSir = false, +}) => { + return isModalOpen ? ( + <EuiModal onClose={onCloseCaseModal} data-test-subj="all-cases-modal"> + <EuiModalHeader> + <EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle> + </EuiModalHeader> + <EuiModalBody> + <FormContext + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + caseType={caseType} + onSuccess={onSuccess} + > + <CreateCaseForm + withSteps={false} + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + /> + <Container> + <SubmitCaseButton /> + </Container> + </FormContext> + </EuiModalBody> + </EuiModal> + ) : null; +}; + +export const CreateCaseModal = memo(CreateModalComponent); + +CreateCaseModal.displayName = 'CreateCaseModal'; diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/index.test.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/index.test.tsx new file mode 100644 index 0000000000000..df9e6f0af60d9 --- /dev/null +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/index.test.tsx @@ -0,0 +1,151 @@ +/* + * 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, { ReactNode } from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { useKibana } from '../../common/lib/kibana'; +import { useCreateCaseModal, UseCreateCaseModalProps, UseCreateCaseModalReturnedValues } from '.'; +import { TestProviders } from '../../common/mock'; + +jest.mock('../../common/lib/kibana'); +jest.mock('../create/form_context', () => { + return { + FormContext: ({ + children, + onSuccess, + }: { + children: ReactNode; + onSuccess: ({ id }: { id: string }) => Promise<void>; + }) => { + return ( + <> + <button + type="button" + data-test-subj="form-context-on-success" + onClick={async () => { + await onSuccess({ id: 'case-id' }); + }} + > + {'Form submit'} + </button> + {children} + </> + ); + }, + }; +}); + +jest.mock('../create/form', () => { + return { + CreateCaseForm: () => { + return <>{'form'}</>; + }, + }; +}); + +jest.mock('../create/submit_button', () => { + return { + SubmitCaseButton: () => { + return <>{'Submit'}</>; + }, + }; +}); + +const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>; +const onCaseCreated = jest.fn(); + +describe('useCreateCaseModal', () => { + let navigateToApp: jest.Mock; + + beforeEach(() => { + navigateToApp = jest.fn(); + useKibanaMock().services.application.navigateToApp = navigateToApp; + }); + + it('init', async () => { + const { result } = renderHook<UseCreateCaseModalProps, UseCreateCaseModalReturnedValues>( + () => useCreateCaseModal({ onCaseCreated }), + { + wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, + } + ); + + expect(result.current.isModalOpen).toBe(false); + }); + + it('opens the modal', async () => { + const { result } = renderHook<UseCreateCaseModalProps, UseCreateCaseModalReturnedValues>( + () => useCreateCaseModal({ onCaseCreated }), + { + wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, + } + ); + + act(() => { + result.current.openModal(); + }); + + expect(result.current.isModalOpen).toBe(true); + }); + + it('closes the modal', async () => { + const { result } = renderHook<UseCreateCaseModalProps, UseCreateCaseModalReturnedValues>( + () => useCreateCaseModal({ onCaseCreated }), + { + wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, + } + ); + + act(() => { + result.current.openModal(); + result.current.closeModal(); + }); + + expect(result.current.isModalOpen).toBe(false); + }); + + it('returns a memoized value', async () => { + const { result, rerender } = renderHook< + UseCreateCaseModalProps, + UseCreateCaseModalReturnedValues + >(() => useCreateCaseModal({ onCaseCreated }), { + wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, + }); + + const result1 = result.current; + act(() => rerender()); + const result2 = result.current; + + expect(Object.is(result1, result2)).toBe(true); + }); + + it('closes the modal when creating a case', async () => { + const { result } = renderHook<UseCreateCaseModalProps, UseCreateCaseModalReturnedValues>( + () => useCreateCaseModal({ onCaseCreated }), + { + wrapper: ({ children }) => <TestProviders>{children}</TestProviders>, + } + ); + + act(() => { + result.current.openModal(); + }); + + const modal = result.current.modal; + render(<TestProviders>{modal}</TestProviders>); + + act(() => { + userEvent.click(screen.getByText('Form submit')); + }); + + expect(result.current.isModalOpen).toBe(false); + expect(onCaseCreated).toHaveBeenCalledWith({ id: 'case-id' }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx new file mode 100644 index 0000000000000..7da3f49be721d --- /dev/null +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { CaseType } from '../../../common'; +import { Case } from '../../containers/types'; +import { CreateCaseModal } from './create_case_modal'; + +export interface UseCreateCaseModalProps { + onCaseCreated: (theCase: Case) => void; + caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; +} +export interface UseCreateCaseModalReturnedValues { + modal: JSX.Element; + isModalOpen: boolean; + closeModal: () => void; + openModal: () => void; +} + +export const useCreateCaseModal = ({ + caseType = CaseType.individual, + onCaseCreated, + hideConnectorServiceNowSir = false, +}: UseCreateCaseModalProps) => { + const [isModalOpen, setIsModalOpen] = useState<boolean>(false); + const closeModal = useCallback(() => setIsModalOpen(false), []); + const openModal = useCallback(() => setIsModalOpen(true), []); + const onSuccess = useCallback( + async (theCase) => { + onCaseCreated(theCase); + closeModal(); + }, + [onCaseCreated, closeModal] + ); + + const state = useMemo( + () => ({ + modal: ( + <CreateCaseModal + caseType={caseType} + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + isModalOpen={isModalOpen} + onCloseCaseModal={closeModal} + onSuccess={onSuccess} + /> + ), + isModalOpen, + closeModal, + openModal, + }), + [caseType, closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal] + ); + + return state; +}; diff --git a/x-pack/plugins/cases/public/components/wrappers/index.tsx b/x-pack/plugins/cases/public/components/wrappers/index.tsx new file mode 100644 index 0000000000000..3b33e9304da83 --- /dev/null +++ b/x-pack/plugins/cases/public/components/wrappers/index.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; + +export const WhitePageWrapper = styled.div` + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border-top: ${({ theme }) => theme.eui.euiBorderThin}; + flex: 1 1 auto; +`; + +export const SectionWrapper = styled.div` + box-sizing: content-box; + margin: 0 auto; + max-width: 1175px; + width: 100%; +`; + +export const HeaderWrapper = styled.div` + padding: ${({ theme }) => + `${theme.eui.paddingSizes.l} ${theme.eui.paddingSizes.l} 0 ${theme.eui.paddingSizes.l}`}; +`; diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts new file mode 100644 index 0000000000000..4dbb10da95b2d --- /dev/null +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -0,0 +1,114 @@ +/* + * 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 { + ActionLicense, + AllCases, + BulkUpdateStatus, + Case, + CasesStatus, + CaseUserActions, + FetchCasesProps, + SortFieldCase, +} from '../types'; +import { + actionLicenses, + allCases, + basicCase, + basicCaseCommentPatch, + basicCasePost, + casesStatus, + caseUserActions, + pushedCase, + respReporters, + tags, +} from '../mock'; +import { + CasePatchRequest, + CasePostRequest, + CommentRequest, + User, + CaseStatuses, +} from '../../../common'; + +export const getCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise<Case> => { + return Promise.resolve(basicCase); +}; + +export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => + Promise.resolve(casesStatus); + +export const getTags = async (signal: AbortSignal): Promise<string[]> => Promise.resolve(tags); + +export const getReporters = async (signal: AbortSignal): Promise<User[]> => + Promise.resolve(respReporters); + +export const getCaseUserActions = async ( + caseId: string, + signal: AbortSignal +): Promise<CaseUserActions[]> => Promise.resolve(caseUserActions); + +export const getCases = async ({ + filterOptions = { + search: '', + reporters: [], + status: CaseStatuses.open, + tags: [], + }, + queryParams = { + page: 1, + perPage: 5, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + signal, +}: FetchCasesProps): Promise<AllCases> => Promise.resolve(allCases); + +export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => + Promise.resolve(basicCasePost); + +export const patchCase = async ( + caseId: string, + updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>, + version: string, + signal: AbortSignal +): Promise<Case[]> => Promise.resolve([basicCase]); + +export const patchCasesStatus = async ( + cases: BulkUpdateStatus[], + signal: AbortSignal +): Promise<Case[]> => Promise.resolve(allCases.cases); + +export const postComment = async ( + newComment: CommentRequest, + caseId: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(basicCase); + +export const patchComment = async ( + caseId: string, + commentId: string, + commentUpdate: string, + version: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(basicCaseCommentPatch); + +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<boolean> => + Promise.resolve(true); + +export const pushCase = async ( + caseId: string, + connectorId: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(pushedCase); + +export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => + Promise.resolve(actionLicenses); diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx new file mode 100644 index 0000000000000..3e71a05df7cc1 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -0,0 +1,465 @@ +/* + * 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 { KibanaServices } from '../common/lib/kibana'; + +import { ConnectorTypes, CommentType, CaseStatuses } from '../../common'; +import { CASES_URL } from '../../common'; + +import { + deleteCases, + getActionLicense, + getCase, + getCases, + getCasesStatus, + getCaseUserActions, + getReporters, + getTags, + patchCase, + patchCasesStatus, + patchComment, + postCase, + postComment, + pushCase, +} from './api'; + +import { + actionLicenses, + allCases, + basicCase, + allCasesSnake, + basicCaseSnake, + pushedCaseSnake, + casesStatus, + casesSnake, + cases, + caseUserActions, + pushedCase, + reporters, + respReporters, + tags, + caseUserActionsSnake, + casesStatusSnake, +} from './mock'; + +import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../common/lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Case Configuration API', () => { + describe('deleteCases', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(''); + }); + const data = ['1', '2']; + + test('check url, method, signal', async () => { + await deleteCases(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'DELETE', + query: { ids: JSON.stringify(data) }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await deleteCases(data, abortCtrl.signal); + expect(resp).toEqual(''); + }); + }); + + describe('getActionLicense', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(actionLicenses); + }); + + test('check url, method, signal', async () => { + await getActionLicense(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`/api/actions/list_action_types`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getActionLicense(abortCtrl.signal); + expect(resp).toEqual(actionLicenses); + }); + }); + + describe('getCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = basicCase.id; + + test('check url, method, signal', async () => { + await getCase(data, true, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}`, { + method: 'GET', + query: { includeComments: true }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCase(data, true, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + + describe('getCases', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(allCasesSnake); + }); + test('check url, method, signal', async () => { + await getCases({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters: [], + tags: [], + }, + signal: abortCtrl.signal, + }); + }); + + test('correctly applies filters', async () => { + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + reporters: [...respReporters, { username: null, full_name: null, email: null }], + tags, + status: CaseStatuses.open, + search: 'hello', + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters, + tags: ['"coke"', '"pepsi"'], + search: 'hello', + status: CaseStatuses.open, + }, + signal: abortCtrl.signal, + }); + }); + + test('tags with weird chars get handled gracefully', async () => { + const weirdTags: string[] = ['(', '"double"']; + + await getCases({ + filterOptions: { + ...DEFAULT_FILTER_OPTIONS, + reporters: [...respReporters, { username: null, full_name: null, email: null }], + tags: weirdTags, + status: CaseStatuses.open, + search: 'hello', + }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/_find`, { + method: 'GET', + query: { + ...DEFAULT_QUERY_PARAMS, + reporters, + tags: ['"("', '"\\"double\\""'], + search: 'hello', + status: CaseStatuses.open, + }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCases({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + expect(resp).toEqual({ ...allCases }); + }); + }); + + describe('getCasesStatus', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(casesStatusSnake); + }); + test('check url, method, signal', async () => { + await getCasesStatus(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/status`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCasesStatus(abortCtrl.signal); + expect(resp).toEqual(casesStatus); + }); + }); + + describe('getCaseUserActions', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseUserActionsSnake); + }); + + test('check url, method, signal', async () => { + await getCaseUserActions(basicCase.id, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/user_actions`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCaseUserActions(basicCase.id, abortCtrl.signal); + expect(resp).toEqual(caseUserActions); + }); + }); + + describe('getReporters', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(respReporters); + }); + + test('check url, method, signal', async () => { + await getReporters(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/reporters`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getReporters(abortCtrl.signal); + expect(resp).toEqual(respReporters); + }); + }); + + describe('getTags', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(tags); + }); + + test('check url, method, signal', async () => { + await getTags(abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/tags`, { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getTags(abortCtrl.signal); + expect(resp).toEqual(tags); + }); + }); + + describe('patchCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue([basicCaseSnake]); + }); + const data = { description: 'updated description' }; + test('check url, method, signal', async () => { + await patchCase(basicCase.id, data, basicCase.version, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'PATCH', + body: JSON.stringify({ + cases: [{ ...data, id: basicCase.id, version: basicCase.version }], + }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCase( + basicCase.id, + { description: 'updated description' }, + basicCase.version, + abortCtrl.signal + ); + expect(resp).toEqual({ ...[basicCase] }); + }); + }); + + describe('patchCasesStatus', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(casesSnake); + }); + const data = [ + { + status: CaseStatuses.closed, + id: basicCase.id, + version: basicCase.version, + }, + ]; + + test('check url, method, signal', async () => { + await patchCasesStatus(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'PATCH', + body: JSON.stringify({ cases: data }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCasesStatus(data, abortCtrl.signal); + expect(resp).toEqual({ ...cases }); + }); + }); + + describe('patchComment', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + + test('check url, method, signal', async () => { + await patchComment( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal + ); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { + method: 'PATCH', + body: JSON.stringify({ + comment: 'updated comment', + type: CommentType.user, + id: basicCase.comments[0].id, + version: basicCase.comments[0].version, + }), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchComment( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal + ); + expect(resp).toEqual(basicCase); + }); + }); + + describe('postCase', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = { + description: 'description', + tags: ['tag'], + title: 'title', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + settings: { + syncAlerts: true, + }, + }; + + test('check url, method, signal', async () => { + await postCase(data, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}`, { + method: 'POST', + body: JSON.stringify(data), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postCase(data, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + + describe('postComment', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(basicCaseSnake); + }); + const data = { + comment: 'comment', + type: CommentType.user as const, + }; + + test('check url, method, signal', async () => { + await postComment(data, basicCase.id, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/comments`, { + method: 'POST', + body: JSON.stringify(data), + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postComment(data, basicCase.id, abortCtrl.signal); + expect(resp).toEqual(basicCase); + }); + }); + + describe('pushCase', () => { + const connectorId = 'connectorId'; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(pushedCaseSnake); + }); + + test('check url, method, signal', async () => { + await pushCase(basicCase.id, connectorId, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith( + `${CASES_URL}/${basicCase.id}/connector/${connectorId}/_push`, + { + method: 'POST', + body: JSON.stringify({}), + signal: abortCtrl.signal, + } + ); + }); + + test('happy path', async () => { + const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal); + expect(resp).toEqual(pushedCase); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts new file mode 100644 index 0000000000000..5827083bfdbd2 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -0,0 +1,347 @@ +/* + * 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 { assign, omit } from 'lodash'; + +import { + CasePatchRequest, + CasePostRequest, + CaseResponse, + CasesFindResponse, + CasesResponse, + CasesStatusResponse, + CaseType, + CaseUserActionsResponse, + CommentRequest, + CommentType, + SubCasePatchRequest, + SubCaseResponse, + SubCasesResponse, + User, +} from '../../common'; + +import { + ACTION_TYPES_URL, + CASE_REPORTERS_URL, + CASE_STATUS_URL, + CASE_TAGS_URL, + CASES_URL, + SUB_CASE_DETAILS_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../common'; + +import { + getCaseCommentsUrl, + getCasePushUrl, + getCaseDetailsUrl, + getCaseUserActionUrl, + getSubCaseDetailsUrl, + getSubCaseUserActionUrl, +} from '../../common'; + +import { KibanaServices } from '../common/lib/kibana'; +import { StatusAll } from '../components/status'; + +import { + ActionLicense, + AllCases, + BulkUpdateStatus, + Case, + CasesStatus, + FetchCasesProps, + SortFieldCase, + CaseUserActions, +} from './types'; + +import { + convertToCamelCase, + convertAllCasesToCamel, + convertArrayToCamelCase, + decodeCaseResponse, + decodeCasesResponse, + decodeCasesFindResponse, + decodeCasesStatusResponse, + decodeCaseUserActionsResponse, +} from './utils'; + +export const getCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>(getCaseDetailsUrl(caseId), { + method: 'GET', + query: { + includeComments, + }, + signal, + }); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const getSubCase = async ( + caseId: string, + subCaseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise<Case> => { + const [caseResponse, subCaseResponse] = await Promise.all([ + KibanaServices.get().http.fetch<CaseResponse>(getCaseDetailsUrl(caseId), { + method: 'GET', + query: { + includeComments: false, + }, + signal, + }), + KibanaServices.get().http.fetch<SubCaseResponse>(getSubCaseDetailsUrl(caseId, subCaseId), { + method: 'GET', + query: { + includeComments, + }, + signal, + }), + ]); + const response = assign<CaseResponse, SubCaseResponse>(caseResponse, subCaseResponse); + const subCaseIndex = response.subCaseIds?.findIndex((scId) => scId === response.id) ?? -1; + response.title = `${response.title}${subCaseIndex >= 0 ? ` ${subCaseIndex + 1}` : ''}`; + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => { + const response = await KibanaServices.get().http.fetch<CasesStatusResponse>(CASE_STATUS_URL, { + method: 'GET', + signal, + }); + return convertToCamelCase<CasesStatusResponse, CasesStatus>(decodeCasesStatusResponse(response)); +}; + +export const getTags = async (signal: AbortSignal): Promise<string[]> => { + const response = await KibanaServices.get().http.fetch<string[]>(CASE_TAGS_URL, { + method: 'GET', + signal, + }); + return response ?? []; +}; + +export const getReporters = async (signal: AbortSignal): Promise<User[]> => { + const response = await KibanaServices.get().http.fetch<User[]>(CASE_REPORTERS_URL, { + method: 'GET', + signal, + }); + return response ?? []; +}; + +export const getCaseUserActions = async ( + caseId: string, + signal: AbortSignal +): Promise<CaseUserActions[]> => { + const response = await KibanaServices.get().http.fetch<CaseUserActionsResponse>( + getCaseUserActionUrl(caseId), + { + method: 'GET', + signal, + } + ); + return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[]; +}; + +export const getSubCaseUserActions = async ( + caseId: string, + subCaseId: string, + signal: AbortSignal +): Promise<CaseUserActions[]> => { + const response = await KibanaServices.get().http.fetch<CaseUserActionsResponse>( + getSubCaseUserActionUrl(caseId, subCaseId), + { + method: 'GET', + signal, + } + ); + return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[]; +}; + +export const getCases = async ({ + filterOptions = { + onlyCollectionType: false, + search: '', + reporters: [], + status: StatusAll, + tags: [], + }, + queryParams = { + page: 1, + perPage: 20, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + signal, +}: FetchCasesProps): Promise<AllCases> => { + const query = { + reporters: filterOptions.reporters.map((r) => r.username ?? '').filter((r) => r !== ''), + tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`), + status: filterOptions.status, + ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), + ...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}), + ...queryParams, + }; + const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, { + method: 'GET', + query: query.status === StatusAll ? omit(query, ['status']) : query, + signal, + }); + return convertAllCasesToCamel(decodeCasesFindResponse(response)); +}; + +export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>(CASES_URL, { + method: 'POST', + body: JSON.stringify(newCase), + signal, + }); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const patchCase = async ( + caseId: string, + updatedCase: Pick< + CasePatchRequest, + 'description' | 'status' | 'tags' | 'title' | 'settings' | 'connector' + >, + version: string, + signal: AbortSignal +): Promise<Case[]> => { + const response = await KibanaServices.get().http.fetch<CasesResponse>(CASES_URL, { + method: 'PATCH', + body: JSON.stringify({ cases: [{ ...updatedCase, id: caseId, version }] }), + signal, + }); + return convertToCamelCase<CasesResponse, Case[]>(decodeCasesResponse(response)); +}; + +export const patchSubCase = async ( + caseId: string, + subCaseId: string, + updatedSubCase: Pick<SubCasePatchRequest, 'status'>, + version: string, + signal: AbortSignal +): Promise<Case[]> => { + const subCaseResponse = await KibanaServices.get().http.fetch<SubCasesResponse>( + SUB_CASE_DETAILS_URL, + { + method: 'PATCH', + body: JSON.stringify({ cases: [{ ...updatedSubCase, id: caseId, version }] }), + signal, + } + ); + const caseResponse = await KibanaServices.get().http.fetch<CaseResponse>( + getCaseDetailsUrl(caseId), + { + method: 'GET', + query: { + includeComments: false, + }, + signal, + } + ); + const response = subCaseResponse.map((subCaseResp) => assign(caseResponse, subCaseResp)); + return convertToCamelCase<CasesResponse, Case[]>(decodeCasesResponse(response)); +}; + +export const patchCasesStatus = async ( + cases: BulkUpdateStatus[], + signal: AbortSignal +): Promise<Case[]> => { + const response = await KibanaServices.get().http.fetch<CasesResponse>(CASES_URL, { + method: 'PATCH', + body: JSON.stringify({ cases }), + signal, + }); + return convertToCamelCase<CasesResponse, Case[]>(decodeCasesResponse(response)); +}; + +export const postComment = async ( + newComment: CommentRequest, + caseId: string, + signal: AbortSignal, + subCaseId?: string +): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>( + `${CASES_URL}/${caseId}/comments`, + { + method: 'POST', + body: JSON.stringify(newComment), + ...(subCaseId ? { query: { subCaseId } } : {}), + signal, + } + ); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const patchComment = async ( + caseId: string, + commentId: string, + commentUpdate: string, + version: string, + signal: AbortSignal, + subCaseId?: string +): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>(getCaseCommentsUrl(caseId), { + method: 'PATCH', + body: JSON.stringify({ + comment: commentUpdate, + type: CommentType.user, + id: commentId, + version, + }), + ...(subCaseId ? { query: { subCaseId } } : {}), + signal, + }); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<string> => { + const response = await KibanaServices.get().http.fetch<string>(CASES_URL, { + method: 'DELETE', + query: { ids: JSON.stringify(caseIds) }, + signal, + }); + return response; +}; + +export const deleteSubCases = async (caseIds: string[], signal: AbortSignal): Promise<string> => { + const response = await KibanaServices.get().http.fetch<string>(SUB_CASES_PATCH_DEL_URL, { + method: 'DELETE', + query: { ids: JSON.stringify(caseIds) }, + signal, + }); + return response; +}; + +export const pushCase = async ( + caseId: string, + connectorId: string, + signal: AbortSignal +): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>( + getCasePushUrl(caseId, connectorId), + { + method: 'POST', + body: JSON.stringify({}), + signal, + } + ); + + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => { + const response = await KibanaServices.get().http.fetch<ActionLicense[]>(ACTION_TYPES_URL, { + method: 'GET', + signal, + }); + return response; +}; diff --git a/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts new file mode 100644 index 0000000000000..ea4b92706b4d1 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts @@ -0,0 +1,36 @@ +/* + * 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 { + CasesConfigurePatch, + CasesConfigureRequest, + ActionConnector, + ActionTypeConnector, +} from '../../../../common'; + +import { ApiProps } from '../../types'; +import { CaseConfigure } from '../types'; +import { connectorsMock, caseConfigurationCamelCaseResponseMock, actionTypesMock } from '../mock'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise<ActionConnector[]> => + Promise.resolve(connectorsMock); + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure> => + Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const fetchActionTypes = async ({ signal }: ApiProps): Promise<ActionTypeConnector[]> => + Promise.resolve(actionTypesMock); diff --git a/x-pack/plugins/cases/public/containers/configure/api.test.ts b/x-pack/plugins/cases/public/containers/configure/api.test.ts new file mode 100644 index 0000000000000..ae749b4391776 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/api.test.ts @@ -0,0 +1,154 @@ +/* + * 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 { + fetchConnectors, + getCaseConfigure, + postCaseConfigure, + patchCaseConfigure, + fetchActionTypes, +} from './api'; +import { + connectorsMock, + actionTypesMock, + caseConfigurationMock, + caseConfigurationResposeMock, + caseConfigurationCamelCaseResponseMock, +} from './mock'; +import { ConnectorTypes } from '../../../common'; +import { KibanaServices } from '../../common/lib/kibana'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../common/lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Case Configuration API', () => { + describe('fetch connectors', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(connectorsMock); + }); + + test('check url, method, signal', async () => { + await fetchConnectors({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/connectors/_find', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await fetchConnectors({ signal: abortCtrl.signal }); + expect(resp).toEqual(connectorsMock); + }); + }); + + describe('fetch configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, method, signal', async () => { + await getCaseConfigure({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + + test('return null on empty response', async () => { + fetchMock.mockResolvedValue({}); + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toBe(null); + }); + }); + + describe('create configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: + '{"connector":{"id":"123","name":"My connector","type":".jira","fields":null},"closure_type":"close-by-user"}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); + + describe('update configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await patchCaseConfigure( + { + connector: { id: '456', name: 'My Connector 2', type: ConnectorTypes.none, fields: null }, + version: 'WzHJ12', + }, + abortCtrl.signal + ); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: + '{"connector":{"id":"456","name":"My Connector 2","type":".none","fields":null},"version":"WzHJ12"}', + method: 'PATCH', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCaseConfigure( + { + connector: { id: '456', name: 'My Connector 2', type: ConnectorTypes.none, fields: null }, + version: 'WzHJ12', + }, + abortCtrl.signal + ); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); + + describe('fetch actionTypes', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(actionTypesMock); + }); + + test('check url, method, signal', async () => { + await fetchActionTypes({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/actions/list_action_types', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await fetchActionTypes({ signal: abortCtrl.signal }); + expect(resp).toEqual(actionTypesMock); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/configure/api.ts b/x-pack/plugins/cases/public/containers/configure/api.ts new file mode 100644 index 0000000000000..006370fcb5533 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/api.ts @@ -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 { isEmpty } from 'lodash/fp'; +import { + ActionConnector, + ActionTypeConnector, + CasesConfigurePatch, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../common'; +import { KibanaServices } from '../../common/lib/kibana'; + +import { + CASE_CONFIGURE_CONNECTORS_URL, + CASE_CONFIGURE_URL, + ACTION_TYPES_URL, +} from '../../../common'; + +import { ApiProps } from '../types'; +import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; +import { CaseConfigure } from './types'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise<ActionConnector[]> => { + const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { + method: 'GET', + signal, + }); + + return response; +}; + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure | null> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'GET', + signal, + } + ); + + return !isEmpty(response) + ? convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ) + : null; +}; + +export const getConnectorMappings = async ({ signal }: ApiProps): Promise<ActionConnector[]> => { + const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { + method: 'GET', + signal, + }); + + return response; +}; + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise<CaseConfigure> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'POST', + body: JSON.stringify(caseConfiguration), + signal, + } + ); + return convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ); +}; + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise<CaseConfigure> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'PATCH', + body: JSON.stringify(caseConfiguration), + signal, + } + ); + return convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ); +}; + +export const fetchActionTypes = async ({ signal }: ApiProps): Promise<ActionTypeConnector[]> => { + const response = await KibanaServices.get().http.fetch(ACTION_TYPES_URL, { + method: 'GET', + signal, + }); + + return response; +}; diff --git a/x-pack/plugins/cases/public/containers/configure/mock.ts b/x-pack/plugins/cases/public/containers/configure/mock.ts new file mode 100644 index 0000000000000..766452e3e58e7 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/mock.ts @@ -0,0 +1,160 @@ +/* + * 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 { + ActionConnector, + ActionTypeConnector, + CasesConfigureResponse, + CasesConfigureRequest, + ConnectorTypes, +} from '../../../common'; +import { CaseConfigure, CaseConnectorMapping } from './types'; + +export const mappings: CaseConnectorMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; + +export const connectorsMock: ActionConnector[] = [ + { + id: 'servicenow-1', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + }, + isPreconfigured: false, + }, + { + id: 'resilient-2', + actionTypeId: '.resilient', + name: 'My Connector 2', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + isPreconfigured: false, + }, + { + id: 'jira-1', + actionTypeId: '.jira', + name: 'Jira', + config: { + apiUrl: 'https://instance.atlassian.ne', + }, + isPreconfigured: false, + }, + { + id: 'servicenow-sir', + actionTypeId: '.servicenow-sir', + name: 'My Connector SIR', + config: { + apiUrl: 'https://instance1.service-now.com', + }, + isPreconfigured: false, + }, +]; + +export const actionTypesMock: ActionTypeConnector[] = [ + { + id: '.email', + name: 'Email', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.index', + name: 'Index', + minimumLicenseRequired: 'basic', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.servicenow', + name: 'ServiceNow', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.jira', + name: 'Jira', + minimumLicenseRequired: 'gold', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.resilient', + name: 'IBM Resilient', + minimumLicenseRequired: 'platinum', + enabled: false, + enabledInConfig: true, + enabledInLicense: true, + }, +]; + +export const caseConfigurationResposeMock: CasesConfigureResponse = { + created_at: '2020-04-06T13:03:18.657Z', + created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + connector: { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: null, + }, + closure_type: 'close-by-pushing', + error: null, + mappings: [], + updated_at: '2020-04-06T14:03:18.657Z', + updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; + +export const caseConfigurationMock: CasesConfigureRequest = { + connector: { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: null, + }, + closure_type: 'close-by-user', +}; + +export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { + createdAt: '2020-04-06T13:03:18.657Z', + createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + connector: { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: null, + }, + closureType: 'close-by-pushing', + error: null, + mappings: [], + updatedAt: '2020-04-06T14:03:18.657Z', + updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; diff --git a/x-pack/plugins/cases/public/containers/configure/translations.ts b/x-pack/plugins/cases/public/containers/configure/translations.ts new file mode 100644 index 0000000000000..e77b9f57c8f4c --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../translations'; + +export const SUCCESS_CONFIGURE = i18n.translate('xpack.cases.configure.successSaveToast', { + defaultMessage: 'Saved external connection settings', +}); diff --git a/x-pack/plugins/cases/public/containers/configure/types.ts b/x-pack/plugins/cases/public/containers/configure/types.ts new file mode 100644 index 0000000000000..b021ae2163fa2 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/types.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticUser } from '../types'; +import { + ActionConnector, + ActionTypeConnector, + ActionType, + CaseConnector, + CaseField, + CasesConfigure, + ClosureType, + ThirdPartyField, +} from '../../../common'; + +export { + ActionConnector, + ActionTypeConnector, + ActionType, + CaseConnector, + CaseField, + ClosureType, + ThirdPartyField, +}; + +export interface CaseConnectorMapping { + actionType: ActionType; + source: CaseField; + target: string; +} + +export interface CaseConfigure { + closureType: ClosureType; + connector: CasesConfigure['connector']; + createdAt: string; + createdBy: ElasticUser; + error: string | null; + mappings: CaseConnectorMapping[]; + updatedAt: string; + updatedBy: ElasticUser; + version: string; +} diff --git a/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx new file mode 100644 index 0000000000000..25017f7931db8 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/use_action_types.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useActionTypes, UseActionTypesResponse } from './use_action_types'; +import { actionTypesMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useActionTypes', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + actionTypes: [], + refetchActionTypes: result.current.refetchActionTypes, + }); + }); + }); + + test('fetch action types', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + actionTypes: actionTypesMock, + refetchActionTypes: result.current.refetchActionTypes, + }); + }); + }); + + test('refetch actionTypes', async () => { + const spyOnfetchActionTypes = jest.spyOn(api, 'fetchActionTypes'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.refetchActionTypes(); + expect(spyOnfetchActionTypes).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when refetching actionTypes', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.refetchActionTypes(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('unhappy path', async () => { + const spyOnfetchActionTypes = jest.spyOn(api, 'fetchActionTypes'); + spyOnfetchActionTypes.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseActionTypesResponse>(() => + useActionTypes() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + actionTypes: [], + refetchActionTypes: result.current.refetchActionTypes, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx b/x-pack/plugins/cases/public/containers/configure/use_action_types.tsx new file mode 100644 index 0000000000000..206952661e672 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/use_action_types.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 { useState, useEffect, useCallback, useRef } from 'react'; + +import { useStateToaster, errorToToaster } from '../../components/toasters'; +import * as i18n from '../translations'; +import { fetchActionTypes } from './api'; +import { ActionTypeConnector } from './types'; + +export interface UseActionTypesResponse { + loading: boolean; + actionTypes: ActionTypeConnector[]; + refetchActionTypes: () => void; +} + +export const useActionTypes = (): UseActionTypesResponse => { + const [, dispatchToaster] = useStateToaster(); + const [loading, setLoading] = useState(true); + const [actionTypes, setActionTypes] = useState<ActionTypeConnector[]>([]); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + const queryFirstTime = useRef(true); + + const refetchActionTypes = useCallback(async () => { + try { + setLoading(true); + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + const res = await fetchActionTypes({ signal: abortCtrlRef.current.signal }); + + if (!isCancelledRef.current) { + setLoading(false); + setActionTypes(res); + } + } catch (error) { + if (!isCancelledRef.current) { + setLoading(false); + setActionTypes([]); + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + } + }, [dispatchToaster]); + + useEffect(() => { + if (queryFirstTime.current) { + refetchActionTypes(); + queryFirstTime.current = false; + } + + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + queryFirstTime.current = true; + }; + }, [refetchActionTypes]); + + return { + loading, + actionTypes, + refetchActionTypes, + }; +}; diff --git a/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx new file mode 100644 index 0000000000000..4e4db4cb5e82e --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx @@ -0,0 +1,326 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { + initialState, + useCaseConfigure, + ReturnUseCaseConfigure, + ConnectorConfiguration, +} from './use_configure'; +import { mappings, caseConfigurationCamelCaseResponseMock } from './mock'; +import * as api from './api'; +import { ConnectorTypes } from '../../../common'; + +jest.mock('./api'); +const mockErrorToToaster = jest.fn(); +jest.mock('../../components/toasters', () => { + const original = jest.requireActual('../../components/toasters'); + return { + ...original, + errorToToaster: () => mockErrorToToaster(), + }; +}); +const configuration: ConnectorConfiguration = { + connector: { + id: '456', + name: 'My connector 2', + type: ConnectorTypes.none, + fields: null, + }, + closureType: 'close-by-pushing', +}; + +describe('useConfigure', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialState, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMappings: result.current.setMappings, + }); + }); + }); + + test('fetch case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialState, + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connector: caseConfigurationCamelCaseResponseMock.connector, + currentConfiguration: { + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connector: caseConfigurationCamelCaseResponseMock.connector, + }, + mappings: [], + firstLoad: true, + loading: false, + persistCaseConfigure: result.current.persistCaseConfigure, + refetchCaseConfigure: result.current.refetchCaseConfigure, + setClosureType: result.current.setClosureType, + setConnector: result.current.setConnector, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setMappings: result.current.setMappings, + version: caseConfigurationCamelCaseResponseMock.version, + }); + }); + }); + + test('refetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2); + }); + }); + + test('correctly sets mappings', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current.mappings).toEqual([]); + result.current.setMappings(mappings); + expect(result.current.mappings).toEqual(mappings); + }); + }); + + test('set isLoading to true when fetching case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('persist case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.persistCaseConfigure(configuration); + expect(result.current.persistLoading).toBeTruthy(); + }); + }); + + test('save case configuration - postCaseConfigure', async () => { + // When there is no version, a configuration is created. Otherwise is updated. + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(mockErrorToToaster).not.toHaveBeenCalled(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current.connector.id).toEqual('123'); + await waitForNextUpdate(); + expect(result.current.connector.id).toEqual('456'); + }); + }); + + test('Displays error when present - getCaseConfigure', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + error: 'uh oh homeboy', + version: '', + }) + ); + + await act(async () => { + const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(mockErrorToToaster).toHaveBeenCalled(); + }); + }); + + test('Displays error when present - postCaseConfigure', async () => { + // When there is no version, a configuration is created. Otherwise is updated. + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + error: 'uh oh homeboy', + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(mockErrorToToaster).not.toHaveBeenCalled(); + + result.current.persistCaseConfigure(configuration); + expect(mockErrorToToaster).not.toHaveBeenCalled(); + await waitForNextUpdate(); + expect(mockErrorToToaster).toHaveBeenCalled(); + }); + }); + + test('save case configuration - patchCaseConfigure', async () => { + const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); + spyOnPatchCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current.connector.id).toEqual('123'); + await waitForNextUpdate(); + expect(result.current.connector.id).toEqual('456'); + }); + }); + + test('unhappy path - fetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + ...initialState, + loading: false, + persistCaseConfigure: result.current.persistCaseConfigure, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + setClosureType: result.current.setClosureType, + setConnector: result.current.setConnector, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setMappings: result.current.setMappings, + }); + }); + }); + + test('unhappy path - persist case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current).toEqual({ + ...initialState, + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connector: caseConfigurationCamelCaseResponseMock.connector, + currentConfiguration: { + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connector: caseConfigurationCamelCaseResponseMock.connector, + }, + firstLoad: true, + loading: false, + mappings: [], + persistCaseConfigure: result.current.persistCaseConfigure, + refetchCaseConfigure: result.current.refetchCaseConfigure, + setClosureType: result.current.setClosureType, + setConnector: result.current.setConnector, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setMappings: result.current.setMappings, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/configure/use_configure.tsx b/x-pack/plugins/cases/public/containers/configure/use_configure.tsx new file mode 100644 index 0000000000000..3d5e43b2772a9 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/use_configure.tsx @@ -0,0 +1,361 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useCallback, useReducer, useRef } from 'react'; +import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; + +import { useStateToaster, errorToToaster, displaySuccessToast } from '../../components/toasters'; +import * as i18n from './translations'; +import { ClosureType, CaseConfigure, CaseConnector, CaseConnectorMapping } from './types'; +import { ConnectorTypes } from '../../../common'; + +export type ConnectorConfiguration = { connector: CaseConnector } & { + closureType: CaseConfigure['closureType']; +}; + +export interface State extends ConnectorConfiguration { + currentConfiguration: ConnectorConfiguration; + firstLoad: boolean; + loading: boolean; + mappings: CaseConnectorMapping[]; + persistLoading: boolean; + version: string; +} +export type Action = + | { + type: 'setCurrentConfiguration'; + currentConfiguration: ConnectorConfiguration; + } + | { + type: 'setConnector'; + connector: CaseConnector; + } + | { + type: 'setLoading'; + payload: boolean; + } + | { + type: 'setFirstLoad'; + payload: boolean; + } + | { + type: 'setPersistLoading'; + payload: boolean; + } + | { + type: 'setVersion'; + payload: string; + } + | { + type: 'setClosureType'; + closureType: ClosureType; + } + | { + type: 'setMappings'; + mappings: CaseConnectorMapping[]; + }; + +export const configureCasesReducer = (state: State, action: Action) => { + switch (action.type) { + case 'setLoading': + return { + ...state, + loading: action.payload, + }; + case 'setFirstLoad': + return { + ...state, + firstLoad: action.payload, + }; + case 'setPersistLoading': + return { + ...state, + persistLoading: action.payload, + }; + case 'setVersion': + return { + ...state, + version: action.payload, + }; + case 'setCurrentConfiguration': { + return { + ...state, + currentConfiguration: { ...action.currentConfiguration }, + }; + } + case 'setConnector': { + return { + ...state, + connector: action.connector, + }; + } + case 'setClosureType': { + return { + ...state, + closureType: action.closureType, + }; + } + case 'setMappings': { + return { + ...state, + mappings: action.mappings, + }; + } + default: + return state; + } +}; + +export interface ReturnUseCaseConfigure extends State { + persistCaseConfigure: ({ connector, closureType }: ConnectorConfiguration) => unknown; + refetchCaseConfigure: () => void; + setClosureType: (closureType: ClosureType) => void; + setConnector: (connector: CaseConnector) => void; + setCurrentConfiguration: (configuration: ConnectorConfiguration) => void; + setMappings: (newMapping: CaseConnectorMapping[]) => void; +} + +export const initialState: State = { + closureType: 'close-by-user', + connector: { + fields: null, + id: 'none', + name: 'none', + type: ConnectorTypes.none, + }, + currentConfiguration: { + closureType: 'close-by-user', + connector: { + fields: null, + id: 'none', + name: 'none', + type: ConnectorTypes.none, + }, + }, + firstLoad: false, + loading: true, + mappings: [], + persistLoading: false, + version: '', +}; + +export const useCaseConfigure = (): ReturnUseCaseConfigure => { + const [state, dispatch] = useReducer(configureCasesReducer, initialState); + + const setCurrentConfiguration = useCallback((configuration: ConnectorConfiguration) => { + dispatch({ + currentConfiguration: configuration, + type: 'setCurrentConfiguration', + }); + }, []); + + const setConnector = useCallback((connector: CaseConnector) => { + dispatch({ + connector, + type: 'setConnector', + }); + }, []); + + const setClosureType = useCallback((closureType: ClosureType) => { + dispatch({ + closureType, + type: 'setClosureType', + }); + }, []); + + const setMappings = useCallback((mappings: CaseConnectorMapping[]) => { + dispatch({ + mappings, + type: 'setMappings', + }); + }, []); + + const setLoading = useCallback((isLoading: boolean) => { + dispatch({ + payload: isLoading, + type: 'setLoading', + }); + }, []); + + const setFirstLoad = useCallback((isFirstLoad: boolean) => { + dispatch({ + payload: isFirstLoad, + type: 'setFirstLoad', + }); + }, []); + + const setPersistLoading = useCallback((isPersistLoading: boolean) => { + dispatch({ + payload: isPersistLoading, + type: 'setPersistLoading', + }); + }, []); + + const setVersion = useCallback((version: string) => { + dispatch({ + payload: version, + type: 'setVersion', + }); + }, []); + + const [, dispatchToaster] = useStateToaster(); + const isCancelledRefetchRef = useRef(false); + const abortCtrlRefetchRef = useRef(new AbortController()); + + const isCancelledPersistRef = useRef(false); + const abortCtrlPersistRef = useRef(new AbortController()); + + const refetchCaseConfigure = useCallback(async () => { + try { + isCancelledRefetchRef.current = false; + abortCtrlRefetchRef.current.abort(); + abortCtrlRefetchRef.current = new AbortController(); + + setLoading(true); + const res = await getCaseConfigure({ signal: abortCtrlRefetchRef.current.signal }); + + if (!isCancelledRefetchRef.current) { + if (res != null) { + setConnector(res.connector); + if (setClosureType != null) { + setClosureType(res.closureType); + } + setVersion(res.version); + setMappings(res.mappings); + + if (!state.firstLoad) { + setFirstLoad(true); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + closureType: res.closureType, + connector: { + ...res.connector, + }, + }); + } + } + if (res.error != null) { + errorToToaster({ + dispatchToaster, + error: new Error(res.error), + title: i18n.ERROR_TITLE, + }); + } + } + setLoading(false); + } + } catch (error) { + if (!isCancelledRefetchRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + dispatchToaster, + error: error.body && error.body.message ? new Error(error.body.message) : error, + title: i18n.ERROR_TITLE, + }); + } + setLoading(false); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.firstLoad]); + + const persistCaseConfigure = useCallback( + async ({ connector, closureType }: ConnectorConfiguration) => { + try { + isCancelledPersistRef.current = false; + abortCtrlPersistRef.current.abort(); + abortCtrlPersistRef.current = new AbortController(); + setPersistLoading(true); + + const connectorObj = { + connector, + closure_type: closureType, + }; + + const res = + state.version.length === 0 + ? await postCaseConfigure(connectorObj, abortCtrlPersistRef.current.signal) + : await patchCaseConfigure( + { + ...connectorObj, + version: state.version, + }, + abortCtrlPersistRef.current.signal + ); + + if (!isCancelledPersistRef.current) { + setConnector(res.connector); + if (setClosureType) { + setClosureType(res.closureType); + } + setVersion(res.version); + setMappings(res.mappings); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + closureType: res.closureType, + connector: { + ...res.connector, + }, + }); + } + if (res.error != null) { + errorToToaster({ + dispatchToaster, + error: new Error(res.error), + title: i18n.ERROR_TITLE, + }); + } + displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); + setPersistLoading(false); + } + } catch (error) { + if (!isCancelledPersistRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + setConnector(state.currentConfiguration.connector); + setPersistLoading(false); + } + } + }, + [ + dispatchToaster, + setClosureType, + setConnector, + setCurrentConfiguration, + setMappings, + setPersistLoading, + setVersion, + state, + ] + ); + + useEffect(() => { + refetchCaseConfigure(); + return () => { + isCancelledRefetchRef.current = true; + abortCtrlRefetchRef.current.abort(); + isCancelledPersistRef.current = true; + abortCtrlPersistRef.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + ...state, + refetchCaseConfigure, + persistCaseConfigure, + setCurrentConfiguration, + setConnector, + setClosureType, + setMappings, + }; +}; diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_connectors.test.tsx new file mode 100644 index 0000000000000..ed1dfcbc40c87 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/use_connectors.test.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 { renderHook, act } from '@testing-library/react-hooks'; +import { useConnectors, UseConnectorsResponse } from './use_connectors'; +import { connectorsMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useConnectors', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseConnectorsResponse>(() => + useConnectors() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: true, + connectors: [], + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); + + test('fetch connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseConnectorsResponse>(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + connectors: connectorsMock, + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); + + test('refetch connectors', async () => { + const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseConnectorsResponse>(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchConnectors(); + expect(spyOnfetchConnectors).toHaveBeenCalledTimes(2); + }); + }); + + test('set isLoading to true when refetching connectors', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseConnectorsResponse>(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchConnectors(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('unhappy path', async () => { + const spyOnfetchConnectors = jest.spyOn(api, 'fetchConnectors'); + spyOnfetchConnectors.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseConnectorsResponse>(() => + useConnectors() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + loading: false, + connectors: [], + refetchConnectors: result.current.refetchConnectors, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx new file mode 100644 index 0000000000000..b385a2676e044 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/configure/use_connectors.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useCallback, useRef } from 'react'; + +import { useStateToaster, errorToToaster } from '../../components/toasters'; +import * as i18n from '../translations'; +import { fetchConnectors } from './api'; +import { ActionConnector } from './types'; + +export interface UseConnectorsResponse { + loading: boolean; + connectors: ActionConnector[]; + refetchConnectors: () => void; +} + +export const useConnectors = (): UseConnectorsResponse => { + const [, dispatchToaster] = useStateToaster(); + const [loading, setLoading] = useState(true); + const [connectors, setConnectors] = useState<ActionConnector[]>([]); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const refetchConnectors = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + setLoading(true); + const res = await fetchConnectors({ signal: abortCtrlRef.current.signal }); + + if (!isCancelledRef.current) { + setLoading(false); + setConnectors(res); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + + setLoading(false); + setConnectors([]); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + refetchConnectors(); + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + loading, + connectors, + refetchConnectors, + }; +}; diff --git a/x-pack/plugins/cases/public/containers/constants.ts b/x-pack/plugins/cases/public/containers/constants.ts new file mode 100644 index 0000000000000..be030f4d2f75b --- /dev/null +++ b/x-pack/plugins/cases/public/containers/constants.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEFAULT_TABLE_ACTIVE_PAGE = 1; +export const DEFAULT_TABLE_LIMIT = 5; diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts new file mode 100644 index 0000000000000..1e7cec29de56b --- /dev/null +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -0,0 +1,377 @@ +/* + * 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 { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types'; + +import { + AssociationType, + CaseResponse, + CasesFindResponse, + CasesResponse, + CasesStatusResponse, + CaseStatuses, + CaseType, + CaseUserActionsResponse, + CommentResponse, + CommentType, + ConnectorTypes, + UserAction, + UserActionField, +} from '../../common'; +import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; +export { connectorsMock } from './configure/mock'; + +export const basicCaseId = 'basic-case-id'; +export const basicSubCaseId = 'basic-sub-case-id'; +const basicCommentId = 'basic-comment-id'; +const basicCreatedAt = '2020-02-19T23:06:33.798Z'; +const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; +const laterTime = '2020-02-28T15:02:57.995Z'; + +export const elasticUser = { + fullName: 'Leslie Knope', + username: 'lknope', + email: 'leslie.knope@elastic.co', +}; + +export const tags: string[] = ['coke', 'pepsi']; + +export const basicComment: Comment = { + associationType: AssociationType.case, + comment: 'Solve this fast!', + type: CommentType.user, + id: basicCommentId, + createdAt: basicCreatedAt, + createdBy: elasticUser, + pushedAt: null, + pushedBy: null, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', +}; + +export const alertComment: Comment = { + alertId: 'alert-id-1', + associationType: AssociationType.case, + index: 'alert-index-1', + type: CommentType.alert, + id: 'alert-comment-id', + createdAt: basicCreatedAt, + createdBy: elasticUser, + pushedAt: null, + pushedBy: null, + rule: { + id: 'rule-id-1', + name: 'Awesome rule', + }, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', +}; + +export const basicCase: Case = { + type: CaseType.individual, + closedAt: null, + closedBy: null, + id: basicCaseId, + comments: [basicComment], + createdAt: basicCreatedAt, + createdBy: elasticUser, + connector: { + id: '123', + name: 'My Connector', + type: ConnectorTypes.none, + fields: null, + }, + description: 'Security banana Issue', + externalService: null, + status: CaseStatuses.open, + tags, + title: 'Another horrible breach!!', + totalComment: 1, + totalAlerts: 0, + updatedAt: basicUpdatedAt, + updatedBy: elasticUser, + version: 'WzQ3LDFd', + settings: { + syncAlerts: true, + }, + subCaseIds: [], +}; + +export const collectionCase: Case = { + type: CaseType.collection, + closedAt: null, + closedBy: null, + id: 'collection-id', + comments: [basicComment], + createdAt: basicCreatedAt, + createdBy: elasticUser, + connector: { + id: '123', + name: 'My Connector', + type: ConnectorTypes.none, + fields: null, + }, + description: 'Security banana Issue', + externalService: null, + status: CaseStatuses.open, + tags, + title: 'Another horrible breach in a collection!!', + totalComment: 1, + totalAlerts: 0, + updatedAt: basicUpdatedAt, + updatedBy: elasticUser, + version: 'WzQ3LDFd', + settings: { + syncAlerts: true, + }, + subCases: [], + subCaseIds: [], +}; + +export const basicCasePost: Case = { + ...basicCase, + updatedAt: null, + updatedBy: null, +}; + +export const basicCommentPatch: Comment = { + ...basicComment, + updatedAt: basicUpdatedAt, + updatedBy: { + username: 'elastic', + }, +}; + +export const basicCaseCommentPatch = { + ...basicCase, + comments: [basicCommentPatch], +}; + +export const casesStatus: CasesStatus = { + countOpenCases: 20, + countInProgressCases: 40, + countClosedCases: 130, +}; + +export const basicPush = { + connectorId: '123', + connectorName: 'connector name', + externalId: 'external_id', + externalTitle: 'external title', + externalUrl: 'basicPush.com', + pushedAt: basicUpdatedAt, + pushedBy: elasticUser, +}; + +export const pushedCase: Case = { + ...basicCase, + externalService: basicPush, +}; + +const basicAction = { + actionAt: basicCreatedAt, + actionBy: elasticUser, + oldValue: null, + newValue: 'what a cool value', + caseId: basicCaseId, + commentId: null, +}; + +export const cases: Case[] = [ + basicCase, + { ...pushedCase, id: '1', totalComment: 0, comments: [] }, + { ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] }, + { ...basicCase, id: '3', totalComment: 0, comments: [] }, + { ...basicCase, id: '4', totalComment: 0, comments: [] }, +]; + +export const allCases: AllCases = { + cases, + page: 1, + perPage: 5, + total: 10, + ...casesStatus, +}; + +export const actionLicenses: ActionLicense[] = [ + { + id: '.servicenow', + name: 'ServiceNow', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '.jira', + name: 'Jira', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, +]; + +// Snake case for mock api responses +export const elasticUserSnake = { + full_name: 'Leslie Knope', + username: 'lknope', + email: 'leslie.knope@elastic.co', +}; + +export const basicCommentSnake: CommentResponse = { + associationType: AssociationType.case, + comment: 'Solve this fast!', + type: CommentType.user, + id: basicCommentId, + created_at: basicCreatedAt, + created_by: elasticUserSnake, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzQ3LDFc', +}; + +export const basicCaseSnake: CaseResponse = { + ...basicCase, + status: CaseStatuses.open, + closed_at: null, + closed_by: null, + comments: [basicCommentSnake], + connector: { + id: '123', + name: 'My Connector', + type: ConnectorTypes.none, + fields: null, + }, + created_at: basicCreatedAt, + created_by: elasticUserSnake, + external_service: null, + updated_at: basicUpdatedAt, + updated_by: elasticUserSnake, +} as CaseResponse; + +export const casesStatusSnake: CasesStatusResponse = { + count_closed_cases: 130, + count_in_progress_cases: 40, + count_open_cases: 20, +}; + +export const pushSnake = { + connector_id: '123', + connector_name: 'connector name', + external_id: 'external_id', + external_title: 'external title', + external_url: 'basicPush.com', +}; + +export const basicPushSnake = { + ...pushSnake, + pushed_at: basicUpdatedAt, + pushed_by: elasticUserSnake, +}; + +export const pushedCaseSnake = { + ...basicCaseSnake, + external_service: basicPushSnake, +}; + +export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; +export const respReporters = [ + { username: 'alexis', full_name: null, email: null }, + { username: 'kim', full_name: null, email: null }, + { username: 'maria', full_name: null, email: null }, + { username: 'steph', full_name: null, email: null }, +]; +export const casesSnake: CasesResponse = [ + basicCaseSnake, + { ...pushedCaseSnake, id: '1', totalComment: 0, comments: [] }, + { ...pushedCaseSnake, updated_at: laterTime, id: '2', totalComment: 0, comments: [] }, + { ...basicCaseSnake, id: '3', totalComment: 0, comments: [] }, + { ...basicCaseSnake, id: '4', totalComment: 0, comments: [] }, +]; + +export const allCasesSnake: CasesFindResponse = { + cases: casesSnake, + page: 1, + per_page: 5, + total: 10, + ...casesStatusSnake, +}; + +const basicActionSnake = { + action_at: basicCreatedAt, + action_by: elasticUserSnake, + old_value: null, + new_value: 'what a cool value', + case_id: basicCaseId, + comment_id: null, +}; +export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ + ...basicActionSnake, + action_id: `${af[0]}-${a}`, + action_field: af, + action: a, + comment_id: af[0] === 'comment' ? basicCommentId : null, + new_value: + a === 'push-to-service' && af[0] === 'pushed' + ? JSON.stringify(basicPushSnake) + : basicAction.newValue, +}); + +export const caseUserActionsSnake: CaseUserActionsResponse = [ + getUserActionSnake(['description'], 'create'), + getUserActionSnake(['comment'], 'create'), + getUserActionSnake(['description'], 'update'), +]; + +// user actions + +export const getUserAction = (af: UserActionField, a: UserAction) => ({ + ...basicAction, + actionId: `${af[0]}-${a}`, + actionField: af, + action: a, + commentId: af[0] === 'comment' ? basicCommentId : null, + newValue: + a === 'push-to-service' && af[0] === 'pushed' + ? JSON.stringify(basicPushSnake) + : basicAction.newValue, +}); + +export const getAlertUserAction = () => ({ + ...basicAction, + actionId: 'alert-action-id', + actionField: ['comment'], + action: 'create', + commentId: 'alert-comment-id', + newValue: '{"type":"alert","alertId":"alert-id-1","index":"index-id-1"}', +}); + +export const caseUserActions: CaseUserActions[] = [ + getUserAction(['description'], 'create'), + getUserAction(['comment'], 'create'), + getUserAction(['description'], 'update'), +]; + +// components tests +export const useGetCasesMockState: UseGetCasesState = { + data: allCases, + loading: [], + selectedCases: [], + isError: false, + queryParams: DEFAULT_QUERY_PARAMS, + filterOptions: DEFAULT_FILTER_OPTIONS, +}; + +export const basicCaseClosed: Case = { + ...basicCase, + closedAt: '2020-02-25T23:06:33.798Z', + closedBy: elasticUser, + status: CaseStatuses.closed, +}; diff --git a/x-pack/plugins/cases/public/containers/translations.ts b/x-pack/plugins/cases/public/containers/translations.ts new file mode 100644 index 0000000000000..966a5e158923f --- /dev/null +++ b/x-pack/plugins/cases/public/containers/translations.ts @@ -0,0 +1,90 @@ +/* + * 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'; + +export * from '../common/translations'; + +export const ERROR_TITLE = i18n.translate('xpack.cases.containers.errorTitle', { + defaultMessage: 'Error fetching data', +}); + +export const ERROR_DELETING = i18n.translate('xpack.cases.containers.errorDeletingTitle', { + defaultMessage: 'Error deleting data', +}); + +export const UPDATED_CASE = (caseTitle: string) => + i18n.translate('xpack.cases.containers.updatedCase', { + values: { caseTitle }, + defaultMessage: 'Updated "{caseTitle}"', + }); + +export const DELETED_CASES = (totalCases: number, caseTitle?: string) => + i18n.translate('xpack.cases.containers.deletedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Deleted {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const CLOSED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.cases.containers.closedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const REOPENED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.cases.containers.reopenedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Opened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const MARK_IN_PROGRESS_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.cases.containers.markInProgressCases', { + values: { caseTitle, totalCases }, + defaultMessage: + 'Marked {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}} as in progress', + }); + +export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) => + i18n.translate('xpack.cases.containers.pushToExternalService', { + values: { serviceName }, + defaultMessage: 'Successfully sent to { serviceName }', + }); + +export const ERROR_GET_FIELDS = i18n.translate('xpack.cases.configure.errorGetFields', { + defaultMessage: 'Error getting fields from service', +}); + +export const SYNC_CASE = (caseTitle: string) => + i18n.translate('xpack.cases.containers.syncCase', { + values: { caseTitle }, + defaultMessage: 'Alerts in "{caseTitle}" have been synced', + }); + +export const STATUS_CHANGED_TOASTER_TEXT = i18n.translate( + 'xpack.cases.containers.statusChangeToasterText', + { + defaultMessage: 'Alerts in this case have been also had their status updated', + } +); diff --git a/x-pack/plugins/cases/public/containers/types.ts b/x-pack/plugins/cases/public/containers/types.ts new file mode 100644 index 0000000000000..db6c6e678d188 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/types.ts @@ -0,0 +1,175 @@ +/* + * 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 { + User, + UserActionField, + UserAction, + CaseConnector, + CommentRequest, + CaseStatuses, + CaseAttributes, + CasePatchRequest, + CaseType, + AssociationType, +} from '../../common'; +import { CaseStatusWithAllStatus } from '../components/status'; + +export { CaseConnector, ActionConnector, CaseStatuses } from '../../common'; + +export type Comment = CommentRequest & { + associationType: AssociationType; + id: string; + createdAt: string; + createdBy: ElasticUser; + pushedAt: string | null; + pushedBy: string | null; + updatedAt: string | null; + updatedBy: ElasticUser | null; + version: string; +}; +export interface CaseUserActions { + actionId: string; + actionField: UserActionField; + action: UserAction; + actionAt: string; + actionBy: ElasticUser; + caseId: string; + commentId: string | null; + newValue: string | null; + oldValue: string | null; +} + +export interface CaseExternalService { + pushedAt: string; + pushedBy: ElasticUser; + connectorId: string; + connectorName: string; + externalId: string; + externalTitle: string; + externalUrl: string; +} + +interface BasicCase { + id: string; + closedAt: string | null; + closedBy: ElasticUser | null; + comments: Comment[]; + createdAt: string; + createdBy: ElasticUser; + status: CaseStatuses; + title: string; + totalAlerts: number; + totalComment: number; + updatedAt: string | null; + updatedBy: ElasticUser | null; + version: string; +} + +export interface SubCase extends BasicCase { + associationType: AssociationType; + caseParentId: string; +} + +export interface Case extends BasicCase { + connector: CaseConnector; + description: string; + externalService: CaseExternalService | null; + subCases?: SubCase[] | null; + subCaseIds: string[]; + settings: CaseAttributes['settings']; + tags: string[]; + type: CaseType; +} + +export interface QueryParams { + page: number; + perPage: number; + sortField: SortFieldCase; + sortOrder: 'asc' | 'desc'; +} + +export interface FilterOptions { + search: string; + status: CaseStatusWithAllStatus; + tags: string[]; + reporters: User[]; + onlyCollectionType?: boolean; +} + +export interface CasesStatus { + countClosedCases: number | null; + countOpenCases: number | null; + countInProgressCases: number | null; +} + +export interface AllCases extends CasesStatus { + cases: Case[]; + page: number; + perPage: number; + total: number; +} + +export enum SortFieldCase { + createdAt = 'createdAt', + closedAt = 'closedAt', + updatedAt = 'updatedAt', +} + +export interface ElasticUser { + readonly email?: string | null; + readonly fullName?: string | null; + readonly username?: string | null; +} + +export interface FetchCasesProps extends ApiProps { + queryParams?: QueryParams; + filterOptions?: FilterOptions; +} + +export interface ApiProps { + signal: AbortSignal; +} + +export interface BulkUpdateStatus { + status: string; + id: string; + version: string; +} +export interface ActionLicense { + id: string; + name: string; + enabled: boolean; + enabledInConfig: boolean; + enabledInLicense: boolean; +} + +export interface DeleteCase { + id: string; + type: CaseType | null; + title?: string; +} + +export interface FieldMappings { + id: string; + title?: string; +} + +export type UpdateKey = keyof Pick< + CasePatchRequest, + 'connector' | 'description' | 'status' | 'tags' | 'title' | 'settings' +>; + +export interface UpdateByKey { + updateKey: UpdateKey; + updateValue: CasePatchRequest[UpdateKey]; + fetchCaseUserActions?: (caseId: string, caseConnectorId: string, subCaseId?: string) => void; + updateCase?: (newCase: Case) => void; + caseData: Case; + onSuccess?: () => void; + onError?: () => void; +} diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx new file mode 100644 index 0000000000000..8b5993255552a --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx @@ -0,0 +1,204 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { CaseStatuses } from '../../common'; +import { + DEFAULT_FILTER_OPTIONS, + DEFAULT_QUERY_PARAMS, + initialData, + useGetCases, + UseGetCases, +} from './use_get_cases'; +import { UpdateKey } from './types'; +import { allCases, basicCase } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetCases', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: initialData, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: false, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + + it('calls getCases with correct arguments', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + await act(async () => { + const { waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetCases).toBeCalledWith({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + }); + + it('fetch cases', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + data: allCases, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: false, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + it('dispatch update case property', async () => { + const spyOnPatchCase = jest.spyOn(api, 'patchCase'); + await act(async () => { + const updateCase = { + updateKey: 'description' as UpdateKey, + updateValue: 'description update', + caseId: basicCase.id, + refetchCasesStatus: jest.fn(), + version: '99999', + }; + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.dispatchUpdateCaseProperty(updateCase); + expect(result.current.loading).toEqual(['caseUpdate']); + expect(spyOnPatchCase).toBeCalledWith( + basicCase.id, + { [updateCase.updateKey]: updateCase.updateValue }, + updateCase.version, + abortCtrl.signal + ); + }); + }); + + it('refetch cases', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCases(); + expect(spyOnGetCases).toHaveBeenCalledTimes(2); + }); + }); + + it('set isLoading to true when refetching case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCases(); + + expect(result.current.loading).toEqual(['cases']); + }); + }); + + it('unhappy path', async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + spyOnGetCases.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + data: initialData, + dispatchUpdateCaseProperty: result.current.dispatchUpdateCaseProperty, + filterOptions: DEFAULT_FILTER_OPTIONS, + isError: true, + loading: [], + queryParams: DEFAULT_QUERY_PARAMS, + refetchCases: result.current.refetchCases, + selectedCases: [], + setFilters: result.current.setFilters, + setQueryParams: result.current.setQueryParams, + setSelectedCases: result.current.setSelectedCases, + }); + }); + }); + it('set filters', async () => { + await act(async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + const newFilters = { + search: 'new', + tags: ['new'], + status: CaseStatuses.closed, + }; + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setFilters(newFilters); + await waitForNextUpdate(); + expect(spyOnGetCases.mock.calls[1][0]).toEqual({ + filterOptions: { ...DEFAULT_FILTER_OPTIONS, ...newFilters }, + queryParams: DEFAULT_QUERY_PARAMS, + signal: abortCtrl.signal, + }); + }); + }); + it('set query params', async () => { + await act(async () => { + const spyOnGetCases = jest.spyOn(api, 'getCases'); + const newQueryParams = { + page: 2, + }; + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setQueryParams(newQueryParams); + await waitForNextUpdate(); + expect(spyOnGetCases.mock.calls[1][0]).toEqual({ + filterOptions: DEFAULT_FILTER_OPTIONS, + queryParams: { ...DEFAULT_QUERY_PARAMS, ...newQueryParams }, + signal: abortCtrl.signal, + }); + }); + }); + it('set selected cases', async () => { + await act(async () => { + const selectedCases = [basicCase]; + const { result, waitForNextUpdate } = renderHook<string, UseGetCases>(() => useGetCases()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.setSelectedCases(selectedCases); + expect(result.current.selectedCases).toEqual(selectedCases); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx new file mode 100644 index 0000000000000..e06a47954cdd4 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx @@ -0,0 +1,255 @@ +/* + * 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 { useCallback, useEffect, useReducer, useRef } from 'react'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; +import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case, UpdateByKey } from './types'; +import { errorToToaster, useStateToaster } from '../components/toasters'; +import * as i18n from './translations'; +import { getCases, patchCase } from './api'; +import { StatusAll } from '../components/status'; + +export interface UseGetCasesState { + data: AllCases; + filterOptions: FilterOptions; + isError: boolean; + loading: string[]; + queryParams: QueryParams; + selectedCases: Case[]; +} + +export interface UpdateCase extends Omit<UpdateByKey, 'caseData'> { + caseId: string; + version: string; + refetchCasesStatus: () => void; +} + +export type Action = + | { type: 'FETCH_INIT'; payload: string } + | { + type: 'FETCH_CASES_SUCCESS'; + payload: AllCases; + } + | { type: 'FETCH_FAILURE'; payload: string } + | { type: 'FETCH_UPDATE_CASE_SUCCESS' } + | { type: 'UPDATE_FILTER_OPTIONS'; payload: Partial<FilterOptions> } + | { type: 'UPDATE_QUERY_PARAMS'; payload: Partial<QueryParams> } + | { type: 'UPDATE_TABLE_SELECTIONS'; payload: Case[] }; + +const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { + switch (action.type) { + case 'FETCH_INIT': + return { + ...state, + isError: false, + loading: [...state.loading.filter((e) => e !== action.payload), action.payload], + }; + case 'FETCH_UPDATE_CASE_SUCCESS': + return { + ...state, + loading: state.loading.filter((e) => e !== 'caseUpdate'), + }; + case 'FETCH_CASES_SUCCESS': + return { + ...state, + data: action.payload, + isError: false, + loading: state.loading.filter((e) => e !== 'cases'), + }; + case 'FETCH_FAILURE': + return { + ...state, + isError: true, + loading: state.loading.filter((e) => e !== action.payload), + }; + case 'UPDATE_FILTER_OPTIONS': + return { + ...state, + filterOptions: { + ...state.filterOptions, + ...action.payload, + }, + }; + case 'UPDATE_QUERY_PARAMS': + return { + ...state, + queryParams: { + ...state.queryParams, + ...action.payload, + }, + }; + case 'UPDATE_TABLE_SELECTIONS': + return { + ...state, + selectedCases: action.payload, + }; + default: + return state; + } +}; + +export const DEFAULT_FILTER_OPTIONS: FilterOptions = { + search: '', + reporters: [], + status: StatusAll, + tags: [], + onlyCollectionType: false, +}; + +export const DEFAULT_QUERY_PARAMS: QueryParams = { + page: DEFAULT_TABLE_ACTIVE_PAGE, + perPage: DEFAULT_TABLE_LIMIT, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', +}; + +export const initialData: AllCases = { + cases: [], + countClosedCases: null, + countInProgressCases: null, + countOpenCases: null, + page: 0, + perPage: 0, + total: 0, +}; +export interface UseGetCases extends UseGetCasesState { + dispatchUpdateCaseProperty: ({ + updateKey, + updateValue, + caseId, + version, + refetchCasesStatus, + }: UpdateCase) => void; + refetchCases: () => void; + setFilters: (filters: Partial<FilterOptions>) => void; + setQueryParams: (queryParams: Partial<QueryParams>) => void; + setSelectedCases: (mySelectedCases: Case[]) => void; +} + +export const useGetCases = ( + initialQueryParams?: QueryParams, + initialFilterOptions?: FilterOptions +): UseGetCases => { + const [state, dispatch] = useReducer(dataFetchReducer, { + data: initialData, + filterOptions: initialFilterOptions ?? DEFAULT_FILTER_OPTIONS, + isError: false, + loading: [], + queryParams: initialQueryParams ?? DEFAULT_QUERY_PARAMS, + selectedCases: [], + }); + const [, dispatchToaster] = useStateToaster(); + const didCancelFetchCases = useRef(false); + const didCancelUpdateCases = useRef(false); + const abortCtrlFetchCases = useRef(new AbortController()); + const abortCtrlUpdateCases = useRef(new AbortController()); + + const setSelectedCases = useCallback((mySelectedCases: Case[]) => { + dispatch({ type: 'UPDATE_TABLE_SELECTIONS', payload: mySelectedCases }); + }, []); + + const setQueryParams = useCallback((newQueryParams: Partial<QueryParams>) => { + dispatch({ type: 'UPDATE_QUERY_PARAMS', payload: newQueryParams }); + }, []); + + const setFilters = useCallback((newFilters: Partial<FilterOptions>) => { + dispatch({ type: 'UPDATE_FILTER_OPTIONS', payload: newFilters }); + }, []); + + const fetchCases = useCallback(async (filterOptions: FilterOptions, queryParams: QueryParams) => { + try { + didCancelFetchCases.current = false; + abortCtrlFetchCases.current.abort(); + abortCtrlFetchCases.current = new AbortController(); + dispatch({ type: 'FETCH_INIT', payload: 'cases' }); + + const response = await getCases({ + filterOptions, + queryParams, + signal: abortCtrlFetchCases.current.signal, + }); + + if (!didCancelFetchCases.current) { + dispatch({ + type: 'FETCH_CASES_SUCCESS', + payload: response, + }); + } + } catch (error) { + if (!didCancelFetchCases.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + dispatch({ type: 'FETCH_FAILURE', payload: 'cases' }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const dispatchUpdateCaseProperty = useCallback( + async ({ updateKey, updateValue, caseId, refetchCasesStatus, version }: UpdateCase) => { + try { + didCancelUpdateCases.current = false; + abortCtrlUpdateCases.current.abort(); + abortCtrlUpdateCases.current = new AbortController(); + dispatch({ type: 'FETCH_INIT', payload: 'caseUpdate' }); + + await patchCase( + caseId, + { [updateKey]: updateValue }, + // saved object versions are typed as string | undefined, hope that's not true + version ?? '', + abortCtrlUpdateCases.current.signal + ); + + if (!didCancelUpdateCases.current) { + dispatch({ type: 'FETCH_UPDATE_CASE_SUCCESS' }); + fetchCases(state.filterOptions, state.queryParams); + refetchCasesStatus(); + } + } catch (error) { + if (!didCancelUpdateCases.current) { + if (error.name !== 'AbortError') { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + } + dispatch({ type: 'FETCH_FAILURE', payload: 'caseUpdate' }); + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [state.filterOptions, state.queryParams] + ); + + const refetchCases = useCallback(() => { + fetchCases(state.filterOptions, state.queryParams); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.filterOptions, state.queryParams]); + + useEffect(() => { + fetchCases(state.filterOptions, state.queryParams); + return () => { + didCancelFetchCases.current = true; + didCancelUpdateCases.current = true; + abortCtrlFetchCases.current.abort(); + abortCtrlUpdateCases.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.queryParams, state.filterOptions]); + + return { + ...state, + dispatchUpdateCaseProperty, + refetchCases, + setFilters, + setQueryParams, + setSelectedCases, + }; +}; diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx new file mode 100644 index 0000000000000..8042e560df350 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { useGetTags, UseGetTags } from './use_get_tags'; +import { tags } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +describe('useGetTags', () => { + const abortCtrl = new AbortController(); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + tags: [], + isLoading: true, + isError: false, + fetchTags: result.current.fetchTags, + }); + }); + }); + + it('calls getTags api', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + await act(async () => { + const { waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(spyOnGetTags).toBeCalledWith(abortCtrl.signal); + }); + }); + + it('fetch tags', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + tags, + isLoading: false, + isError: false, + fetchTags: result.current.fetchTags, + }); + }); + }); + + it('refetch tags', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.fetchTags(); + expect(spyOnGetTags).toHaveBeenCalledTimes(2); + }); + }); + + it('unhappy path', async () => { + const spyOnGetTags = jest.spyOn(api, 'getTags'); + spyOnGetTags.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UseGetTags>(() => useGetTags()); + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + tags: [], + isLoading: false, + isError: true, + fetchTags: result.current.fetchTags, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.tsx new file mode 100644 index 0000000000000..33b863fba5da3 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_get_tags.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useReducer, useRef, useCallback } from 'react'; +import { errorToToaster, useStateToaster } from '../components/toasters'; +import { getTags } from './api'; +import * as i18n from './translations'; + +export interface TagsState { + tags: string[]; + isLoading: boolean; + isError: boolean; +} +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: string[] } + | { type: 'FETCH_FAILURE' }; + +export interface UseGetTags extends TagsState { + fetchTags: () => void; +} + +const dataFetchReducer = (state: TagsState, action: Action): TagsState => { + switch (action.type) { + case 'FETCH_INIT': + return { + ...state, + isLoading: true, + isError: false, + }; + case 'FETCH_SUCCESS': + return { + ...state, + isLoading: false, + isError: false, + tags: action.payload, + }; + case 'FETCH_FAILURE': + return { + ...state, + isLoading: false, + isError: true, + }; + default: + return state; + } +}; +const initialData: string[] = []; + +export const useGetTags = (): UseGetTags => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: true, + isError: false, + tags: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const callFetch = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + dispatch({ type: 'FETCH_INIT' }); + + const response = await getTags(abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS', payload: response }); + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + dispatch({ type: 'FETCH_FAILURE' }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + callFetch(); + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return { ...state, fetchTags: callFetch }; +}; diff --git a/x-pack/plugins/cases/public/containers/use_post_case.test.tsx b/x-pack/plugins/cases/public/containers/use_post_case.test.tsx new file mode 100644 index 0000000000000..72ea368f10317 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_post_case.test.tsx @@ -0,0 +1,114 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { usePostCase, UsePostCase } from './use_post_case'; +import * as api from './api'; +import { ConnectorTypes } from '../../common'; +import { basicCasePost } from './mock'; + +jest.mock('./api'); + +describe('usePostCase', () => { + const abortCtrl = new AbortController(); + const samplePost = { + description: 'description', + tags: ['tags'], + title: 'title', + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + settings: { + syncAlerts: true, + }, + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + postCase: result.current.postCase, + }); + }); + }); + + it('calls postCase with correct arguments', async () => { + const spyOnPostCase = jest.spyOn(api, 'postCase'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + + result.current.postCase(samplePost); + await waitForNextUpdate(); + expect(spyOnPostCase).toBeCalledWith(samplePost, abortCtrl.signal); + }); + }); + + it('calls postCase with correct result', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + + const postData = await result.current.postCase(samplePost); + expect(postData).toEqual(basicCasePost); + }); + }); + + it('post case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + postCase: result.current.postCase, + }); + }); + }); + + it('set isLoading to true when posting case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPostCase = jest.spyOn(api, 'postCase'); + spyOnPostCase.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostCase>(() => usePostCase()); + await waitForNextUpdate(); + result.current.postCase(samplePost); + + expect(result.current).toEqual({ + isLoading: false, + isError: true, + postCase: result.current.postCase, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/use_post_case.tsx b/x-pack/plugins/cases/public/containers/use_post_case.tsx new file mode 100644 index 0000000000000..503ac8bf0209d --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_post_case.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useReducer, useCallback, useRef, useEffect } from 'react'; +import { CasePostRequest } from '../../common'; +import { errorToToaster, useStateToaster } from '../components/toasters'; +import { postCase } from './api'; +import * as i18n from './translations'; +import { Case } from './types'; +interface NewCaseState { + isLoading: boolean; + isError: boolean; +} +type Action = { type: 'FETCH_INIT' } | { type: 'FETCH_SUCCESS' } | { type: 'FETCH_FAILURE' }; + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + switch (action.type) { + case 'FETCH_INIT': + return { + ...state, + isLoading: true, + isError: false, + }; + case 'FETCH_SUCCESS': + return { + ...state, + isLoading: false, + isError: false, + }; + case 'FETCH_FAILURE': + return { + ...state, + isLoading: false, + isError: true, + }; + default: + return state; + } +}; +export interface UsePostCase extends NewCaseState { + postCase: (data: CasePostRequest) => Promise<Case | undefined>; +} +export const usePostCase = (): UsePostCase => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + }); + const [, dispatchToaster] = useStateToaster(); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const postMyCase = useCallback(async (data: CasePostRequest) => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + dispatch({ type: 'FETCH_INIT' }); + const response = await postCase(data, abortCtrlRef.current.signal); + + if (!isCancelledRef.current) { + dispatch({ type: 'FETCH_SUCCESS' }); + } + return response; + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + dispatch({ type: 'FETCH_FAILURE' }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + return () => { + isCancelledRef.current = true; + abortCtrlRef.current.abort(); + }; + }, []); + return { ...state, postCase: postMyCase }; +}; diff --git a/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx b/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx new file mode 100644 index 0000000000000..3d43180d60aff --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { usePostPushToService, UsePostPushToService } from './use_post_push_to_service'; +import { pushedCase } from './mock'; +import * as api from './api'; +import { CaseConnector, ConnectorTypes } from '../../common'; + +jest.mock('./api'); + +describe('usePostPushToService', () => { + const abortCtrl = new AbortController(); + const connector = { + id: '123', + name: 'connector name', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'Low', parent: null }, + } as CaseConnector; + const caseId = pushedCase.id; + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + pushCaseToExternalService: result.current.pushCaseToExternalService, + }); + }); + }); + + it('calls pushCase with correct arguments', async () => { + const spyOnPushToService = jest.spyOn(api, 'pushCase'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.pushCaseToExternalService({ caseId, connector }); + await waitForNextUpdate(); + expect(spyOnPushToService).toBeCalledWith(caseId, connector.id, abortCtrl.signal); + }); + }); + + it('post push to service', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.pushCaseToExternalService({ caseId, connector }); + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + isError: false, + pushCaseToExternalService: result.current.pushCaseToExternalService, + }); + }); + }); + + it('set isLoading to true when pushing case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.pushCaseToExternalService({ caseId, connector }); + expect(result.current.isLoading).toBe(true); + }); + }); + + it('unhappy path', async () => { + const spyOnPushToService = jest.spyOn(api, 'pushCase'); + spyOnPushToService.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, UsePostPushToService>(() => + usePostPushToService() + ); + await waitForNextUpdate(); + result.current.pushCaseToExternalService({ caseId, connector }); + + expect(result.current).toEqual({ + isLoading: false, + isError: true, + pushCaseToExternalService: result.current.pushCaseToExternalService, + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/use_post_push_to_service.tsx b/x-pack/plugins/cases/public/containers/use_post_push_to_service.tsx new file mode 100644 index 0000000000000..636edd33b5e92 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/use_post_push_to_service.tsx @@ -0,0 +1,112 @@ +/* + * 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 { useReducer, useCallback, useRef, useEffect } from 'react'; +import { CaseConnector } from '../../common'; +import { errorToToaster, useStateToaster, displaySuccessToast } from '../components/toasters'; + +import { pushCase } from './api'; +import * as i18n from './translations'; +import { Case } from './types'; + +interface PushToServiceState { + isLoading: boolean; + isError: boolean; +} +type Action = { type: 'FETCH_INIT' } | { type: 'FETCH_SUCCESS' } | { type: 'FETCH_FAILURE' }; + +const dataFetchReducer = (state: PushToServiceState, action: Action): PushToServiceState => { + switch (action.type) { + case 'FETCH_INIT': + return { + ...state, + isLoading: true, + isError: false, + }; + case 'FETCH_SUCCESS': + return { + ...state, + isLoading: false, + isError: false, + }; + case 'FETCH_FAILURE': + return { + ...state, + isLoading: false, + isError: true, + }; + default: + return state; + } +}; + +interface PushToServiceRequest { + caseId: string; + connector: CaseConnector; +} + +export interface UsePostPushToService extends PushToServiceState { + pushCaseToExternalService: ({ + caseId, + connector, + }: PushToServiceRequest) => Promise<Case | undefined>; +} + +export const usePostPushToService = (): UsePostPushToService => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + }); + const [, dispatchToaster] = useStateToaster(); + const cancel = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const pushCaseToExternalService = useCallback( + async ({ caseId, connector }: PushToServiceRequest) => { + try { + abortCtrlRef.current.abort(); + cancel.current = false; + abortCtrlRef.current = new AbortController(); + dispatch({ type: 'FETCH_INIT' }); + + const response = await pushCase(caseId, connector.id, abortCtrlRef.current.signal); + + if (!cancel.current) { + dispatch({ type: 'FETCH_SUCCESS' }); + displaySuccessToast( + i18n.SUCCESS_SEND_TO_EXTERNAL_SERVICE(connector.name), + dispatchToaster + ); + } + + return response; + } catch (error) { + if (!cancel.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + dispatch({ type: 'FETCH_FAILURE' }); + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + useEffect(() => { + return () => { + abortCtrlRef.current.abort(); + cancel.current = true; + }; + }, []); + + return { ...state, pushCaseToExternalService }; +}; diff --git a/x-pack/plugins/cases/public/containers/utils.test.ts b/x-pack/plugins/cases/public/containers/utils.test.ts new file mode 100644 index 0000000000000..6c1fb60298938 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/utils.test.ts @@ -0,0 +1,170 @@ +/* + * 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 { + valueToUpdateIsSettings, + valueToUpdateIsStatus, + createUpdateSuccessToaster, +} from './utils'; + +import { Case } from './types'; + +const caseBeforeUpdate = { + comments: [ + { + type: 'alert', + }, + ], + settings: { + syncAlerts: true, + }, +} as Case; + +const caseAfterUpdate = { title: 'My case' } as Case; + +describe('utils', () => { + describe('valueToUpdateIsSettings', () => { + it('returns true if key is settings', () => { + expect(valueToUpdateIsSettings('settings', 'value')).toBe(true); + }); + + it('returns false if key is NOT settings', () => { + expect(valueToUpdateIsSettings('tags', 'value')).toBe(false); + }); + }); + + describe('valueToUpdateIsStatus', () => { + it('returns true if key is status', () => { + expect(valueToUpdateIsStatus('status', 'value')).toBe(true); + }); + + it('returns false if key is NOT status', () => { + expect(valueToUpdateIsStatus('tags', 'value')).toBe(false); + }); + }); + + describe('createUpdateSuccessToaster', () => { + it('creates the correct toast when sync alerts is turned on and case has alerts', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + caseBeforeUpdate, + caseAfterUpdate, + 'settings', + { + syncAlerts: true, + } + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Alerts in "My case" have been synced', + }); + }); + + it('creates the correct toast when sync alerts is turned on and case does NOT have alerts', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + { ...caseBeforeUpdate, comments: [] }, + caseAfterUpdate, + 'settings', + { + syncAlerts: true, + } + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + + it('creates the correct toast when sync alerts is turned off and case has alerts', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + caseBeforeUpdate, + caseAfterUpdate, + 'settings', + { + syncAlerts: false, + } + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + + it('creates the correct toast when the status change, case has alerts, and sync alerts is on', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + caseBeforeUpdate, + caseAfterUpdate, + 'status', + 'closed' + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + text: 'Alerts in this case have been also had their status updated', + }); + }); + + it('creates the correct toast when the status change, case has alerts, and sync alerts is off', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + { ...caseBeforeUpdate, settings: { syncAlerts: false } }, + caseAfterUpdate, + 'status', + 'closed' + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + + it('creates the correct toast when the status change, case does NOT have alerts, and sync alerts is on', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + { ...caseBeforeUpdate, comments: [] }, + caseAfterUpdate, + 'status', + 'closed' + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + + it('creates the correct toast if not a status or a setting', () => { + // We remove the id as is randomly generated + const { id, ...toast } = createUpdateSuccessToaster( + caseBeforeUpdate, + caseAfterUpdate, + 'title', + 'My new title' + ); + + expect(toast).toEqual({ + color: 'success', + iconType: 'check', + title: 'Updated "My case"', + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts new file mode 100644 index 0000000000000..a7eeaff1c2637 --- /dev/null +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -0,0 +1,150 @@ +/* + * 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 uuid from 'uuid'; +import { set } from '@elastic/safer-lodash-set'; +import { camelCase, isArray, isObject } from 'lodash'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { + CasesFindResponse, + CasesFindResponseRt, + CaseResponse, + CaseResponseRt, + CasesResponse, + CasesResponseRt, + CasesStatusResponseRt, + CasesStatusResponse, + throwErrors, + CasesConfigureResponse, + CaseConfigureResponseRt, + CaseUserActionsResponse, + CaseUserActionsResponseRt, + CommentType, + CasePatchRequest, +} from '../../common'; +import { AppToast, ToasterError } from '../components/toasters'; +import { AllCases, Case, UpdateByKey } from './types'; +import * as i18n from './translations'; + +export const getTypedPayload = <T>(a: unknown): T => a as T; + +export const parseString = (params: string) => { + try { + return JSON.parse(params); + } catch { + return null; + } +}; + +export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => + arrayOfSnakes.reduce((acc: unknown[], value) => { + if (isArray(value)) { + return [...acc, convertArrayToCamelCase(value)]; + } else if (isObject(value)) { + return [...acc, convertToCamelCase(value)]; + } else { + return [...acc, value]; + } + }, []); + +export const convertToCamelCase = <T, U extends {}>(snakeCase: T): U => + Object.entries(snakeCase).reduce((acc, [key, value]) => { + if (isArray(value)) { + set(acc, camelCase(key), convertArrayToCamelCase(value)); + } else if (isObject(value)) { + set(acc, camelCase(key), convertToCamelCase(value)); + } else { + set(acc, camelCase(key), value); + } + return acc; + }, {} as U); + +export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): AllCases => ({ + cases: snakeCases.cases.map((snakeCase) => convertToCamelCase<CaseResponse, Case>(snakeCase)), + countOpenCases: snakeCases.count_open_cases, + countInProgressCases: snakeCases.count_in_progress_cases, + countClosedCases: snakeCases.count_closed_cases, + page: snakeCases.page, + perPage: snakeCases.per_page, + total: snakeCases.total, +}); + +export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => + pipe( + CasesStatusResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const createToasterPlainError = (message: string) => new ToasterError([message]); + +export const decodeCaseResponse = (respCase?: CaseResponse) => + pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesResponse = (respCase?: CasesResponse) => + pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => + pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => + pipe( + CaseConfigureResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsResponse) => + pipe( + CaseUserActionsResponseRt.decode(respUserActions), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const valueToUpdateIsSettings = ( + key: UpdateByKey['updateKey'], + value: UpdateByKey['updateValue'] +): value is CasePatchRequest['settings'] => key === 'settings'; + +export const valueToUpdateIsStatus = ( + key: UpdateByKey['updateKey'], + value: UpdateByKey['updateValue'] +): value is CasePatchRequest['status'] => key === 'status'; + +export const createUpdateSuccessToaster = ( + caseBeforeUpdate: Case, + caseAfterUpdate: Case, + key: UpdateByKey['updateKey'], + value: UpdateByKey['updateValue'] +): AppToast => { + const caseHasAlerts = caseBeforeUpdate.comments.some( + (comment) => comment.type === CommentType.alert + ); + + const toast: AppToast = { + id: uuid.v4(), + color: 'success', + iconType: 'check', + title: i18n.UPDATED_CASE(caseAfterUpdate.title), + }; + + if (valueToUpdateIsSettings(key, value) && value?.syncAlerts && caseHasAlerts) { + return { + ...toast, + title: i18n.SYNC_CASE(caseAfterUpdate.title), + }; + } + + if (valueToUpdateIsStatus(key, value) && caseHasAlerts && caseBeforeUpdate.settings.syncAlerts) { + return { + ...toast, + text: i18n.STATUS_CHANGED_TOASTER_TEXT, + }; + } + + return toast; +}; diff --git a/x-pack/plugins/cases/public/get_create_case.tsx b/x-pack/plugins/cases/public/get_create_case.tsx new file mode 100644 index 0000000000000..ec13d9ae9e305 --- /dev/null +++ b/x-pack/plugins/cases/public/get_create_case.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { CreateCaseProps } from './components/create'; + +export const getCreateCaseLazy = (props: CreateCaseProps) => { + const CreateCaseLazy = lazy(() => import('./components/create')); + return ( + <Suspense fallback={<EuiLoadingSpinner />}> + <CreateCaseLazy {...props} /> + </Suspense> + ); +}; diff --git a/x-pack/plugins/cases/public/index.tsx b/x-pack/plugins/cases/public/index.tsx new file mode 100644 index 0000000000000..1cf2d2e8d7067 --- /dev/null +++ b/x-pack/plugins/cases/public/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import React from 'react'; +import { CasesUiPlugin } from './plugin'; + +export const TestComponent = () => <div>{'Hello from cases plugin!'}</div>; + +export function plugin(initializerContext: PluginInitializerContext) { + return new CasesUiPlugin(initializerContext); +} + +export { CasesUiPlugin }; +export * from './plugin'; +export * from './types'; diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts new file mode 100644 index 0000000000000..c594e8677a086 --- /dev/null +++ b/x-pack/plugins/cases/public/plugin.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { TestComponent } from '.'; +import { CasesUiStart, SetupPlugins, StartPlugins } from './types'; +import { getCreateCaseLazy } from './get_create_case'; +import { KibanaServices } from './common/lib/kibana'; + +export class CasesUiPlugin implements Plugin<void, CasesUiStart, SetupPlugins, StartPlugins> { + private kibanaVersion: string; + + constructor(initializerContext: PluginInitializerContext) { + this.kibanaVersion = initializerContext.env.packageInfo.version; + } + public setup() {} + + public start(core: CoreStart, plugins: StartPlugins): CasesUiStart { + KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion }); + return { + casesComponent: TestComponent, + getCreateCase: (props) => { + return getCreateCaseLazy(props); + }, + }; + } + + public stop() {} +} diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts new file mode 100644 index 0000000000000..07a0b2c723914 --- /dev/null +++ b/x-pack/plugins/cases/public/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from 'kibana/public'; +import { ReactElement } from 'react'; +import { + TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, + TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, +} from '../../triggers_actions_ui/public'; +import { CreateCaseProps } from './components/create'; + +export interface SetupPlugins { + triggersActionsUi: TriggersActionsSetup; +} + +export interface StartPlugins { + triggersActionsUi: TriggersActionsStart; +} + +export type StartServices = CoreStart & StartPlugins; + +export interface CasesUiStart { + casesComponent: () => JSX.Element; + getCreateCase: (props: CreateCaseProps) => ReactElement<CreateCaseProps>; +} diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.test.ts b/x-pack/plugins/cases/server/client/alerts/update_status.test.ts index 5dfe6060da1db..d6456cb3183ef 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.test.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseStatuses } from '../../../common/api'; +import { CaseStatuses } from '../../../common'; import { createMockSavedObjectsRepository } from '../../routes/api/__fixtures__'; import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index fe301dcca37ac..9cbe2a448d3b4 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - ConnectorTypes, - CaseStatuses, - CaseType, - CasesClientPostRequest, -} from '../../../common/api'; +import { ConnectorTypes, CaseStatuses, CaseType, CasesClientPostRequest } from '../../../common'; import { isCaseError } from '../../common/error'; import { diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 59f9688836341..1dbb2dc496a99 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -22,7 +22,7 @@ import { CasePostRequest, CaseType, User, -} from '../../../common/api'; +} from '../../../common'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { getConnectorFromConfiguration, diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index fa556986ee8d3..e230e665da865 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { flattenCaseSavedObject } from '../../routes/api/utils'; -import { CaseResponseRt, CaseResponse } from '../../../common/api'; +import { CaseResponseRt, CaseResponse } from '../../../common'; import { CaseServiceSetup } from '../../services'; import { countAlertsForID } from '../../common'; import { createCaseError } from '../../common/error'; diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 490519187f49e..0e589b901c8d1 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -12,7 +12,7 @@ import { CaseUserActionsResponse, AssociationType, CommentResponseAlertsType, -} from '../../../common/api'; +} from '../../../common'; import { BasicParams } from './types'; diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 3217178768f89..eeaf91b13fa89 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -29,7 +29,7 @@ import { User, ESCasesConfigureAttributes, CaseType, -} from '../../../common/api'; +} from '../../../common'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createIncident, getCommentContextFromAttributes } from './utils'; diff --git a/x-pack/plugins/cases/server/client/cases/types.ts b/x-pack/plugins/cases/server/client/cases/types.ts index f1d56e7132bd1..fb400675136ef 100644 --- a/x-pack/plugins/cases/server/client/cases/types.ts +++ b/x-pack/plugins/cases/server/client/cases/types.ts @@ -19,7 +19,7 @@ import { PushToServiceApiParamsSIR as ServiceNowSIRPushToServiceApiParams, ServiceNowITSMIncident, } from '../../../../actions/server/builtin_action_types/servicenow/types'; -import { CaseResponse, ConnectorMappingsAttributes } from '../../../common/api'; +import { CaseResponse, ConnectorMappingsAttributes } from '../../../common'; export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident; export type PushToServiceApiParams = diff --git a/x-pack/plugins/cases/server/client/cases/update.test.ts b/x-pack/plugins/cases/server/client/cases/update.test.ts index 79c3b2838c3b2..18b4e8d9d7b66 100644 --- a/x-pack/plugins/cases/server/client/cases/update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/update.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypes, CasesPatchRequest, CaseStatuses } from '../../../common/api'; +import { ConnectorTypes, CasesPatchRequest, CaseStatuses } from '../../../common'; import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index ff3c0a62407a1..6a59bf60a4ece 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -38,7 +38,7 @@ import { AssociationType, CommentAttributes, User, -} from '../../../common/api'; +} from '../../../common'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; import { getCaseToUpdate, diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 859114a5e8fb0..c24812048376e 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -539,7 +539,7 @@ describe('utils', () => { commentId: 'comment-user-1', }, { - comment: 'Elastic Security Alerts attached to the case: 3', + comment: 'Elastic Alerts attached to the case: 3', commentId: 'mock-id-1-total-alerts', }, ]); @@ -569,7 +569,7 @@ describe('utils', () => { commentId: 'comment-user-1', }, { - comment: 'Elastic Security Alerts attached to the case: 4', + comment: 'Elastic Alerts attached to the case: 4', commentId: 'mock-id-1-total-alerts', }, ]); diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index 7e77bf4ac84cc..7749bce8042eb 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -20,7 +20,7 @@ import { CommentAttributes, CommentRequestUserType, CommentRequestAlertType, -} from '../../../common/api'; +} from '../../../common'; import { ActionsClient } from '../../../../actions/server'; import { externalServiceFormatters, FormatterConnectorTypes } from '../../connectors'; import { CasesClientGetAlertsResponse } from '../../client/alerts/types'; @@ -184,7 +184,7 @@ export const createIncident = async ({ if (totalAlerts > 0) { comments.push({ - comment: `Elastic Security Alerts attached to the case: ${totalAlerts}`, + comment: `Elastic Alerts attached to the case: ${totalAlerts}`, commentId: `${theCase.id}-total-alerts`, }); } diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index 8f9058654d6fd..3bd25b6b61bc5 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -31,7 +31,7 @@ import { CaseUserActionServiceSetup, AlertServiceContract, } from '../services'; -import { CasesPatchRequest, CasePostRequest, User } from '../../common/api'; +import { CasesPatchRequest, CasePostRequest, User } from '../../common'; import { get } from './cases/get'; import { get as getUserActions } from './user_actions/get'; import { get as getAlerts } from './alerts/get'; diff --git a/x-pack/plugins/cases/server/client/comments/add.test.ts b/x-pack/plugins/cases/server/client/comments/add.test.ts index 23b7bc37dc814..bd04e0ea6ef14 100644 --- a/x-pack/plugins/cases/server/client/comments/add.test.ts +++ b/x-pack/plugins/cases/server/client/comments/add.test.ts @@ -6,7 +6,7 @@ */ import { omit } from 'lodash/fp'; -import { CommentType } from '../../../common/api'; +import { CommentType } from '../../../common'; import { isCaseError } from '../../common/error'; import { createMockSavedObjectsRepository, diff --git a/x-pack/plugins/cases/server/client/comments/add.ts b/x-pack/plugins/cases/server/client/comments/add.ts index 45746613dc1d4..98b914fb7486b 100644 --- a/x-pack/plugins/cases/server/client/comments/add.ts +++ b/x-pack/plugins/cases/server/client/comments/add.ts @@ -25,7 +25,7 @@ import { User, CommentRequestAlertType, AlertCommentRequestRt, -} from '../../../common/api'; +} from '../../../common'; import { buildCaseUserActionItem, buildCommentUserActionItem, @@ -36,7 +36,7 @@ import { CommentableCase, createAlertUpdateRequest } from '../../common'; import { CasesClientHandler } from '..'; import { createCaseError } from '../../common/error'; import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; -import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common'; async function getSubCase({ caseService, diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.test.ts b/x-pack/plugins/cases/server/client/configure/get_fields.test.ts index 2e2973516d0fd..c474361293da4 100644 --- a/x-pack/plugins/cases/server/client/configure/get_fields.test.ts +++ b/x-pack/plugins/cases/server/client/configure/get_fields.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypes } from '../../../common/api'; +import { ConnectorTypes } from '../../../common'; import { createMockSavedObjectsRepository, mockCaseMappings } from '../../routes/api/__fixtures__'; import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; diff --git a/x-pack/plugins/cases/server/client/configure/get_fields.ts b/x-pack/plugins/cases/server/client/configure/get_fields.ts index deabae33810b2..8d899f0df1a76 100644 --- a/x-pack/plugins/cases/server/client/configure/get_fields.ts +++ b/x-pack/plugins/cases/server/client/configure/get_fields.ts @@ -7,7 +7,7 @@ import Boom from '@hapi/boom'; -import { GetFieldsResponse } from '../../../common/api'; +import { GetFieldsResponse } from '../../../common'; import { ConfigureFields } from '../types'; import { createDefaultMapping, formatFields } from './utils'; diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.test.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.test.ts index 0ec2fc8b4621d..7d9593899bb2e 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.test.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypes } from '../../../common/api'; +import { ConnectorTypes } from '../../../common'; import { createMockSavedObjectsRepository, mockCaseMappings } from '../../routes/api/__fixtures__'; import { createCasesClientWithMockSavedObjectsClient } from '../mocks'; diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.ts index 558c961f89e5b..1f767ea682843 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract, Logger } from 'src/core/server'; import { ActionsClient } from '../../../../actions/server'; -import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common/api'; +import { ConnectorMappingsAttributes, ConnectorTypes } from '../../../common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server/saved_objects'; import { ConnectorMappingsServiceSetup } from '../../services'; diff --git a/x-pack/plugins/cases/server/client/configure/mock.ts b/x-pack/plugins/cases/server/client/configure/mock.ts index ee214de9b51d4..ad982a5cc1243 100644 --- a/x-pack/plugins/cases/server/client/configure/mock.ts +++ b/x-pack/plugins/cases/server/client/configure/mock.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - ConnectorField, - ConnectorMappingsAttributes, - ConnectorTypes, -} from '../../../common/api/connectors'; +import { ConnectorField, ConnectorMappingsAttributes, ConnectorTypes } from '../../../common'; import { JiraGetFieldsResponse, ResilientGetFieldsResponse, diff --git a/x-pack/plugins/cases/server/client/configure/utils.test.ts b/x-pack/plugins/cases/server/client/configure/utils.test.ts index 403854693e36c..bf571388994c0 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.test.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.test.ts @@ -11,7 +11,7 @@ export { ServiceNowGetFieldsResponse, } from '../../../../actions/server/types'; import { createDefaultMapping, formatFields } from './utils'; -import { ConnectorTypes } from '../../../common/api/connectors'; +import { ConnectorTypes } from '../../../common'; import { mappings, formatFieldsTestData } from './mock'; describe('client/configure/utils', () => { diff --git a/x-pack/plugins/cases/server/client/configure/utils.ts b/x-pack/plugins/cases/server/client/configure/utils.ts index 80e6c7a3b886c..b9ef813735e25 100644 --- a/x-pack/plugins/cases/server/client/configure/utils.ts +++ b/x-pack/plugins/cases/server/client/configure/utils.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - ConnectorField, - ConnectorMappingsAttributes, - ConnectorTypes, -} from '../../../common/api/connectors'; +import { ConnectorField, ConnectorMappingsAttributes, ConnectorTypes } from '../../../common'; import { JiraGetFieldsResponse, ResilientGetFieldsResponse, diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index c62b3913da763..3311b7ac6f921 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -18,7 +18,7 @@ import { GetFieldsResponse, CaseUserActionsResponse, User, -} from '../../common/api'; +} from '../../common'; import { AlertInfo } from '../common'; import { CaseConfigureServiceSetup, diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index f6371b8e8b1e7..79b8ef25ab0f6 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -11,7 +11,7 @@ import { CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT, } from '../../saved_object_types'; -import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; +import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common'; import { CaseUserActionServiceSetup } from '../../services'; interface GetParams { diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 1ff5b7beadcaf..3daccf87bdc19 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -27,7 +27,7 @@ import { ESCaseAttributes, SubCaseAttributes, User, -} from '../../../common/api'; +} from '../../../common'; import { transformESConnectorToCaseConnector } from '../../routes/api/cases/helpers'; import { flattenCommentSavedObjects, diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 5e6a86358de25..df16fe4f0a67d 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; -import { AssociationType, CommentAttributes, CommentRequest, CommentType } from '../../common/api'; +import { AssociationType, CommentAttributes, CommentRequest, CommentType } from '../../common'; import { transformNewComment } from '../routes/api/utils'; import { combineFilters, countAlerts, countAlertsForID, groupTotalAlertsByID } from './utils'; diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index dce26f3d5998a..d3bc3850e4210 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -6,13 +6,7 @@ */ import { SavedObjectsFindResult, SavedObjectsFindResponse } from 'kibana/server'; -import { - CaseStatuses, - CommentAttributes, - CommentRequest, - CommentType, - User, -} from '../../common/api'; +import { CaseStatuses, CommentAttributes, CommentRequest, CommentType, User } from '../../common'; import { UpdateAlertRequest } from '../client/types'; import { getAlertInfoFromComments } from '../routes/api/utils'; diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 122f6bd77c693..e1a322c4b1c94 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -18,7 +18,7 @@ import { AssociationType, CaseResponse, CasesResponse, -} from '../../../common/api'; +} from '../../../common'; import { connectorMappingsServiceMock, createCaseServiceMock, diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index da993faf0ef5c..d223c70221e37 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -8,12 +8,7 @@ import { curry } from 'lodash'; import { Logger } from 'src/core/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; -import { - CasePatchRequest, - CasePostRequest, - CommentRequest, - CommentType, -} from '../../../common/api'; +import { CasePatchRequest, CasePostRequest, CommentRequest, CommentType } from '../../../common'; import { createExternalCasesClient } from '../../client'; import { CaseExecutorParamsSchema, CaseConfigurationSchema, CommentSchemaType } from './schema'; import { diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index ac34ad40cfa13..dce18119d1704 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentType } from '../../../common/api'; +import { CommentType } from '../../../common'; import { validateConnector } from './validators'; // Reserved for future implementation diff --git a/x-pack/plugins/cases/server/connectors/case/types.ts b/x-pack/plugins/cases/server/connectors/case/types.ts index 6a7dfd9c2e687..a71007f0b4946 100644 --- a/x-pack/plugins/cases/server/connectors/case/types.ts +++ b/x-pack/plugins/cases/server/connectors/case/types.ts @@ -16,7 +16,7 @@ import { ConnectorSchema, CommentSchema, } from './schema'; -import { CaseResponse, CasesResponse } from '../../../common/api'; +import { CaseResponse, CasesResponse } from '../../../common'; export type CaseConfiguration = TypeOf<typeof CaseConfigurationSchema>; export type Connector = TypeOf<typeof ConnectorSchema>; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index a6b6e193361be..ecf04e4f7b0f1 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -17,7 +17,7 @@ import { serviceNowITSMExternalServiceFormatter } from './servicenow/itsm_format import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatter'; import { jiraExternalServiceFormatter } from './jira/external_service_formatter'; import { resilientExternalServiceFormatter } from './resilient/external_service_formatter'; -import { CommentRequest, CommentType } from '../../common/api'; +import { CommentRequest, CommentType } from '../../common'; export * from './types'; export { transformConnectorComment } from './case'; diff --git a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts b/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts index 0bfaf7cdbd9e3..f5d76aeddf313 100644 --- a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseResponse } from '../../../common/api'; +import { CaseResponse } from '../../../common'; import { jiraExternalServiceFormatter } from './external_service_formatter'; describe('Jira formatter', () => { diff --git a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts index 74376d295fea5..15ee2fd468dda 100644 --- a/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/jira/external_service_formatter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JiraFieldsType, ConnectorJiraTypeFields } from '../../../common/api'; +import { JiraFieldsType, ConnectorJiraTypeFields } from '../../../common'; import { ExternalServiceFormatter } from '../types'; interface ExternalServiceParams extends JiraFieldsType { diff --git a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts b/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts index 01280e9692b5e..b7096179b0fab 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseResponse } from '../../../common/api'; +import { CaseResponse } from '../../../common'; import { resilientExternalServiceFormatter } from './external_service_formatter'; describe('IBM Resilient formatter', () => { diff --git a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts b/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts index 76554dce32797..6dea452565d7c 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/external_service_formatter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ResilientFieldsType, ConnectorResillientTypeFields } from '../../../common/api'; +import { ResilientFieldsType, ConnectorResillientTypeFields } from '../../../common'; import { ExternalServiceFormatter } from '../types'; const format: ExternalServiceFormatter<ResilientFieldsType>['format'] = (theCase) => { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts index b49eed6a4ad26..a4fa8a198fea7 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_formatter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../../../common/api'; +import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../../../common'; import { ExternalServiceFormatter } from '../types'; const format: ExternalServiceFormatter<ServiceNowITSMFieldsType>['format'] = (theCase) => { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts index ea3a4e41e17b8..78242e4c3848a 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_formmater.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseResponse } from '../../../common/api'; +import { CaseResponse } from '../../../common'; import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter'; describe('ITSM formatter', () => { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts index 4faca62c6e706..1f7716424cfa9 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseResponse } from '../../../common/api'; +import { CaseResponse } from '../../../common'; import { serviceNowSIRExternalServiceFormatter } from './sir_formatter'; describe('ITSM formatter', () => { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts index d2458e6c7ae53..1c528cd2b47bf 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_formatter.ts @@ -5,7 +5,7 @@ * 2.0. */ import { get } from 'lodash/fp'; -import { ConnectorServiceNowSIRTypeFields } from '../../../common/api'; +import { ConnectorServiceNowSIRTypeFields } from '../../../common'; import { ExternalServiceFormatter } from '../types'; interface ExternalServiceParams { dest_ip: string | null; diff --git a/x-pack/plugins/cases/server/connectors/types.ts b/x-pack/plugins/cases/server/connectors/types.ts index f6c284b74667b..fae1ec2976bc0 100644 --- a/x-pack/plugins/cases/server/connectors/types.ts +++ b/x-pack/plugins/cases/server/connectors/types.ts @@ -13,7 +13,7 @@ import { ActionType, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../actions/server/types'; -import { CaseResponse, ConnectorTypes } from '../../common/api'; +import { CaseResponse, ConnectorTypes } from '../../common'; import { CasesClientGetAlertsResponse } from '../client/alerts/types'; import { CaseServiceSetup, diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 0c661cc18c21b..82e2e0b10e771 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -10,7 +10,7 @@ import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as ActionsPluginSetup } from '../../actions/server'; -import { APP_ID } from '../common/constants'; +import { APP_ID } from '../common'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index f2318c45e6ed3..c9d7ac4125141 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -17,7 +17,7 @@ import { ConnectorTypes, ESCaseAttributes, ESCasesConfigureAttributes, -} from '../../../../common/api'; +} from '../../../../common'; import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, diff --git a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts index ae14b44e7dffe..9df94cd0923c9 100644 --- a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts @@ -10,7 +10,7 @@ import { CasePostRequest, CasesConfigureRequest, ConnectorTypes, -} from '../../../../common/api'; +} from '../../../../common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../../actions/server/types'; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts index fd250b74fff1e..77db06680fd59 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_all_comments.ts @@ -10,8 +10,7 @@ import { schema } from '@kbn/config-schema'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -import { AssociationType } from '../../../../../common/api'; +import { AssociationType, CASE_COMMENTS_URL } from '../../../../../common'; export function initDeleteAllCommentsApi({ caseService, diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.test.ts index dcbcd7b9e246d..d0968c3232459 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.test.ts @@ -16,7 +16,7 @@ import { mockCaseComments, } from '../../__fixtures__'; import { initDeleteCommentApi } from './delete_comment'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common'; describe('DELETE comment', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts index f1c5fdc2b7cc8..3ba93142bdcce 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/delete_comment.ts @@ -12,7 +12,7 @@ import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_obje import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common'; export function initDeleteCommentApi({ caseService, diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts index 57ddd84e8742c..75d0f9f59657a 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts @@ -19,10 +19,10 @@ import { CommentsResponseRt, SavedObjectFindOptionsRt, throwErrors, -} from '../../../../../common/api'; +} from '../../../../../common'; import { RouteDeps } from '../../types'; import { escapeHatch, transformComments, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL } from '../../../../../common'; import { defaultPage, defaultPerPage } from '../..'; const FindQueryParamsRt = rt.partial({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts index 770efe0109744..a400f944dddfa 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_all_comment.ts @@ -8,10 +8,10 @@ import { schema } from '@kbn/config-schema'; import { SavedObjectsFindResponse } from 'kibana/server'; -import { AllCommentsResponseRt, CommentAttributes } from '../../../../../common/api'; +import { AllCommentsResponseRt, CommentAttributes } from '../../../../../common'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObjects, wrapError } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL } from '../../../../../common'; import { defaultSortField } from '../../../../common'; export function initGetAllCommentsApi({ caseService, router, logger }: RouteDeps) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.test.ts index 8ee43eaba8a82..46accdc58d460 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.test.ts @@ -17,7 +17,7 @@ import { } from '../../__fixtures__'; import { flattenCommentSavedObject } from '../../utils'; import { initGetCommentApi } from './get_comment'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common'; describe('GET comment', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts index 9dedfccd3a250..f86f733306043 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/get_comment.ts @@ -7,10 +7,10 @@ import { schema } from '@kbn/config-schema'; -import { CommentResponseRt } from '../../../../../common/api'; +import { CommentResponseRt } from '../../../../../common'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common'; export function initGetCommentApi({ caseService, router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.test.ts index 9cc0575f9bb94..32a0133d455c2 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.test.ts @@ -17,8 +17,8 @@ import { mockCases, } from '../../__fixtures__'; import { initPatchCommentApi } from './patch_comment'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -import { CommentType } from '../../../../../common/api'; +import { CASE_COMMENTS_URL } from '../../../../../common'; +import { CommentType } from '../../../../../common'; describe('PATCH comment', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts index f5db2dc004a1d..b47236f4693cf 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/patch_comment.ts @@ -14,12 +14,12 @@ import Boom from '@hapi/boom'; import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { CommentableCase } from '../../../../common'; -import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common/api'; +import { CommentPatchRequestRt, throwErrors, User } from '../../../../../common'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError, decodeCommentRequest } from '../../utils'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; +import { CASE_COMMENTS_URL } from '../../../../../common'; import { CaseServiceSetup } from '../../../../services'; interface CombinedCaseParams { diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.test.ts index 807ec0d089a52..27d5c47d47399 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.test.ts @@ -17,8 +17,8 @@ import { mockCaseComments, } from '../../__fixtures__'; import { initPostCommentApi } from './post_comment'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -import { CommentType } from '../../../../../common/api'; +import { CASE_COMMENTS_URL } from '../../../../../common'; +import { CommentType } from '../../../../../common'; describe('POST comment', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts index 110a16a610014..47d41b60165d7 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/post_comment.ts @@ -8,8 +8,8 @@ import { schema } from '@kbn/config-schema'; import { escapeHatch, wrapError } from '../../utils'; import { RouteDeps } from '../../types'; -import { CASE_COMMENTS_URL } from '../../../../../common/constants'; -import { CommentRequest } from '../../../../../common/api'; +import { CASE_COMMENTS_URL } from '../../../../../common'; +import { CommentRequest } from '../../../../../common'; export function initPostCommentApi({ router, logger }: RouteDeps) { router.post( diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts index f328844acfd00..626f53cdf4263 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.test.ts @@ -17,9 +17,8 @@ import { } from '../../__fixtures__'; import { initGetCaseConfigure } from './get_configure'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_URL, ConnectorTypes } from '../../../../../common'; import { mappings } from '../../../../client/configure/mock'; -import { ConnectorTypes } from '../../../../../common/api/connectors'; import { CasesClient } from '../../../../client'; describe('GET configuration', () => { diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts index c916bd8f4140b..03ac3dd8b13b3 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_configure.ts @@ -6,10 +6,10 @@ */ import Boom from '@hapi/boom'; -import { CaseConfigureResponseRt, ConnectorMappingsAttributes } from '../../../../../common/api'; +import { CaseConfigureResponseRt, ConnectorMappingsAttributes } from '../../../../../common'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_URL } from '../../../../../common'; import { transformESConnectorToCaseConnector } from '../helpers'; export function initGetCaseConfigure({ caseConfigureService, router, logger }: RouteDeps) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.test.ts index 3fa0fe2f83f79..082adf7b4803f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.test.ts @@ -17,7 +17,7 @@ import { } from '../../__fixtures__'; import { initCaseConfigureGetActionConnector } from './get_connectors'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common'; import { getActions } from '../../__mocks__/request_responses'; describe('GET connectors', () => { diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts index 81ffc06355ff5..7aec7e4f086b4 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/get_connectors.ts @@ -12,10 +12,7 @@ import { ActionType } from '../../../../../../actions/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../../../actions/server/types'; -import { - CASE_CONFIGURE_CONNECTORS_URL, - SUPPORTED_CONNECTORS, -} from '../../../../../common/constants'; +import { CASE_CONFIGURE_CONNECTORS_URL, SUPPORTED_CONNECTORS } from '../../../../../common'; const isConnectorSupported = ( action: FindActionResult, diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts index 48d88e0f622f5..c4e2b6af1cd6b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.test.ts @@ -17,8 +17,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initPatchCaseConfigure } from './patch_configure'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -import { ConnectorTypes } from '../../../../../common/api/connectors'; +import { CASE_CONFIGURE_URL, ConnectorTypes } from '../../../../../common'; import { CasesClient } from '../../../../client'; describe('PATCH configuration', () => { diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts index ba0ea6eb17936..5fe38cf0efe48 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/patch_configure.ts @@ -15,10 +15,10 @@ import { CaseConfigureResponseRt, throwErrors, ConnectorMappingsAttributes, -} from '../../../../../common/api'; +} from '../../../../../common'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_URL } from '../../../../../common'; import { transformCaseConnectorToEsConnector, transformESConnectorToCaseConnector, diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts index 882a10742d733..35b662078fe9c 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.test.ts @@ -18,8 +18,7 @@ import { import { initPostCaseConfigure } from './post_configure'; import { newConfiguration } from '../../__mocks__/request_responses'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; -import { ConnectorTypes } from '../../../../../common/api/connectors'; +import { CASE_CONFIGURE_URL, ConnectorTypes } from '../../../../../common'; import { CasesClient } from '../../../../client'; describe('POST configuration', () => { diff --git a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts index 469151a126898..74ad02f47e178 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/configure/post_configure.ts @@ -15,10 +15,10 @@ import { CaseConfigureResponseRt, throwErrors, ConnectorMappingsAttributes, -} from '../../../../../common/api'; +} from '../../../../../common'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; -import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; +import { CASE_CONFIGURE_URL } from '../../../../../common'; import { transformCaseConnectorToEsConnector, transformESConnectorToCaseConnector, diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.test.ts index a441a027769bf..7748a079ceb4d 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.test.ts @@ -17,7 +17,7 @@ import { mockCaseComments, } from '../__fixtures__'; import { initDeleteCasesApi } from './delete_cases'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL } from '../../../../common'; describe('DELETE case', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts index 5f2a6c67220c3..43710dfab93eb 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts @@ -11,7 +11,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL } from '../../../../common'; import { CaseServiceSetup } from '../../../services'; async function deleteSubCases({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.test.ts index ca9f731ca5010..75586896390fc 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.test.ts @@ -15,7 +15,7 @@ import { mockCases, } from '../__fixtures__'; import { initFindCasesApi } from './find_cases'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL } from '../../../../common'; import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects'; describe('FIND all cases', () => { diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index bc6907f52b9eb..97455e9e08f7b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -16,10 +16,10 @@ import { CasesFindRequestRt, throwErrors, caseStatuses, -} from '../../../../common/api'; +} from '../../../../common'; import { transformCases, wrapError, escapeHatch } from '../utils'; import { RouteDeps } from '../types'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL } from '../../../../common'; import { constructQueryOptions } from './helpers'; export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.test.ts index b9312331b4df2..768bbca62f3fe 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.test.ts @@ -8,7 +8,7 @@ import { kibanaResponseFactory, RequestHandler, SavedObject } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; -import { ConnectorTypes, ESCaseAttributes } from '../../../../common/api'; +import { ConnectorTypes, ESCaseAttributes } from '../../../../common'; import { createMockSavedObjectsRepository, createRoute, @@ -21,7 +21,7 @@ import { } from '../__fixtures__'; import { flattenCaseSavedObject } from '../utils'; import { initGetCaseApi } from './get_case'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; +import { CASE_DETAILS_URL } from '../../../../common'; describe('GET case', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index f464f7e47fe7a..e2d08dcd23f2e 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; +import { CASE_DETAILS_URL } from '../../../../common'; export function initGetCaseApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.test.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.test.ts index f7cfebeaea749..a1d25aa295799 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.test.ts @@ -11,7 +11,7 @@ import { ConnectorTypes, ESCaseConnector, ESCasesConfigureAttributes, -} from '../../../../common/api'; +} from '../../../../common'; import { mockCaseConfigure } from '../__fixtures__'; import { transformCaseConnectorToEsConnector, diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts index 8659ab02d6d53..5f51c9b1f8d8c 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts @@ -11,14 +11,15 @@ import deepEqual from 'fast-deep-equal'; import { SavedObjectsFindResponse } from 'kibana/server'; import { CaseConnector, - ESCaseConnector, - ESCasesConfigureAttributes, - ConnectorTypes, CaseStatuses, CaseType, + ConnectorTypeFields, + ConnectorTypes, + ESCaseConnector, + ESCasesConfigureAttributes, + ESConnectorFields, SavedObjectFindOptions, -} from '../../../../common/api'; -import { ESConnectorFields, ConnectorTypeFields } from '../../../../common/api/connectors'; +} from '../../../../common'; import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../saved_object_types'; import { sortToSnake } from '../utils'; import { combineFilters } from '../../../common'; diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts index b3f87211c9547..96a891441ea5f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.test.ts @@ -17,7 +17,7 @@ import { } from '../__fixtures__'; import { initPatchCasesApi } from './patch_cases'; import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects'; -import { CaseStatuses } from '../../../../common/api'; +import { CaseStatuses } from '../../../../common'; describe('PATCH cases', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts index 8e779087bcafe..092f88c1a8a20 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts @@ -7,8 +7,8 @@ import { escapeHatch, wrapError } from '../utils'; import { RouteDeps } from '../types'; -import { CASES_URL } from '../../../../common/constants'; -import { CasesPatchRequest } from '../../../../common/api'; +import { CASES_URL } from '../../../../common'; +import { CasesPatchRequest } from '../../../../common'; export function initPatchCasesApi({ router, logger }: RouteDeps) { router.patch( diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts index e1669203d3ded..669d3a5e58874 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.test.ts @@ -15,9 +15,9 @@ import { mockCases, } from '../__fixtures__'; import { initPostCaseApi } from './post_case'; -import { CASES_URL } from '../../../../common/constants'; +import { CASES_URL } from '../../../../common'; import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects'; -import { ConnectorTypes, CaseStatuses } from '../../../../common/api'; +import { ConnectorTypes, CaseStatuses } from '../../../../common'; describe('POST cases', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts index e2d71c5837353..a7951a1a71344 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts @@ -8,8 +8,8 @@ import { wrapError, escapeHatch } from '../utils'; import { RouteDeps } from '../types'; -import { CASES_URL } from '../../../../common/constants'; -import { CasePostRequest } from '../../../../common/api'; +import { CASES_URL } from '../../../../common'; +import { CasePostRequest } from '../../../../common'; export function initPostCaseApi({ router, logger }: RouteDeps) { router.post( diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts index fb0ba5e3b5d9a..378d092c8be0b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.test.ts @@ -20,7 +20,7 @@ import { } from '../__fixtures__'; import { initPushCaseApi } from './push_case'; import { CasesRequestHandlerContext } from '../../../types'; -import { getCasePushUrl } from '../../../../common/api/helpers'; +import { getCasePushUrl } from '../../../../common'; describe('Push case', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts index 7395758210cf4..9bfb30e0d63ad 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts @@ -12,9 +12,9 @@ import { identity } from 'fp-ts/lib/function'; import { wrapError, escapeHatch } from '../utils'; -import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api'; +import { throwErrors, CasePushRequestParamsRt } from '../../../../common'; import { RouteDeps } from '../types'; -import { CASE_PUSH_URL } from '../../../../common/constants'; +import { CASE_PUSH_URL } from '../../../../common'; export function initPushCaseApi({ router, logger }: RouteDeps) { router.post( diff --git a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts index e5433f4972239..53fdc298ef267 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { UsersRt } from '../../../../../common/api'; +import { UsersRt } from '../../../../../common'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_REPORTERS_URL } from '../../../../../common/constants'; +import { CASE_REPORTERS_URL } from '../../../../../common'; export function initGetReportersApi({ caseService, router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts index 1c399a415e470..60ad0c60f944f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.test.ts @@ -15,8 +15,8 @@ import { mockCases, } from '../../__fixtures__'; import { initGetCasesStatusApi } from './get_status'; -import { CASE_STATUS_URL } from '../../../../../common/constants'; -import { CaseType } from '../../../../../common/api'; +import { CASE_STATUS_URL } from '../../../../../common'; +import { CaseType } from '../../../../../common'; describe('GET status', () => { let routeHandler: RequestHandler<any, any, any>; diff --git a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts index d0addfff09124..73642fdee0eac 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/status/get_status.ts @@ -8,8 +8,8 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CasesStatusResponseRt, caseStatuses } from '../../../../../common/api'; -import { CASE_STATUS_URL } from '../../../../../common/constants'; +import { CasesStatusResponseRt, caseStatuses } from '../../../../../common'; +import { CASE_STATUS_URL } from '../../../../../common'; import { constructQueryOptions } from '../helpers'; export function initGetCasesStatusApi({ caseService, router, logger }: RouteDeps) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts index fd33afbd7df8e..ef60c743ec822 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/delete_sub_cases.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; import { buildCaseUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common'; import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; export function initDeleteSubCasesApi({ diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts index c24dde1944f83..81d5517b8ce59 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/find_sub_cases.ts @@ -17,10 +17,10 @@ import { SubCasesFindRequestRt, SubCasesFindResponseRt, throwErrors, -} from '../../../../../common/api'; +} from '../../../../../common'; import { RouteDeps } from '../../types'; import { escapeHatch, transformSubCases, wrapError } from '../../utils'; -import { SUB_CASES_URL } from '../../../../../common/constants'; +import { SUB_CASES_URL } from '../../../../../common'; import { constructQueryOptions } from '../helpers'; import { defaultPage, defaultPerPage } from '../..'; diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts index 32dcc924e1a08..b5ebfb4de348b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/get_sub_case.ts @@ -7,10 +7,10 @@ import { schema } from '@kbn/config-schema'; -import { SubCaseResponseRt } from '../../../../../common/api'; +import { SubCaseResponseRt } from '../../../../../common'; import { RouteDeps } from '../../types'; import { flattenSubCaseSavedObject, wrapError } from '../../utils'; -import { SUB_CASE_DETAILS_URL } from '../../../../../common/constants'; +import { SUB_CASE_DETAILS_URL } from '../../../../../common'; import { countAlertsForID } from '../../../../common'; export function initGetSubCaseApi({ caseService, router, logger }: RouteDeps) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts index 08836615e1d39..0b142fb5279e5 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -35,8 +35,8 @@ import { SubCasesResponseRt, User, CommentAttributes, -} from '../../../../../common/api'; -import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; +} from '../../../../../common'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common'; import { RouteDeps } from '../../types'; import { escapeHatch, diff --git a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts index f066aa70ec472..d70d6e0b57ee9 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts @@ -7,7 +7,7 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_TAGS_URL } from '../../../../../common/constants'; +import { CASE_TAGS_URL } from '../../../../../common'; export function initGetTagsApi({ caseService, router }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts index b5c564648c185..48393b6af34ae 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; +import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../../common'; export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/utils.test.ts b/x-pack/plugins/cases/server/routes/api/utils.test.ts index f6bc1e4f71897..2df17e3abacfa 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.test.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.test.ts @@ -30,7 +30,7 @@ import { AssociationType, CaseType, CaseResponse, -} from '../../../common/api'; +} from '../../../common'; describe('Utils', () => { describe('transformNewCase', () => { diff --git a/x-pack/plugins/cases/server/routes/api/utils.ts b/x-pack/plugins/cases/server/routes/api/utils.ts index 8e8862f4157f1..9234472c13f5d 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.ts @@ -41,7 +41,7 @@ import { SubCasesFindResponse, User, AlertCommentRequestRt, -} from '../../../common/api'; +} from '../../../common'; import { transformESConnectorToCaseConnector } from './cases/helpers'; import { SortFieldCase } from './types'; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index bf9694d7e6bb0..8bbc481124870 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -14,7 +14,7 @@ import { CaseType, AssociationType, ESConnectorFields, -} from '../../common/api'; +} from '../../common'; interface UnsanitizedCaseConnector { connector_id: string; diff --git a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts index ba3bcaa65091c..56f842c10e8f5 100644 --- a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts @@ -8,9 +8,7 @@ import yargs from 'yargs'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; -import { CaseResponse, CaseType, ConnectorTypes } from '../../../common/api'; -import { CommentType } from '../../../common/api/cases/comment'; -import { CASES_URL } from '../../../common/constants'; +import { CaseResponse, CaseType, CommentType, ConnectorTypes, CASES_URL } from '../../../common'; import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common'; import { ContextTypeGeneratedAlertType, createAlertsString } from '../../connectors'; diff --git a/x-pack/plugins/cases/server/services/alerts/index.test.ts b/x-pack/plugins/cases/server/services/alerts/index.test.ts index 042e415b77e43..28c3a6278d544 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.test.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.test.ts @@ -6,7 +6,7 @@ */ import { KibanaRequest } from 'kibana/server'; -import { CaseStatuses } from '../../../common/api'; +import { CaseStatuses } from '../../../common'; import { AlertService, AlertServiceContract } from '.'; import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index 6ce4db61ab956..876814719442c 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, Logger } from 'kibana/server'; -import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { MAX_ALERTS_PER_SUB_CASE } from '../../../common'; import { UpdateAlertRequest } from '../../client/types'; import { AlertInfo } from '../../common'; import { createCaseError } from '../../common/error'; diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 46dca4d9a0d0e..0ca63bce2d1d0 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -13,7 +13,7 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; -import { ESCasesConfigureAttributes, SavedObjectFindOptions } from '../../../common/api'; +import { ESCasesConfigureAttributes, SavedObjectFindOptions } from '../../../common'; import { CASE_CONFIGURE_SAVED_OBJECT } from '../../saved_object_types'; interface ClientArgs { diff --git a/x-pack/plugins/cases/server/services/connector_mappings/index.ts b/x-pack/plugins/cases/server/services/connector_mappings/index.ts index d4fda10276d2b..82f37190b4ecc 100644 --- a/x-pack/plugins/cases/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/cases/server/services/connector_mappings/index.ts @@ -13,7 +13,7 @@ import { SavedObjectsFindResponse, } from 'kibana/server'; -import { ConnectorMappings, SavedObjectFindOptions } from '../../../common/api'; +import { ConnectorMappings, SavedObjectFindOptions } from '../../../common'; import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../saved_object_types'; interface ClientArgs { diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 11ceb48d11e9f..18b78300e6632 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -33,7 +33,7 @@ import { CaseResponse, caseTypeField, CasesFindRequest, -} from '../../common/api'; +} from '../../common'; import { combineFilters, defaultSortField, groupTotalAlertsByID } from '../common'; import { defaultPage, defaultPerPage } from '../routes/api'; import { diff --git a/x-pack/plugins/cases/server/services/reporters/read_reporters.ts b/x-pack/plugins/cases/server/services/reporters/read_reporters.ts index d2708780b2ccf..b47fa185ff78e 100644 --- a/x-pack/plugins/cases/server/services/reporters/read_reporters.ts +++ b/x-pack/plugins/cases/server/services/reporters/read_reporters.ts @@ -7,7 +7,7 @@ import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; -import { CaseAttributes, User } from '../../../common/api'; +import { CaseAttributes, User } from '../../../common'; import { CASE_SAVED_OBJECT } from '../../saved_object_types'; export const convertToReporters = (caseObjects: Array<SavedObject<CaseAttributes>>): User[] => diff --git a/x-pack/plugins/cases/server/services/tags/read_tags.ts b/x-pack/plugins/cases/server/services/tags/read_tags.ts index 4c4a948453730..a00b0b6f26fb7 100644 --- a/x-pack/plugins/cases/server/services/tags/read_tags.ts +++ b/x-pack/plugins/cases/server/services/tags/read_tags.ts @@ -7,7 +7,7 @@ import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; -import { CaseAttributes } from '../../../common/api'; +import { CaseAttributes } from '../../../common'; import { CASE_SAVED_OBJECT } from '../../saved_object_types'; export const convertToTags = (tagObjects: Array<SavedObject<CaseAttributes>>): string[] => diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index c600a96234b3d..be32717039d9d 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -17,7 +17,7 @@ import { User, UserActionFieldType, SubCaseAttributes, -} from '../../../common/api'; +} from '../../../common'; import { isTwoArraysDifference, transformESConnectorToCaseConnector, diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index 785c81021b584..a038d843a5331 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -12,7 +12,7 @@ import { SavedObjectReference, } from 'kibana/server'; -import { CaseUserActionAttributes } from '../../../common/api'; +import { CaseUserActionAttributes } from '../../../common'; import { CASE_USER_ACTION_SAVED_OBJECT, CASE_SAVED_OBJECT, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 143384d160471..47606983b8368 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -23,6 +23,8 @@ export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults'; export const DEFAULT_APP_REFRESH_INTERVAL = 'securitySolution:refreshIntervalDefaults'; export const DEFAULT_SIGNALS_INDEX = '.siem-signals'; +// The DEFAULT_MAX_SIGNALS value exists also in `x-pack/plugins/cases/common/constants.ts` +// If either changes, engineer should ensure both values are updated export const DEFAULT_MAX_SIGNALS = 100; export const DEFAULT_SEARCH_AFTER_PAGE_SIZE = 100; export const DEFAULT_ANOMALY_SCORE = 'securitySolution:defaultAnomalyScore'; @@ -206,3 +208,10 @@ export const showAllOthersBucket: string[] = [ 'destination.ip', 'user.name', ]; + +/* + Feature Flag for Cases RAC UI + DO NOT MERGE to master as true, dev only +*/ + +export const USE_RAC_CASES_UI = false; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index d4551f76ae390..50a5f62740271 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -7,6 +7,7 @@ "requiredPlugins": [ "actions", "alerting", + "cases", "data", "dataEnhanced", "embeddable", diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 9c06fc032f819..6ffce4f2af454 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -13,7 +13,7 @@ import { noop } from 'lodash/fp'; import { TestProviders } from '../../../common/mock'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { CommentRequest, CommentType } from '../../../../../cases/common/api'; +import { CommentRequest, CommentType } from '../../../../../cases/common'; import { useInsertTimeline } from '../use_insert_timeline'; import { usePostComment } from '../../containers/use_post_comment'; import { AddComment, AddCommentRefObject } from '.'; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index acd27e99a857f..ff5ef11fd923f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -9,10 +9,10 @@ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; -import { CommentType } from '../../../../../cases/common/api'; +import { CommentType } from '../../../../../cases/common'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; -import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form'; +import { MarkdownEditorForm } from '../../../common/components/markdown_editor'; import { Form, useForm, UseField, useFormData } from '../../../shared_imports'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx index 2cf7d3c6c555b..bf625fc065089 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/schema.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { CommentRequestUserType } from '../../../../../cases/common/api'; +import { CommentRequestUserType } from '../../../../../cases/common'; import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx index daa988641fbab..0353f48e6ee38 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx @@ -8,7 +8,7 @@ import { Dispatch } from 'react'; import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { Case, SubCase } from '../../containers/types'; import { UpdateCase } from '../../containers/use_get_cases'; import { statuses } from '../status'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx index 86f854fd0a145..079943d8cbd3b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx @@ -19,7 +19,7 @@ import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; -import { CaseStatuses, CaseType } from '../../../../../cases/common/api'; +import { CaseStatuses, CaseType } from '../../../../../cases/common'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { Case, SubCase } from '../../containers/types'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx index 43f0d9df49e94..f40e159306e92 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx @@ -10,7 +10,7 @@ import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui'; import styled from 'styled-components'; import { Case, SubCase } from '../../containers/types'; import { CasesColumns } from './columns'; -import { AssociationType } from '../../../../../cases/common/api'; +import { AssociationType } from '../../../../../cases/common'; type ExpandedRowMap = Record<string, Element> | {}; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts index 8962d67319371..0d5eb2c9ba407 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts @@ -6,7 +6,7 @@ */ import { filter } from 'lodash/fp'; -import { AssociationType, CaseStatuses, CaseType } from '../../../../../cases/common/api'; +import { AssociationType, CaseStatuses, CaseType } from '../../../../../cases/common'; import { Case, SubCase } from '../../containers/types'; import { statuses } from '../status'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 0fafdaf81f095..c079bbc991601 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -14,7 +14,7 @@ import { TestProviders } from '../../../common/mock'; import { casesStatus, useGetCasesMockState, collectionCase } from '../../containers/mock'; import * as i18n from './translations'; -import { CaseStatuses, CaseType } from '../../../../../cases/common/api'; +import { CaseStatuses, CaseType } from '../../../../../cases/common'; import { useKibana } from '../../../common/lib/kibana'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { useDeleteCases } from '../../containers/use_delete_cases'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index c5748a321c19b..a0820486f423f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -22,7 +22,7 @@ import styled, { css } from 'styled-components'; import classnames from 'classnames'; import * as i18n from './translations'; -import { CaseStatuses, CaseType } from '../../../../../cases/common/api'; +import { CaseStatuses, CaseType } from '../../../../../cases/common'; import { getCasesColumns } from './columns'; import { Case, DeleteCase, FilterOptions, SortFieldCase, SubCase } from '../../containers/types'; import { useGetCases, UpdateCase } from '../../containers/use_get_cases'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx index 5c9f11d1e3a83..f31eda12b3399 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { StatusFilter } from './status_filter'; import { StatusAll } from '../status'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx index 48a642aaf51a9..c4486365cd292 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { TestProviders } from '../../../common/mock'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx index ff5b511ef9026..434ae46fcfb7a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx @@ -10,7 +10,7 @@ import { isEqual } from 'lodash/fp'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { FilterOptions } from '../../containers/types'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; diff --git a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx index 24897a14f0754..e90ae2b036866 100644 --- a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { statuses, CaseStatusWithAllStatus } from '../status'; import * as i18n from './translations'; import { Case } from '../../containers/types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/helpers.test.ts b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/helpers.test.ts index 8e26c0fd7a7ff..64d37de0a6ea9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { basicCase } from '../../containers/mock'; import { getStatusDate, getStatusTitle } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/helpers.ts index 68a243040145a..9dd666c72335b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/helpers.ts +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { Case } from '../../containers/types'; import { statuses } from '../status'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx index 63ce441732251..fd4e49400d464 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx @@ -16,7 +16,7 @@ import { EuiFlexItem, EuiIconTip, } from '@elastic/eui'; -import { CaseStatuses, CaseType } from '../../../../../cases/common/api'; +import { CaseStatuses, CaseType } from '../../../../../cases/common'; import * as i18n from '../case_view/translations'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; import { Actions } from './actions'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.test.tsx index 4e414706d1fd7..1f3b9c39017d9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { StatusContextMenu } from './status_context_menu'; describe('SyncAlertsSwitch', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.tsx index 92dcd16a86193..298d0d7695e8e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.tsx @@ -8,7 +8,7 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { memoize } from 'lodash/fp'; import { EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { caseStatuses, CaseStatuses } from '../../../../../cases/common/api'; +import { caseStatuses, CaseStatuses } from '../../../../../cases/common'; import { Status } from '../status'; interface Props { diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx index 18a76e2766d8d..657a19d40fdd9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { AssociationType, CommentType } from '../../../../../cases/common/api'; +import { AssociationType, CommentType } from '../../../../../cases/common'; import { Comment } from '../../containers/types'; import { getManualAlertIdsWithNoRuleId, buildAlertsQuery } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.ts index 7211f4bca6a37..741880d886c89 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.ts +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.ts @@ -6,7 +6,7 @@ */ import { isEmpty } from 'lodash'; -import { CommentType } from '../../../../../cases/common/api'; +import { CommentType } from '../../../../../cases/common'; import { Comment } from '../../containers/types'; export const getManualAlertIdsWithNoRuleId = (comments: Comment[]): string[] => { diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index f28c7791d0110..75f91c8ef3035 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -28,8 +28,7 @@ import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; -import { ConnectorTypes } from '../../../../../cases/common/api/connectors'; -import { CaseType } from '../../../../../cases/common/api'; +import { CaseType, ConnectorTypes } from '../../../../../cases/common'; const mockDispatch = jest.fn(); jest.mock('react-redux', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 892663c783293..e16f1d7683abc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -17,7 +17,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; -import { CaseStatuses, CaseAttributes, CaseType } from '../../../../../cases/common/api'; +import { CaseStatuses, CaseAttributes, CaseType } from '../../../../../cases/common'; import { Case, CaseConnector } from '../../containers/types'; import { getCaseDetailsUrl, getCaseUrl, useFormatUrl } from '../../../common/components/link_to'; import { gutterTimeline } from '../../../common/lib/helpers'; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx index ccc697a2ae84e..e18e0ef004ceb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/__mock__/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypes } from '../../../../../../cases/common/api'; +import { ConnectorTypes } from '../../../../../../cases/common'; import { ActionConnector } from '../../../containers/configure/types'; import { UseConnectorsResponse } from '../../../containers/configure/use_connectors'; import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx index c34651c3e1dc4..1c01bb3fdeb7b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx @@ -12,7 +12,7 @@ import { Connectors, Props } from './connectors'; import { TestProviders } from '../../../common/mock'; import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors } from './__mock__'; -import { ConnectorTypes } from '../../../../../cases/common/api/connectors'; +import { ConnectorTypes } from '../../../../../cases/common'; describe('Connectors', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx index 1e0ae95ff901c..c0a5e3c4c8f72 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.tsx @@ -21,7 +21,7 @@ import * as i18n from './translations'; import { ActionConnector, CaseConnectorMapping } from '../../containers/configure/types'; import { Mapping } from './mapping'; -import { ConnectorTypes } from '../../../../../cases/common/api/connectors'; +import { ConnectorTypes } from '../../../../../cases/common'; const EuiFormRowExtended = styled(EuiFormRow)` .euiFormRow__labelWrapper { diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx index 01d975a445ab4..27f7f4d50a0c9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; -import { ConnectorTypes } from '../../../../../cases/common/api'; +import { ConnectorTypes } from '../../../../../cases/common'; import { ActionConnector } from '../../containers/configure/types'; import { connectorsConfiguration } from '../connectors'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index 8dbefdb731141..e78cd4c509d5d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -33,7 +33,7 @@ import { useConnectorsResponse, useActionTypesResponse, } from './__mock__'; -import { ConnectorTypes } from '../../../../../cases/common/api/connectors'; +import { ConnectorTypes } from '../../../../../cases/common'; jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/configure/use_connectors'); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx index 25155ff77c2d0..e951498c6c3c9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx @@ -10,7 +10,7 @@ import styled, { css } from 'styled-components'; import { EuiCallOut } from '@elastic/eui'; -import { SUPPORTED_CONNECTORS } from '../../../../../cases/common/constants'; +import { SUPPORTED_CONNECTORS } from '../../../../../cases/common'; import { useKibana } from '../../../common/lib/kibana'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useActionTypes } from '../../containers/configure/use_action_types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts b/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts index db14371b625d8..dfb19250f5bd6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypeFields, ConnectorTypes } from '../../../../../cases/common/api'; +import { ConnectorTypeFields, ConnectorTypes } from '../../../../../cases/common'; import { CaseField, ActionType, diff --git a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx index 63c6f265b1ab2..f0e77648cee6c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx @@ -11,7 +11,7 @@ import { EuiFormRow } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; -import { ActionConnector } from '../../../../../cases/common/api'; +import { ActionConnector } from '../../../../../cases/common'; interface ConnectorSelectorProps { connectors: ActionConnector[]; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx index af9a86b0b711b..dded090eb3f98 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx @@ -10,7 +10,7 @@ import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; import { connectorsConfiguration } from '.'; -import { ConnectorTypes } from '../../../../../cases/common/api/connectors'; +import { ConnectorTypes } from '../../../../../cases/common'; interface ConnectorCardProps { connectorType: ConnectorTypes; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx index 05161456976c6..b182c878d78e6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { ActionParamsProps } from '../../../../../../triggers_actions_ui/public/types'; -import { CommentType } from '../../../../../../cases/common/api'; +import { CommentType } from '../../../../../../cases/common'; import { CaseActionParams } from './types'; import { ExistingCase } from './existing_case'; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx index 3c6c5f47c6d12..c503a62ef515e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx @@ -6,7 +6,7 @@ */ import React, { memo, useMemo, useCallback } from 'react'; -import { CaseType } from '../../../../../../cases/common/api'; +import { CaseType } from '../../../../../../cases/common'; import { useGetCases, DEFAULT_QUERY_PARAMS, diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx index 841c2a9e38f6d..035f1fa2b63ac 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { CaseActionConnector, ConnectorFieldsProps } from './types'; import { getCaseConnectors } from '.'; -import { ConnectorTypeFields } from '../../../../../cases/common/api/connectors'; +import { ConnectorTypeFields } from '../../../../../cases/common'; interface Props extends Omit<ConnectorFieldsProps<ConnectorTypeFields['fields']>, 'connector'> { connector: CaseActionConnector | null; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts index dad7070aad705..76f6ccb6a1adb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts @@ -15,7 +15,7 @@ import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType, ResilientFieldsType, -} from '../../../../../cases/common/api/connectors'; +} from '../../../../../cases/common'; export { getActionType as getCaseConnectorUI } from './case'; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx index 22e80d43f34e1..985537e799596 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx @@ -10,7 +10,7 @@ import { map } from 'lodash/fp'; import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; -import { ConnectorTypes, JiraFieldsType } from '../../../../../../cases/common/api/connectors'; +import { ConnectorTypes, JiraFieldsType } from '../../../../../../cases/common'; import { useKibana } from '../../../../common/lib/kibana'; import { ConnectorFieldsProps } from '../types'; import { useGetIssueTypes } from './use_get_issue_types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/index.ts index 40e59a081a449..1069e489ada09 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/jira/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/index.ts @@ -8,7 +8,7 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { JiraFieldsType } from '../../../../../../cases/common/api/connectors'; +import { JiraFieldsType } from '../../../../../../cases/common'; import * as i18n from './translations'; export * from './types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx index b1fbfb1169d08..ae9b5a4dd6f49 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx @@ -21,7 +21,7 @@ import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; import * as i18n from './translations'; -import { ConnectorTypes, ResilientFieldsType } from '../../../../../../cases/common/api/connectors'; +import { ConnectorTypes, ResilientFieldsType } from '../../../../../../cases/common'; import { ConnectorCard } from '../card'; const ResilientFieldsComponent: React.FunctionComponent< diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/index.ts index 8a2603f39e102..21850cdfe4d92 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/index.ts @@ -8,7 +8,7 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ResilientFieldsType } from '../../../../../../cases/common/api/connectors'; +import { ResilientFieldsType } from '../../../../../../cases/common'; import * as i18n from './translations'; export * from './types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts index b342095c39ff0..02441b2b9f7aa 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts @@ -8,10 +8,7 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { - ServiceNowITSMFieldsType, - ServiceNowSIRFieldsType, -} from '../../../../../../cases/common/api/connectors'; +import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType } from '../../../../../../cases/common'; import * as i18n from './translations'; export const getServiceNowITSMCaseConnector = (): CaseConnector<ServiceNowITSMFieldsType> => { diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx index accb8450802d4..f705c9005e480 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -10,10 +10,7 @@ import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@el import * as i18n from './translations'; import { ConnectorFieldsProps } from '../types'; -import { - ConnectorTypes, - ServiceNowITSMFieldsType, -} from '../../../../../../cases/common/api/connectors'; +import { ConnectorTypes, ServiceNowITSMFieldsType } from '../../../../../../cases/common'; import { useKibana } from '../../../../common/lib/kibana'; import { ConnectorCard } from '../card'; import { useGetChoices } from './use_get_choices'; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx index 63502e3454fcf..2bac7e01a00b2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -8,10 +8,7 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; -import { - ConnectorTypes, - ServiceNowSIRFieldsType, -} from '../../../../../../cases/common/api/connectors'; +import { ConnectorTypes, ServiceNowSIRFieldsType } from '../../../../../../cases/common'; import { useKibana } from '../../../../common/lib/kibana'; import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts index 11452b966670b..86f0238dd450f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts @@ -12,9 +12,9 @@ import { CaseField, ActionConnector, ConnectorTypeFields, -} from '../../../../../cases/common/api'; +} from '../../../../../cases/common'; -export { ThirdPartyField as AllThirdPartyFields } from '../../../../../cases/common/api'; +export { ThirdPartyField as AllThirdPartyFields } from '../../../../../cases/common'; export type CaseActionConnector = ActionConnector; export interface ThirdPartyField { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx index 7912d97528cd2..516cc5a0d23a5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx @@ -8,7 +8,7 @@ import React, { memo, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { ConnectorTypes } from '../../../../../cases/common/api'; +import { ConnectorTypes } from '../../../../../cases/common'; import { UseField, useFormData, FieldHook, useFormContext } from '../../../shared_imports'; import { useConnectors } from '../../containers/configure/use_connectors'; import { ConnectorSelector } from '../connector_selector/form'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx index 99626c4cfb797..9d14acc96c192 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.test.tsx @@ -10,7 +10,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { act, waitFor } from '@testing-library/react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { ConnectorTypes } from '../../../../../cases/common/api'; +import { ConnectorTypes } from '../../../../../cases/common'; import { TestProviders } from '../../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; import { useGetTags } from '../../containers/use_get_tags'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index b575dfe42f074..597726e7bb3f3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -19,7 +19,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service' import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Case } from '../../containers/types'; -import { CaseType, ConnectorTypes } from '../../../../../cases/common/api'; +import { CaseType, ConnectorTypes } from '../../../../../cases/common'; const initialCaseValue: FormProps = { description: '', diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 9f904350b772e..484a45248d8c0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -18,6 +18,8 @@ import { FormContext } from './form_context'; import { useInsertTimeline } from '../use_insert_timeline'; import { fieldName as descriptionFieldName } from './description'; import { SubmitCaseButton } from './submit_button'; +import { USE_RAC_CASES_UI } from '../../../../common/constants'; +import { useKibana } from '../../../common/lib/kibana'; export const CommonUseField = getUseField({ component: Field }); @@ -39,6 +41,7 @@ const InsertTimeline = () => { }; export const Create = React.memo(() => { + const { cases } = useKibana().services; const history = useHistory(); const onSuccess = useCallback( async ({ id }) => { @@ -53,32 +56,39 @@ export const Create = React.memo(() => { return ( <EuiPanel> - <FormContext onSuccess={onSuccess}> - <CreateCaseForm /> - <Container> - <EuiFlexGroup - alignItems="center" - justifyContent="flexEnd" - gutterSize="xs" - responsive={false} - > - <EuiFlexItem grow={false}> - <EuiButtonEmpty - data-test-subj="create-case-cancel" - size="s" - onClick={handleSetIsCancel} - iconType="cross" - > - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <SubmitCaseButton /> - </EuiFlexItem> - </EuiFlexGroup> - </Container> - <InsertTimeline /> - </FormContext> + {USE_RAC_CASES_UI ? ( + cases.getCreateCase({ + onCancel: handleSetIsCancel, + onSuccess, + }) + ) : ( + <FormContext onSuccess={onSuccess}> + <CreateCaseForm /> + <Container> + <EuiFlexGroup + alignItems="center" + justifyContent="flexEnd" + gutterSize="xs" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="create-case-cancel" + size="s" + onClick={handleSetIsCancel} + iconType="cross" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <SubmitCaseButton /> + </EuiFlexItem> + </EuiFlexGroup> + </Container> + <InsertTimeline /> + </FormContext> + )} </EuiPanel> ); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts index 6e17be8d53e5a..a983add030a1e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { CasePostRequest, CaseType } from '../../../../../cases/common/api'; -import { ConnectorTypes } from '../../../../../cases/common/api/connectors'; +import { CasePostRequest, CaseType, ConnectorTypes } from '../../../../../cases/common'; import { choices } from '../connectors/mock'; export const sampleTags = ['coke', 'pepsi']; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx index b069a484d314c..38321cdbeab50 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/schema.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { CasePostRequest, ConnectorTypeFields } from '../../../../../cases/common/api'; +import { CasePostRequest, ConnectorTypeFields } from '../../../../../cases/common'; import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx index f76adfd2a840f..0ecb66d542334 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx @@ -21,9 +21,8 @@ import styled from 'styled-components'; import { noop } from 'lodash/fp'; import { Form, UseField, useForm } from '../../../shared_imports'; -import { ConnectorTypeFields } from '../../../../../cases/common/api/connectors'; +import { ActionConnector, ConnectorTypeFields } from '../../../../../cases/common'; import { ConnectorSelector } from '../connector_selector/form'; -import { ActionConnector } from '../../../../../cases/common/api'; import { ConnectorFieldsForm } from '../connectors/fields_form'; import { getConnectorById } from '../configure_cases/utils'; import { CaseUserActions } from '../../containers/types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx index 6bf4eb95bc049..3c019369fa08b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { StatusActionButton } from './button'; describe('StatusActionButton', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/button.tsx b/x-pack/plugins/security_solution/public/cases/components/status/button.tsx index 5a0d98fc8a11a..6aa8f540e2e95 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/button.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/button.tsx @@ -8,7 +8,7 @@ import React, { memo, useCallback, useMemo } from 'react'; import { EuiButton } from '@elastic/eui'; -import { CaseStatuses, caseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses, caseStatuses } from '../../../../../cases/common'; import { statuses } from './config'; interface Props { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/config.ts b/x-pack/plugins/security_solution/public/cases/components/status/config.ts index 47a74549f03cc..b7bc7dfa36110 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/config.ts +++ b/x-pack/plugins/security_solution/public/cases/components/status/config.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import * as i18n from './translations'; import { AllCaseStatus, Statuses, StatusAll } from './types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/status/stats.test.tsx b/x-pack/plugins/security_solution/public/cases/components/status/stats.test.tsx index 266ceb04e4335..0bf3297361446 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/stats.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/stats.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { Stats } from './stats'; describe('Stats', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/stats.tsx b/x-pack/plugins/security_solution/public/cases/components/status/stats.tsx index 43001c2cf5947..93b8479a55d71 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/stats.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/stats.tsx @@ -7,7 +7,7 @@ import React, { memo, useMemo } from 'react'; import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { statuses } from './config'; export interface Props { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/status.test.tsx b/x-pack/plugins/security_solution/public/cases/components/status/status.test.tsx index eff9d73c2adf9..05c3b95e163e6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/status.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/status.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { Status } from './status'; describe('Stats', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/types.ts b/x-pack/plugins/security_solution/public/cases/components/status/types.ts index 5618e7802579d..bbe44bce55515 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/types.ts +++ b/x-pack/plugins/security_solution/public/cases/components/status/types.ts @@ -6,7 +6,7 @@ */ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; export const StatusAll = 'all' as const; type StatusAllType = typeof StatusAll; diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index decd37a7646e7..ec5a3825ff652 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -16,7 +16,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { CommentType, CaseStatuses } from '../../../../../cases/common/api'; +import { CommentType, CaseStatuses } from '../../../../../cases/common'; import { Ecs } from '../../../../common/ecs'; import { ActionIconItem } from '../../../timelines/components/timeline/body/actions/action_icon_item'; import { usePostComment } from '../../containers/use_post_comment'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index 10ad3d35004ba..54a5dd1263961 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -10,7 +10,7 @@ import styled from 'styled-components'; import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { Case, SubCase } from '../../containers/types'; import { AllCases } from '../all_cases'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx index 0b30f6ac94e03..23cc11ef2ef28 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/index.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { Case, SubCase } from '../../containers/types'; import { AllCasesModal } from './all_cases_modal'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 4b5eb00d95a80..627dc61c36b0c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -14,7 +14,7 @@ import { CreateCaseForm } from '../create/form'; import { SubmitCaseButton } from '../create/submit_button'; import { Case } from '../../containers/types'; import * as i18n from '../../translations'; -import { CaseType } from '../../../../../cases/common/api'; +import { CaseType } from '../../../../../cases/common'; export interface CreateCaseModalProps { isModalOpen: boolean; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx index 5d2f54bd1f142..e29ee3f8712da 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { CaseType } from '../../../../../cases/common/api'; +import { CaseType } from '../../../../../cases/common'; import { Case } from '../../containers/types'; import { CreateCaseModal } from './create_case_modal'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx index c058473bbfe3f..928d0167bbe85 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx @@ -13,12 +13,11 @@ import '../../../common/mock/match_media'; import { usePushToService, ReturnUsePushToService, UsePushToService } from '.'; import { TestProviders } from '../../../common/mock'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses, ConnectorTypes } from '../../../../../cases/common'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { basicPush, actionLicenses } from '../../containers/mock'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { connectorsMock } from '../../containers/configure/mock'; -import { ConnectorTypes } from '../../../../../cases/common/api/connectors'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx index d83ddb08b51d2..42284cfa7da49 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx @@ -17,7 +17,7 @@ import { getConfigureCasesUrl, useFormatUrl } from '../../../common/components/l import { CaseCallOut } from '../callout'; import { getLicenseError, getKibanaConfigError } from './helpers'; import * as i18n from './translations'; -import { CaseConnector, ActionConnector, CaseStatuses } from '../../../../../cases/common/api'; +import { CaseConnector, ActionConnector, CaseStatuses } from '../../../../../cases/common'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { LinkAnchor } from '../../../common/components/links'; import { SecurityPageName } from '../../../app/types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx index a62c6c0ef682d..84408557eb5ae 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../../../cases/common/api'; +import { CaseStatuses } from '../../../../../cases/common'; import { basicPush, getUserAction } from '../../containers/mock'; import { getLabelTitle, getPushedServiceLabelTitle, getConnectorLabelTitle } from './helpers'; import { connectorsMock } from '../../containers/configure/mock'; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx index cc8d560f91b1f..a97e2e98cb9af 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx @@ -15,7 +15,7 @@ import { ActionConnector, CaseStatuses, CommentType, -} from '../../../../../cases/common/api'; +} from '../../../../../cases/common'; import { CaseUserActions } from '../../containers/types'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { parseString } from '../../containers/utils'; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index f8d6872a4b740..d372d62ab16bb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -30,7 +30,7 @@ import { AlertCommentRequestRt, CommentType, ContextTypeUserRt, -} from '../../../../../cases/common/api'; +} from '../../../../../cases/common'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { parseString } from '../../containers/utils'; import { OnUpdateFields } from '../case_view'; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.test.tsx index 3bfdf2d2c5e62..25080d61a951b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.test.tsx @@ -11,7 +11,7 @@ import { mount } from 'enzyme'; import { TestProviders } from '../../../common/mock'; import { useKibana } from '../../../common/lib/kibana'; import { AlertCommentEvent } from './user_action_alert_comment_event'; -import { CommentType } from '../../../../../cases/common/api'; +import { CommentType } from '../../../../../cases/common'; const props = { alertId: 'alert-id-1', diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx index a72bebbaf0999..a1b6587cfeecb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx @@ -15,7 +15,7 @@ import { getRuleDetailsUrl, useFormatUrl } from '../../../common/components/link import { SecurityPageName } from '../../../app/types'; import * as i18n from './translations'; -import { CommentType } from '../../../../../cases/common/api'; +import { CommentType } from '../../../../../cases/common'; import { LinkAnchor } from '../../../common/components/links'; interface Props { diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 644c7dbf716bf..ca7ab5eb9d7dd 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -8,9 +8,14 @@ import { assign, omit } from 'lodash'; import { + ACTION_TYPES_URL, + CASE_REPORTERS_URL, + CASE_STATUS_URL, + CASE_TAGS_URL, CasePatchRequest, CasePostRequest, CaseResponse, + CASES_URL, CasesFindResponse, CasesResponse, CasesStatusResponse, @@ -18,30 +23,19 @@ import { CaseUserActionsResponse, CommentRequest, CommentType, - SubCasePatchRequest, - SubCaseResponse, - SubCasesResponse, - User, -} from '../../../../cases/common/api'; - -import { - ACTION_TYPES_URL, - CASE_REPORTERS_URL, - CASE_STATUS_URL, - CASE_TAGS_URL, - CASES_URL, - SUB_CASE_DETAILS_URL, - SUB_CASES_PATCH_DEL_URL, -} from '../../../../cases/common/constants'; - -import { getCaseCommentsUrl, - getCasePushUrl, getCaseDetailsUrl, + getCasePushUrl, getCaseUserActionUrl, getSubCaseDetailsUrl, getSubCaseUserActionUrl, -} from '../../../../cases/common/api/helpers'; + SUB_CASE_DETAILS_URL, + SUB_CASES_PATCH_DEL_URL, + SubCasePatchRequest, + SubCaseResponse, + SubCasesResponse, + User, +} from '../../../../cases/common'; import { KibanaServices } from '../../common/lib/kibana'; import { StatusAll } from '../components/status'; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts index 943724ef08398..c165c493c16d9 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/api.ts @@ -7,20 +7,16 @@ import { isEmpty } from 'lodash/fp'; import { + ACTION_TYPES_URL, ActionConnector, ActionTypeConnector, + CASE_CONFIGURE_CONNECTORS_URL, + CASE_CONFIGURE_URL, CasesConfigurePatch, - CasesConfigureResponse, CasesConfigureRequest, -} from '../../../../../cases/common/api'; + CasesConfigureResponse, +} from '../../../../../cases/common'; import { KibanaServices } from '../../../common/lib/kibana'; - -import { - CASE_CONFIGURE_CONNECTORS_URL, - CASE_CONFIGURE_URL, - ACTION_TYPES_URL, -} from '../../../../../cases/common/constants'; - import { ApiProps } from '../types'; import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; import { CaseConfigure } from './types'; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx index 2ec2a73363bfe..2087753b26039 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx @@ -15,7 +15,7 @@ import { } from '../../../common/components/toasters'; import * as i18n from './translations'; import { ClosureType, CaseConfigure, CaseConnector, CaseConnectorMapping } from './types'; -import { ConnectorTypes } from '../../../../../cases/common/api/connectors'; +import { ConnectorTypes } from '../../../../../cases/common'; export type ConnectorConfiguration = { connector: CaseConnector } & { closureType: CaseConfigure['closureType']; diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index 6feb5a1501a76..d1c17ea56df65 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -6,20 +6,20 @@ */ import { - User, - UserActionField, - UserAction, - CaseConnector, - CommentRequest, - CaseStatuses, + AssociationType, CaseAttributes, + CaseConnector, CasePatchRequest, + CaseStatuses, CaseType, - AssociationType, -} from '../../../../cases/common/api'; + CommentRequest, + User, + UserAction, + UserActionField, +} from '../../../../cases/common'; import { CaseStatusWithAllStatus } from '../components/status'; -export { CaseConnector, ActionConnector, CaseStatuses } from '../../../../cases/common/api'; +export { CaseConnector, ActionConnector, CaseStatuses } from '../../../../cases/common'; export type Comment = CommentRequest & { associationType: AssociationType; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx index d39da93a06a48..ffb964982d302 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx @@ -6,7 +6,7 @@ */ import { useCallback, useReducer, useRef, useEffect } from 'react'; -import { CaseStatuses } from '../../../../cases/common/api'; +import { CaseStatuses } from '../../../../cases/common'; import { displaySuccessToast, errorToToaster, diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index 7c33e4481b2aa..e447476d02282 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.ts @@ -13,22 +13,22 @@ import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { - CasesFindResponse, - CasesFindResponseRt, + CaseConfigureResponseRt, + CasePatchRequest, CaseResponse, CaseResponseRt, + CasesConfigureResponse, + CasesFindResponse, + CasesFindResponseRt, CasesResponse, CasesResponseRt, - CasesStatusResponseRt, CasesStatusResponse, - throwErrors, - CasesConfigureResponse, - CaseConfigureResponseRt, + CasesStatusResponseRt, CaseUserActionsResponse, CaseUserActionsResponseRt, CommentType, - CasePatchRequest, -} from '../../../../cases/common/api'; + throwErrors, +} from '../../../../cases/common'; import { AppToast, ToasterError } from '../../common/components/toasters'; import { AllCases, Case, UpdateByKey } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx index 875bc5e647077..c19e5c26bdc94 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx @@ -6,7 +6,7 @@ */ import { useEffect, useRef, useState } from 'react'; -import { ACTION_URL } from '../../../../../../cases/common/constants'; +import { ACTION_URL } from '../../../../../../cases/common'; import { KibanaServices } from '../../../../common/lib/kibana'; interface CaseAction { diff --git a/x-pack/plugins/security_solution/public/index.ts b/x-pack/plugins/security_solution/public/index.ts index f1d1bc3e6280b..55262fe039b4e 100644 --- a/x-pack/plugins/security_solution/public/index.ts +++ b/x-pack/plugins/security_solution/public/index.ts @@ -7,8 +7,8 @@ import { PluginInitializerContext } from '../../../../src/core/public'; import { Plugin } from './plugin'; -import { PluginSetup, PluginStart } from './types'; +import { PluginSetup } from './types'; export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); -export { Plugin, PluginSetup, PluginStart }; +export { Plugin, PluginSetup }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index 01a85f6309c3f..4443688fd249d 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -12,7 +12,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; // eslint-disable-next-line no-restricted-imports import isEmpty from 'lodash/isEmpty'; -import { throwErrors } from '../../../../cases/common/api'; +import { throwErrors } from '../../../../cases/common'; import { TimelineResponse, TimelineResponseType, diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index e88077679e1b6..e3d2c345a2a66 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -22,6 +22,7 @@ import { TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, } from '../../triggers_actions_ui/public'; +import { CasesUiStart } from '../../cases/public'; import { SecurityPluginSetup } from '../../security/public'; import { ResolverPluginSetup } from './resolver/types'; import { Inspect } from '../common/search_strategy'; @@ -47,6 +48,7 @@ export interface SetupPlugins { } export interface StartPlugins { + cases: CasesUiStart; data: DataPublicPluginStart; embeddable: EmbeddableStart; inspector: InspectorStart; From 044a94ac461aec7edf5a889cc99aee8097e6e220 Mon Sep 17 00:00:00 2001 From: John Schulz <john.schulz@elastic.co> Date: Tue, 16 Mar 2021 20:51:57 -0400 Subject: [PATCH 40/44] [Fleet] Add test/fix for invalid/missing ids in bulk agent reassign (#94632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem While working on changes for bulk reassign https://github.com/elastic/kibana/issues/90437, I found that the server has a runtime error and returns a 500 if given an invalid or missing id. <details><summary>server error stack trace</summary> ``` │ proc [kibana] server log [12:21:48.953] [error][fleet][plugins] TypeError: Cannot read property 'policy_revision_idx' of undefined │ proc [kibana] at map (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/helpers.ts:15:34) │ proc [kibana] at Array.map (<anonymous>) │ proc [kibana] at getAgents (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/crud.ts:191:32) │ proc [kibana] at runMicrotasks (<anonymous>) │ proc [kibana] at processTicksAndRejections (internal/process/task_queues.js:93:5) │ proc [kibana] at Object.reassignAgents (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/reassign.ts:91:9) │ proc [kibana] at postBulkAgentsReassignHandler (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/routes/agent/handlers.ts:314:21) │ proc [kibana] at Router.handle (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:272:30) │ proc [kibana] at handler (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:227:11) │ proc [kibana] at exports.Manager.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/toolkit.js:60:28) │ proc [kibana] at Object.internals.handler (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:46:20) │ proc [kibana] at exports.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:31:20) │ proc [kibana] at Request._lifecycle (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:370:32) │ proc [kibana] at Request._execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:279:9) ``` </details> <details><summary>see test added in this PR fail on master</summary> ``` 1) Fleet Endpoints reassign agent(s) bulk reassign agents should allow to reassign multiple agents by id -- some invalid: Error: expected 200 "OK", got 500 "Internal Server Error" ``` </details> ## Root cause Debugging runtime error in `searchHitToAgent` found some TS type mismatches for the ES values being returned. Perhaps from one or more of the recent changes to ES client & Fleet Server. Based on `test:jest` and `test:ftr`, it appears the possible types are `GetResponse` or `SearchResponse`, instead of only an `ESSearchHit`. https://github.com/elastic/kibana/pull/94632/files#diff-254d0f427979efc3b442f78762302eb28fb9c8857df68ea04f8d411e052f939cL11 While a `.search` result will include return matched values, a `.get` or `.mget` will return a row for each input and a `found: boolean`. e.g. `{ _id: "does-not-exist", found: false }`. The error occurs when [`searchHitToAgent`](https://github.com/jfsiii/kibana/blob/1702cf98f018c41ec0a080d829a12403168ac242/x-pack/plugins/fleet/server/services/agents/helpers.ts#L11) is run on a get miss instead of a search hit. ## PR Changes * Added a test to ensure it doesn't fail if invalid or missing IDs are given * Moved the `bulk_reassign` tests to their own test section * Filter out any missing results before calling `searchHitToAgent`, to match current behavior * Consolidate repeated arguments into and code for getting agents into single [function](https://github.com/elastic/kibana/pull/94632/files#diff-f7377ed9ad56eaa8ea188b64e957e771ccc7a7652fd1eaf44251c25b930f8448R70-R87): and [TS type](https://github.com/elastic/kibana/pull/94632/files#diff-f7377ed9ad56eaa8ea188b64e957e771ccc7a7652fd1eaf44251c25b930f8448R61-R68) * Rename some agent service functions to be more explicit (IMO) but behavior maintained. Same API names exported. This moves toward the "one result (success or error) per given id" approach for https://github.com/elastic/kibana/issues/90437 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- x-pack/plugins/fleet/server/plugin.ts | 8 +- .../fleet/server/routes/agent/handlers.ts | 9 +- .../fleet/server/routes/agent/index.ts | 2 +- .../server/routes/agent/upgrade_handler.ts | 4 +- .../server/routes/agent_policy/handlers.ts | 4 +- .../fleet/server/services/agent_policy.ts | 4 +- .../agents/checkin/state_new_actions.ts | 4 +- .../fleet/server/services/agents/crud.ts | 92 ++++++--- .../fleet/server/services/agents/helpers.ts | 9 +- .../server/services/agents/reassign.test.ts | 4 +- .../fleet/server/services/agents/reassign.ts | 52 +++-- .../fleet/server/services/agents/status.ts | 6 +- .../fleet/server/services/agents/unenroll.ts | 44 +--- .../fleet/server/services/agents/update.ts | 4 +- .../fleet/server/services/agents/upgrade.ts | 37 +--- x-pack/plugins/fleet/server/services/index.ts | 6 +- .../apis/agents/reassign.ts | 194 ++++++++++-------- 17 files changed, 247 insertions(+), 236 deletions(-) diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 3289a762e57cb..a62da8eb41a99 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -80,8 +80,8 @@ import { import { getAgentStatusById, authenticateAgentWithAccessToken, - listAgents, - getAgent, + getAgentsByKuery, + getAgentById, } from './services/agents'; import { agentCheckinState } from './services/agents/checkin/state'; import { registerFleetUsageCollector } from './collectors/register'; @@ -322,8 +322,8 @@ export class FleetPlugin }, }, agentService: { - getAgent, - listAgents, + getAgent: getAgentById, + listAgents: getAgentsByKuery, getAgentStatusById, authenticateAgentWithAccessToken, }, diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 3745304334719..e6188a83c49e9 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -44,8 +44,7 @@ export const getAgentHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; try { - const agent = await AgentService.getAgent(esClient, request.params.agentId); - + const agent = await AgentService.getAgentById(esClient, request.params.agentId); const body: GetOneAgentResponse = { item: { ...agent, @@ -134,8 +133,7 @@ export const updateAgentHandler: RequestHandler< await AgentService.updateAgent(esClient, request.params.agentId, { user_provided_metadata: request.body.user_provided_metadata, }); - const agent = await AgentService.getAgent(esClient, request.params.agentId); - + const agent = await AgentService.getAgentById(esClient, request.params.agentId); const body = { item: { ...agent, @@ -245,7 +243,7 @@ export const getAgentsHandler: RequestHandler< const esClient = context.core.elasticsearch.client.asCurrentUser; try { - const { agents, total, page, perPage } = await AgentService.listAgents(esClient, { + const { agents, total, page, perPage } = await AgentService.getAgentsByKuery(esClient, { page: request.query.page, perPage: request.query.perPage, showInactive: request.query.showInactive, @@ -310,6 +308,7 @@ export const postBulkAgentsReassignHandler: RequestHandler< const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asInternalUser; + try { const results = await AgentService.reassignAgents( soClient, diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 6236808a5378e..ec75768e816fe 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -125,7 +125,7 @@ export const registerAPIRoutes = (router: IRouter, config: FleetConfigType) => { options: { tags: [`access:${PLUGIN_ID}-all`] }, }, postNewAgentActionHandlerBuilder({ - getAgent: AgentService.getAgent, + getAgent: AgentService.getAgentById, createAgentAction: AgentService.createAgentAction, }) ); diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index f3267c95b0181..279018ef4212c 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -15,7 +15,7 @@ import * as AgentService from '../../services/agents'; import { appContextService } from '../../services'; import { defaultIngestErrorHandler } from '../../errors'; import { isAgentUpgradeable } from '../../../common/services'; -import { getAgent } from '../../services/agents'; +import { getAgentById } from '../../services/agents'; export const postAgentUpgradeHandler: RequestHandler< TypeOf<typeof PostAgentUpgradeRequestSchema.params>, @@ -36,7 +36,7 @@ export const postAgentUpgradeHandler: RequestHandler< }, }); } - const agent = await getAgent(esClient, request.params.agentId); + const agent = await getAgentById(esClient, request.params.agentId); if (agent.unenrollment_started_at || agent.unenrolled_at) { return response.customError({ statusCode: 400, diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index e68b5ce66c4a9..0d37979ef9acb 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -11,7 +11,7 @@ import bluebird from 'bluebird'; import { fullAgentPolicyToYaml } from '../../../common/services'; import { appContextService, agentPolicyService, packagePolicyService } from '../../services'; -import { listAgents } from '../../services/agents'; +import { getAgentsByKuery } from '../../services/agents'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import type { GetAgentPoliciesRequestSchema, @@ -58,7 +58,7 @@ export const getAgentPoliciesHandler: RequestHandler< await bluebird.map( items, (agentPolicy: GetAgentPoliciesResponseItem) => - listAgents(esClient, { + getAgentsByKuery(esClient, { showInactive: false, perPage: 0, page: 1, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 31c184c598b12..2cafe2fe57c01 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -43,7 +43,7 @@ import { } from '../errors'; import { getFullAgentPolicyKibanaConfig } from '../../common/services/full_agent_policy_kibana_config'; -import { createAgentPolicyAction, listAgents } from './agents'; +import { createAgentPolicyAction, getAgentsByKuery } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; @@ -520,7 +520,7 @@ class AgentPolicyService { throw new Error('The default agent policy cannot be deleted'); } - const { total } = await listAgents(esClient, { + const { total } = await getAgentsByKuery(esClient, { showInactive: false, perPage: 0, page: 1, diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts index 37320cb24fe3f..7dc19f63a5adb 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts @@ -39,7 +39,7 @@ import { getAgentPolicyActionByIds, } from '../actions'; import { appContextService } from '../../app_context'; -import { getAgent, updateAgent } from '../crud'; +import { getAgentById, updateAgent } from '../crud'; import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; @@ -266,7 +266,7 @@ export function agentCheckinStateNewActionsFactory() { (action) => action.type === 'INTERNAL_POLICY_REASSIGN' ); if (hasConfigReassign) { - return from(getAgent(esClient, agent.id)).pipe( + return from(getAgentById(esClient, agent.id)).pipe( concatMap((refreshedAgent) => { if (!refreshedAgent.policy_id) { throw new Error('Agent does not have a policy assigned'); diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index 06353d831c60f..52a6b98bd0c41 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import type { SearchResponse } from 'elasticsearch'; +import type { SearchResponse, MGetResponse, GetResponse } from 'elasticsearch'; import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; import type { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; @@ -14,7 +14,6 @@ import { appContextService, agentPolicyService } from '../../services'; import type { FleetServerAgent } from '../../../common'; import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; -import type { ESSearchHit } from '../../../../../../typings/elasticsearch'; import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; import type { KueryNode } from '../../../../../../src/plugins/data/server'; import { esKuery } from '../../../../../../src/plugins/data/server'; @@ -59,7 +58,35 @@ export function removeSOAttributes(kuery: string) { return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); } -export async function listAgents( +export type GetAgentsOptions = + | { + agentIds: string[]; + } + | { + kuery: string; + showInactive?: boolean; + }; + +export async function getAgents(esClient: ElasticsearchClient, options: GetAgentsOptions) { + let initialResults = []; + + if ('agentIds' in options) { + initialResults = await getAgentsById(esClient, options.agentIds); + } else if ('kuery' in options) { + initialResults = ( + await getAllAgentsByKuery(esClient, { + kuery: options.kuery, + showInactive: options.showInactive ?? false, + }) + ).agents; + } else { + throw new IngestManagerError('Cannot get agents'); + } + + return initialResults; +} + +export async function getAgentsByKuery( esClient: ElasticsearchClient, options: ListWithKuery & { showInactive: boolean; @@ -91,8 +118,7 @@ export async function listAgents( const kueryNode = _joinFilters(filters); const body = kueryNode ? { query: esKuery.toElasticsearchQuery(kueryNode) } : {}; - - const res = await esClient.search({ + const res = await esClient.search<SearchResponse<FleetServerAgent>>({ index: AGENTS_INDEX, from: (page - 1) * perPage, size: perPage, @@ -101,27 +127,24 @@ export async function listAgents( body, }); - let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent); - let total = res.body.hits.total.value; - + let agents = res.body.hits.hits.map(searchHitToAgent); // filtering for a range on the version string will not work, // nor does filtering on a flattened field (local_metadata), so filter here if (showUpgradeable) { - agentResults = agentResults.filter((agent) => + agents = agents.filter((agent) => isAgentUpgradeable(agent, appContextService.getKibanaVersion()) ); - total = agentResults.length; } return { - agents: res.body.hits.hits.map(searchHitToAgent), - total, + agents, + total: agents.length, page, perPage, }; } -export async function listAllAgents( +export async function getAllAgentsByKuery( esClient: ElasticsearchClient, options: Omit<ListWithKuery, 'page' | 'perPage'> & { showInactive: boolean; @@ -130,7 +153,7 @@ export async function listAllAgents( agents: Agent[]; total: number; }> { - const res = await listAgents(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT }); + const res = await getAgentsByKuery(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT }); return { agents: res.agents, @@ -161,34 +184,51 @@ export async function countInactiveAgents( return res.body.hits.total.value; } -export async function getAgent(esClient: ElasticsearchClient, agentId: string) { +export async function getAgentById(esClient: ElasticsearchClient, agentId: string) { + const agentNotFoundError = new AgentNotFoundError(`Agent ${agentId} not found`); try { - const agentHit = await esClient.get<ESSearchHit<FleetServerAgent>>({ + const agentHit = await esClient.get<GetResponse<FleetServerAgent>>({ index: AGENTS_INDEX, id: agentId, }); + + if (agentHit.body.found === false) { + throw agentNotFoundError; + } const agent = searchHitToAgent(agentHit.body); return agent; } catch (err) { if (isESClientError(err) && err.meta.statusCode === 404) { - throw new AgentNotFoundError(`Agent ${agentId} not found`); + throw agentNotFoundError; } throw err; } } -export async function getAgents( +async function getAgentDocuments( esClient: ElasticsearchClient, agentIds: string[] -): Promise<Agent[]> { - const body = { docs: agentIds.map((_id) => ({ _id })) }; - - const res = await esClient.mget({ - body, +): Promise<Array<GetResponse<FleetServerAgent>>> { + const res = await esClient.mget<MGetResponse<FleetServerAgent>>({ index: AGENTS_INDEX, + body: { docs: agentIds.map((_id) => ({ _id })) }, }); - const agents = res.body.docs.map(searchHitToAgent); + + return res.body.docs || []; +} + +export async function getAgentsById( + esClient: ElasticsearchClient, + agentIds: string[], + options: { includeMissing?: boolean } = { includeMissing: false } +): Promise<Agent[]> { + const allDocs = await getAgentDocuments(esClient, agentIds); + const agentDocs = options.includeMissing + ? allDocs + : allDocs.filter((res) => res._id && res._source); + const agents = agentDocs.map((doc) => searchHitToAgent(doc)); + return agents; } @@ -201,7 +241,7 @@ export async function getAgentByAccessAPIKeyId( q: `access_api_key_id:${escapeSearchQueryPhrase(accessAPIKeyId)}`, }); - const [agent] = res.body.hits.hits.map(searchHitToAgent); + const agent = searchHitToAgent(res.body.hits.hits[0]); if (!agent) { throw new AgentNotFoundError('Agent not found'); @@ -288,7 +328,7 @@ export async function getAgentPolicyForAgent( esClient: ElasticsearchClient, agentId: string ) { - const agent = await getAgent(esClient, agentId); + const agent = await getAgentById(esClient, agentId); if (!agent.policy_id) { return; } diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts index 3fdb347ed246b..bcc065badcd50 100644 --- a/x-pack/plugins/fleet/server/services/agents/helpers.ts +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -5,10 +5,15 @@ * 2.0. */ -import type { ESSearchHit } from '../../../../../../typings/elasticsearch'; +import type { GetResponse, SearchResponse } from 'elasticsearch'; + import type { Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; -export function searchHitToAgent(hit: ESSearchHit<FleetServerAgent>): Agent { +type FleetServerAgentESResponse = + | GetResponse<FleetServerAgent> + | SearchResponse<FleetServerAgent>['hits']['hits'][0]; + +export function searchHitToAgent(hit: FleetServerAgentESResponse): Agent { return { id: hit._id, ...hit._source, diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts index 29e09312dcd16..987f461587233 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts @@ -121,7 +121,7 @@ function createClientsMock() { case unmanagedAgentPolicySO2.id: return unmanagedAgentPolicySO2; default: - throw new Error('Not found'); + throw new Error(`${id} not found`); } }); soClientMock.bulkGet.mockImplementation(async (options) => { @@ -147,7 +147,7 @@ function createClientsMock() { case agentInUnmanagedDoc._id: return { body: agentInUnmanagedDoc }; default: - throw new Error('Not found'); + throw new Error(`${id} not found`); } }); // @ts-expect-error diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index b221be55cd460..74e60c42b9973 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -8,16 +8,12 @@ import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; +import type { Agent } from '../../types'; import { agentPolicyService } from '../agent_policy'; import { AgentReassignmentError } from '../../errors'; -import { - getAgents, - getAgentPolicyForAgent, - listAllAgents, - updateAgent, - bulkUpdateAgents, -} from './crud'; +import { getAgents, getAgentPolicyForAgent, updateAgent, bulkUpdateAgents } from './crud'; +import type { GetAgentsOptions } from './index'; import { createAgentAction, bulkCreateAgentActions } from './actions'; export async function reassignAgent( @@ -71,13 +67,7 @@ export async function reassignAgentIsAllowed( export async function reassignAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: - | { - agentIds: string[]; - } - | { - kuery: string; - }, + options: { agents: Agent[] } | GetAgentsOptions, newAgentPolicyId: string ): Promise<{ items: Array<{ id: string; success: boolean; error?: Error }> }> { const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); @@ -85,25 +75,29 @@ export async function reassignAgents( throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); } - // Filter to agents that do not already use the new agent policy ID - const agents = - 'agentIds' in options - ? await getAgents(esClient, options.agentIds) - : ( - await listAllAgents(esClient, { - kuery: options.kuery, - showInactive: false, - }) - ).agents; - // And which are allowed to unenroll + const allResults = 'agents' in options ? options.agents : await getAgents(esClient, options); + // which are allowed to unenroll const settled = await Promise.allSettled( - agents.map((agent) => + allResults.map((agent) => reassignAgentIsAllowed(soClient, esClient, agent.id, newAgentPolicyId).then((_) => agent) ) ); - const agentsToUpdate = agents.filter( - (agent, index) => settled[index].status === 'fulfilled' && agent.policy_id !== newAgentPolicyId - ); + + // Filter to agents that do not already use the new agent policy ID + const agentsToUpdate = allResults.filter((agent, index) => { + if (settled[index].status === 'fulfilled') { + if (agent.policy_id === newAgentPolicyId) { + settled[index] = { + status: 'rejected', + reason: new AgentReassignmentError( + `${agent.id} is already assigned to ${newAgentPolicyId}` + ), + }; + } else { + return true; + } + } + }); const res = await bulkUpdateAgents( esClient, diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 930f3ca22ccb1..f3fb01655974e 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -14,13 +14,13 @@ import { AgentStatusKueryHelper } from '../../../common/services'; import { esKuery } from '../../../../../../src/plugins/data/server'; import type { KueryNode } from '../../../../../../src/plugins/data/server'; -import { getAgent, listAgents, removeSOAttributes } from './crud'; +import { getAgentById, getAgentsByKuery, removeSOAttributes } from './crud'; export async function getAgentStatusById( esClient: ElasticsearchClient, agentId: string ): Promise<AgentStatus> { - const agent = await getAgent(esClient, agentId); + const agent = await getAgentById(esClient, agentId); return AgentStatusKueryHelper.getAgentStatus(agent); } @@ -64,7 +64,7 @@ export async function getAgentStatusForAgentPolicy( AgentStatusKueryHelper.buildKueryForUpdatingAgents(), ], (kuery) => - listAgents(esClient, { + getAgentsByKuery(esClient, { showInactive: false, perPage: 0, page: 1, diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index 96ac11c89f687..14f9aa46e9fa6 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -11,12 +11,12 @@ import * as APIKeyService from '../api_keys'; import { AgentUnenrollmentError } from '../../errors'; import { createAgentAction, bulkCreateAgentActions } from './actions'; +import type { GetAgentsOptions } from './crud'; import { - getAgent, + getAgentById, + getAgents, updateAgent, getAgentPolicyForAgent, - getAgents, - listAllAgents, bulkUpdateAgents, } from './crud'; @@ -56,23 +56,9 @@ export async function unenrollAgent( export async function unenrollAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: - | { - agentIds: string[]; - } - | { - kuery: string; - } + options: GetAgentsOptions ) { - const agents = - 'agentIds' in options - ? await getAgents(esClient, options.agentIds) - : ( - await listAllAgents(esClient, { - kuery: options.kuery, - showInactive: false, - }) - ).agents; + const agents = await getAgents(esClient, options); // Filter to agents that are not already unenrolled, or unenrolling const agentsEnrolled = agents.filter( @@ -116,7 +102,7 @@ export async function forceUnenrollAgent( esClient: ElasticsearchClient, agentId: string ) { - const agent = await getAgent(esClient, agentId); + const agent = await getAgentById(esClient, agentId); await Promise.all([ agent.access_api_key_id @@ -136,24 +122,10 @@ export async function forceUnenrollAgent( export async function forceUnenrollAgents( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: - | { - agentIds: string[]; - } - | { - kuery: string; - } + options: GetAgentsOptions ) { // Filter to agents that are not already unenrolled - const agents = - 'agentIds' in options - ? await getAgents(esClient, options.agentIds) - : ( - await listAllAgents(esClient, { - kuery: options.kuery, - showInactive: false, - }) - ).agents; + const agents = await getAgents(esClient, options); const agentsToUpdate = agents.filter((agent) => !agent.unenrolled_at); const now = new Date().toISOString(); const apiKeys: string[] = []; diff --git a/x-pack/plugins/fleet/server/services/agents/update.ts b/x-pack/plugins/fleet/server/services/agents/update.ts index 28f1788f3f9b8..74386efe65613 100644 --- a/x-pack/plugins/fleet/server/services/agents/update.ts +++ b/x-pack/plugins/fleet/server/services/agents/update.ts @@ -9,7 +9,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { listAgents } from './crud'; +import { getAgentsByKuery } from './crud'; import { unenrollAgent } from './unenroll'; export async function unenrollForAgentPolicyId( @@ -20,7 +20,7 @@ export async function unenrollForAgentPolicyId( let hasMore = true; let page = 1; while (hasMore) { - const { agents } = await listAgents(esClient, { + const { agents } = await getAgentsByKuery(esClient, { kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${policyId}"`, page: page++, perPage: 1000, diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index c45b161a79366..12623be0ed044 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -15,13 +15,8 @@ import { isAgentUpgradeable } from '../../../common/services'; import { appContextService } from '../app_context'; import { bulkCreateAgentActions, createAgentAction } from './actions'; -import { - getAgents, - listAllAgents, - updateAgent, - bulkUpdateAgents, - getAgentPolicyForAgent, -} from './crud'; +import type { GetAgentsOptions } from './crud'; +import { getAgents, updateAgent, bulkUpdateAgents, getAgentPolicyForAgent } from './crud'; export async function sendUpgradeAgentAction({ soClient, @@ -82,31 +77,15 @@ export async function ackAgentUpgraded( export async function sendUpgradeAgentsActions( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - options: - | { - agentIds: string[]; - sourceUri: string | undefined; - version: string; - force?: boolean; - } - | { - kuery: string; - sourceUri: string | undefined; - version: string; - force?: boolean; - } + options: GetAgentsOptions & { + sourceUri: string | undefined; + version: string; + force?: boolean; + } ) { const kibanaVersion = appContextService.getKibanaVersion(); // Filter out agents currently unenrolling, agents unenrolled, and agents not upgradeable - const agents = - 'agentIds' in options - ? await getAgents(esClient, options.agentIds) - : ( - await listAllAgents(esClient, { - kuery: options.kuery, - showInactive: false, - }) - ).agents; + const agents = await getAgents(esClient, options); // upgradeable if they pass the version check const upgradeableAgents = options.force diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index 2783ab36cda44..e9a8024a032e5 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -10,7 +10,7 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/ser import type { AgentStatus, Agent, EsAssetReference } from '../types'; -import type { getAgent, listAgents } from './agents'; +import type { getAgentById, getAgentsByKuery } from './agents'; import type { agentPolicyService } from './agent_policy'; import * as settingsService from './settings'; @@ -46,7 +46,7 @@ export interface AgentService { /** * Get an Agent by id */ - getAgent: typeof getAgent; + getAgent: typeof getAgentById; /** * Authenticate an agent with access toekn */ @@ -61,7 +61,7 @@ export interface AgentService { /** * List agents */ - listAgents: typeof listAgents; + listAgents: typeof getAgentsByKuery; } export interface AgentPolicyServiceInterface { diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts index 7b72e7af6bbad..77da9ecce3294 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts @@ -14,7 +14,7 @@ export default function (providerContext: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('fleet_reassign_agent', () => { + describe('reassign agent(s)', () => { before(async () => { await esArchiver.load('fleet/empty_fleet_server'); }); @@ -29,99 +29,121 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.unload('fleet/empty_fleet_server'); }); - it('should allow to reassign single agent', async () => { - await supertest - .put(`/api/fleet/agents/agent1/reassign`) - .set('kbn-xsrf', 'xxx') - .send({ - policy_id: 'policy2', - }) - .expect(200); - const { body } = await supertest.get(`/api/fleet/agents/agent1`); - expect(body.item.policy_id).to.eql('policy2'); - }); + describe('reassign single agent', () => { + it('should allow to reassign single agent', async () => { + await supertest + .put(`/api/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy2', + }) + .expect(200); + const { body } = await supertest.get(`/api/fleet/agents/agent1`); + expect(body.item.policy_id).to.eql('policy2'); + }); - it('should throw an error for invalid policy id for single reassign', async () => { - await supertest - .put(`/api/fleet/agents/agent1/reassign`) - .set('kbn-xsrf', 'xxx') - .send({ - policy_id: 'INVALID_ID', - }) - .expect(404); - }); + it('should throw an error for invalid policy id for single reassign', async () => { + await supertest + .put(`/api/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'INVALID_ID', + }) + .expect(404); + }); - it('should allow to reassign multiple agents by id', async () => { - await supertest - .post(`/api/fleet/agents/bulk_reassign`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: ['agent2', 'agent3'], - policy_id: 'policy2', - }) - .expect(200); - const [agent2data, agent3data] = await Promise.all([ - supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), - supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), - ]); - expect(agent2data.body.item.policy_id).to.eql('policy2'); - expect(agent3data.body.item.policy_id).to.eql('policy2'); - }); + it('can reassign from unmanaged policy to unmanaged', async () => { + // policy2 is not managed + // reassign succeeds + await supertest + .put(`/api/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy2', + }) + .expect(200); + }); + + it('cannot reassign from unmanaged policy to managed', async () => { + // agent1 is enrolled in policy1. set policy1 to managed + await supertest + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxx') + .send({ name: 'Test policy', namespace: 'default', is_managed: true }) + .expect(200); - it('should allow to reassign multiple agents by kuery', async () => { - await supertest - .post(`/api/fleet/agents/bulk_reassign`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: 'fleet-agents.active: true', - policy_id: 'policy2', - }) - .expect(200); - const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); - expect(body.total).to.eql(4); - body.list.forEach((agent: any) => { - expect(agent.policy_id).to.eql('policy2'); + // reassign fails + await supertest + .put(`/api/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy2', + }) + .expect(400); }); }); - it('should throw an error for invalid policy id for bulk reassign', async () => { - await supertest - .post(`/api/fleet/agents/bulk_reassign`) - .set('kbn-xsrf', 'xxx') - .send({ - agents: ['agent2', 'agent3'], - policy_id: 'INVALID_ID', - }) - .expect(404); - }); + describe('bulk reassign agents', () => { + it('should allow to reassign multiple agents by id', async () => { + await supertest + .post(`/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + policy_id: 'policy2', + }) + .expect(200); + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(agent2data.body.item.policy_id).to.eql('policy2'); + expect(agent3data.body.item.policy_id).to.eql('policy2'); + }); - it('can reassign from unmanaged policy to unmanaged', async () => { - // policy2 is not managed - // reassign succeeds - await supertest - .put(`/api/fleet/agents/agent1/reassign`) - .set('kbn-xsrf', 'xxx') - .send({ - policy_id: 'policy2', - }) - .expect(200); - }); - it('cannot reassign from unmanaged policy to managed', async () => { - // agent1 is enrolled in policy1. set policy1 to managed - await supertest - .put(`/api/fleet/agent_policies/policy1`) - .set('kbn-xsrf', 'xxx') - .send({ name: 'Test policy', namespace: 'default', is_managed: true }) - .expect(200); + it('should allow to reassign multiple agents by id -- some invalid', async () => { + await supertest + .post(`/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'INVALID_ID', 'agent3', 'MISSING_ID', 'etc'], + policy_id: 'policy2', + }) + .expect(200); + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent2`), + supertest.get(`/api/fleet/agents/agent3`), + ]); + expect(agent2data.body.item.policy_id).to.eql('policy2'); + expect(agent3data.body.item.policy_id).to.eql('policy2'); + }); - // reassign fails - await supertest - .put(`/api/fleet/agents/agent1/reassign`) - .set('kbn-xsrf', 'xxx') - .send({ - policy_id: 'policy2', - }) - .expect(400); + it('should allow to reassign multiple agents by kuery', async () => { + await supertest + .post(`/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'fleet-agents.active: true', + policy_id: 'policy2', + }) + .expect(200); + const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(4); + body.list.forEach((agent: any) => { + expect(agent.policy_id).to.eql('policy2'); + }); + }); + + it('should throw an error for invalid policy id for bulk reassign', async () => { + await supertest + .post(`/api/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + policy_id: 'INVALID_ID', + }) + .expect(404); + }); }); }); } From bbee40c819822291cdbf935475652b5a0fe07f9b Mon Sep 17 00:00:00 2001 From: Yara Tercero <yctercero@users.noreply.github.com> Date: Tue, 16 Mar 2021 18:46:20 -0700 Subject: [PATCH 41/44] [Lists][Exceptions] - Adding basic linting, i18n and storybook support (#94772) ### Summary In preparation for moving all the exceptions UI components into the lists plugin adds some linting, adds the lists plugin to the i18n config and adds storybook support. Tried to add a bit stricter linting than exists in the security solution right now, rules that we've talked about wanting to enable. --- .eslintrc.js | 57 ++++++++++++++++++++++++- src/dev/storybook/aliases.ts | 1 + test/scripts/jenkins_storybook.sh | 1 + x-pack/.i18nrc.json | 1 + x-pack/plugins/lists/.storybook/main.js | 8 ++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/lists/.storybook/main.js diff --git a/.eslintrc.js b/.eslintrc.js index 1eb8faa2469c8..b70090a50e64d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -947,7 +947,62 @@ module.exports = { }, }, { - // typescript and javascript for front and back end + // typescript for /public and /common + files: ['x-pack/plugins/lists/public/*.{ts,tsx}', 'x-pack/plugins/lists/common/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-for-in-array': 'error', + }, + }, + { + // typescript for /public and /common + files: ['x-pack/plugins/lists/public/*.{ts,tsx}', 'x-pack/plugins/lists/common/*.{ts,tsx}'], + plugins: ['react'], + env: { + jest: true, + }, + rules: { + 'react/boolean-prop-naming': 'error', + 'react/button-has-type': 'error', + 'react/display-name': 'error', + 'react/forbid-dom-props': 'error', + 'react/no-access-state-in-setstate': 'error', + 'react/no-children-prop': 'error', + 'react/no-danger-with-children': 'error', + 'react/no-deprecated': 'error', + 'react/no-did-mount-set-state': 'error', + 'react/no-did-update-set-state': 'error', + 'react/no-direct-mutation-state': 'error', + 'react/no-find-dom-node': 'error', + 'react/no-redundant-should-component-update': 'error', + 'react/no-render-return-value': 'error', + 'react/no-typos': 'error', + 'react/no-string-refs': 'error', + 'react/no-this-in-sfc': 'error', + 'react/no-unescaped-entities': 'error', + 'react/no-unsafe': 'error', + 'react/no-unused-prop-types': 'error', + 'react/no-unused-state': 'error', + 'react/sort-comp': 'error', + 'react/void-dom-elements-no-children': 'error', + 'react/jsx-no-comment-textnodes': 'error', + 'react/jsx-no-literals': 'error', + 'react/jsx-no-target-blank': 'error', + 'react/jsx-fragments': 'error', + 'react/jsx-sort-default-props': 'error', + }, + }, + { + files: ['x-pack/plugins/lists/public/**/!(*.test).{js,mjs,ts,tsx}'], + plugins: ['react-perf'], + rules: { + 'react-perf/jsx-no-new-object-as-prop': 'error', + 'react-perf/jsx-no-new-array-as-prop': 'error', + 'react-perf/jsx-no-new-function-as-prop': 'error', + 'react/jsx-no-bind': 'error', + }, + }, + { + // typescript and javascript for front and back files: ['x-pack/plugins/lists/**/*.{js,mjs,ts,tsx}'], plugins: ['eslint-plugin-node'], env: { diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index f1a3737747573..e0f0432c61463 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -22,4 +22,5 @@ export const storybookAliases = { ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/.storybook', observability: 'x-pack/plugins/observability/.storybook', presentation: 'src/plugins/presentation_util/storybook', + lists: 'x-pack/plugins/lists/.storybook', }; diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh index abddedf95a0a6..5c99654f16cbe 100755 --- a/test/scripts/jenkins_storybook.sh +++ b/test/scripts/jenkins_storybook.sh @@ -21,3 +21,4 @@ yarn storybook --site security_solution yarn storybook --site ui_actions_enhanced yarn storybook --site observability yarn storybook --site presentation +yarn storybook --site lists diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 14579a6461bb7..663ae32f9128a 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -33,6 +33,7 @@ "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", + "xpack.lists": "plugins/lists", "xpack.logstash": ["plugins/logstash"], "xpack.main": "legacy/plugins/xpack_main", "xpack.maps": ["plugins/maps"], diff --git a/x-pack/plugins/lists/.storybook/main.js b/x-pack/plugins/lists/.storybook/main.js new file mode 100644 index 0000000000000..86b48c32f103e --- /dev/null +++ b/x-pack/plugins/lists/.storybook/main.js @@ -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. + */ + +module.exports = require('@kbn/storybook').defaultConfig; From 83c6bcc554df0099c92ec6864c7adc63d24522d5 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger <walter@elastic.co> Date: Wed, 17 Mar 2021 07:53:46 +0100 Subject: [PATCH 42/44] [ML] Anomaly Detection: Migrate validation messages links to use docLinks. (#94568) - To make use of the docsLinks service which is only usable in client side code, Anomaly Detection's validation messages are not fully returned from the server anymore. Instead just the message ID and necessary metadata to parse the message template gets returned. - getMessages() no longer uses inline hard coded documentation links but picks links from the docsLinks service. - The code that rendered the messages originally on the server has been move to a function parseMessages() which can now be used on the client side and accepts the docsLinks services to get URLs to documentation from it. - This means we no longer need to get the current version/branch information for the server side code. - Tests have been updated to reflect the changes: API integration tests only check for the now reduced messages containing only message IDs and metadata. The expected results of the API integration tests are used as mocks for the client side function parseMessages(), this allows use to cover the same code and messages as previously. --- .../public/doc_links/doc_links_service.ts | 9 + .../ml/common/constants/messages.test.mock.ts | 81 ++++++++ .../ml/common/constants/messages.test.ts | 178 ++++++++++++++++++ .../plugins/ml/common/constants/messages.ts | 137 ++++++++------ .../validate_job/validate_job_view.js | 4 +- .../job_validation/job_validation.test.ts | 27 --- .../models/job_validation/job_validation.ts | 42 +---- x-pack/plugins/ml/server/plugin.ts | 4 +- .../ml/server/routes/job_validation.ts | 7 +- .../apis/ml/job_validation/validate.ts | 153 +-------------- 10 files changed, 363 insertions(+), 279 deletions(-) create mode 100644 x-pack/plugins/ml/common/constants/messages.test.mock.ts create mode 100644 x-pack/plugins/ml/common/constants/messages.test.ts diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 6e52245e16bbf..759253aec80e5 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -161,7 +161,16 @@ export class DocLinksService { aggregations: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-aggregation.html`, anomalyDetection: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/xpack-ml.html`, anomalyDetectionJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-jobs.html`, + anomalyDetectionConfiguringCategories: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-categories.html`, + anomalyDetectionBucketSpan: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#bucket-span`, + anomalyDetectionCardinality: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#cardinality`, + anomalyDetectionCreateJobs: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html`, + anomalyDetectionDetectors: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#detectors`, + anomalyDetectionInfluencers: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-influencers.html`, + anomalyDetectionJobResource: `${ELASTICSEARCH_DOCS}ml-put-job.html#ml-put-job-path-parms`, + anomalyDetectionJobResourceAnalysisConfig: `${ELASTICSEARCH_DOCS}ml-put-job.html#put-analysisconfig`, anomalyDetectionJobTips: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#job-tips`, + anomalyDetectionModelMemoryLimits: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/create-jobs.html#model-memory-limits`, calendars: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-calendars.html`, classificationEvaluation: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-classification`, customRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-rules.html`, diff --git a/x-pack/plugins/ml/common/constants/messages.test.mock.ts b/x-pack/plugins/ml/common/constants/messages.test.mock.ts new file mode 100644 index 0000000000000..6e539617604c1 --- /dev/null +++ b/x-pack/plugins/ml/common/constants/messages.test.mock.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* To keep tests in sync, these mocks should be used in API intregation tests + * as expected values to check against, and in the client side jest tests to be + * the values used as function arguments for `parseMessages()` to retrieve the + * messages populated with translations and documentation links. + */ + +export const basicValidJobMessages = [ + { + id: 'job_id_valid', + }, + { + id: 'detectors_function_not_empty', + }, + { + id: 'success_bucket_span', + bucketSpan: '15m', + }, + { + id: 'success_time_range', + }, + { + id: 'success_mml', + }, +]; + +export const basicInvalidJobMessages = [ + { + id: 'job_id_invalid', + }, + { + id: 'detectors_function_not_empty', + }, + { + id: 'bucket_span_valid', + bucketSpan: '15m', + }, + { + id: 'skipped_extended_tests', + }, +]; + +export const nonBasicIssuesMessages = [ + { + id: 'job_id_valid', + }, + { + id: 'detectors_function_not_empty', + }, + { + id: 'cardinality_model_plot_high', + }, + { + id: 'cardinality_partition_field', + fieldName: 'order_id', + }, + { + id: 'bucket_span_high', + }, + { + bucketSpanCompareFactor: 25, + id: 'time_range_short', + minTimeSpanReadable: '2 hours', + }, + { + id: 'success_influencers', + }, + { + id: 'half_estimated_mml_greater_than_mml', + mml: '1MB', + }, + { + id: 'missing_summary_count_field_name', + }, +]; diff --git a/x-pack/plugins/ml/common/constants/messages.test.ts b/x-pack/plugins/ml/common/constants/messages.test.ts new file mode 100644 index 0000000000000..1141eea2c176d --- /dev/null +++ b/x-pack/plugins/ml/common/constants/messages.test.ts @@ -0,0 +1,178 @@ +/* + * 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 { docLinksServiceMock } from 'src/core/public/mocks'; + +import { parseMessages } from './messages'; + +import { + basicValidJobMessages, + basicInvalidJobMessages, + nonBasicIssuesMessages, +} from './messages.test.mock'; + +describe('Constants: Messages parseMessages()', () => { + const docLinksService = docLinksServiceMock.createStartContract(); + + it('should parse valid job configuration messages', () => { + expect(parseMessages(basicValidJobMessages, docLinksService)).toStrictEqual([ + { + heading: 'Job ID format is valid', + id: 'job_id_valid', + status: 'success', + text: + 'Lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores, starts and ends with an alphanumeric character, and is no more than 64 characters long.', + url: + 'https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/ml-put-job.html#ml-put-job-path-parms', + }, + { + heading: 'Detector functions', + id: 'detectors_function_not_empty', + status: 'success', + text: 'Presence of detector functions validated in all detectors.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#detectors', + }, + { + bucketSpan: '15m', + heading: 'Bucket span', + id: 'success_bucket_span', + status: 'success', + text: 'Format of "15m" is valid and passed validation checks.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#bucket-span', + }, + { + heading: 'Time range', + id: 'success_time_range', + status: 'success', + text: 'Valid and long enough to model patterns in the data.', + }, + { + heading: 'Model memory limit', + id: 'success_mml', + status: 'success', + text: 'Valid and within the estimated model memory limit.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#model-memory-limits', + }, + ]); + }); + + it('should parse basic invalid job configuration messages', () => { + expect(parseMessages(basicInvalidJobMessages, docLinksService)).toStrictEqual([ + { + id: 'job_id_invalid', + status: 'error', + text: + 'Job ID is invalid. It can contain lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores and must start and end with an alphanumeric character.', + url: + 'https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/ml-put-job.html#ml-put-job-path-parms', + }, + { + heading: 'Detector functions', + id: 'detectors_function_not_empty', + status: 'success', + text: 'Presence of detector functions validated in all detectors.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#detectors', + }, + { + bucketSpan: '15m', + heading: 'Bucket span', + id: 'bucket_span_valid', + status: 'success', + text: 'Format of "15m" is valid.', + url: + 'https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/ml-put-job.html#put-analysisconfig', + }, + { + id: 'skipped_extended_tests', + status: 'warning', + text: + 'Skipped additional checks because the basic requirements of the job configuration were not met.', + }, + ]); + }); + + it('should parse non-basic issues messages', () => { + expect(parseMessages(nonBasicIssuesMessages, docLinksService)).toStrictEqual([ + { + heading: 'Job ID format is valid', + id: 'job_id_valid', + status: 'success', + text: + 'Lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores, starts and ends with an alphanumeric character, and is no more than 64 characters long.', + url: + 'https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/ml-put-job.html#ml-put-job-path-parms', + }, + { + heading: 'Detector functions', + id: 'detectors_function_not_empty', + status: 'success', + text: 'Presence of detector functions validated in all detectors.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#detectors', + }, + { + id: 'cardinality_model_plot_high', + status: 'warning', + text: + 'The estimated cardinality of undefined of fields relevant to creating model plots might result in resource intensive jobs.', + }, + { + fieldName: 'order_id', + id: 'cardinality_partition_field', + status: 'warning', + text: + 'Cardinality of partition_field "order_id" is above 1000 and might result in high memory usage.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#cardinality', + }, + { + heading: 'Bucket span', + id: 'bucket_span_high', + status: 'info', + text: + 'Bucket span is 1 day or more. Be aware that days are considered as UTC days, not local days.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#bucket-span', + }, + { + bucketSpanCompareFactor: 25, + heading: 'Time range', + id: 'time_range_short', + minTimeSpanReadable: '2 hours', + status: 'warning', + text: + 'The selected or available time range might be too short. The recommended minimum time range should be at least 2 hours and 25 times the bucket span.', + }, + { + id: 'success_influencers', + status: 'success', + text: 'Influencer configuration passed the validation checks.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/ml-influencers.html', + }, + { + id: 'half_estimated_mml_greater_than_mml', + mml: '1MB', + status: 'warning', + text: + 'The specified model memory limit is less than half of the estimated model memory limit and will likely hit the hard limit.', + url: + 'https://www.elastic.co/guide/en/machine-learning/mocked-test-branch/create-jobs.html#model-memory-limits', + }, + { + id: 'missing_summary_count_field_name', + status: 'error', + text: + 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', + }, + ]); + }); +}); diff --git a/x-pack/plugins/ml/common/constants/messages.ts b/x-pack/plugins/ml/common/constants/messages.ts index 551bb364ea357..0327e8746c7d8 100644 --- a/x-pack/plugins/ml/common/constants/messages.ts +++ b/x-pack/plugins/ml/common/constants/messages.ts @@ -7,8 +7,13 @@ import { once } from 'lodash'; import { i18n } from '@kbn/i18n'; + +import type { DocLinksStart } from 'kibana/public'; + import { JOB_ID_MAX_LENGTH, VALIDATION_STATUS } from './validation'; +import { renderTemplate } from '../util/string_utils'; + export type MessageId = keyof ReturnType<typeof getMessages>; export interface JobValidationMessageDef { @@ -40,9 +45,9 @@ export type JobValidationMessage = { [key: string]: any; }; -export const getMessages = once(() => { - const createJobsDocsUrl = `https://www.elastic.co/guide/en/machine-learning/{{version}}/create-jobs.html`; - +// This is still consumed by a legacy class based React component. +// Once we migrate that component to use hooks, we may replace `once()` with `useMemo()`. +export const getMessages = once((docLinks?: DocLinksStart) => { return { categorizer_detector_missing_per_partition_field: { status: VALIDATION_STATUS.ERROR, @@ -53,8 +58,7 @@ export const getMessages = once(() => { 'Partition field must be set for detectors that reference "mlcategory" when per-partition categorization is enabled.', } ), - url: - 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-categories.html', + url: docLinks?.links.ml.anomalyDetectionConfiguringCategories, }, categorizer_varying_per_partition_fields: { status: VALIDATION_STATUS.ERROR, @@ -69,8 +73,7 @@ export const getMessages = once(() => { }, } ), - url: - 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-categories.html', + url: docLinks?.links.ml.anomalyDetectionConfiguringCategories, }, field_not_aggregatable: { status: VALIDATION_STATUS.ERROR, @@ -80,16 +83,14 @@ export const getMessages = once(() => { fieldName: '"{{fieldName}}"', }, }), - url: - 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-aggregation.html', + url: docLinks?.links.ml.aggregrations, }, fields_not_aggregatable: { status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.fieldsNotAggregatableMessage', { defaultMessage: 'One of the detector fields is not an aggregatable field.', }), - url: - 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-aggregation.html', + url: docLinks?.links.ml.aggregrations, }, cardinality_no_results: { status: VALIDATION_STATUS.WARNING, @@ -120,8 +121,7 @@ export const getMessages = once(() => { }, } ), - url: - 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-aggregation.html', + url: docLinks?.links.ml.aggregrations, }, cardinality_by_field: { status: VALIDATION_STATUS.WARNING, @@ -132,7 +132,7 @@ export const getMessages = once(() => { fieldName: 'by_field "{{fieldName}}"', }, }), - url: `${createJobsDocsUrl}#cardinality`, + url: docLinks?.links.ml.anomalyDetectionCardinality, }, cardinality_over_field_low: { status: VALIDATION_STATUS.WARNING, @@ -146,7 +146,7 @@ export const getMessages = once(() => { }, } ), - url: `${createJobsDocsUrl}#cardinality`, + url: docLinks?.links.ml.anomalyDetectionCardinality, }, cardinality_over_field_high: { status: VALIDATION_STATUS.WARNING, @@ -160,7 +160,7 @@ export const getMessages = once(() => { }, } ), - url: `${createJobsDocsUrl}#cardinality`, + url: docLinks?.links.ml.anomalyDetectionCardinality, }, cardinality_partition_field: { status: VALIDATION_STATUS.WARNING, @@ -174,7 +174,7 @@ export const getMessages = once(() => { }, } ), - url: `${createJobsDocsUrl}#cardinality`, + url: docLinks?.links.ml.anomalyDetectionCardinality, }, cardinality_model_plot_high: { status: VALIDATION_STATUS.WARNING, @@ -198,8 +198,7 @@ export const getMessages = once(() => { defaultMessage: 'Categorization filters checks passed.', } ), - url: - 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-configuring-categories.html', + url: docLinks?.links.ml.anomalyDetectionConfiguringCategories, }, categorization_filters_invalid: { status: VALIDATION_STATUS.ERROR, @@ -214,16 +213,14 @@ export const getMessages = once(() => { }, } ), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-analysisconfig', + url: docLinks?.links.ml.anomalyDetectionJobResourceAnalysisConfig, }, bucket_span_empty: { status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.bucketSpanEmptyMessage', { defaultMessage: 'The bucket span field must be specified.', }), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-analysisconfig', + url: docLinks?.links.ml.anomalyDetectionJobResourceAnalysisConfig, }, bucket_span_estimation_mismatch: { status: VALIDATION_STATUS.INFO, @@ -244,7 +241,7 @@ export const getMessages = once(() => { }, } ), - url: `${createJobsDocsUrl}#bucket-span`, + url: docLinks?.links.ml.anomalyDetectionBucketSpan, }, bucket_span_high: { status: VALIDATION_STATUS.INFO, @@ -255,7 +252,7 @@ export const getMessages = once(() => { defaultMessage: 'Bucket span is 1 day or more. Be aware that days are considered as UTC days, not local days.', }), - url: `${createJobsDocsUrl}#bucket-span`, + url: docLinks?.links.ml.anomalyDetectionBucketSpan, }, bucket_span_valid: { status: VALIDATION_STATUS.SUCCESS, @@ -268,8 +265,7 @@ export const getMessages = once(() => { bucketSpan: '"{{bucketSpan}}"', }, }), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-analysisconfig', + url: docLinks?.links.ml.anomalyDetectionJobResourceAnalysisConfig, }, bucket_span_invalid: { status: VALIDATION_STATUS.ERROR, @@ -280,8 +276,7 @@ export const getMessages = once(() => { defaultMessage: 'The specified bucket span is not a valid time interval format e.g. 10m, 1h. It also needs to be higher than zero.', }), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-analysisconfig', + url: docLinks?.links.ml.anomalyDetectionJobResourceAnalysisConfig, }, detectors_duplicates: { status: VALIDATION_STATUS.ERROR, @@ -298,21 +293,21 @@ export const getMessages = once(() => { partitionFieldNameParam: `'partition_field_name'`, }, }), - url: `${createJobsDocsUrl}#detectors`, + url: docLinks?.links.ml.anomalyDetectionDetectors, }, detectors_empty: { status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.detectorsEmptyMessage', { defaultMessage: 'No detectors were found. At least one detector must be specified.', }), - url: `${createJobsDocsUrl}#detectors`, + url: docLinks?.links.ml.anomalyDetectionDetectors, }, detectors_function_empty: { status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.detectorsFunctionEmptyMessage', { defaultMessage: 'One of the detector functions is empty.', }), - url: `${createJobsDocsUrl}#detectors`, + url: docLinks?.links.ml.anomalyDetectionDetectors, }, detectors_function_not_empty: { status: VALIDATION_STATUS.SUCCESS, @@ -328,7 +323,7 @@ export const getMessages = once(() => { defaultMessage: 'Presence of detector functions validated in all detectors.', } ), - url: `${createJobsDocsUrl}#detectors`, + url: docLinks?.links.ml.anomalyDetectionDetectors, }, index_fields_invalid: { status: VALIDATION_STATUS.ERROR, @@ -349,7 +344,7 @@ export const getMessages = once(() => { 'The job configuration includes more than 3 influencers. ' + 'Consider using fewer influencers or creating multiple jobs.', }), - url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', + url: docLinks?.links.ml.anomalyDetectionInfluencers, }, influencer_low: { status: VALIDATION_STATUS.WARNING, @@ -357,7 +352,7 @@ export const getMessages = once(() => { defaultMessage: 'No influencers have been configured. Picking an influencer is strongly recommended.', }), - url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', + url: docLinks?.links.ml.anomalyDetectionInfluencers, }, influencer_low_suggestion: { status: VALIDATION_STATUS.WARNING, @@ -369,7 +364,7 @@ export const getMessages = once(() => { values: { influencerSuggestion: '{{influencerSuggestion}}' }, } ), - url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', + url: docLinks?.links.ml.anomalyDetectionInfluencers, }, influencer_low_suggestions: { status: VALIDATION_STATUS.WARNING, @@ -381,15 +376,14 @@ export const getMessages = once(() => { values: { influencerSuggestion: '{{influencerSuggestion}}' }, } ), - url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', + url: docLinks?.links.ml.anomalyDetectionInfluencers, }, job_id_empty: { status: VALIDATION_STATUS.ERROR, text: i18n.translate('xpack.ml.models.jobValidation.messages.jobIdEmptyMessage', { defaultMessage: 'Job ID field must not be empty.', }), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', + url: docLinks?.links.ml.anomalyDetectionJobResource, }, job_id_invalid: { status: VALIDATION_STATUS.ERROR, @@ -398,8 +392,7 @@ export const getMessages = once(() => { 'Job ID is invalid. It can contain lowercase alphanumeric (a-z and 0-9) characters, ' + 'hyphens or underscores and must start and end with an alphanumeric character.', }), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', + url: docLinks?.links.ml.anomalyDetectionJobResource, }, job_id_invalid_max_length: { status: VALIDATION_STATUS.ERROR, @@ -413,8 +406,7 @@ export const getMessages = once(() => { }, } ), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', + url: docLinks?.links.ml.anomalyDetectionJobResource, }, job_id_valid: { status: VALIDATION_STATUS.SUCCESS, @@ -430,8 +422,7 @@ export const getMessages = once(() => { maxLength: JOB_ID_MAX_LENGTH, }, }), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', + url: docLinks?.links.ml.anomalyDetectionJobResource, }, job_group_id_invalid: { status: VALIDATION_STATUS.ERROR, @@ -440,8 +431,7 @@ export const getMessages = once(() => { 'One of the job group names is invalid. They can contain lowercase ' + 'alphanumeric (a-z and 0-9) characters, hyphens or underscores and must start and end with an alphanumeric character.', }), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', + url: docLinks?.links.ml.anomalyDetectionJobResource, }, job_group_id_invalid_max_length: { status: VALIDATION_STATUS.ERROR, @@ -455,8 +445,7 @@ export const getMessages = once(() => { }, } ), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', + url: docLinks?.links.ml.anomalyDetectionJobResource, }, job_group_id_valid: { status: VALIDATION_STATUS.SUCCESS, @@ -472,8 +461,7 @@ export const getMessages = once(() => { maxLength: JOB_ID_MAX_LENGTH, }, }), - url: - 'https://www.elastic.co/guide/en/elasticsearch/reference/{{version}}/ml-job-resource.html#ml-job-resource', + url: docLinks?.links.ml.anomalyDetectionJobResource, }, missing_summary_count_field_name: { status: VALIDATION_STATUS.ERROR, @@ -500,7 +488,7 @@ export const getMessages = once(() => { text: i18n.translate('xpack.ml.models.jobValidation.messages.successCardinalityMessage', { defaultMessage: 'Cardinality of detector fields is within recommended bounds.', }), - url: `${createJobsDocsUrl}#cardinality`, + url: docLinks?.links.ml.anomalyDetectionCardinality, }, success_bucket_span: { status: VALIDATION_STATUS.SUCCESS, @@ -511,14 +499,14 @@ export const getMessages = once(() => { defaultMessage: 'Format of {bucketSpan} is valid and passed validation checks.', values: { bucketSpan: '"{{bucketSpan}}"' }, }), - url: `${createJobsDocsUrl}#bucket-span`, + url: docLinks?.links.ml.anomalyDetectionBucketSpan, }, success_influencers: { status: VALIDATION_STATUS.SUCCESS, text: i18n.translate('xpack.ml.models.jobValidation.messages.successInfluencersMessage', { defaultMessage: 'Influencer configuration passed the validation checks.', }), - url: 'https://www.elastic.co/guide/en/machine-learning/{{version}}/ml-influencers.html', + url: docLinks?.links.ml.anomalyDetectionInfluencers, }, estimated_mml_greater_than_max_mml: { status: VALIDATION_STATUS.WARNING, @@ -556,7 +544,7 @@ export const getMessages = once(() => { '1MB and should be specified in bytes e.g. 10MB.', values: { mml: '{{mml}}' }, }), - url: `${createJobsDocsUrl}#model-memory-limits`, + url: docLinks?.links.ml.anomalyDetectionModelMemoryLimits, }, half_estimated_mml_greater_than_mml: { status: VALIDATION_STATUS.WARNING, @@ -568,7 +556,7 @@ export const getMessages = once(() => { 'memory limit and will likely hit the hard limit.', } ), - url: `${createJobsDocsUrl}#model-memory-limits`, + url: docLinks?.links.ml.anomalyDetectionModelMemoryLimits, }, estimated_mml_greater_than_mml: { status: VALIDATION_STATUS.INFO, @@ -579,7 +567,7 @@ export const getMessages = once(() => { 'The estimated model memory limit is greater than the model memory limit you have configured.', } ), - url: `${createJobsDocsUrl}#model-memory-limits`, + url: docLinks?.links.ml.anomalyDetectionModelMemoryLimits, }, success_mml: { status: VALIDATION_STATUS.SUCCESS, @@ -589,7 +577,7 @@ export const getMessages = once(() => { text: i18n.translate('xpack.ml.models.jobValidation.messages.successMmlMessage', { defaultMessage: 'Valid and within the estimated model memory limit.', }), - url: `${createJobsDocsUrl}#model-memory-limits`, + url: docLinks?.links.ml.anomalyDetectionModelMemoryLimits, }, success_time_range: { status: VALIDATION_STATUS.SUCCESS, @@ -640,3 +628,36 @@ export const getMessages = once(() => { }, }; }); + +export const parseMessages = ( + validationMessages: JobValidationMessage[], + docLinks: DocLinksStart +) => { + const messages = getMessages(docLinks); + + return validationMessages.map((message) => { + const messageId = message.id as MessageId; + const messageDef = messages[messageId] as JobValidationMessageDef; + if (typeof messageDef !== 'undefined') { + // render the message template with the provided metadata + if (typeof messageDef.heading !== 'undefined') { + message.heading = renderTemplate(messageDef.heading, message); + } + message.text = renderTemplate(messageDef.text, message); + // check if the error message provides a link with further information + // if so, add it to the message to be returned with it + if (typeof messageDef.url !== 'undefined') { + message.url = messageDef.url; + } + + message.status = messageDef.status; + } else { + message.text = i18n.translate('xpack.ml.models.jobValidation.unknownMessageIdErrorMessage', { + defaultMessage: '{messageId} (unknown message id)', + values: { messageId }, + }); + } + + return message; + }); +}; diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index c05e0fa21b304..14e242ee69211 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -28,6 +28,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { getDocLinks } from '../../util/dependency_cache'; +import { parseMessages } from '../../../../common/constants/messages'; import { VALIDATION_STATUS } from '../../../../common/constants/validation'; import { Callout, statusToEuiIconType } from '../callout'; import { getMostSevereMessageStatus } from '../../../../common/util/validation_utils'; @@ -132,7 +133,8 @@ export class ValidateJobUI extends Component { this.props.ml .validateJob({ duration, fields, job }) - .then((messages) => { + .then((validationMessages) => { + const messages = parseMessages(validationMessages, getDocLinks()); shouldShowLoadingIndicator = false; const messagesContainError = messages.some((m) => m.status === VALIDATION_STATUS.ERROR); diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index c3c3d52465d40..949159b67d33a 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -8,7 +8,6 @@ import { IScopedClusterClient } from 'kibana/server'; import { validateJob, ValidateJobPayload } from './job_validation'; -import { JobValidationMessage } from '../../../common/constants/messages'; import { HITS_TOTAL_RELATION } from '../../../common/types/es_client'; import type { MlClient } from '../../lib/ml_client'; @@ -277,7 +276,6 @@ describe('ML - validateJob', () => { }); }); - // Failing https://github.com/elastic/kibana/issues/65865 it('basic validation passes, extended checks return some messages', () => { const payload = getBasicPayload(); return validateJob(mlClusterClient, mlClient, payload).then((messages) => { @@ -291,7 +289,6 @@ describe('ML - validateJob', () => { }); }); - // Failing https://github.com/elastic/kibana/issues/65866 it('categorization job using mlcategory passes aggregatable field check', () => { const payload: any = { job: { @@ -358,7 +355,6 @@ describe('ML - validateJob', () => { }); }); - // Failing https://github.com/elastic/kibana/issues/65867 it('script field not reported as non aggregatable', () => { const payload: any = { job: { @@ -401,27 +397,4 @@ describe('ML - validateJob', () => { ]); }); }); - - // the following two tests validate the correct template rendering of - // urls in messages with {{version}} in them to be replaced with the - // specified version. (defaulting to 'current') - const docsTestPayload = getBasicPayload() as any; - docsTestPayload.job.analysis_config.detectors = [{ function: 'count', by_field_name: 'airline' }]; - it('creates a docs url pointing to the current docs version', () => { - return validateJob(mlClusterClient, mlClient, docsTestPayload).then((messages) => { - const message = messages[ - messages.findIndex((m) => m.id === 'field_not_aggregatable') - ] as JobValidationMessage; - expect(message.url!.search('/current/')).not.toBe(-1); - }); - }); - - it('creates a docs url pointing to the master docs version', () => { - return validateJob(mlClusterClient, mlClient, docsTestPayload, 'master').then((messages) => { - const message = messages[ - messages.findIndex((m) => m.id === 'field_not_aggregatable') - ] as JobValidationMessage; - expect(message.url!.search('/master/')).not.toBe(-1); - }); - }); }); diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 5f2fe180577c9..31d98753f0bd1 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -5,17 +5,11 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import Boom from '@hapi/boom'; import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { fieldsServiceProvider } from '../fields_service'; -import { renderTemplate } from '../../../common/util/string_utils'; -import { - getMessages, - MessageId, - JobValidationMessageDef, -} from '../../../common/constants/messages'; +import { getMessages, MessageId, JobValidationMessage } from '../../../common/constants/messages'; import { VALIDATION_STATUS } from '../../../common/constants/validation'; import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_utils'; @@ -40,7 +34,6 @@ export async function validateJob( client: IScopedClusterClient, mlClient: MlClient, payload: ValidateJobPayload, - kbnVersion = 'current', isSecurityDisabled?: boolean ) { const messages = getMessages(); @@ -55,7 +48,7 @@ export async function validateJob( // if so, run the extended tests and merge the messages. // otherwise just return the basic test messages. const basicValidation = basicJobValidation(job, fields, {}, true); - let validationMessages; + let validationMessages: JobValidationMessage[]; if (basicValidation.valid === true) { // remove basic success messages from tests @@ -113,36 +106,7 @@ export async function validateJob( validationMessages.push({ id: 'skipped_extended_tests' }); } - return uniqWithIsEqual(validationMessages).map((message) => { - const messageId = message.id as MessageId; - const messageDef = messages[messageId] as JobValidationMessageDef; - if (typeof messageDef !== 'undefined') { - // render the message template with the provided metadata - if (typeof messageDef.heading !== 'undefined') { - message.heading = renderTemplate(messageDef.heading, message); - } - message.text = renderTemplate(messageDef.text, message); - // check if the error message provides a link with further information - // if so, add it to the message to be returned with it - if (typeof messageDef.url !== 'undefined') { - // the link is also treated as a template so we're able to dynamically link to - // documentation links matching the running version of Kibana. - message.url = renderTemplate(messageDef.url, { version: kbnVersion! }); - } - - message.status = messageDef.status; - } else { - message.text = i18n.translate( - 'xpack.ml.models.jobValidation.unknownMessageIdErrorMessage', - { - defaultMessage: '{messageId} (unknown message id)', - values: { messageId }, - } - ); - } - - return message; - }); + return uniqWithIsEqual(validationMessages); } catch (error) { throw Boom.badRequest(error); } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 3aee2ec89a6e1..c4ee1fd76530e 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -67,7 +67,6 @@ export type MlPluginStart = void; export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, PluginsSetup, PluginsStart> { private log: Logger; - private version: string; private mlLicense: MlLicense; private capabilities: CapabilitiesStart | null = null; private clusterClient: IClusterClient | null = null; @@ -79,7 +78,6 @@ export class MlServerPlugin constructor(ctx: PluginInitializerContext) { this.log = ctx.logger.get(); - this.version = ctx.env.packageInfo.branch; this.mlLicense = new MlLicense(); this.isMlReady = new Promise((resolve) => (this.setMlReady = resolve)); } @@ -182,7 +180,7 @@ export class MlServerPlugin jobServiceRoutes(routeInit); notificationRoutes(routeInit); resultsServiceRoutes(routeInit); - jobValidationRoutes(routeInit, this.version); + jobValidationRoutes(routeInit); savedObjectsRoutes(routeInit, { getSpaces, resolveMlCapabilities, diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 6840f827831e8..ae5c66f35215b 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -27,10 +27,7 @@ type CalculateModelMemoryLimitPayload = TypeOf<typeof modelMemoryLimitSchema>; /** * Routes for job validation */ -export function jobValidationRoutes( - { router, mlLicense, routeGuard }: RouteInitialization, - version: string -) { +export function jobValidationRoutes({ router, mlLicense, routeGuard }: RouteInitialization) { function calculateModelMemoryLimit( client: IScopedClusterClient, mlClient: MlClient, @@ -191,12 +188,10 @@ export function jobValidationRoutes( }, routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { try { - // version corresponds to the version used in documentation links. const resp = await validateJob( client, mlClient, request.body, - version, mlLicense.isSecurityEnabled() === false ); diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts index 5b87d6e0670c8..3f349d0b289e8 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -6,18 +6,20 @@ */ import expect from '@kbn/expect'; +import { + basicValidJobMessages, + basicInvalidJobMessages, + nonBasicIssuesMessages, +} from '../../../../../../x-pack/plugins/ml/common/constants/messages.test.mock'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../../functional/services/ml/security_common'; import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; -import pkg from '../../../../../../package.json'; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); - const VALIDATED_SEPARATELY = 'this value is not validated directly'; - describe('Validate job', function () { before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); @@ -75,44 +77,7 @@ export default ({ getService }: FtrProviderContext) => { .send(requestBody) .expect(200); - expect(body).to.eql([ - { - id: 'job_id_valid', - heading: 'Job ID format is valid', - text: - 'Lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores, starts and ends with an alphanumeric character, and is no more than 64 characters long.', - url: `https://www.elastic.co/guide/en/elasticsearch/reference/${pkg.branch}/ml-job-resource.html#ml-job-resource`, - status: 'success', - }, - { - id: 'detectors_function_not_empty', - heading: 'Detector functions', - text: 'Presence of detector functions validated in all detectors.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#detectors`, - status: 'success', - }, - { - id: 'success_bucket_span', - bucketSpan: '15m', - heading: 'Bucket span', - text: 'Format of "15m" is valid and passed validation checks.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#bucket-span`, - status: 'success', - }, - { - id: 'success_time_range', - heading: 'Time range', - text: 'Valid and long enough to model patterns in the data.', - status: 'success', - }, - { - id: 'success_mml', - heading: 'Model memory limit', - text: 'Valid and within the estimated model memory limit.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#model-memory-limits`, - status: 'success', - }, - ]); + expect(body).to.eql(basicValidJobMessages); }); it('should recognize a basic invalid job configuration and skip advanced checks', async () => { @@ -156,36 +121,7 @@ export default ({ getService }: FtrProviderContext) => { .send(requestBody) .expect(200); - expect(body).to.eql([ - { - id: 'job_id_invalid', - text: - 'Job ID is invalid. It can contain lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores and must start and end with an alphanumeric character.', - url: `https://www.elastic.co/guide/en/elasticsearch/reference/${pkg.branch}/ml-job-resource.html#ml-job-resource`, - status: 'error', - }, - { - id: 'detectors_function_not_empty', - heading: 'Detector functions', - text: 'Presence of detector functions validated in all detectors.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#detectors`, - status: 'success', - }, - { - id: 'bucket_span_valid', - bucketSpan: '15m', - heading: 'Bucket span', - text: 'Format of "15m" is valid.', - url: `https://www.elastic.co/guide/en/elasticsearch/reference/${pkg.branch}/ml-job-resource.html#ml-analysisconfig`, - status: 'success', - }, - { - id: 'skipped_extended_tests', - text: - 'Skipped additional checks because the basic requirements of the job configuration were not met.', - status: 'warning', - }, - ]); + expect(body).to.eql(basicInvalidJobMessages); }); it('should recognize non-basic issues in job configuration', async () => { @@ -244,74 +180,7 @@ export default ({ getService }: FtrProviderContext) => { } }); - const expectedResponse = [ - { - id: 'job_id_valid', - heading: 'Job ID format is valid', - text: - 'Lowercase alphanumeric (a-z and 0-9) characters, hyphens or underscores, starts and ends with an alphanumeric character, and is no more than 64 characters long.', - url: `https://www.elastic.co/guide/en/elasticsearch/reference/${pkg.branch}/ml-job-resource.html#ml-job-resource`, - status: 'success', - }, - { - id: 'detectors_function_not_empty', - heading: 'Detector functions', - text: 'Presence of detector functions validated in all detectors.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#detectors`, - status: 'success', - }, - { - id: 'cardinality_model_plot_high', - modelPlotCardinality: VALIDATED_SEPARATELY, - text: VALIDATED_SEPARATELY, - status: VALIDATED_SEPARATELY, - }, - { - id: 'cardinality_partition_field', - fieldName: 'order_id', - text: - 'Cardinality of partition_field "order_id" is above 1000 and might result in high memory usage.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#cardinality`, - status: 'warning', - }, - { - id: 'bucket_span_high', - heading: 'Bucket span', - text: - 'Bucket span is 1 day or more. Be aware that days are considered as UTC days, not local days.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#bucket-span`, - status: 'info', - }, - { - bucketSpanCompareFactor: 25, - id: 'time_range_short', - minTimeSpanReadable: '2 hours', - heading: 'Time range', - text: - 'The selected or available time range might be too short. The recommended minimum time range should be at least 2 hours and 25 times the bucket span.', - status: 'warning', - }, - { - id: 'success_influencers', - text: 'Influencer configuration passed the validation checks.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/ml-influencers.html`, - status: 'success', - }, - { - id: 'half_estimated_mml_greater_than_mml', - mml: '1MB', - text: - 'The specified model memory limit is less than half of the estimated model memory limit and will likely hit the hard limit.', - url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#model-memory-limits`, - status: 'warning', - }, - { - id: 'missing_summary_count_field_name', - status: 'error', - text: - 'A job configured with a datafeed with aggregations must set summary_count_field_name; use doc_count or suitable alternative.', - }, - ]; + const expectedResponse = nonBasicIssuesMessages; expect(body.length).to.eql( expectedResponse.length, @@ -327,12 +196,6 @@ export default ({ getService }: FtrProviderContext) => { if (entry.id === 'cardinality_model_plot_high') { // don't check the exact value of modelPlotCardinality as this is an approximation expect(responseEntry).to.have.property('modelPlotCardinality'); - expect(responseEntry) - .to.have.property('text') - .match( - /^The estimated cardinality of [0-9]+ of fields relevant to creating model plots might result in resource intensive jobs./ - ); - expect(responseEntry).to.have.property('status', 'warning'); } else { expect(responseEntry).to.eql(entry); } From bdcd2ec859cb8386a4bbbd261cc4cf26b51b78ae Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Wed, 17 Mar 2021 11:17:48 +0300 Subject: [PATCH 43/44] Replace EuiCodeBlock with JsonCodeEditor in DiscoverGrid (#92442) * Replace EuiCodeBlock with JsonCodeEditor in DiscoverGrid * Add optional "hasLineNumbers" property to JsonCodeEditor and removed line numbers from the popover * Update json_code_editor snapshot * Add functional test for cell expanded content popover * Remove unused code * Fix geo point case and refactor some code Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/discover_grid/constants.ts | 2 + .../discover_grid/discover_grid.tsx | 59 +++++++------ ...rid_schema.tsx => discover_grid_schema.ts} | 17 ---- .../get_render_cell_value.test.tsx | 88 ++++++++++++------- .../discover_grid/get_render_cell_value.tsx | 16 +++- .../json_code_editor.test.tsx.snap | 9 +- .../json_code_editor.test.tsx | 2 +- .../json_code_editor/json_code_editor.tsx | 17 ++-- .../discover/public/{plugin.ts => plugin.tsx} | 3 +- .../apps/discover/_data_grid_doc_table.ts | 20 +++++ test/functional/apps/visualize/_inspector.ts | 3 +- test/functional/page_objects/tile_map_page.ts | 3 +- test/functional/services/index.ts | 2 + test/functional/services/inspector.ts | 13 --- test/functional/services/monaco_editor.ts | 28 ++++++ .../maps/documents_source/docvalue_fields.js | 3 +- 16 files changed, 176 insertions(+), 109 deletions(-) rename src/plugins/discover/public/application/components/discover_grid/{discover_grid_schema.tsx => discover_grid_schema.ts} (72%) rename src/plugins/discover/public/{plugin.ts => plugin.tsx} (99%) create mode 100644 test/functional/services/monaco_editor.ts diff --git a/src/plugins/discover/public/application/components/discover_grid/constants.ts b/src/plugins/discover/public/application/components/discover_grid/constants.ts index 015d0b65246f2..de2781cf159c3 100644 --- a/src/plugins/discover/public/application/components/discover_grid/constants.ts +++ b/src/plugins/discover/public/application/components/discover_grid/constants.ts @@ -24,3 +24,5 @@ export const toolbarVisibility = { }, showStyleSelector: false, }; + +export const defaultMonacoEditorWidth = 370; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index a0dcc2c2af466..380b4dc5e8e9a 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -22,7 +22,7 @@ import { } from '@elastic/eui'; import { IndexPattern } from '../../../kibana_services'; import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { getPopoverContents, getSchemaDetectors } from './discover_grid_schema'; +import { getSchemaDetectors } from './discover_grid_schema'; import { DiscoverGridFlyout } from './discover_grid_flyout'; import { DiscoverGridContext } from './discover_grid_context'; import { getRenderCellValueFn } from './get_render_cell_value'; @@ -36,6 +36,7 @@ import { import { defaultPageSize, gridStyle, pageSizeArr, toolbarVisibility } from './constants'; import { DiscoverServices } from '../../../build_services'; import { getDisplayedColumns } from '../../helpers/columns'; +import { KibanaContextProvider } from '../../../../../kibana_react/public'; interface SortObj { id: string; @@ -219,7 +220,6 @@ export const DiscoverGrid = ({ [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns] ); const schemaDetectors = useMemo(() => getSchemaDetectors(), []); - const popoverContents = useMemo(() => getPopoverContents(), []); const columnsVisibility = useMemo( () => ({ visibleColumns: getVisibleColumns(displayedColumns, indexPattern, showTimeCol) as string[], @@ -259,34 +259,35 @@ export const DiscoverGrid = ({ }} > <> - <EuiDataGridMemoized - aria-describedby={randomId} - aria-labelledby={ariaLabelledBy} - columns={euiGridColumns} - columnVisibility={columnsVisibility} - data-test-subj="docTable" - gridStyle={gridStyle as EuiDataGridStyle} - leadingControlColumns={lead} - onColumnResize={(col: { columnId: string; width: number }) => { - if (onResize) { - onResize(col); + <KibanaContextProvider services={{ uiSettings: services.uiSettings }}> + <EuiDataGridMemoized + aria-describedby={randomId} + aria-labelledby={ariaLabelledBy} + columns={euiGridColumns} + columnVisibility={columnsVisibility} + data-test-subj="docTable" + gridStyle={gridStyle as EuiDataGridStyle} + leadingControlColumns={lead} + onColumnResize={(col: { columnId: string; width: number }) => { + if (onResize) { + onResize(col); + } + }} + pagination={paginationObj} + renderCellValue={renderCellValue} + rowCount={rowCount} + schemaDetectors={schemaDetectors} + sorting={sorting as EuiDataGridSorting} + toolbarVisibility={ + defaultColumns + ? { + ...toolbarVisibility, + showColumnSelector: false, + } + : toolbarVisibility } - }} - pagination={paginationObj} - popoverContents={popoverContents} - renderCellValue={renderCellValue} - rowCount={rowCount} - schemaDetectors={schemaDetectors} - sorting={sorting as EuiDataGridSorting} - toolbarVisibility={ - defaultColumns - ? { - ...toolbarVisibility, - showColumnSelector: false, - } - : toolbarVisibility - } - /> + /> + </KibanaContextProvider> {showDisclaimer && ( <p className="dscDiscoverGrid__footer"> diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.ts similarity index 72% rename from src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx rename to src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.ts index ca5b2c9f19918..0aa6dadd633e0 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_schema.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import React from 'react'; -import { EuiCodeBlock, EuiDataGridPopoverContents } from '@elastic/eui'; import { kibanaJSON } from './constants'; import { KBN_FIELD_TYPES } from '../../../../../data/common'; @@ -43,18 +41,3 @@ export function getSchemaDetectors() { }, ]; } - -/** - * Returns custom popover content for certain schemas - */ -export function getPopoverContents(): EuiDataGridPopoverContents { - return { - [kibanaJSON]: ({ children }) => { - return ( - <EuiCodeBlock isCopyable language="json" paddingSize="none" transparentBackground={true}> - {children} - </EuiCodeBlock> - ); - }, - }; -} diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx index a1447a9a83672..f1025a0881d1f 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx @@ -7,10 +7,25 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { ReactWrapper, shallow } from 'enzyme'; import { getRenderCellValueFn } from './get_render_cell_value'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; +jest.mock('../../../../../kibana_react/public', () => ({ + useUiSetting: () => true, + withKibana: (comp: ReactWrapper) => { + return comp; + }, +})); + +jest.mock('../../../kibana_services', () => ({ + getServices: () => ({ + uiSettings: { + get: jest.fn(), + }, + }), +})); + const rowsSource = [ { _id: '1', @@ -139,20 +154,25 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(` - "<span>{ - "_id": "1", - "_index": "test", - "_type": "test", - "_score": 1, - "_source": { - "bytes": 100, - "extension": ".gz" - }, - "highlight": { - "extension": "@kibana-highlighted-field.gz@/kibana-highlighted-field" + expect(component).toMatchInlineSnapshot(` + <JsonCodeEditor + json={ + Object { + "_id": "1", + "_index": "test", + "_score": 1, + "_source": Object { + "bytes": 100, + "extension": ".gz", + }, + "_type": "test", + "highlight": Object { + "extension": "@kibana-highlighted-field.gz@/kibana-highlighted-field", + }, + } } - }</span>" + width={370} + /> `); }); @@ -226,24 +246,30 @@ describe('Discover grid cell rendering', function () { setCellProps={jest.fn()} /> ); - expect(component.html()).toMatchInlineSnapshot(` - "<span>{ - "_id": "1", - "_index": "test", - "_type": "test", - "_score": 1, - "fields": { - "bytes": [ - 100 - ], - "extension": [ - ".gz" - ] - }, - "highlight": { - "extension": "@kibana-highlighted-field.gz@/kibana-highlighted-field" + expect(component).toMatchInlineSnapshot(` + <JsonCodeEditor + json={ + Object { + "_id": "1", + "_index": "test", + "_score": 1, + "_source": undefined, + "_type": "test", + "fields": Object { + "bytes": Array [ + 100, + ], + "extension": Array [ + ".gz", + ], + }, + "highlight": Object { + "extension": "@kibana-highlighted-field.gz@/kibana-highlighted-field", + }, + } } - }</span>" + width={370} + /> `); }); diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index b1eb5eb9ada0e..dce0a82934c25 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -19,6 +19,8 @@ import { import { IndexPattern } from '../../../kibana_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; +import { JsonCodeEditor } from '../json_code_editor/json_code_editor'; +import { defaultMonacoEditorWidth } from './constants'; export const getRenderCellValueFn = ( indexPattern: IndexPattern, @@ -26,7 +28,7 @@ export const getRenderCellValueFn = ( rowsFlattened: Array<Record<string, unknown>>, useNewFieldsApi: boolean ) => ({ rowIndex, columnId, isDetails, setCellProps }: EuiDataGridCellValueElementProps) => { - const row = rows ? (rows[rowIndex] as Record<string, unknown>) : undefined; + const row = rows ? rows[rowIndex] : undefined; const rowFlattened = rowsFlattened ? (rowsFlattened[rowIndex] as Record<string, unknown>) : undefined; @@ -106,10 +108,18 @@ export const getRenderCellValueFn = ( ); } + if (typeof rowFlattened[columnId] === 'object' && isDetails) { + return ( + <JsonCodeEditor + json={rowFlattened[columnId] as Record<string, any>} + width={defaultMonacoEditorWidth} + /> + ); + } + if (field && field.type === '_source') { if (isDetails) { - // nicely formatted JSON for the expanded view - return <span>{JSON.stringify(row, null, 2)}</span>; + return <JsonCodeEditor json={row} width={defaultMonacoEditorWidth} />; } const formatted = indexPattern.formatHit(row); diff --git a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap index 4f27158eee04f..8f07614813495 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap +++ b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap @@ -6,9 +6,7 @@ exports[`returns the \`JsonCodeEditor\` component 1`] = ` direction="column" gutterSize="s" > - <EuiFlexItem - grow={true} - > + <EuiFlexItem> <EuiSpacer size="s" /> @@ -31,9 +29,7 @@ exports[`returns the \`JsonCodeEditor\` component 1`] = ` </EuiCopy> </div> </EuiFlexItem> - <EuiFlexItem - grow={true} - > + <EuiFlexItem> <CodeEditor aria-label="Read only JSON view of an elasticsearch document" editorDidMount={[Function]} @@ -43,6 +39,7 @@ exports[`returns the \`JsonCodeEditor\` component 1`] = ` Object { "automaticLayout": true, "fontSize": 12, + "lineNumbers": "off", "minimap": Object { "enabled": false, }, diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx index 4ccb3010d5a2b..9ec60410b591e 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx @@ -18,5 +18,5 @@ it('returns the `JsonCodeEditor` component', () => { _score: 1, _source: { test: 123 }, }; - expect(shallow(<JsonCodeEditor hit={value} />)).toMatchSnapshot(); + expect(shallow(<JsonCodeEditor json={value} />)).toMatchSnapshot(); }); diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx index 85d6aad755250..50a29dde85891 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx @@ -13,7 +13,6 @@ import { i18n } from '@kbn/i18n'; import { monaco, XJsonLang } from '@kbn/monaco'; import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { CodeEditor } from '../../../../../kibana_react/public'; -import { DocViewRenderProps } from '../../../application/doc_views/doc_views_types'; const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', { defaultMessage: 'Read only JSON view of an elasticsearch document', @@ -22,8 +21,14 @@ const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel' defaultMessage: 'Copy to clipboard', }); -export const JsonCodeEditor = ({ hit }: DocViewRenderProps) => { - const jsonValue = JSON.stringify(hit, null, 2); +interface JsonCodeEditorProps { + json: Record<string, any>; + width?: string | number; + hasLineNumbers?: boolean; +} + +export const JsonCodeEditor = ({ json, width, hasLineNumbers }: JsonCodeEditorProps) => { + const jsonValue = JSON.stringify(json, null, 2); // setting editor height based on lines height and count to stretch and fit its content const setEditorCalculatedHeight = useCallback((editor) => { @@ -43,7 +48,7 @@ export const JsonCodeEditor = ({ hit }: DocViewRenderProps) => { return ( <EuiFlexGroup className="dscJsonCodeEditor" direction="column" gutterSize="s"> - <EuiFlexItem grow={true}> + <EuiFlexItem> <EuiSpacer size="s" /> <div className="eui-textRight"> <EuiCopy textToCopy={jsonValue}> @@ -55,9 +60,10 @@ export const JsonCodeEditor = ({ hit }: DocViewRenderProps) => { </EuiCopy> </div> </EuiFlexItem> - <EuiFlexItem grow={true}> + <EuiFlexItem> <CodeEditor languageId={XJsonLang.ID} + width={width} value={jsonValue} onChange={() => {}} editorDidMount={setEditorCalculatedHeight} @@ -65,6 +71,7 @@ export const JsonCodeEditor = ({ hit }: DocViewRenderProps) => { options={{ automaticLayout: true, fontSize: 12, + lineNumbers: hasLineNumbers ? 'on' : 'off', minimap: { enabled: false, }, diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.tsx similarity index 99% rename from src/plugins/discover/public/plugin.ts rename to src/plugins/discover/public/plugin.tsx index 47161c2b8298e..0e0836e3d9573 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.tsx @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import React from 'react'; import angular, { auto } from 'angular'; import { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; @@ -187,7 +188,7 @@ export class DiscoverPlugin defaultMessage: 'JSON', }), order: 20, - component: JsonCodeEditor, + component: ({ hit }) => <JsonCodeEditor json={hit} hasLineNumbers />, }); const { diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts index 5eeafc4d78f67..fb19111d92c68 100644 --- a/test/functional/apps/discover/_data_grid_doc_table.ts +++ b/test/functional/apps/discover/_data_grid_doc_table.ts @@ -10,11 +10,13 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); const dataGrid = getService('dataGrid'); const log = getService('log'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const monacoEditor = getService('monacoEditor'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'logstash-*', @@ -56,6 +58,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + it('should show popover with expanded cell content by click on expand button', async () => { + log.debug('open popover with expanded cell content to get json from the editor'); + const documentCell = await dataGrid.getCellElement(1, 3); + await documentCell.click(); + const expandCellContentButton = await documentCell.findByClassName( + 'euiDataGridRowCell__expandButtonIcon' + ); + await expandCellContentButton.click(); + const popoverJson = await monacoEditor.getCodeEditorValue(); + + log.debug('open expanded document flyout to get json'); + await dataGrid.clickRowToggle(); + await find.clickByCssSelectorWhenNotDisabled('#kbn_doc_viewer_tab_1'); + const flyoutJson = await monacoEditor.getCodeEditorValue(); + + expect(popoverJson).to.be(flyoutJson); + }); + describe('expand a document row', function () { const rowToInspect = 1; diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/_inspector.ts index 47fcc6e64d0b1..edb2f87aab13e 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/_inspector.ts @@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); + const monacoEditor = getService('monacoEditor'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); @@ -42,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await inspector.openInspectorRequestsView(); const requestTab = await inspector.getOpenRequestDetailRequestButton(); await requestTab.click(); - const requestJSON = JSON.parse(await inspector.getCodeEditorValue()); + const requestJSON = JSON.parse(await monacoEditor.getCodeEditorValue()); expect(requestJSON.aggs['2'].max).property('missing', 10); }); diff --git a/test/functional/page_objects/tile_map_page.ts b/test/functional/page_objects/tile_map_page.ts index c11e4f1558bee..db17268f20a15 100644 --- a/test/functional/page_objects/tile_map_page.ts +++ b/test/functional/page_objects/tile_map_page.ts @@ -14,6 +14,7 @@ export function TileMapPageProvider({ getService, getPageObjects }: FtrProviderC const retry = getService('retry'); const log = getService('log'); const inspector = getService('inspector'); + const monacoEditor = getService('monacoEditor'); const { header } = getPageObjects(['header']); class TileMapPage { @@ -40,7 +41,7 @@ export function TileMapPageProvider({ getService, getPageObjects }: FtrProviderC await testSubjects.click('inspectorViewChooserRequests'); await testSubjects.click('inspectorRequestDetailRequest'); - return await inspector.getCodeEditorValue(); + return await monacoEditor.getCodeEditorValue(); } public async getMapBounds(): Promise<object> { diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 94d7b71c640c3..07d5ef950d21e 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -46,6 +46,7 @@ import { ListingTableProvider } from './listing_table'; import { SavedQueryManagementComponentProvider } from './saved_query_management_component'; import { KibanaSupertestProvider } from './supertest'; import { MenuToggleProvider } from './menu_toggle'; +import { MonacoEditorProvider } from './monaco_editor'; export const services = { ...commonServiceProviders, @@ -81,5 +82,6 @@ export const services = { elasticChart: ElasticChartProvider, supertest: KibanaSupertestProvider, managementMenu: ManagementMenuProvider, + monacoEditor: MonacoEditorProvider, MenuToggle: MenuToggleProvider, }; diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index 4dc248116ccfd..c9cf159d0d38e 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function InspectorProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); - const browser = getService('browser'); const renderable = getService('renderable'); const flyout = getService('flyout'); const testSubjects = getService('testSubjects'); @@ -235,18 +234,6 @@ export function InspectorProvider({ getService }: FtrProviderContext) { public getOpenRequestDetailResponseButton() { return testSubjects.find('inspectorRequestDetailResponse'); } - - public async getCodeEditorValue() { - let request: string = ''; - - await retry.try(async () => { - request = await browser.execute( - () => (window as any).MonacoEnvironment.monaco.editor.getModels()[0].getValue() as string - ); - }); - - return request; - } } return new Inspector(); diff --git a/test/functional/services/monaco_editor.ts b/test/functional/services/monaco_editor.ts new file mode 100644 index 0000000000000..e0763659be9c5 --- /dev/null +++ b/test/functional/services/monaco_editor.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function MonacoEditorProvider({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const browser = getService('browser'); + + return new (class MonacoEditor { + public async getCodeEditorValue() { + let request: string = ''; + + await retry.try(async () => { + request = await browser.execute( + () => (window as any).MonacoEnvironment.monaco.editor.getModels()[0].getValue() as string + ); + }); + + return request; + } + })(); +} diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index 5a21791b2c567..a49ab7d7dd980 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const inspector = getService('inspector'); + const monacoEditor = getService('monacoEditor'); const testSubjects = getService('testSubjects'); const security = getService('security'); @@ -27,7 +28,7 @@ export default function ({ getPageObjects, getService }) { await inspector.open(); await inspector.openInspectorRequestsView(); await testSubjects.click('inspectorRequestDetailResponse'); - const responseBody = await inspector.getCodeEditorValue(); + const responseBody = await monacoEditor.getCodeEditorValue(); await inspector.close(); return JSON.parse(responseBody); } From 25e586acd0eac7207379872c59e4a40c9e9026d0 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet <pierre.gayvallet@gmail.com> Date: Wed, 17 Mar 2021 10:16:18 +0100 Subject: [PATCH 44/44] Migrate the optimizer mixin to core (#94272) * migrate optimizer mixin to core apps * fix core_app tests * add integration tests, extract selectCompressedFile * add CoreApp unit test * more unit tests * unit tests for bundle_route * more unit tests * remove /src/optimize/ from codeowners * fix case * NIT --- .github/CODEOWNERS | 1 - .../bundle_routes/bundle_route.test.mocks.ts | 12 + .../bundle_routes/bundle_route.test.ts | 70 +++++ .../core_app/bundle_routes/bundles_route.ts | 49 +++ .../bundle_routes/dynamic_asset_response.ts | 124 ++++++++ .../bundle_routes/file_hash.test.mocks.ts | 15 + .../core_app/bundle_routes/file_hash.test.ts | 72 +++++ .../core_app/bundle_routes/file_hash.ts | 32 ++ .../bundle_routes/file_hash_cache.test.ts | 33 ++ .../bundle_routes}/file_hash_cache.ts | 18 +- src/core/server/core_app/bundle_routes/fs.ts | 17 + .../server/core_app/bundle_routes/index.ts} | 6 +- .../register_bundle_routes.test.mocks.ts | 16 + .../register_bundle_routes.test.ts | 101 ++++++ .../bundle_routes/register_bundle_routes.ts | 69 ++++ .../bundle_routes/select_compressed_file.ts | 56 ++++ .../server/core_app/bundle_routes/utils.ts} | 37 +-- .../server/core_app/core_app.test.mocks.ts} | 6 +- src/core/server/core_app/core_app.test.ts | 35 ++- src/core/server/core_app/core_app.ts | 20 +- .../__fixtures__/outside_output.js | 0 .../__fixtures__/plugin/foo/gzip_chunk.js} | 2 +- .../__fixtures__/plugin/foo/gzip_chunk.js.gz | Bin 0 -> 256 bytes .../__fixtures__/plugin/foo/image.png | Bin .../__fixtures__/plugin/foo/plugin.js | 0 .../integration_tests/bundle_routes.test.ts | 171 ++++++++++ src/core/server/server.ts | 2 +- src/legacy/server/kbn_server.js | 6 +- .../bundles_route/bundles_route.test.ts | 296 ------------------ src/optimize/bundles_route/bundles_route.ts | 131 -------- .../bundles_route/dynamic_asset_response.ts | 157 ---------- .../bundles_route/proxy_bundles_route.ts | 35 --- src/optimize/np_ui_plugin_public_dirs.ts | 40 --- src/optimize/optimize_mixin.ts | 28 -- 34 files changed, 917 insertions(+), 740 deletions(-) create mode 100644 src/core/server/core_app/bundle_routes/bundle_route.test.mocks.ts create mode 100644 src/core/server/core_app/bundle_routes/bundle_route.test.ts create mode 100644 src/core/server/core_app/bundle_routes/bundles_route.ts create mode 100644 src/core/server/core_app/bundle_routes/dynamic_asset_response.ts create mode 100644 src/core/server/core_app/bundle_routes/file_hash.test.mocks.ts create mode 100644 src/core/server/core_app/bundle_routes/file_hash.test.ts create mode 100644 src/core/server/core_app/bundle_routes/file_hash.ts create mode 100644 src/core/server/core_app/bundle_routes/file_hash_cache.test.ts rename src/{optimize/bundles_route => core/server/core_app/bundle_routes}/file_hash_cache.ts (59%) create mode 100644 src/core/server/core_app/bundle_routes/fs.ts rename src/{optimize/jest.config.js => core/server/core_app/bundle_routes/index.ts} (77%) create mode 100644 src/core/server/core_app/bundle_routes/register_bundle_routes.test.mocks.ts create mode 100644 src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts create mode 100644 src/core/server/core_app/bundle_routes/register_bundle_routes.ts create mode 100644 src/core/server/core_app/bundle_routes/select_compressed_file.ts rename src/{optimize/bundles_route/file_hash.ts => core/server/core_app/bundle_routes/utils.ts} (51%) rename src/{optimize/bundles_route/index.ts => core/server/core_app/core_app.test.mocks.ts} (70%) rename src/{optimize/bundles_route => core/server/core_app/integration_tests}/__fixtures__/outside_output.js (100%) rename src/{optimize/index.ts => core/server/core_app/integration_tests/__fixtures__/plugin/foo/gzip_chunk.js} (87%) create mode 100644 src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/gzip_chunk.js.gz rename src/{optimize/bundles_route => core/server/core_app/integration_tests}/__fixtures__/plugin/foo/image.png (100%) rename src/{optimize/bundles_route => core/server/core_app/integration_tests}/__fixtures__/plugin/foo/plugin.js (100%) create mode 100644 src/core/server/core_app/integration_tests/bundle_routes.test.ts delete mode 100644 src/optimize/bundles_route/bundles_route.test.ts delete mode 100644 src/optimize/bundles_route/bundles_route.ts delete mode 100644 src/optimize/bundles_route/dynamic_asset_response.ts delete mode 100644 src/optimize/bundles_route/proxy_bundles_route.ts delete mode 100644 src/optimize/np_ui_plugin_public_dirs.ts delete mode 100644 src/optimize/optimize_mixin.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4f8d3fa4dc429..508cd8f9e8007 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -145,7 +145,6 @@ # Operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations -/src/optimize/ @elastic/kibana-operations /packages/*eslint*/ @elastic/kibana-operations /packages/*babel*/ @elastic/kibana-operations /packages/kbn-dev-utils*/ @elastic/kibana-operations diff --git a/src/core/server/core_app/bundle_routes/bundle_route.test.mocks.ts b/src/core/server/core_app/bundle_routes/bundle_route.test.mocks.ts new file mode 100644 index 0000000000000..c7839f6a26e8b --- /dev/null +++ b/src/core/server/core_app/bundle_routes/bundle_route.test.mocks.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const createDynamicAssetHandlerMock = jest.fn(); +jest.doMock('./dynamic_asset_response', () => ({ + createDynamicAssetHandler: createDynamicAssetHandlerMock, +})); diff --git a/src/core/server/core_app/bundle_routes/bundle_route.test.ts b/src/core/server/core_app/bundle_routes/bundle_route.test.ts new file mode 100644 index 0000000000000..377d8432ae9a9 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/bundle_route.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { createDynamicAssetHandlerMock } from './bundle_route.test.mocks'; + +import { httpServiceMock } from '../../http/http_service.mock'; +import { FileHashCache } from './file_hash_cache'; +import { registerRouteForBundle } from './bundles_route'; + +describe('registerRouteForBundle', () => { + let router: ReturnType<typeof httpServiceMock.createRouter>; + let fileHashCache: FileHashCache; + + beforeEach(() => { + router = httpServiceMock.createRouter(); + fileHashCache = new FileHashCache(); + }); + + afterEach(() => { + createDynamicAssetHandlerMock.mockReset(); + }); + + it('calls `router.get` with the correct parameters', () => { + const handler = jest.fn(); + createDynamicAssetHandlerMock.mockReturnValue(handler); + + registerRouteForBundle(router, { + isDist: false, + publicPath: '/public-path/', + bundlesPath: '/bundle-path', + fileHashCache, + routePath: '/route-path/', + }); + + expect(router.get).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledWith( + { + path: '/route-path/{path*}', + options: { + authRequired: false, + }, + validate: expect.any(Object), + }, + handler + ); + }); + + it('calls `createDynamicAssetHandler` with the correct parameters', () => { + registerRouteForBundle(router, { + isDist: false, + publicPath: '/public-path/', + bundlesPath: '/bundle-path', + fileHashCache, + routePath: '/route-path/', + }); + + expect(createDynamicAssetHandlerMock).toHaveBeenCalledTimes(1); + expect(createDynamicAssetHandlerMock).toHaveBeenCalledWith({ + isDist: false, + publicPath: '/public-path/', + bundlesPath: '/bundle-path', + fileHashCache, + }); + }); +}); diff --git a/src/core/server/core_app/bundle_routes/bundles_route.ts b/src/core/server/core_app/bundle_routes/bundles_route.ts new file mode 100644 index 0000000000000..c15babe13a2ce --- /dev/null +++ b/src/core/server/core_app/bundle_routes/bundles_route.ts @@ -0,0 +1,49 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from '../../http'; +import { createDynamicAssetHandler } from './dynamic_asset_response'; +import { FileHashCache } from './file_hash_cache'; + +export function registerRouteForBundle( + router: IRouter, + { + publicPath, + routePath, + bundlesPath, + fileHashCache, + isDist, + }: { + publicPath: string; + routePath: string; + bundlesPath: string; + fileHashCache: FileHashCache; + isDist: boolean; + } +) { + router.get( + { + path: `${routePath}{path*}`, + options: { + authRequired: false, + }, + validate: { + params: schema.object({ + path: schema.string(), + }), + }, + }, + createDynamicAssetHandler({ + publicPath, + bundlesPath, + isDist, + fileHashCache, + }) + ); +} diff --git a/src/core/server/core_app/bundle_routes/dynamic_asset_response.ts b/src/core/server/core_app/bundle_routes/dynamic_asset_response.ts new file mode 100644 index 0000000000000..1ad03608999c7 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/dynamic_asset_response.ts @@ -0,0 +1,124 @@ +/* + * 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 { createReadStream } from 'fs'; +import { resolve, extname } from 'path'; +import mime from 'mime-types'; +import agent from 'elastic-apm-node'; + +import { fstat, close } from './fs'; +import { RequestHandler } from '../../http'; +import { IFileHashCache } from './file_hash_cache'; +import { getFileHash } from './file_hash'; +import { selectCompressedFile } from './select_compressed_file'; + +const MINUTE = 60; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + +/** + * Serve asset for the requested path. This is designed + * to replicate a subset of the features provided by Hapi's Inert + * plugin including: + * - ensure path is not traversing out of the bundle directory + * - manage use file descriptors for file access to efficiently + * interact with the file multiple times in each request + * - generate and cache etag for the file + * - write correct headers to response for client-side caching + * and invalidation + * - stream file to response + * + * It differs from Inert in some important ways: + * - cached hash/etag is based on the file on disk, but modified + * by the public path so that individual public paths have + * different etags, but can share a cache + */ +export const createDynamicAssetHandler = ({ + bundlesPath, + fileHashCache, + isDist, + publicPath, +}: { + bundlesPath: string; + publicPath: string; + fileHashCache: IFileHashCache; + isDist: boolean; +}): RequestHandler<{ path: string }, {}, {}> => { + return async (ctx, req, res) => { + agent.setTransactionName('GET ?/bundles/?'); + + let fd: number | undefined; + let fileEncoding: 'gzip' | 'br' | undefined; + + try { + const path = resolve(bundlesPath, req.params.path); + + // prevent path traversal, only process paths that resolve within bundlesPath + if (!path.startsWith(bundlesPath)) { + return res.forbidden({ + body: 'EACCES', + }); + } + + // we use and manage a file descriptor mostly because + // that's what Inert does, and since we are accessing + // the file 2 or 3 times per request it seems logical + ({ fd, fileEncoding } = await selectCompressedFile( + req.headers['accept-encoding'] as string, + path + )); + + let headers: Record<string, string>; + if (isDist) { + headers = { 'cache-control': `max-age=${365 * DAY}` }; + } else { + const stat = await fstat(fd); + const hash = await getFileHash(fileHashCache, path, stat, fd); + headers = { + etag: `${hash}-${publicPath}`, + 'cache-control': 'must-revalidate', + }; + } + + // If we manually selected a compressed file, specify the encoding header. + // Otherwise, let Hapi automatically gzip the response. + if (fileEncoding) { + headers['content-encoding'] = fileEncoding; + } + + const fileExt = extname(path); + const contentType = mime.lookup(fileExt); + const mediaType = mime.contentType(contentType || fileExt); + headers['content-type'] = mediaType || ''; + + const content = createReadStream(null as any, { + fd, + start: 0, + autoClose: true, + }); + + return res.ok({ + body: content, + headers, + }); + } catch (error) { + if (fd) { + try { + await close(fd); + } catch (_) { + // ignore errors from close, we already have one to report + // and it's very likely they are the same + } + } + if (error.code === 'ENOENT') { + return res.notFound(); + } + throw error; + } + }; +}; diff --git a/src/core/server/core_app/bundle_routes/file_hash.test.mocks.ts b/src/core/server/core_app/bundle_routes/file_hash.test.mocks.ts new file mode 100644 index 0000000000000..d7f6812ba5d29 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/file_hash.test.mocks.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const generateFileHashMock = jest.fn(); +export const getFileCacheKeyMock = jest.fn(); + +jest.doMock('./utils', () => ({ + generateFileHash: generateFileHashMock, + getFileCacheKey: getFileCacheKeyMock, +})); diff --git a/src/core/server/core_app/bundle_routes/file_hash.test.ts b/src/core/server/core_app/bundle_routes/file_hash.test.ts new file mode 100644 index 0000000000000..918f435156344 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/file_hash.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { generateFileHashMock, getFileCacheKeyMock } from './file_hash.test.mocks'; + +import { resolve } from 'path'; +import { Stats } from 'fs'; +import { getFileHash } from './file_hash'; +import { IFileHashCache } from './file_hash_cache'; + +const mockedCache = (): jest.Mocked<IFileHashCache> => ({ + del: jest.fn(), + get: jest.fn(), + set: jest.fn(), +}); + +describe('getFileHash', () => { + const sampleFilePath = resolve(__dirname, 'foo.js'); + const fd = 42; + const stats: Stats = { ino: 42, size: 9000 } as any; + + beforeEach(() => { + getFileCacheKeyMock.mockImplementation((path: string, stat: Stats) => `${path}-${stat.ino}`); + }); + + afterEach(() => { + generateFileHashMock.mockReset(); + getFileCacheKeyMock.mockReset(); + }); + + it('returns the value from cache if present', async () => { + const cache = mockedCache(); + cache.get.mockReturnValue(Promise.resolve('cached-hash')); + + const hash = await getFileHash(cache, sampleFilePath, stats, fd); + + expect(cache.get).toHaveBeenCalledTimes(1); + expect(generateFileHashMock).not.toHaveBeenCalled(); + expect(hash).toEqual('cached-hash'); + }); + + it('computes the value if not present in cache', async () => { + const cache = mockedCache(); + cache.get.mockReturnValue(undefined); + + generateFileHashMock.mockReturnValue(Promise.resolve('computed-hash')); + + const hash = await getFileHash(cache, sampleFilePath, stats, fd); + + expect(generateFileHashMock).toHaveBeenCalledTimes(1); + expect(generateFileHashMock).toHaveBeenCalledWith(fd); + expect(hash).toEqual('computed-hash'); + }); + + it('sets the value in the cache if not present', async () => { + const computedHashPromise = Promise.resolve('computed-hash'); + generateFileHashMock.mockReturnValue(computedHashPromise); + + const cache = mockedCache(); + cache.get.mockReturnValue(undefined); + + await getFileHash(cache, sampleFilePath, stats, fd); + + expect(cache.set).toHaveBeenCalledTimes(1); + expect(cache.set).toHaveBeenCalledWith(`${sampleFilePath}-${stats.ino}`, computedHashPromise); + }); +}); diff --git a/src/core/server/core_app/bundle_routes/file_hash.ts b/src/core/server/core_app/bundle_routes/file_hash.ts new file mode 100644 index 0000000000000..e309873254999 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/file_hash.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { Stats } from 'fs'; +import { generateFileHash, getFileCacheKey } from './utils'; +import { IFileHashCache } from './file_hash_cache'; + +/** + * Get the hash of a file via a file descriptor + */ +export async function getFileHash(cache: IFileHashCache, path: string, stat: Stats, fd: number) { + const key = getFileCacheKey(path, stat); + + const cached = cache.get(key); + if (cached) { + return await cached; + } + + const promise = generateFileHash(fd).catch((error) => { + // don't cache failed attempts + cache.del(key); + throw error; + }); + + cache.set(key, promise); + return await promise; +} diff --git a/src/core/server/core_app/bundle_routes/file_hash_cache.test.ts b/src/core/server/core_app/bundle_routes/file_hash_cache.test.ts new file mode 100644 index 0000000000000..fb519c660e637 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/file_hash_cache.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FileHashCache } from './file_hash_cache'; + +describe('FileHashCache', () => { + it('returns the value stored', async () => { + const cache = new FileHashCache(); + cache.set('foo', Promise.resolve('bar')); + expect(await cache.get('foo')).toEqual('bar'); + }); + + it('can manually delete values', () => { + const cache = new FileHashCache(); + cache.set('foo', Promise.resolve('bar')); + cache.del('foo'); + expect(cache.get('foo')).toBeUndefined(); + }); + + it('only preserves a given amount of entries', async () => { + const cache = new FileHashCache(1); + cache.set('foo', Promise.resolve('bar')); + cache.set('hello', Promise.resolve('dolly')); + + expect(await cache.get('hello')).toEqual('dolly'); + expect(cache.get('foo')).toBeUndefined(); + }); +}); diff --git a/src/optimize/bundles_route/file_hash_cache.ts b/src/core/server/core_app/bundle_routes/file_hash_cache.ts similarity index 59% rename from src/optimize/bundles_route/file_hash_cache.ts rename to src/core/server/core_app/bundle_routes/file_hash_cache.ts index 9d288ccb77194..8242a5b595d60 100644 --- a/src/optimize/bundles_route/file_hash_cache.ts +++ b/src/core/server/core_app/bundle_routes/file_hash_cache.ts @@ -8,8 +8,22 @@ import LruCache from 'lru-cache'; -export class FileHashCache { - private lru = new LruCache<string, Promise<string>>(100); +/** @internal */ +export interface IFileHashCache { + get(key: string): Promise<string> | undefined; + + set(key: string, value: Promise<string>): void; + + del(key: string): void; +} + +/** @internal */ +export class FileHashCache implements IFileHashCache { + private lru: LruCache<string, Promise<string>>; + + constructor(maxSize: number = 250) { + this.lru = new LruCache(maxSize); + } get(key: string) { return this.lru.get(key); diff --git a/src/core/server/core_app/bundle_routes/fs.ts b/src/core/server/core_app/bundle_routes/fs.ts new file mode 100644 index 0000000000000..913b5c8423553 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/fs.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// can't use fs/promises when working with streams using file descriptors +// see https://github.com/nodejs/node/issues/35862 + +import Fs from 'fs'; +import { promisify } from 'util'; + +export const open = promisify(Fs.open); +export const close = promisify(Fs.close); +export const fstat = promisify(Fs.fstat); diff --git a/src/optimize/jest.config.js b/src/core/server/core_app/bundle_routes/index.ts similarity index 77% rename from src/optimize/jest.config.js rename to src/core/server/core_app/bundle_routes/index.ts index 8469778d775a2..5b2374a74356a 100644 --- a/src/optimize/jest.config.js +++ b/src/core/server/core_app/bundle_routes/index.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['<rootDir>/src/optimize'], -}; +export { registerBundleRoutes } from './register_bundle_routes'; diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.mocks.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.mocks.ts new file mode 100644 index 0000000000000..9c93f5d403c33 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.mocks.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const registerRouteForBundleMock = jest.fn(); +jest.doMock('./bundles_route', () => ({ + registerRouteForBundle: registerRouteForBundleMock, +})); + +jest.doMock('@kbn/ui-shared-deps', () => ({ + distDir: 'uiSharedDepsDistDir', +})); diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts new file mode 100644 index 0000000000000..d51c369146957 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { registerRouteForBundleMock } from './register_bundle_routes.test.mocks'; + +import { PackageInfo } from '@kbn/config'; +import { httpServiceMock } from '../../http/http_service.mock'; +import { UiPlugins } from '../../plugins'; +import { registerBundleRoutes } from './register_bundle_routes'; +import { FileHashCache } from './file_hash_cache'; + +const createPackageInfo = (parts: Partial<PackageInfo> = {}): PackageInfo => ({ + ...parts, + buildNum: 42, + buildSha: 'sha', + dist: true, + branch: 'master', + version: '8.0.0', +}); + +const createUiPlugins = (...ids: string[]): UiPlugins => ({ + browserConfigs: new Map(), + public: new Map(), + internal: ids.reduce((map, id) => { + map.set(id, { + publicTargetDir: `/plugins/${id}/public-target-dir`, + }); + return map; + }, new Map()), +}); + +describe('registerBundleRoutes', () => { + let router: ReturnType<typeof httpServiceMock.createRouter>; + + beforeEach(() => { + router = httpServiceMock.createRouter(); + }); + + afterEach(() => { + registerRouteForBundleMock.mockReset(); + }); + + it('registers core and shared-dep bundles', () => { + registerBundleRoutes({ + router, + serverBasePath: '/server-base-path', + packageInfo: createPackageInfo(), + uiPlugins: createUiPlugins(), + }); + + expect(registerRouteForBundleMock).toHaveBeenCalledTimes(2); + + expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { + fileHashCache: expect.any(FileHashCache), + isDist: true, + bundlesPath: 'uiSharedDepsDistDir', + publicPath: '/server-base-path/42/bundles/kbn-ui-shared-deps/', + routePath: '/42/bundles/kbn-ui-shared-deps/', + }); + + expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { + fileHashCache: expect.any(FileHashCache), + isDist: true, + bundlesPath: expect.stringMatching(/src\/core\/target\/public/), + publicPath: '/server-base-path/42/bundles/core/', + routePath: '/42/bundles/core/', + }); + }); + + it('registers plugin bundles', () => { + registerBundleRoutes({ + router, + serverBasePath: '/server-base-path', + packageInfo: createPackageInfo(), + uiPlugins: createUiPlugins('plugin-a', 'plugin-b'), + }); + + expect(registerRouteForBundleMock).toHaveBeenCalledTimes(4); + + expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { + fileHashCache: expect.any(FileHashCache), + isDist: true, + bundlesPath: '/plugins/plugin-a/public-target-dir', + publicPath: '/server-base-path/42/bundles/plugin/plugin-a/', + routePath: '/42/bundles/plugin/plugin-a/', + }); + + expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { + fileHashCache: expect.any(FileHashCache), + isDist: true, + bundlesPath: '/plugins/plugin-b/public-target-dir', + publicPath: '/server-base-path/42/bundles/plugin/plugin-b/', + routePath: '/42/bundles/plugin/plugin-b/', + }); + }); +}); diff --git a/src/core/server/core_app/bundle_routes/register_bundle_routes.ts b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts new file mode 100644 index 0000000000000..ee54f8ef34622 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/register_bundle_routes.ts @@ -0,0 +1,69 @@ +/* + * 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 { join } from 'path'; +import { PackageInfo } from '@kbn/config'; +import { distDir as uiSharedDepsDistDir } from '@kbn/ui-shared-deps'; +import { IRouter } from '../../http'; +import { UiPlugins } from '../../plugins'; +import { fromRoot } from '../../utils'; +import { FileHashCache } from './file_hash_cache'; +import { registerRouteForBundle } from './bundles_route'; + +/** + * Creates the routes that serves files from `bundlesPath`. + * + * @param {Object} options + * @property {Array<{id,path}>} options.npUiPluginPublicDirs array of ids and paths that should be served for new platform plugins + * @property {string} options.regularBundlesPath + * @property {string} options.basePublicPath + * + * @return Array.of({Hapi.Route}) + */ +export function registerBundleRoutes({ + router, + serverBasePath, // serverBasePath + uiPlugins, + packageInfo, +}: { + router: IRouter; + serverBasePath: string; + uiPlugins: UiPlugins; + packageInfo: PackageInfo; +}) { + const { dist: isDist, buildNum } = packageInfo; + // rather than calculate the fileHash on every request, we + // provide a cache object to `resolveDynamicAssetResponse()` that + // will store the most recently used hashes. + const fileHashCache = new FileHashCache(); + + registerRouteForBundle(router, { + publicPath: `${serverBasePath}/${buildNum}/bundles/kbn-ui-shared-deps/`, + routePath: `/${buildNum}/bundles/kbn-ui-shared-deps/`, + bundlesPath: uiSharedDepsDistDir, + fileHashCache, + isDist, + }); + registerRouteForBundle(router, { + publicPath: `${serverBasePath}/${buildNum}/bundles/core/`, + routePath: `/${buildNum}/bundles/core/`, + bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), + fileHashCache, + isDist, + }); + + [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir }]) => { + registerRouteForBundle(router, { + publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/`, + routePath: `/${buildNum}/bundles/plugin/${id}/`, + bundlesPath: publicTargetDir, + fileHashCache, + isDist, + }); + }); +} diff --git a/src/core/server/core_app/bundle_routes/select_compressed_file.ts b/src/core/server/core_app/bundle_routes/select_compressed_file.ts new file mode 100644 index 0000000000000..c7b071a9c3548 --- /dev/null +++ b/src/core/server/core_app/bundle_routes/select_compressed_file.ts @@ -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 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 { extname } from 'path'; +import Accept from 'accept'; +import { open } from './fs'; + +declare module 'accept' { + // @types/accept does not include the `preferences` argument so we override the type to include it + export function encodings(encodingHeader?: string, preferences?: string[]): string[]; +} + +async function tryToOpenFile(filePath: string) { + try { + return await open(filePath, 'r'); + } catch (e) { + if (e.code === 'ENOENT') { + return undefined; + } else { + throw e; + } + } +} + +export async function selectCompressedFile(acceptEncodingHeader: string | undefined, path: string) { + let fd: number | undefined; + let fileEncoding: 'gzip' | 'br' | undefined; + const ext = extname(path); + + const supportedEncodings = Accept.encodings(acceptEncodingHeader, ['br', 'gzip']); + + // do not bother trying to look compressed versions for anything else than js or css files + if (ext === '.js' || ext === '.css') { + if (supportedEncodings[0] === 'br') { + fileEncoding = 'br'; + fd = await tryToOpenFile(`${path}.br`); + } + if (!fd && supportedEncodings.includes('gzip')) { + fileEncoding = 'gzip'; + fd = await tryToOpenFile(`${path}.gz`); + } + } + + if (!fd) { + fileEncoding = undefined; + // Use raw open to trigger exception if it does not exist + fd = await open(path, 'r'); + } + + return { fd, fileEncoding }; +} diff --git a/src/optimize/bundles_route/file_hash.ts b/src/core/server/core_app/bundle_routes/utils.ts similarity index 51% rename from src/optimize/bundles_route/file_hash.ts rename to src/core/server/core_app/bundle_routes/utils.ts index 1f5b1a979407c..a2adefcfa73c2 100644 --- a/src/optimize/bundles_route/file_hash.ts +++ b/src/core/server/core_app/bundle_routes/utils.ts @@ -6,33 +6,19 @@ * Side Public License, v 1. */ +import { createReadStream, Stats } from 'fs'; import { createHash } from 'crypto'; -import Fs from 'fs'; - import * as Rx from 'rxjs'; -import { takeUntil, map } from 'rxjs/operators'; - -import { FileHashCache } from './file_hash_cache'; - -/** - * Get the hash of a file via a file descriptor - */ -export async function getFileHash(cache: FileHashCache, path: string, stat: Fs.Stats, fd: number) { - const key = `${path}:${stat.ino}:${stat.size}:${stat.mtime.getTime()}`; - - const cached = cache.get(key); - if (cached) { - return await cached; - } +import { map, takeUntil } from 'rxjs/operators'; +export const generateFileHash = (fd: number): Promise<string> => { const hash = createHash('sha1'); - const read = Fs.createReadStream(null as any, { + const read = createReadStream(null as any, { fd, start: 0, autoClose: false, }); - - const promise = Rx.merge( + return Rx.merge( Rx.fromEvent<Buffer>(read, 'data'), Rx.fromEvent<Error>(read, 'error').pipe( map((error) => { @@ -42,13 +28,8 @@ export async function getFileHash(cache: FileHashCache, path: string, stat: Fs.S ) .pipe(takeUntil(Rx.fromEvent(read, 'end'))) .forEach((chunk) => hash.update(chunk)) - .then(() => hash.digest('hex')) - .catch((error) => { - // don't cache failed attempts - cache.del(key); - throw error; - }); + .then(() => hash.digest('hex')); +}; - cache.set(key, promise); - return await promise; -} +export const getFileCacheKey = (path: string, stat: Stats) => + `${path}:${stat.ino}:${stat.size}:${stat.mtime.getTime()}`; diff --git a/src/optimize/bundles_route/index.ts b/src/core/server/core_app/core_app.test.mocks.ts similarity index 70% rename from src/optimize/bundles_route/index.ts rename to src/core/server/core_app/core_app.test.mocks.ts index 086bce552c5d0..d45df8dd52d71 100644 --- a/src/optimize/bundles_route/index.ts +++ b/src/core/server/core_app/core_app.test.mocks.ts @@ -6,5 +6,7 @@ * Side Public License, v 1. */ -export { createBundlesRoute } from './bundles_route'; -export { createProxyBundlesRoute } from './proxy_bundles_route'; +export const registerBundleRoutesMock = jest.fn(); +jest.doMock('./bundle_routes', () => ({ + registerBundleRoutes: registerBundleRoutesMock, +})); diff --git a/src/core/server/core_app/core_app.test.ts b/src/core/server/core_app/core_app.test.ts index e08a8e0be0a41..ad7af3ac8b84d 100644 --- a/src/core/server/core_app/core_app.test.ts +++ b/src/core/server/core_app/core_app.test.ts @@ -6,28 +6,42 @@ * Side Public License, v 1. */ +import { registerBundleRoutesMock } from './core_app.test.mocks'; + import { mockCoreContext } from '../core_context.mock'; import { coreMock } from '../mocks'; import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; +import type { UiPlugins } from '../plugins'; import { CoreApp } from './core_app'; +const emptyPlugins = (): UiPlugins => ({ + internal: new Map(), + public: new Map(), + browserConfigs: new Map(), +}); + describe('CoreApp', () => { + let coreContext: ReturnType<typeof mockCoreContext.create>; let coreApp: CoreApp; let internalCoreSetup: ReturnType<typeof coreMock.createInternalSetup>; let httpResourcesRegistrar: ReturnType<typeof httpResourcesMock.createRegistrar>; beforeEach(() => { - const coreContext = mockCoreContext.create(); + coreContext = mockCoreContext.create(); internalCoreSetup = coreMock.createInternalSetup(); httpResourcesRegistrar = httpResourcesMock.createRegistrar(); internalCoreSetup.httpResources.createRegistrar.mockReturnValue(httpResourcesRegistrar); coreApp = new CoreApp(coreContext); }); + afterEach(() => { + registerBundleRoutesMock.mockReset(); + }); + describe('`/status` route', () => { it('is registered with `authRequired: false` is the status page is anonymous', () => { internalCoreSetup.status.isStatusPageAnonymous.mockReturnValue(true); - coreApp.setup(internalCoreSetup); + coreApp.setup(internalCoreSetup, emptyPlugins()); expect(httpResourcesRegistrar.register).toHaveBeenCalledWith( { @@ -43,7 +57,7 @@ describe('CoreApp', () => { it('is registered with `authRequired: true` is the status page is not anonymous', () => { internalCoreSetup.status.isStatusPageAnonymous.mockReturnValue(false); - coreApp.setup(internalCoreSetup); + coreApp.setup(internalCoreSetup, emptyPlugins()); expect(httpResourcesRegistrar.register).toHaveBeenCalledWith( { @@ -60,7 +74,7 @@ describe('CoreApp', () => { describe('`/app/{id}/{any*}` route', () => { it('is registered with the correct parameters', () => { - coreApp.setup(internalCoreSetup); + coreApp.setup(internalCoreSetup, emptyPlugins()); expect(httpResourcesRegistrar.register).toHaveBeenCalledWith( { @@ -74,4 +88,17 @@ describe('CoreApp', () => { ); }); }); + + it('calls `registerBundleRoutes` with the correct options', () => { + const uiPlugins = emptyPlugins(); + coreApp.setup(internalCoreSetup, uiPlugins); + + expect(registerBundleRoutesMock).toHaveBeenCalledTimes(1); + expect(registerBundleRoutesMock).toHaveBeenCalledWith({ + uiPlugins, + router: expect.any(Object), + packageInfo: coreContext.env.packageInfo, + serverBasePath: internalCoreSetup.http.basePath.serverBasePath, + }); + }); }); diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts index 24ddc305d8232..dac941767ebb5 100644 --- a/src/core/server/core_app/core_app.ts +++ b/src/core/server/core_app/core_app.ts @@ -7,27 +7,32 @@ */ import Path from 'path'; -import { fromRoot } from '../../../core/server/utils'; +import { Env } from '@kbn/config'; +import { fromRoot } from '../utils'; import { InternalCoreSetup } from '../internal_types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; +import { registerBundleRoutes } from './bundle_routes'; +import { UiPlugins } from '../plugins'; /** @internal */ export class CoreApp { private readonly logger: Logger; + private readonly env: Env; constructor(core: CoreContext) { this.logger = core.logger.get('core-app'); + this.env = core.env; } - setup(coreSetup: InternalCoreSetup) { + setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) { this.logger.debug('Setting up core app.'); - this.registerDefaultRoutes(coreSetup); + this.registerDefaultRoutes(coreSetup, uiPlugins); this.registerStaticDirs(coreSetup); } - private registerDefaultRoutes(coreSetup: InternalCoreSetup) { + private registerDefaultRoutes(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) { const httpSetup = coreSetup.http; const router = httpSetup.createRouter(''); const resources = coreSetup.httpResources.createRegistrar(router); @@ -48,6 +53,13 @@ export class CoreApp { res.ok({ body: { version: '0.0.1' } }) ); + registerBundleRoutes({ + router, + uiPlugins, + packageInfo: this.env.packageInfo, + serverBasePath: coreSetup.http.basePath.serverBasePath, + }); + resources.register( { path: '/app/{id}/{any*}', diff --git a/src/optimize/bundles_route/__fixtures__/outside_output.js b/src/core/server/core_app/integration_tests/__fixtures__/outside_output.js similarity index 100% rename from src/optimize/bundles_route/__fixtures__/outside_output.js rename to src/core/server/core_app/integration_tests/__fixtures__/outside_output.js diff --git a/src/optimize/index.ts b/src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/gzip_chunk.js similarity index 87% rename from src/optimize/index.ts rename to src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/gzip_chunk.js index 3073c62d55b40..ca84988e8f978 100644 --- a/src/optimize/index.ts +++ b/src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/gzip_chunk.js @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { optimizeMixin } from './optimize_mixin'; +module.exports = 'GZIP-CHUNK'; diff --git a/src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/gzip_chunk.js.gz b/src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/gzip_chunk.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..fbf388e74ee70d5575ba071c2a6f091141bcea7c GIT binary patch literal 256 zcmV+b0ssCViwFqR#YtcQ17~_^a4lnKb#7}eYI6W}j=@gDFc3uV`HHz!s)7StD=t(B z5C~PO;K0pwmS!c#8`<m9{5^3|R1W1lGjCqAtgysdhB5NKQ#^Q^lwBelU59(~VPHes zNQ7rcAyEsJORXGQijY8sH`RNI1Sdfw@3Nlvu=_~VQ_y7aIx~OQ+R==jqe8ni*G1wK zZz&$8=9XK+Chz>YS>u446-LSE?T8?XJl$!>31939baXUm0dknk`oXgej_}Re)v#Ko z5#-4!OQ<i|oKz(KF)6tJWXu#cRn<$IJsCO<5;frti^tFB&DHwp{q1G3s(t{7q?t^D G0RR9V>U&23 literal 0 HcmV?d00001 diff --git a/src/optimize/bundles_route/__fixtures__/plugin/foo/image.png b/src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/image.png similarity index 100% rename from src/optimize/bundles_route/__fixtures__/plugin/foo/image.png rename to src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/image.png diff --git a/src/optimize/bundles_route/__fixtures__/plugin/foo/plugin.js b/src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/plugin.js similarity index 100% rename from src/optimize/bundles_route/__fixtures__/plugin/foo/plugin.js rename to src/core/server/core_app/integration_tests/__fixtures__/plugin/foo/plugin.js diff --git a/src/core/server/core_app/integration_tests/bundle_routes.test.ts b/src/core/server/core_app/integration_tests/bundle_routes.test.ts new file mode 100644 index 0000000000000..fbe2e9285ba29 --- /dev/null +++ b/src/core/server/core_app/integration_tests/bundle_routes.test.ts @@ -0,0 +1,171 @@ +/* + * 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 { resolve } from 'path'; +import { readFile } from 'fs/promises'; +import supertest from 'supertest'; +import { contextServiceMock } from '../../context/context_service.mock'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { HttpService, IRouter } from '../../http'; +import { createHttpServer } from '../../http/test_utils'; +import { registerRouteForBundle } from '../bundle_routes/bundles_route'; +import { FileHashCache } from '../bundle_routes/file_hash_cache'; + +const buildNum = 1234; +const fooPluginFixture = resolve(__dirname, './__fixtures__/plugin/foo'); + +describe('bundle routes', () => { + let server: HttpService; + let contextSetup: ReturnType<typeof contextServiceMock.createSetupContract>; + let logger: ReturnType<typeof loggingSystemMock.create>; + let fileHashCache: FileHashCache; + + beforeEach(() => { + contextSetup = contextServiceMock.createSetupContract(); + logger = loggingSystemMock.create(); + fileHashCache = new FileHashCache(); + + server = createHttpServer({ logger }); + }); + + afterEach(async () => { + await server.stop(); + }); + + const registerFooPluginRoute = ( + router: IRouter, + { isDist = false }: { isDist?: boolean } = {} + ) => { + registerRouteForBundle(router, { + isDist, + fileHashCache, + bundlesPath: fooPluginFixture, + routePath: `/${buildNum}/bundles/plugin/foo/`, + publicPath: `/${buildNum}/bundles/plugin/foo/`, + }); + }; + + it('serves images inside from the bundle path', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextSetup, + }); + + registerFooPluginRoute(createRouter('')); + await server.start(); + + const response = await supertest(innerServer.listener) + .get(`/${buildNum}/bundles/plugin/foo/image.png`) + .expect(200); + + const actualImage = await readFile(resolve(fooPluginFixture, 'image.png')); + expect(response.get('content-type')).toEqual('image/png'); + expect(response.body).toEqual(actualImage); + }); + + it('serves uncompressed js files', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextSetup, + }); + + registerFooPluginRoute(createRouter('')); + await server.start(); + + const response = await supertest(innerServer.listener) + .get(`/${buildNum}/bundles/plugin/foo/plugin.js`) + .expect(200); + + const actualFile = await readFile(resolve(fooPluginFixture, 'plugin.js')); + expect(response.get('content-type')).toEqual('application/javascript; charset=utf-8'); + expect(actualFile.toString('utf8')).toEqual(response.text); + }); + + it('returns 404 for files outside of the bundlePath', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextSetup, + }); + + registerFooPluginRoute(createRouter('')); + await server.start(); + + await supertest(innerServer.listener) + .get(`/${buildNum}/bundles/plugin/foo/../outside_output.js`) + .expect(404); + }); + + it('returns 404 for non-existing files', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextSetup, + }); + + registerFooPluginRoute(createRouter('')); + await server.start(); + + await supertest(innerServer.listener) + .get(`/${buildNum}/bundles/plugin/foo/missing.js`) + .expect(404); + }); + + it('returns gzip version if present', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextSetup, + }); + + registerFooPluginRoute(createRouter('')); + await server.start(); + + const response = await supertest(innerServer.listener) + .get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`) + .expect(200); + + expect(response.get('content-encoding')).toEqual('gzip'); + expect(response.get('content-type')).toEqual('application/javascript; charset=utf-8'); + + const actualFile = await readFile(resolve(fooPluginFixture, 'gzip_chunk.js')); + expect(actualFile.toString('utf8')).toEqual(response.text); + }); + + // supertest does not support brotli compression, cannot test + // this is covered in FTR tests anyway + it.skip('returns br version if present', () => {}); + + describe('in production mode', () => { + it('uses max-age cache-control', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextSetup, + }); + + registerFooPluginRoute(createRouter(''), { isDist: true }); + await server.start(); + + const response = await supertest(innerServer.listener) + .get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`) + .expect(200); + + expect(response.get('cache-control')).toEqual('max-age=31536000'); + expect(response.get('etag')).toBeUndefined(); + }); + }); + + describe('in development mode', () => { + it('uses etag cache-control', async () => { + const { server: innerServer, createRouter } = await server.setup({ + context: contextSetup, + }); + + registerFooPluginRoute(createRouter(''), { isDist: false }); + await server.start(); + + const response = await supertest(innerServer.listener) + .get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`) + .expect(200); + + expect(response.get('cache-control')).toEqual('must-revalidate'); + expect(response.get('etag')).toBeDefined(); + }); + }); +}); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 337dfa8824303..ef5164a8c48e1 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -221,7 +221,7 @@ export class Server { }); this.registerCoreContext(coreSetup); - this.coreApp.setup(coreSetup); + this.coreApp.setup(coreSetup, uiPlugins); setupTransaction?.end(); return coreSetup; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 55593d13d4687..d2eebb7b0cd23 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -15,7 +15,6 @@ import { Config } from './config'; import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; -import { optimizeMixin } from '../../optimize'; /** * @typedef {import('./kbn_server').KibanaConfig} KibanaConfig @@ -63,10 +62,7 @@ export default class KbnServer { coreMixin, - loggingMixin, - - // setup routes that serve the @kbn/optimizer output - optimizeMixin + loggingMixin ) ); diff --git a/src/optimize/bundles_route/bundles_route.test.ts b/src/optimize/bundles_route/bundles_route.test.ts deleted file mode 100644 index 4a5af40a66cfb..0000000000000 --- a/src/optimize/bundles_route/bundles_route.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { resolve } from 'path'; -import { readFileSync } from 'fs'; -import crypto from 'crypto'; - -import Chance from 'chance'; -import Hapi from '@hapi/hapi'; -import Inert from '@hapi/inert'; - -import { createBundlesRoute } from './bundles_route'; - -const chance = new Chance(); -const fooPluginFixture = resolve(__dirname, './__fixtures__/plugin/foo'); -const createHashMock = jest.spyOn(crypto, 'createHash'); - -const randomWordsCache = new Set(); -const uniqueRandomWord = (): string => { - const word = chance.word(); - - if (randomWordsCache.has(word)) { - return uniqueRandomWord(); - } - - randomWordsCache.add(word); - return word; -}; - -function createServer({ - basePublicPath = '', - isDist = false, -}: { - basePublicPath?: string; - isDist?: boolean; -} = {}) { - const buildHash = '1234'; - const npUiPluginPublicDirs = [ - { - id: 'foo', - path: fooPluginFixture, - }, - ]; - - const server = new Hapi.Server(); - server.register([Inert]); - - server.route( - createBundlesRoute({ - basePublicPath, - npUiPluginPublicDirs, - buildHash, - isDist, - }) - ); - - return server; -} - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('validation', () => { - it('validates that basePublicPath is valid', () => { - expect(() => { - createServer({ - // @ts-expect-error intentionally trying to break things - basePublicPath: 123, - }); - }).toThrowErrorMatchingInlineSnapshot(`"basePublicPath must be a string"`); - expect(() => { - createServer({ - // @ts-expect-error intentionally trying to break things - basePublicPath: {}, - }); - }).toThrowErrorMatchingInlineSnapshot(`"basePublicPath must be a string"`); - expect(() => { - createServer({ - basePublicPath: '/a/', - }); - }).toThrowErrorMatchingInlineSnapshot( - `"basePublicPath must be empty OR start and not end with a /"` - ); - expect(() => { - createServer({ - basePublicPath: 'a/', - }); - }).toThrowErrorMatchingInlineSnapshot( - `"basePublicPath must be empty OR start and not end with a /"` - ); - expect(() => { - createServer({ - basePublicPath: '/a', - }); - }).not.toThrowError(); - expect(() => { - createServer({ - basePublicPath: '', - }); - }).not.toThrowError(); - }); -}); - -describe('image', () => { - it('responds with exact file data', async () => { - const server = createServer(); - const response = await server.inject({ - url: '/1234/bundles/plugin/foo/image.png', - }); - - expect(response.statusCode).toBe(200); - const image = readFileSync(resolve(fooPluginFixture, 'image.png')); - expect(response.headers).toHaveProperty('content-length', image.length); - expect(response.headers).toHaveProperty('content-type', 'image/png'); - expect(image).toEqual(response.rawPayload); - }); -}); - -describe('js file', () => { - it('responds with no content-length and exact file data', async () => { - const server = createServer(); - const response = await server.inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }); - - expect(response.statusCode).toBe(200); - expect(response.headers).not.toHaveProperty('content-length'); - expect(response.headers).toHaveProperty( - 'content-type', - 'application/javascript; charset=utf-8' - ); - expect(readFileSync(resolve(fooPluginFixture, 'plugin.js'))).toEqual(response.rawPayload); - }); -}); - -describe('js file outside plugin', () => { - it('responds with a 404', async () => { - const server = createServer(); - - const response = await server.inject({ - url: '/1234/bundles/plugin/foo/../outside_output.js', - }); - - expect(response.statusCode).toBe(404); - expect(response.result).toEqual({ - error: 'Not Found', - message: 'Not Found', - statusCode: 404, - }); - }); -}); - -describe('missing js file', () => { - it('responds with 404', async () => { - const server = createServer(); - - const response = await server.inject({ - url: '/1234/bundles/plugin/foo/non_existent.js', - }); - - expect(response.statusCode).toBe(404); - expect(response.result).toEqual({ - error: 'Not Found', - message: 'Not Found', - statusCode: 404, - }); - }); -}); - -describe('etag', () => { - it('only calculates hash of file on first request', async () => { - const server = createServer(); - - expect(createHashMock).not.toHaveBeenCalled(); - const resp1 = await server.inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }); - - expect(createHashMock).toHaveBeenCalledTimes(1); - createHashMock.mockClear(); - expect(resp1.statusCode).toBe(200); - - const resp2 = await server.inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }); - - expect(createHashMock).not.toHaveBeenCalled(); - expect(resp2.statusCode).toBe(200); - }); - - it('is unique per basePublicPath although content is the same (by default)', async () => { - const basePublicPath1 = `/${uniqueRandomWord()}`; - const basePublicPath2 = `/${uniqueRandomWord()}`; - - const [resp1, resp2] = await Promise.all([ - createServer({ basePublicPath: basePublicPath1 }).inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }), - createServer({ basePublicPath: basePublicPath2 }).inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }), - ]); - - expect(resp1.statusCode).toBe(200); - expect(resp2.statusCode).toBe(200); - - expect(resp1.rawPayload).toEqual(resp2.rawPayload); - - expect(resp1.headers.etag).toEqual(expect.any(String)); - expect(resp2.headers.etag).toEqual(expect.any(String)); - expect(resp1.headers.etag).not.toEqual(resp2.headers.etag); - }); -}); - -describe('cache control', () => { - it('responds with 304 when etag and last modified are sent back', async () => { - const server = createServer(); - const resp = await server.inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }); - - expect(resp.statusCode).toBe(200); - - const resp2 = await server.inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - headers: { - 'if-modified-since': resp.headers['last-modified'], - 'if-none-match': resp.headers.etag, - }, - }); - - expect(resp2.statusCode).toBe(304); - expect(resp2.result).toHaveLength(0); - }); -}); - -describe('caching', () => { - describe('for non-distributable mode', () => { - it('uses "etag" header to invalidate cache', async () => { - const basePublicPath = `/${uniqueRandomWord()}`; - - const responce = await createServer({ basePublicPath }).inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }); - - expect(responce.statusCode).toBe(200); - - expect(responce.headers.etag).toEqual(expect.any(String)); - expect(responce.headers['cache-control']).toBe('must-revalidate'); - }); - - it('creates the same "etag" header for the same content with the same basePath', async () => { - const [resp1, resp2] = await Promise.all([ - createServer({ basePublicPath: '' }).inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }), - createServer({ basePublicPath: '' }).inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }), - ]); - - expect(resp1.statusCode).toBe(200); - expect(resp2.statusCode).toBe(200); - - expect(resp1.rawPayload).toEqual(resp2.rawPayload); - - expect(resp1.headers.etag).toEqual(expect.any(String)); - expect(resp2.headers.etag).toEqual(expect.any(String)); - expect(resp1.headers.etag).toEqual(resp2.headers.etag); - }); - }); - - describe('for distributable mode', () => { - it('commands to cache assets for each release for a year', async () => { - const basePublicPath = `/${uniqueRandomWord()}`; - - const responce = await createServer({ - basePublicPath, - isDist: true, - }).inject({ - url: '/1234/bundles/plugin/foo/plugin.js', - }); - - expect(responce.statusCode).toBe(200); - - expect(responce.headers.etag).toBe(undefined); - expect(responce.headers['cache-control']).toBe('max-age=31536000'); - }); - }); -}); diff --git a/src/optimize/bundles_route/bundles_route.ts b/src/optimize/bundles_route/bundles_route.ts deleted file mode 100644 index b88ca7e5c22b1..0000000000000 --- a/src/optimize/bundles_route/bundles_route.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { extname, join } from 'path'; - -import Hapi from '@hapi/hapi'; -import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import agent from 'elastic-apm-node'; - -import { createDynamicAssetResponse } from './dynamic_asset_response'; -import { FileHashCache } from './file_hash_cache'; -import { assertIsNpUiPluginPublicDirs, NpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; -import { fromRoot } from '../../core/server/utils'; - -/** - * Creates the routes that serves files from `bundlesPath`. - * - * @param {Object} options - * @property {Array<{id,path}>} options.npUiPluginPublicDirs array of ids and paths that should be served for new platform plugins - * @property {string} options.regularBundlesPath - * @property {string} options.basePublicPath - * - * @return Array.of({Hapi.Route}) - */ -export function createBundlesRoute({ - basePublicPath, - npUiPluginPublicDirs = [], - buildHash, - isDist = false, -}: { - basePublicPath: string; - npUiPluginPublicDirs?: NpUiPluginPublicDirs; - buildHash: string; - isDist?: boolean; -}) { - // rather than calculate the fileHash on every request, we - // provide a cache object to `resolveDynamicAssetResponse()` that - // will store the 100 most recently used hashes. - const fileHashCache = new FileHashCache(); - assertIsNpUiPluginPublicDirs(npUiPluginPublicDirs); - - if (typeof basePublicPath !== 'string') { - throw new TypeError('basePublicPath must be a string'); - } - - if (!basePublicPath.match(/(^$|^\/.*[^\/]$)/)) { - throw new TypeError('basePublicPath must be empty OR start and not end with a /'); - } - - return [ - buildRouteForBundles({ - publicPath: `${basePublicPath}/${buildHash}/bundles/kbn-ui-shared-deps/`, - routePath: `/${buildHash}/bundles/kbn-ui-shared-deps/`, - bundlesPath: UiSharedDeps.distDir, - fileHashCache, - isDist, - }), - ...npUiPluginPublicDirs.map(({ id, path }) => - buildRouteForBundles({ - publicPath: `${basePublicPath}/${buildHash}/bundles/plugin/${id}/`, - routePath: `/${buildHash}/bundles/plugin/${id}/`, - bundlesPath: path, - fileHashCache, - isDist, - }) - ), - buildRouteForBundles({ - publicPath: `${basePublicPath}/${buildHash}/bundles/core/`, - routePath: `/${buildHash}/bundles/core/`, - bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), - fileHashCache, - isDist, - }), - ]; -} - -function buildRouteForBundles({ - publicPath, - routePath, - bundlesPath, - fileHashCache, - isDist, -}: { - publicPath: string; - routePath: string; - bundlesPath: string; - fileHashCache: FileHashCache; - isDist: boolean; -}) { - return { - method: 'GET', - path: `${routePath}{path*}`, - config: { - auth: false, - ext: { - onPreHandler: { - method(request: Hapi.Request, h: Hapi.ResponseToolkit) { - const ext = extname(request.params.path); - - agent.setTransactionName('GET ?/bundles/?'); - - if (ext !== '.js' && ext !== '.css') { - return h.continue; - } - - return createDynamicAssetResponse({ - request, - h, - bundlesPath, - fileHashCache, - publicPath, - isDist, - }); - }, - }, - }, - }, - handler: { - directory: { - path: bundlesPath, - listing: false, - lookupCompressed: true, - }, - }, - }; -} diff --git a/src/optimize/bundles_route/dynamic_asset_response.ts b/src/optimize/bundles_route/dynamic_asset_response.ts deleted file mode 100644 index 309fe6dd47d51..0000000000000 --- a/src/optimize/bundles_route/dynamic_asset_response.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Fs from 'fs'; -import { resolve } from 'path'; -import { promisify } from 'util'; - -import Accept from 'accept'; -import Boom from '@hapi/boom'; -import Hapi from '@hapi/hapi'; - -import { FileHashCache } from './file_hash_cache'; -import { getFileHash } from './file_hash'; - -const MINUTE = 60; -const HOUR = 60 * MINUTE; -const DAY = 24 * HOUR; - -const asyncOpen = promisify(Fs.open); -const asyncClose = promisify(Fs.close); -const asyncFstat = promisify(Fs.fstat); - -async function tryToOpenFile(filePath: string) { - try { - return await asyncOpen(filePath, 'r'); - } catch (e) { - if (e.code === 'ENOENT') { - return undefined; - } else { - throw e; - } - } -} - -async function selectCompressedFile(acceptEncodingHeader: string | undefined, path: string) { - let fd: number | undefined; - let fileEncoding: 'gzip' | 'br' | undefined; - - const supportedEncodings = Accept.encodings(acceptEncodingHeader, ['br', 'gzip']); - - if (supportedEncodings[0] === 'br') { - fileEncoding = 'br'; - fd = await tryToOpenFile(`${path}.br`); - } - if (!fd && supportedEncodings.includes('gzip')) { - fileEncoding = 'gzip'; - fd = await tryToOpenFile(`${path}.gz`); - } - if (!fd) { - fileEncoding = undefined; - // Use raw open to trigger exception if it does not exist - fd = await asyncOpen(path, 'r'); - } - - return { fd, fileEncoding }; -} - -/** - * Create a Hapi response for the requested path. This is designed - * to replicate a subset of the features provided by Hapi's Inert - * plugin including: - * - ensure path is not traversing out of the bundle directory - * - manage use file descriptors for file access to efficiently - * interact with the file multiple times in each request - * - generate and cache etag for the file - * - write correct headers to response for client-side caching - * and invalidation - * - stream file to response - * - * It differs from Inert in some important ways: - * - cached hash/etag is based on the file on disk, but modified - * by the public path so that individual public paths have - * different etags, but can share a cache - */ -export async function createDynamicAssetResponse({ - request, - h, - bundlesPath, - publicPath, - fileHashCache, - isDist, -}: { - request: Hapi.Request; - h: Hapi.ResponseToolkit; - bundlesPath: string; - publicPath: string; - fileHashCache: FileHashCache; - isDist: boolean; -}) { - let fd: number | undefined; - let fileEncoding: 'gzip' | 'br' | undefined; - - try { - const path = resolve(bundlesPath, request.params.path); - - // prevent path traversal, only process paths that resolve within bundlesPath - if (!path.startsWith(bundlesPath)) { - throw Boom.forbidden(undefined, 'EACCES'); - } - - // we use and manage a file descriptor mostly because - // that's what Inert does, and since we are accessing - // the file 2 or 3 times per request it seems logical - ({ fd, fileEncoding } = await selectCompressedFile(request.headers['accept-encoding'], path)); - - const stat = await asyncFstat(fd); - const hash = isDist ? undefined : await getFileHash(fileHashCache, path, stat, fd); - - const content = Fs.createReadStream(null as any, { - fd, - start: 0, - autoClose: true, - }); - fd = undefined; // read stream is now responsible for fd - - const response = h - .response(content) - .takeover() - .code(200) - .type(request.server.mime.path(path).type); - - if (isDist) { - response.header('cache-control', `max-age=${365 * DAY}`); - } else { - response.etag(`${hash}-${publicPath}`); - response.header('cache-control', 'must-revalidate'); - } - - // If we manually selected a compressed file, specify the encoding header. - // Otherwise, let Hapi automatically gzip the response. - if (fileEncoding) { - response.header('content-encoding', fileEncoding); - } - - return response; - } catch (error) { - if (fd) { - try { - await asyncClose(fd); - } catch (_) { - // ignore errors from close, we already have one to report - // and it's very likely they are the same - } - } - - if (error.code === 'ENOENT') { - throw Boom.notFound(); - } - - throw Boom.boomify(error); - } -} diff --git a/src/optimize/bundles_route/proxy_bundles_route.ts b/src/optimize/bundles_route/proxy_bundles_route.ts deleted file mode 100644 index cb7f326b961f5..0000000000000 --- a/src/optimize/bundles_route/proxy_bundles_route.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 function createProxyBundlesRoute({ - host, - port, - buildHash, -}: { - host: string; - port: number; - buildHash: string; -}) { - return [buildProxyRouteForBundles(`/${buildHash}/bundles/`, host, port)]; -} - -function buildProxyRouteForBundles(routePath: string, host: string, port: number) { - return { - path: `${routePath}{path*}`, - method: 'GET', - handler: { - proxy: { - host, - port, - passThrough: true, - xforward: true, - }, - }, - config: { auth: false }, - }; -} diff --git a/src/optimize/np_ui_plugin_public_dirs.ts b/src/optimize/np_ui_plugin_public_dirs.ts deleted file mode 100644 index c5a4b8b85ce49..0000000000000 --- a/src/optimize/np_ui_plugin_public_dirs.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 KbnServer from '../legacy/server/kbn_server'; - -export type NpUiPluginPublicDirs = Array<{ - id: string; - path: string; -}>; - -export function getNpUiPluginPublicDirs(kbnServer: KbnServer): NpUiPluginPublicDirs { - return Array.from(kbnServer.newPlatform.__internals.uiPlugins.internal.entries()).map( - ([id, { publicTargetDir }]) => ({ - id, - path: publicTargetDir, - }) - ); -} - -export function isNpUiPluginPublicDirs(x: any): x is NpUiPluginPublicDirs { - return ( - Array.isArray(x) && - x.every( - (s) => typeof s === 'object' && s && typeof s.id === 'string' && typeof s.path === 'string' - ) - ); -} - -export function assertIsNpUiPluginPublicDirs(x: any): asserts x is NpUiPluginPublicDirs { - if (!isNpUiPluginPublicDirs(x)) { - throw new TypeError( - 'npUiPluginPublicDirs must be an array of objects with string `id` and `path` properties' - ); - } -} diff --git a/src/optimize/optimize_mixin.ts b/src/optimize/optimize_mixin.ts deleted file mode 100644 index dc780b0fae44c..0000000000000 --- a/src/optimize/optimize_mixin.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 Hapi from '@hapi/hapi'; - -import { createBundlesRoute } from './bundles_route'; -import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; -import KbnServer, { KibanaConfig } from '../legacy/server/kbn_server'; - -export const optimizeMixin = async ( - kbnServer: KbnServer, - server: Hapi.Server, - config: KibanaConfig -) => { - server.route( - createBundlesRoute({ - basePublicPath: config.get('server.basePath'), - npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), - buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), - isDist: kbnServer.newPlatform.env.packageInfo.dist, - }) - ); -};