diff --git a/.buildkite/scripts/steps/check_types_commits.sh b/.buildkite/scripts/steps/check_types_commits.sh index 113f516efc023..36ea6299194e9 100755 --- a/.buildkite/scripts/steps/check_types_commits.sh +++ b/.buildkite/scripts/steps/check_types_commits.sh @@ -9,18 +9,28 @@ if [[ "${CI-}" == "true" ]]; then sha1=$(git merge-base $GITHUB_PR_TARGET_BRANCH $GITHUB_PR_TRIGGERED_SHA) sha2="${GITHUB_PR_TRIGGERED_SHA-}" else - # Script take between 0 and 2 arguments representing two commit SHA's: - # If 0, it will diff HEAD and HEAD^ - # If 1 (SHA1), it will diff SHA1 and SHA1^ - # If 2 (SHA1, SHA2), it will diff SHA1 and SHA2 - sha1="${1-HEAD}" - sha2="${2-$sha1^}" + if [[ "${1-}" == "--cached" ]]; then + # Only check staged files + sha1=$1 + sha2="" + else + # Script take between 0 and 2 arguments representing two commit SHA's: + # If 0, it will diff HEAD and HEAD^ + # If 1 (SHA1), it will diff SHA1 and SHA1^ + # If 2 (SHA1, SHA2), it will diff SHA1 and SHA2 + sha1="${1-HEAD}" + sha2="${2-$sha1^}" + fi fi uniq_dirs=() uniq_tsconfigs=() -echo "Detecting files changed between $sha1 and $sha2..." +if [[ "$sha1" == "--cached" ]]; then + echo "Detecting files changed in staging area..." +else + echo "Detecting files changed between $sha1 and $sha2..." +fi files=($(git diff --name-only $sha1 $sha2)) @@ -102,7 +112,11 @@ do done if [ ${#uniq_tsconfigs[@]} -eq 0 ]; then - echo "No tsconfig.json files found for changes in $sha1 $sha2" + if [[ "$sha1" == "--cached" ]]; then + echo "No tsconfig.json files found for staged changes" + else + echo "No tsconfig.json files found for changes between $sha1 and $sha2" + fi exit fi diff --git a/package.json b/package.json index a08963d37feea..90d9f8aa5d2d7 100644 --- a/package.json +++ b/package.json @@ -880,7 +880,7 @@ "del": "^6.1.0", "elastic-apm-node": "^4.0.0", "email-addresses": "^5.0.0", - "execa": "^4.0.2", + "execa": "^5.1.1", "expiry-js": "0.1.7", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.1", diff --git a/packages/kbn-management/settings/components/field_row/footer/change_image_link.test.tsx b/packages/kbn-management/settings/components/field_row/footer/change_image_link.test.tsx index 201942bc89d44..c101ef175b395 100644 --- a/packages/kbn-management/settings/components/field_row/footer/change_image_link.test.tsx +++ b/packages/kbn-management/settings/components/field_row/footer/change_image_link.test.tsx @@ -16,7 +16,7 @@ const IMAGE = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8 describe('ChangeImageLink', () => { const defaultProps: ChangeImageLinkProps = { field: { - name: 'test', + id: 'test', type: 'image', ariaAttributes: { ariaLabel: 'test', diff --git a/packages/kbn-management/settings/components/field_row/footer/index.ts b/packages/kbn-management/settings/components/field_row/footer/index.ts index 0322c72010380..fc8cac296885c 100644 --- a/packages/kbn-management/settings/components/field_row/footer/index.ts +++ b/packages/kbn-management/settings/components/field_row/footer/index.ts @@ -7,4 +7,5 @@ */ export { FieldInputFooter, type FieldInputFooterProps } from './input_footer'; -export { InputResetLink, type FieldResetLinkProps } from './reset_link'; +export { InputResetLink } from './reset_link'; +export type { InputResetLinkProps } from './reset_link'; diff --git a/packages/kbn-unified-doc-viewer/package.json b/packages/kbn-unified-doc-viewer/package.json index c301255f057bc..a254194d5d940 100644 --- a/packages/kbn-unified-doc-viewer/package.json +++ b/packages/kbn-unified-doc-viewer/package.json @@ -3,5 +3,8 @@ "private": true, "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "sideEffects": false -} \ No newline at end of file + "sideEffects": [ + "*.css", + "*.scss" + ] +} diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index 487ed95672d83..d6bb3879669ae 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -998,12 +998,12 @@ export default function ({ ) { // will simulate autocomplete on 'GET /a/b/' with a filter by index return { - tokenPath: context.urlTokenPath.slice(0, -1), - predicate: (term) => term.meta === 'index', + tokenPath: context.urlTokenPath?.slice(0, -1), + predicate: (term: any) => term.meta === 'index', }; } else { // will do nothing special - return { tokenPath: context.urlTokenPath, predicate: (term) => true }; + return { tokenPath: context.urlTokenPath, predicate: () => true }; } })(); diff --git a/src/plugins/dashboard/public/dashboard_actions/_dashboard_actions_strings.ts b/src/plugins/dashboard/public/dashboard_actions/_dashboard_actions_strings.ts index fec2d60e01763..4bda6b379c790 100644 --- a/src/plugins/dashboard/public/dashboard_actions/_dashboard_actions_strings.ts +++ b/src/plugins/dashboard/public/dashboard_actions/_dashboard_actions_strings.ts @@ -29,10 +29,6 @@ export const dashboardCopyToDashboardActionStrings = { i18n.translate('dashboard.panel.copyToDashboard.existingDashboardOptionLabel', { defaultMessage: 'Existing dashboard', }), - getDescription: () => - i18n.translate('dashboard.panel.copyToDashboard.description', { - defaultMessage: 'Choose the destination dashboard.', - }), }; export const dashboardAddToLibraryActionStrings = { diff --git a/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_action.tsx b/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_action.tsx index 577ac2dc7a18e..a44c3cd94e878 100644 --- a/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_action.tsx @@ -8,10 +8,10 @@ import React from 'react'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; import type { IEmbeddable } from '@kbn/embeddable-plugin/public'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; -import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import { pluginServices } from '../services/plugin_services'; import { CopyToDashboardModal } from './copy_to_dashboard_modal'; @@ -39,16 +39,12 @@ export class CopyToDashboardAction implements Action session.close()} dashboardId={(embeddable.parent as DashboardContainer).getDashboardSavedObjectId()} embeddable={embeddable} />, - { theme$: this.theme$ } + { theme, i18n } ), { maxWidth: 400, diff --git a/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_modal.tsx b/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_modal.tsx index 2a41cc5a2bd8e..0563b4ac8aab6 100644 --- a/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_modal.tsx @@ -9,21 +9,21 @@ import React, { useCallback, useState } from 'react'; import { omit } from 'lodash'; import { - EuiText, EuiRadio, - EuiPanel, EuiButton, EuiSpacer, EuiFormRow, - EuiFocusTrap, EuiModalBody, EuiButtonEmpty, EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOutsideClickDetector, } from '@elastic/eui'; -import { IEmbeddable, PanelNotFoundError } from '@kbn/embeddable-plugin/public'; +import { + EmbeddablePackageState, + IEmbeddable, + PanelNotFoundError, +} from '@kbn/embeddable-plugin/public'; import { LazyDashboardPicker, withSuspense } from '@kbn/presentation-util-plugin/public'; import { DashboardPanelState } from '../../common'; @@ -33,7 +33,6 @@ import { dashboardCopyToDashboardActionStrings } from './_dashboard_actions_stri import { createDashboardEditUrl, CREATE_NEW_DASHBOARD_URL } from '../dashboard_constants'; interface CopyToDashboardModalProps { - PresentationUtilContext: React.FC; embeddable: IEmbeddable; dashboardId?: string; closeModal: () => void; @@ -42,7 +41,6 @@ interface CopyToDashboardModalProps { const DashboardPicker = withSuspense(LazyDashboardPicker); export function CopyToDashboardModal({ - PresentationUtilContext, dashboardId, embeddable, closeModal, @@ -65,11 +63,15 @@ export function CopyToDashboardModal({ throw new PanelNotFoundError(); } - const state = { + const state: EmbeddablePackageState = { type: embeddable.type, input: { ...omit(panelToCopy.explicitInput, 'id'), }, + size: { + width: panelToCopy.gridData.w, + height: panelToCopy.gridData.h, + }, }; const path = @@ -88,90 +90,73 @@ export function CopyToDashboardModal({ const descriptionId = 'copyToDashboardDescription'; return ( - - -
- - - - {dashboardCopyToDashboardActionStrings.getDisplayName()} - - +
+ + + {dashboardCopyToDashboardActionStrings.getDisplayName()} + + - - <> - -

{dashboardCopyToDashboardActionStrings.getDescription()}

-
- - - -
- {canEditExisting && ( - <> - setDashboardOption('existing')} - /> -
- setSelectedDashboard(dashboard)} - /> -
- - - )} - {canCreateNew && ( - <> - setDashboardOption('new')} - /> - - - )} -
-
-
- -
+ + <> + +
+ {canEditExisting && ( + <> + setDashboardOption('existing')} + /> + +
+ { + setSelectedDashboard(dashboard); + setDashboardOption('existing'); + }} + /> +
+ + + )} + {canCreateNew && ( + <> + setDashboardOption('new')} + /> + + + )} +
+
+ +
- - closeModal()}> - {dashboardCopyToDashboardActionStrings.getCancelButtonName()} - - - {dashboardCopyToDashboardActionStrings.getAcceptButtonName()} - - - -
- - + + closeModal()}> + {dashboardCopyToDashboardActionStrings.getCancelButtonName()} + + + {dashboardCopyToDashboardActionStrings.getAcceptButtonName()} + + +
); } diff --git a/src/plugins/dashboard/public/dashboard_actions/index.ts b/src/plugins/dashboard/public/dashboard_actions/index.ts index 4f0daff8c3390..21cde70691dbd 100644 --- a/src/plugins/dashboard/public/dashboard_actions/index.ts +++ b/src/plugins/dashboard/public/dashboard_actions/index.ts @@ -32,7 +32,7 @@ export const buildAllDashboardActions = async ({ plugins, allowByValueEmbeddables, }: BuildAllDashboardActionsProps) => { - const { uiActions, share, presentationUtil, savedObjectsTaggingOss, contentManagement } = plugins; + const { uiActions, share, savedObjectsTaggingOss, contentManagement } = plugins; const clonePanelAction = new ClonePanelAction(); uiActions.registerAction(clonePanelAction); @@ -73,7 +73,7 @@ export const buildAllDashboardActions = async ({ uiActions.registerAction(libraryNotificationAction); uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); - const copyToDashboardAction = new CopyToDashboardAction(presentationUtil.ContextProvider); + const copyToDashboardAction = new CopyToDashboardAction(core); uiActions.registerAction(copyToDashboardAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, copyToDashboardAction.id); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts index 244e816620ae7..c71e5cfc51d75 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.test.ts @@ -277,7 +277,7 @@ test('creates new embeddable with incoming embeddable if id does not match exist .fn() .mockReturnValue(mockContactCardFactory); - await createDashboard({ + const dashboard = await createDashboard({ getIncomingEmbeddable: () => incomingEmbeddable, getInitialInput: () => ({ panels: { @@ -301,6 +301,75 @@ test('creates new embeddable with incoming embeddable if id does not match exist }), expect.any(Object) ); + expect(dashboard!.getState().explicitInput.panels.i_match.explicitInput).toStrictEqual( + expect.objectContaining({ + id: 'i_match', + firstName: 'wow look at this new panel wow', + }) + ); + expect(dashboard!.getState().explicitInput.panels.i_do_not_match.explicitInput).toStrictEqual( + expect.objectContaining({ + id: 'i_do_not_match', + firstName: 'phew... I will not be replaced', + }) + ); + + // expect panel to be created with the default size. + expect(dashboard!.getState().explicitInput.panels.i_match.gridData.w).toBe(24); + expect(dashboard!.getState().explicitInput.panels.i_match.gridData.h).toBe(15); +}); + +test('creates new embeddable with specified size if size is provided', async () => { + const incomingEmbeddable: EmbeddablePackageState = { + type: CONTACT_CARD_EMBEDDABLE, + input: { + id: 'new_panel', + firstName: 'what a tiny lil panel', + } as ContactCardEmbeddableInput, + size: { width: 1, height: 1 }, + embeddableId: 'new_panel', + }; + const mockContactCardFactory = { + create: jest.fn().mockReturnValue({ destroy: jest.fn() }), + getDefaultInput: jest.fn().mockResolvedValue({}), + }; + pluginServices.getServices().embeddable.getEmbeddableFactory = jest + .fn() + .mockReturnValue(mockContactCardFactory); + + const dashboard = await createDashboard({ + getIncomingEmbeddable: () => incomingEmbeddable, + getInitialInput: () => ({ + panels: { + i_do_not_match: getSampleDashboardPanel({ + explicitInput: { + id: 'i_do_not_match', + firstName: 'phew... I will not be replaced', + }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, + }), + }); + + // flush promises + await new Promise((r) => setTimeout(r, 1)); + + expect(mockContactCardFactory.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'new_panel', + firstName: 'what a tiny lil panel', + }), + expect.any(Object) + ); + expect(dashboard!.getState().explicitInput.panels.new_panel.explicitInput).toStrictEqual( + expect.objectContaining({ + id: 'new_panel', + firstName: 'what a tiny lil panel', + }) + ); + expect(dashboard!.getState().explicitInput.panels.new_panel.gridData.w).toBe(1); + expect(dashboard!.getState().explicitInput.panels.new_panel.gridData.h).toBe(1); }); test('creates a control group from the control group factory and waits for it to be initialized', async () => { diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index 2403a569a80bb..70e81ca7a76d1 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import { v4 } from 'uuid'; import { Subject } from 'rxjs'; import { cloneDeep, identity, pickBy } from 'lodash'; @@ -19,14 +20,20 @@ import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public' import { type ControlGroupContainer, ControlGroupOutput } from '@kbn/controls-plugin/public'; import { GlobalQueryStateFromUrl, syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public'; -import { DashboardContainerInput } from '../../../../common'; import { DashboardContainer } from '../dashboard_container'; import { pluginServices } from '../../../services/plugin_services'; import { DashboardCreationOptions } from '../dashboard_container_factory'; +import { DashboardContainerInput, DashboardPanelState } from '../../../../common'; import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data_views'; +import { findTopLeftMostOpenSpace } from '../../component/panel/dashboard_panel_placement'; import { LoadDashboardReturn } from '../../../services/dashboard_content_management/types'; import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state'; -import { DEFAULT_DASHBOARD_INPUT, GLOBAL_STATE_STORAGE_KEY } from '../../../dashboard_constants'; +import { + DEFAULT_DASHBOARD_INPUT, + DEFAULT_PANEL_HEIGHT, + DEFAULT_PANEL_WIDTH, + GLOBAL_STATE_STORAGE_KEY, +} from '../../../dashboard_constants'; import { startSyncingDashboardControlGroup } from './controls/dashboard_control_group_integration'; import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration'; import { DashboardPublicState } from '../../types'; @@ -274,13 +281,45 @@ export const initializeDashboard = async ({ scrolltoIncomingEmbeddable(container, incomingEmbeddable.embeddableId as string) ); } else { - // otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created. + // otherwise this incoming embeddable is brand new and can be added after the dashboard container is created. + untilDashboardReady().then(async (container) => { - const embeddable = await container.addNewEmbeddable( - incomingEmbeddable.type, - incomingEmbeddable.input - ); - scrolltoIncomingEmbeddable(container, embeddable.id); + const createdEmbeddable = await (async () => { + // if there is no width or height we can add the panel using the default behaviour. + if (!incomingEmbeddable.size) { + return await container.addNewEmbeddable( + incomingEmbeddable.type, + incomingEmbeddable.input + ); + } + + // if the incoming embeddable has an explicit width or height we add the panel to the grid directly. + const { width, height } = incomingEmbeddable.size; + const currentPanels = container.getInput().panels; + const embeddableId = incomingEmbeddable.embeddableId ?? v4(); + const { newPanelPlacement } = findTopLeftMostOpenSpace({ + width: width ?? DEFAULT_PANEL_WIDTH, + height: height ?? DEFAULT_PANEL_HEIGHT, + currentPanels, + }); + const newPanelState: DashboardPanelState = { + explicitInput: { ...incomingEmbeddable.input, id: embeddableId }, + type: incomingEmbeddable.type, + gridData: { + ...newPanelPlacement, + i: embeddableId, + }, + }; + container.updateInput({ + panels: { + ...container.getInput().panels, + [newPanelState.explicitInput.id]: newPanelState, + }, + }); + + return await container.untilEmbeddableLoaded(embeddableId); + })(); + scrolltoIncomingEmbeddable(container, createdEmbeddable.id); }); } } diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 31cc2b1aab236..79aa7716b0160 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -65,7 +65,9 @@ "@kbn/shared-ux-prompt-not-found", "@kbn/content-management-content-editor", "@kbn/serverless", - "@kbn/no-data-page-plugin" + "@kbn/no-data-page-plugin", + "@kbn/react-kibana-mount", + "@kbn/core-lifecycle-browser" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index 74ee31ba71104..db6085f6ea276 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -42,6 +42,10 @@ export interface EmbeddablePackageState { type: string; input: Optional | Optional; embeddableId?: string; + size?: { + width?: number; + height?: number; + }; /** * Pass current search session id when navigating to an editor, diff --git a/src/plugins/presentation_util/kibana.jsonc b/src/plugins/presentation_util/kibana.jsonc index 1dfa765354cf9..91ac6c4194378 100644 --- a/src/plugins/presentation_util/kibana.jsonc +++ b/src/plugins/presentation_util/kibana.jsonc @@ -10,6 +10,7 @@ "requiredPlugins": [ "savedObjects", "kibanaReact", + "contentManagement", "embeddable", "expressions", "dataViews", diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker.tsx deleted file mode 100644 index 0207fe1dc3458..0000000000000 --- a/src/plugins/presentation_util/public/components/dashboard_picker.tsx +++ /dev/null @@ -1,105 +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, { useState, useEffect } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiComboBox } from '@elastic/eui'; -import { pluginServices } from '../services'; - -export interface DashboardPickerProps { - onChange: (dashboard: { name: string; id: string } | null) => void; - isDisabled: boolean; - idsToOmit?: string[]; -} - -interface DashboardOption { - label: string; - value: string; -} - -export function DashboardPicker(props: DashboardPickerProps) { - const [dashboardOptions, setDashboardOptions] = useState([]); - const [isLoadingDashboards, setIsLoadingDashboards] = useState(true); - const [selectedDashboard, setSelectedDashboard] = useState(null); - const [query, setQuery] = useState(''); - - const { isDisabled, onChange } = props; - const { dashboards } = pluginServices.getHooks(); - const { findDashboardsByTitle } = dashboards.useService(); - - useEffect(() => { - // We don't want to manipulate the React state if the component has been unmounted - // while we wait for the saved objects to return. - let cleanedUp = false; - - const fetchDashboards = async () => { - setIsLoadingDashboards(true); - setDashboardOptions([]); - - const objects = await findDashboardsByTitle(query ? `${query}*` : ''); - - if (cleanedUp) { - return; - } - - if (objects) { - setDashboardOptions( - objects - .filter((d) => !d.managed && !(props.idsToOmit ?? []).includes(d.id)) - .map((d) => ({ - value: d.id, - label: d.attributes.title, - 'data-test-subj': `dashboard-picker-option-${d.attributes.title.replaceAll( - ' ', - '-' - )}`, - })) - ); - } - - setIsLoadingDashboards(false); - }; - - fetchDashboards(); - - return () => { - cleanedUp = true; - }; - }, [findDashboardsByTitle, query, props.idsToOmit]); - - return ( - { - if (e.length) { - setSelectedDashboard({ value: e[0].value || '', label: e[0].label }); - onChange({ name: e[0].label, id: e[0].value || '' }); - } else { - setSelectedDashboard(null); - onChange(null); - } - }} - onSearchChange={setQuery} - isDisabled={isDisabled} - isLoading={isLoadingDashboards} - compressed={true} - /> - ); -} - -// required for dynamic import using React.lazy() -// eslint-disable-next-line import/no-default-export -export default DashboardPicker; diff --git a/src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx b/src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.stories.tsx similarity index 100% rename from src/plugins/presentation_util/public/components/dashboard_picker.stories.tsx rename to src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.stories.tsx diff --git a/src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.tsx b/src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.tsx new file mode 100644 index 0000000000000..ee72f9e03c40e --- /dev/null +++ b/src/plugins/presentation_util/public/components/dashboard_picker/dashboard_picker.tsx @@ -0,0 +1,182 @@ +/* + * Copyright 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 { debounce } from 'lodash'; +import { css } from '@emotion/react'; +import React, { useState, useEffect, useMemo } from 'react'; + +import { + EuiText, + useEuiTheme, + EuiSelectable, + EuiInputPopover, + EuiPopoverTitle, + EuiFieldSearch, + EuiHighlight, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ToolbarButton } from '@kbn/kibana-react-plugin/public'; +import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common'; + +import { pluginServices } from '../../services'; + +export interface DashboardPickerProps { + onChange: (dashboard: { name: string; id: string } | null) => void; + isDisabled: boolean; + idsToOmit?: string[]; +} + +interface DashboardOption { + label: string; + value: string; +} + +type DashboardHit = SavedObjectCommon<{ title: string }>; + +export function DashboardPicker({ isDisabled, onChange, idsToOmit }: DashboardPickerProps) { + const { euiTheme } = useEuiTheme(); + + const [isLoading, setIsLoading] = useState(true); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const [dashboardHits, setDashboardHits] = useState([]); + const [dashboardOptions, setDashboardOptions] = useState([]); + + const [query, setQuery] = useState(''); + const [debouncedQuery, setDebouncedQuery] = useState(''); + + const [selectedDashboard, setSelectedDashboard] = useState(null); + + const { + contentManagement: { client: cmClient }, + } = pluginServices.getServices(); + + /** + * Debounce the query to avoid many calls to content management. + */ + const debouncedSetQuery = useMemo( + () => debounce((latestQuery) => setDebouncedQuery(latestQuery), 150), + [] + ); + useEffect(() => { + debouncedSetQuery(query); + }, [debouncedSetQuery, query]); + + /** + * Run query to search for Dashboards when debounced query changes. + */ + useEffect(() => { + let canceled = false; + + (async () => { + setIsLoading(true); + + const response = await cmClient.mSearch({ + contentTypes: [{ contentTypeId: 'dashboard' }], + query: { + text: debouncedQuery ? `${debouncedQuery}*` : undefined, + limit: 30, + }, + }); + if (canceled) return; + if (response && response.hits) { + setDashboardHits(response.hits); + } + + setIsLoading(false); + })(); + + return () => { + canceled = true; + }; + }, [debouncedQuery, cmClient]); + + /** + * Format items with dashboard hits and selected option + */ + useEffect(() => { + setDashboardOptions( + dashboardHits + .filter((d) => !d.managed && !(idsToOmit ?? []).includes(d.id)) + .map((d) => ({ + value: d.id, + label: d.attributes.title, + checked: d.id === selectedDashboard?.value ? 'on' : undefined, + 'data-test-subj': `dashboard-picker-option-${d.attributes.title.replaceAll(' ', '-')}`, + })) + ); + }, [dashboardHits, idsToOmit, selectedDashboard]); + + return ( + setIsPopoverOpen(!isPopoverOpen)} + > + + {selectedDashboard?.label ?? + i18n.translate('presentationUtil.dashboardPicker.noDashboardOptionLabel', { + defaultMessage: 'Select dashboard', + })} + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + > + + setQuery(event.target.value)} + value={query} + data-test-subj="dashboard-picker-search" + placeholder={i18n.translate( + 'presentationUtil.dashboardPicker.searchDashboardPlaceholder', + { + defaultMessage: 'Search dashboards...', + } + )} + /> + + + { + setIsPopoverOpen(false); + + const nextSelectedDashboard: DashboardOption | null = + selected.value === selectedDashboard?.value ? null : selected; + setSelectedDashboard(nextSelectedDashboard); + onChange( + nextSelectedDashboard + ? { name: nextSelectedDashboard.label, id: nextSelectedDashboard.value } + : null + ); + }} + renderOption={(option) => {option.label}} + > + {(list) =>
{list}
} +
+
+ ); +} + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default DashboardPicker; diff --git a/src/plugins/presentation_util/public/components/index.tsx b/src/plugins/presentation_util/public/components/index.tsx index fb66c9c5b2be6..cff38e8a79d2b 100644 --- a/src/plugins/presentation_util/public/components/index.tsx +++ b/src/plugins/presentation_util/public/components/index.tsx @@ -32,7 +32,7 @@ export const LazyLabsBeakerButton = React.lazy(() => import('./labs/labs_beaker_ export const LazyLabsFlyout = React.lazy(() => import('./labs/labs_flyout')); -export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker')); +export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker/dashboard_picker')); export const LazySavedObjectSaveModalDashboard = React.lazy( () => import('./saved_object_save_modal_dashboard') diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.scss b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.scss deleted file mode 100644 index db07073bf1a3a..0000000000000 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.scss +++ /dev/null @@ -1,5 +0,0 @@ -.savAddDashboard__searchDashboards { - margin-left: $euiSizeL; - margin-top: $euiSizeXS; - width: 300px; -} diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index a13d7f0c76d5b..032bd022afd3c 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -16,8 +16,6 @@ import { pluginServices } from '../services'; import { SaveModalDashboardProps } from './types'; import { SaveModalDashboardSelector } from './saved_object_save_modal_dashboard_selector'; -import './saved_object_save_modal_dashboard.scss'; - function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props; const { id: documentId } = documentInfo; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index 33773dbc94faa..ce9d82ddf1db9 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -22,9 +22,7 @@ import { EuiCheckbox, } from '@elastic/eui'; -import DashboardPicker, { DashboardPickerProps } from './dashboard_picker'; - -import './saved_object_save_modal_dashboard.scss'; +import DashboardPicker, { DashboardPickerProps } from './dashboard_picker/dashboard_picker'; export interface SaveModalDashboardSelectorProps { copyOnSave: boolean; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index f5994b3da82e2..280fc4b979ce0 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -10,11 +10,7 @@ import { ExpressionFunction } from '@kbn/expressions-plugin/common'; import { PresentationUtilPlugin } from './plugin'; import { pluginServices } from './services'; -export type { - PresentationCapabilitiesService, - PresentationDashboardsService, - PresentationLabsService, -} from './services'; +export type { PresentationCapabilitiesService, PresentationLabsService } from './services'; export type { KibanaPluginServiceFactory, diff --git a/src/plugins/presentation_util/public/mocks.ts b/src/plugins/presentation_util/public/mocks.ts index 2c59d3ad55aad..b8a69d21916a8 100644 --- a/src/plugins/presentation_util/public/mocks.ts +++ b/src/plugins/presentation_util/public/mocks.ts @@ -9,15 +9,13 @@ import { CoreStart } from '@kbn/core/public'; import { PresentationUtilPluginStart } from './types'; import { pluginServices } from './services'; -import { registry } from './services/plugin_services'; +import { registry as stubRegistry } from './services/plugin_services.story'; import { ReduxToolsPackage, registerExpressionsLanguage } from '.'; import { createReduxEmbeddableTools } from './redux_tools/redux_embeddables/create_redux_embeddable_tools'; import { createReduxTools } from './redux_tools/create_redux_tools'; const createStartContract = (coreStart: CoreStart): PresentationUtilPluginStart => { - pluginServices.setRegistry( - registry.start({ coreStart, startPlugins: { dataViews: {}, uiActions: {} } as any }) - ); + pluginServices.setRegistry(stubRegistry.start({})); const startContract: PresentationUtilPluginStart = { ContextProvider: pluginServices.getContextProvider(), diff --git a/src/plugins/presentation_util/public/services/content_management/content_management.stub.ts b/src/plugins/presentation_util/public/services/content_management/content_management.stub.ts new file mode 100644 index 0000000000000..45e73c629bbab --- /dev/null +++ b/src/plugins/presentation_util/public/services/content_management/content_management.stub.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../create'; +import { PresentationContentManagementService } from './types'; + +type ContentManagementServiceFactory = PluginServiceFactory; + +export const contentManagementServiceFactory: ContentManagementServiceFactory = () => ({ + client: { + get: jest.fn(), + get$: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + search: jest.fn(), + search$: jest.fn(), + mSearch: jest.fn(), + } as unknown as PresentationContentManagementService['client'], +}); diff --git a/src/plugins/presentation_util/public/services/content_management/content_management_service.ts b/src/plugins/presentation_util/public/services/content_management/content_management_service.ts new file mode 100644 index 0000000000000..04876f0d8414a --- /dev/null +++ b/src/plugins/presentation_util/public/services/content_management/content_management_service.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 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 { PresentationUtilPluginStartDeps } from '../../types'; +import { PresentationContentManagementService } from './types'; +import { KibanaPluginServiceFactory } from '../create'; + +export type PresentationContentManagementServiceFactory = KibanaPluginServiceFactory< + PresentationContentManagementService, + PresentationUtilPluginStartDeps +>; + +export const contentManagementServiceFactory: PresentationContentManagementServiceFactory = ({ + startPlugins, +}) => { + return { + client: startPlugins.contentManagement.client, + }; +}; diff --git a/src/plugins/presentation_util/public/services/content_management/types.ts b/src/plugins/presentation_util/public/services/content_management/types.ts new file mode 100644 index 0000000000000..a8b0527d84df8 --- /dev/null +++ b/src/plugins/presentation_util/public/services/content_management/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; + +export interface PresentationContentManagementService { + client: ContentManagementPublicStart['client']; +} diff --git a/src/plugins/presentation_util/public/services/dashboards/dashboards.stub.ts b/src/plugins/presentation_util/public/services/dashboards/dashboards.stub.ts deleted file mode 100644 index d1dabd027aa2f..0000000000000 --- a/src/plugins/presentation_util/public/services/dashboards/dashboards.stub.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { PluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from './types'; - -// TODO (clint): Create set of dashboards to stub and return. - -type DashboardsServiceFactory = PluginServiceFactory; - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -export const dashboardsServiceFactory: DashboardsServiceFactory = () => ({ - findDashboards: async (query: string = '', _fields: string[] = []) => { - if (!query) { - return []; - } - - await sleep(2000); - return []; - }, - findDashboardsByTitle: async (title: string) => { - if (!title) { - return []; - } - - await sleep(2000); - return []; - }, -}); diff --git a/src/plugins/presentation_util/public/services/dashboards/dashboards_service.ts b/src/plugins/presentation_util/public/services/dashboards/dashboards_service.ts deleted file mode 100644 index 68facb8f31d94..0000000000000 --- a/src/plugins/presentation_util/public/services/dashboards/dashboards_service.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 { PresentationUtilPluginStartDeps } from '../../types'; -import { KibanaPluginServiceFactory } from '../create'; -import type { PresentationDashboardsService } from './types'; - -export type DashboardsServiceFactory = KibanaPluginServiceFactory< - PresentationDashboardsService, - PresentationUtilPluginStartDeps ->; - -export interface PartialDashboardAttributes { - title: string; -} -export const dashboardsServiceFactory: DashboardsServiceFactory = ({ coreStart }) => { - const findDashboards = async (query: string = '', fields: string[] = []) => { - const { find } = coreStart.savedObjects.client; - - const { savedObjects } = await find({ - type: 'dashboard', - search: `${query}*`, - searchFields: fields, - }); - - return savedObjects; - }; - - const findDashboardsByTitle = async (title: string = '') => findDashboards(title, ['title']); - - return { - findDashboards, - findDashboardsByTitle, - }; -}; diff --git a/src/plugins/presentation_util/public/services/dashboards/types.ts b/src/plugins/presentation_util/public/services/dashboards/types.ts deleted file mode 100644 index a4fd17a281800..0000000000000 --- a/src/plugins/presentation_util/public/services/dashboards/types.ts +++ /dev/null @@ -1,20 +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 { SimpleSavedObject } from '@kbn/core/public'; -import { PartialDashboardAttributes } from './dashboards_service'; - -export interface PresentationDashboardsService { - findDashboards: ( - query: string, - fields: string[] - ) => Promise>>; - findDashboardsByTitle: ( - title: string - ) => Promise>>; -} diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index b95bb666ae4e2..0989197aae8f0 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -8,8 +8,4 @@ export { pluginServices } from './plugin_services'; -export type { - PresentationCapabilitiesService, - PresentationDashboardsService, - PresentationLabsService, -} from './types'; +export type { PresentationCapabilitiesService, PresentationLabsService } from './types'; diff --git a/src/plugins/presentation_util/public/services/plugin_services.story.ts b/src/plugins/presentation_util/public/services/plugin_services.story.ts index b95b99e1dbca8..14ce570364bd3 100644 --- a/src/plugins/presentation_util/public/services/plugin_services.story.ts +++ b/src/plugins/presentation_util/public/services/plugin_services.story.ts @@ -16,7 +16,7 @@ import { PresentationUtilServices } from './types'; import { capabilitiesServiceFactory } from './capabilities/capabilities.story'; import { dataViewsServiceFactory } from './data_views/data_views.story'; -import { dashboardsServiceFactory } from './dashboards/dashboards.stub'; +import { contentManagementServiceFactory } from './content_management/content_management.stub'; import { labsServiceFactory } from './labs/labs.story'; import { uiActionsServiceFactory } from './ui_actions/ui_actions.stub'; @@ -24,7 +24,7 @@ export const providers: PluginServiceProviders = { capabilities: new PluginServiceProvider(capabilitiesServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), - dashboards: new PluginServiceProvider(dashboardsServiceFactory), + contentManagement: new PluginServiceProvider(contentManagementServiceFactory), uiActions: new PluginServiceProvider(uiActionsServiceFactory), }; diff --git a/src/plugins/presentation_util/public/services/plugin_services.stub.ts b/src/plugins/presentation_util/public/services/plugin_services.stub.ts index 427fbf9a3b6eb..409dcbd12ad6d 100644 --- a/src/plugins/presentation_util/public/services/plugin_services.stub.ts +++ b/src/plugins/presentation_util/public/services/plugin_services.stub.ts @@ -13,15 +13,15 @@ import { PresentationUtilPluginStart, registerExpressionsLanguage } from '..'; import { capabilitiesServiceFactory } from './capabilities/capabilities.story'; import { dataViewsServiceFactory } from './data_views/data_views.story'; -import { dashboardsServiceFactory } from './dashboards/dashboards.stub'; import { labsServiceFactory } from './labs/labs.story'; import { uiActionsServiceFactory } from './ui_actions/ui_actions.stub'; +import { contentManagementServiceFactory } from './content_management/content_management.stub'; export const providers: PluginServiceProviders = { + contentManagement: new PluginServiceProvider(contentManagementServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), - dashboards: new PluginServiceProvider(dashboardsServiceFactory), uiActions: new PluginServiceProvider(uiActionsServiceFactory), }; diff --git a/src/plugins/presentation_util/public/services/plugin_services.ts b/src/plugins/presentation_util/public/services/plugin_services.ts index 266912446b63c..a66d4d75b648f 100644 --- a/src/plugins/presentation_util/public/services/plugin_services.ts +++ b/src/plugins/presentation_util/public/services/plugin_services.ts @@ -17,7 +17,7 @@ import { PresentationUtilPluginStartDeps } from '../types'; import { capabilitiesServiceFactory } from './capabilities/capabilities_service'; import { dataViewsServiceFactory } from './data_views/data_views_service'; -import { dashboardsServiceFactory } from './dashboards/dashboards_service'; +import { contentManagementServiceFactory } from './content_management/content_management_service'; import { uiActionsServiceFactory } from './ui_actions/ui_actions_service'; import { labsServiceFactory } from './labs/labs_service'; import { PresentationUtilServices } from './types'; @@ -29,8 +29,8 @@ export const providers: PluginServiceProviders< capabilities: new PluginServiceProvider(capabilitiesServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), - dashboards: new PluginServiceProvider(dashboardsServiceFactory), uiActions: new PluginServiceProvider(uiActionsServiceFactory), + contentManagement: new PluginServiceProvider(contentManagementServiceFactory), }; export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/types.ts b/src/plugins/presentation_util/public/services/types.ts index 861d4f55068ac..a11d98a463e53 100644 --- a/src/plugins/presentation_util/public/services/types.ts +++ b/src/plugins/presentation_util/public/services/types.ts @@ -7,21 +7,17 @@ */ import { PresentationLabsService } from './labs/types'; -import { PresentationDashboardsService } from './dashboards/types'; import { PresentationCapabilitiesService } from './capabilities/types'; import { PresentationDataViewsService } from './data_views/types'; import { PresentationUiActionsService } from './ui_actions/types'; +import { PresentationContentManagementService } from './content_management/types'; export interface PresentationUtilServices { + contentManagement: PresentationContentManagementService; capabilities: PresentationCapabilitiesService; - dashboards: PresentationDashboardsService; dataViews: PresentationDataViewsService; uiActions: PresentationUiActionsService; labs: PresentationLabsService; } -export type { - PresentationCapabilitiesService, - PresentationDashboardsService, - PresentationLabsService, -}; +export type { PresentationCapabilitiesService, PresentationLabsService }; diff --git a/src/plugins/presentation_util/public/types.ts b/src/plugins/presentation_util/public/types.ts index 3b2785da82c0e..589d4101bdf80 100644 --- a/src/plugins/presentation_util/public/types.ts +++ b/src/plugins/presentation_util/public/types.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public/plugin'; import { registerExpressionsLanguage } from '.'; @@ -23,6 +24,7 @@ export interface PresentationUtilPluginStart { export interface PresentationUtilPluginSetupDeps {} export interface PresentationUtilPluginStartDeps { + contentManagement: ContentManagementPublicStart; dataViews: DataViewsPublicPluginStart; uiActions: UiActionsStart; } diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index 394337e477ef9..1270c0802dff2 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "target/types", + "outDir": "target/types" }, "include": [ "common/**/*", @@ -29,8 +29,8 @@ "@kbn/config-schema", "@kbn/storybook", "@kbn/ui-actions-plugin", + "@kbn/saved-objects-finder-plugin", + "@kbn/content-management-plugin" ], - "exclude": [ - "target/**/*", - ] + "exclude": ["target/**/*"] } diff --git a/test/functional/apps/console/_autocomplete.ts b/test/functional/apps/console/_autocomplete.ts index dd2e5a131efea..2f9d8db9cf9d2 100644 --- a/test/functional/apps/console/_autocomplete.ts +++ b/test/functional/apps/console/_autocomplete.ts @@ -213,11 +213,10 @@ GET _search 'pressRight', 'pressLeft', 'pressLeft', - ]; + ] as const; for (const keyPress of keyPresses) { await PageObjects.console.sleepForDebouncePeriod(); log.debug('Key', keyPress); - // @ts-ignore await PageObjects.console[keyPress](); expect(await PageObjects.console.isAutocompleteVisible()).to.be.eql(false); } @@ -258,7 +257,7 @@ GET _search for (const char of [method.at(-1), ' ', '_']) { await PageObjects.console.sleepForDebouncePeriod(); log.debug('Key type "%s"', char); - await PageObjects.console.enterText(char ?? ''); // e.g. 'Post ' -> 'Post _' + await PageObjects.console.enterText(char!); // e.g. 'Post ' -> 'Post _' } await retry.waitFor('autocomplete to be visible', () => diff --git a/test/functional/apps/dashboard/group3/copy_panel_to.ts b/test/functional/apps/dashboard/group3/copy_panel_to.ts index 81c5406426127..dbafa5d68b5e8 100644 --- a/test/functional/apps/dashboard/group3/copy_panel_to.ts +++ b/test/functional/apps/dashboard/group3/copy_panel_to.ts @@ -85,9 +85,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await label.click(); - await testSubjects.setValue('dashboardPickerInput', fewPanelsTitle); + await testSubjects.click('open-dashboard-picker'); + await testSubjects.setValue('dashboard-picker-search', fewPanelsTitle); await testSubjects.existOrFail(`dashboard-picker-option-few-panels`); - await find.clickByButtonText(fewPanelsTitle); + await testSubjects.click(`dashboard-picker-option-few-panels`); await testSubjects.click('confirmCopyToButton'); await PageObjects.dashboard.waitForRenderComplete(); @@ -113,7 +114,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await label.click(); - await testSubjects.setValue('dashboardPickerInput', fewPanelsTitle); + await testSubjects.click('open-dashboard-picker'); + await testSubjects.setValue('dashboard-picker-search', fewPanelsTitle); await testSubjects.missingOrFail(`dashboard-picker-option-few-panels`); await testSubjects.click('cancelCopyToButton'); diff --git a/test/functional/page_objects/time_to_visualize_page.ts b/test/functional/page_objects/time_to_visualize_page.ts index 280a02354424c..3b51e98029016 100644 --- a/test/functional/page_objects/time_to_visualize_page.ts +++ b/test/functional/page_objects/time_to_visualize_page.ts @@ -75,8 +75,11 @@ export class TimeToVisualizePageObject extends FtrService { await label.click(); if (dashboardId) { - await this.testSubjects.setValue('dashboardPickerInput', dashboardId); - await this.find.clickByButtonText(dashboardId); + await this.testSubjects.click('open-dashboard-picker'); + await this.testSubjects.setValue('dashboard-picker-search', dashboardId); + await this.testSubjects.click( + `dashboard-picker-option-${dashboardId.replaceAll(' ', '-')}` + ); } } diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/index.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/index.ts new file mode 100644 index 0000000000000..69a79adfab2e9 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/index.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. + */ + +export { bulkDeleteRulesRequestBodySchema } from './schemas/latest'; +export { bulkDeleteRulesRequestBodySchema as bulkDeleteRulesRequestBodySchemaV1 } from './schemas/v1'; + +export type { + BulkDeleteRulesResponse, + BulkOperationError, + BulkDeleteRulesRequestBody, +} from './types/latest'; +export type { + BulkDeleteRulesResponse as BulkDeleteRulesResponseV1, + BulkOperationError as BulkOperationErrorV1, + BulkDeleteRulesRequestBody as BulkDeleteRulesRequestBodyV1, +} from './types/v1'; + +export { validateCommonBulkOptions } from './validation/latest'; +export { validateCommonBulkOptions as validateCommonBulkOptionsV1 } from './validation/v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/schemas/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/schemas/latest.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 './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/schemas/v1.ts new file mode 100644 index 0000000000000..7cf5da6665087 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/schemas/v1.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 { schema } from '@kbn/config-schema'; + +export const bulkDeleteRulesRequestBodySchema = schema.object({ + filter: schema.maybe(schema.string()), + ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })), +}); diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/types/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/types/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/types/latest.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 './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/types/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/types/v1.ts new file mode 100644 index 0000000000000..3b7f062b836af --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/types/v1.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 type { TypeOf } from '@kbn/config-schema'; +import { bulkDeleteRulesRequestBodySchemaV1 } from '..'; +import { RuleParamsV1, RuleResponseV1 } from '../../../response'; + +export interface BulkOperationError { + message: string; + status?: number; + rule: { + id: string; + name: string; + }; +} + +export type BulkDeleteRulesRequestBody = TypeOf; + +export interface BulkDeleteRulesResponse { + body: { + rules: Array>; + errors: BulkOperationError[]; + total: number; + taskIdsFailedToBeDeleted: string[]; + }; +} diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/validation/latest.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/validation/latest.ts new file mode 100644 index 0000000000000..25300c97a6d2e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/validation/latest.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 './v1'; diff --git a/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/validation/v1.ts b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/validation/v1.ts new file mode 100644 index 0000000000000..e429d75db3e0c --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/rule/apis/bulk_delete/validation/v1.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 Boom from '@hapi/boom'; +import { BulkDeleteRulesRequestBody } from '..'; + +export const validateCommonBulkOptions = (options: BulkDeleteRulesRequestBody) => { + const filter = options.filter; + const ids = options.ids; + + if (!ids && !filter) { + throw Boom.badRequest( + "Either 'ids' or 'filter' property in method's arguments should be provided" + ); + } + + if (ids?.length === 0) { + throw Boom.badRequest("'ids' property should not be an empty array"); + } + + if (ids && filter) { + throw Boom.badRequest( + "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments" + ); + } +}; diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts similarity index 84% rename from x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts rename to x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts index 8d478b12d65a9..c33cd867a1be8 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts @@ -5,32 +5,33 @@ * 2.0. */ -import { RulesClient, ConstructorOptions } from '../rules_client'; +import { RulesClient, ConstructorOptions } from '../../../../rules_client/rules_client'; import { savedObjectsClientMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; -import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; -import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; -import { RecoveredActionGroup } from '../../../common'; +import { schema } from '@kbn/config-schema'; import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; -import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '@kbn/actions-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { getBeforeSetup, setGlobalDate } from './lib'; -import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; import { loggerMock } from '@kbn/logging-mocks'; +import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock'; +import { RecoveredActionGroup } from '../../../../../common'; +import { AlertingAuthorization } from '../../../../authorization/alerting_authorization'; +import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib'; +import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; import { - defaultRule, - enabledRule1, - enabledRule2, - returnedRule1, - returnedRule2, - siemRule1, -} from './test_helpers'; -import { schema } from '@kbn/config-schema'; -import { migrateLegacyActions } from '../lib'; - -jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => { + enabledRuleForBulkDelete1, + enabledRuleForBulkDelete2, + enabledRuleForBulkDelete3, + returnedRuleForBulkDelete1, + returnedRuleForBulkDelete2, + returnedRuleForBulkDelete3, + siemRuleForBulkDelete1, +} from '../../../../rules_client/tests/test_helpers'; +import { migrateLegacyActions } from '../../../../rules_client/lib'; + +jest.mock('../../../../rules_client/lib/siem_legacy_actions/migrate_legacy_actions', () => { return { migrateLegacyActions: jest.fn(), }; @@ -41,7 +42,7 @@ jest.mock('../lib/siem_legacy_actions/migrate_legacy_actions', () => { resultedReferences: [], }); -jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ +jest.mock('../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ bulkMarkApiKeysForInvalidation: jest.fn(), })); @@ -91,36 +92,6 @@ const getBulkOperationStatusErrorResponse = (statusCode: number) => ({ }, }); -export const enabledRule3 = { - ...defaultRule, - id: 'id3', - attributes: { - ...defaultRule.attributes, - enabled: true, - scheduledTaskId: 'id3', - apiKey: Buffer.from('789:ghi').toString('base64'), - apiKeyCreatedByUser: true, - }, -}; - -export const returnedRule3 = { - actions: [], - alertTypeId: 'fakeType', - apiKey: 'Nzg5OmdoaQ==', - apiKeyCreatedByUser: true, - consumer: 'fakeConsumer', - enabled: true, - id: 'id3', - name: 'fakeName', - notifyWhen: undefined, - params: undefined, - schedule: { - interval: '5m', - }, - scheduledTaskId: 'id3', - snoozeSchedule: [], -}; - beforeEach(() => { getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); jest.clearAllMocks(); @@ -132,7 +103,13 @@ describe('bulkDelete', () => { let rulesClient: RulesClient; const mockCreatePointInTimeFinderAsInternalUser = ( - response = { saved_objects: [enabledRule1, enabledRule2, enabledRule3] } + response = { + saved_objects: [ + enabledRuleForBulkDelete1, + enabledRuleForBulkDelete2, + enabledRuleForBulkDelete3, + ], + } ) => { encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest .fn() @@ -197,11 +174,13 @@ describe('bulkDelete', () => { const result = await rulesClient.bulkDeleteRules({ filter: 'fake_filter' }); expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledWith([ - enabledRule1, - enabledRule2, - enabledRule3, - ]); + expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledWith( + [enabledRuleForBulkDelete1, enabledRuleForBulkDelete2, enabledRuleForBulkDelete3].map( + ({ id }) => ({ id, type: 'alert' }) + ), + undefined + ); + expect(taskManager.bulkRemove).toHaveBeenCalledTimes(1); expect(taskManager.bulkRemove).toHaveBeenCalledWith(['id1', 'id3']); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); @@ -211,7 +190,7 @@ describe('bulkDelete', () => { expect.anything() ); expect(result).toStrictEqual({ - rules: [returnedRule1, returnedRule3], + rules: [returnedRuleForBulkDelete1, returnedRuleForBulkDelete3], errors: [{ message: 'UPS', rule: { id: 'id2', name: 'fakeName' }, status: 500 }], total: 2, taskIdsFailedToBeDeleted: [], @@ -241,25 +220,25 @@ describe('bulkDelete', () => { .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { - yield { saved_objects: [enabledRule1, enabledRule2] }; + yield { saved_objects: [enabledRuleForBulkDelete1, enabledRuleForBulkDelete2] }; }, }) .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { - yield { saved_objects: [enabledRule2] }; + yield { saved_objects: [enabledRuleForBulkDelete2] }; }, }) .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { - yield { saved_objects: [enabledRule2] }; + yield { saved_objects: [enabledRuleForBulkDelete2] }; }, }) .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { - yield { saved_objects: [enabledRule2] }; + yield { saved_objects: [enabledRuleForBulkDelete2] }; }, }); @@ -275,7 +254,7 @@ describe('bulkDelete', () => { expect.anything() ); expect(result).toStrictEqual({ - rules: [returnedRule1], + rules: [returnedRuleForBulkDelete1], errors: [{ message: 'UPS', rule: { id: 'id2', name: 'fakeName' }, status: 409 }], total: 2, taskIdsFailedToBeDeleted: [], @@ -305,19 +284,19 @@ describe('bulkDelete', () => { .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { - yield { saved_objects: [enabledRule1, enabledRule2] }; + yield { saved_objects: [enabledRuleForBulkDelete1, enabledRuleForBulkDelete2] }; }, }) .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { - yield { saved_objects: [enabledRule2] }; + yield { saved_objects: [enabledRuleForBulkDelete2] }; }, }) .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { - yield { saved_objects: [enabledRule2] }; + yield { saved_objects: [enabledRuleForBulkDelete2] }; }, }); @@ -333,7 +312,7 @@ describe('bulkDelete', () => { expect.anything() ); expect(result).toStrictEqual({ - rules: [returnedRule1, returnedRule2], + rules: [returnedRuleForBulkDelete1, returnedRuleForBulkDelete2], errors: [], total: 2, taskIdsFailedToBeDeleted: [], @@ -481,15 +460,21 @@ describe('bulkDelete', () => { .mockResolvedValueOnce({ close: jest.fn(), find: function* asyncGenerator() { - yield { saved_objects: [enabledRule1, enabledRule2, siemRule1] }; + yield { + saved_objects: [ + enabledRuleForBulkDelete1, + enabledRuleForBulkDelete2, + siemRuleForBulkDelete1, + ], + }; }, }); unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({ statuses: [ - { id: enabledRule1.id, type: 'alert', success: true }, - { id: enabledRule2.id, type: 'alert', success: true }, - { id: siemRule1.id, type: 'alert', success: true }, + { id: enabledRuleForBulkDelete1.id, type: 'alert', success: true }, + { id: enabledRuleForBulkDelete2.id, type: 'alert', success: true }, + { id: siemRuleForBulkDelete1.id, type: 'alert', success: true }, ], }); @@ -497,19 +482,19 @@ describe('bulkDelete', () => { expect(migrateLegacyActions).toHaveBeenCalledTimes(3); expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), { - ruleId: enabledRule1.id, + ruleId: enabledRuleForBulkDelete1.id, skipActionsValidation: true, - attributes: enabledRule1.attributes, + attributes: enabledRuleForBulkDelete1.attributes, }); expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), { - ruleId: enabledRule2.id, + ruleId: enabledRuleForBulkDelete2.id, skipActionsValidation: true, - attributes: enabledRule2.attributes, + attributes: enabledRuleForBulkDelete2.attributes, }); expect(migrateLegacyActions).toHaveBeenCalledWith(expect.any(Object), { - ruleId: siemRule1.id, + ruleId: siemRuleForBulkDelete1.id, skipActionsValidation: true, - attributes: siemRule1.attributes, + attributes: siemRuleForBulkDelete1.attributes, }); }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.ts similarity index 61% rename from x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts rename to x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.ts index 03ce78952e9db..e8a7b77a88c3d 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_delete.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.ts @@ -5,30 +5,49 @@ * 2.0. */ import pMap from 'p-map'; +import Boom from '@hapi/boom'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { SavedObjectsBulkUpdateObject } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; -import { RawRule } from '../../types'; -import { convertRuleIdsToKueryNode } from '../../lib'; -import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; -import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; -import { tryToRemoveTasks } from '../common'; -import { API_KEY_GENERATE_CONCURRENCY } from '../common/constants'; +import { convertRuleIdsToKueryNode } from '../../../../lib'; +import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; +import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; +import { tryToRemoveTasks } from '../../../../rules_client/common'; +import { API_KEY_GENERATE_CONCURRENCY } from '../../../../rules_client/common/constants'; import { getAuthorizationFilter, checkAuthorizationAndGetTotal, - getAlertFromRaw, migrateLegacyActions, -} from '../lib'; +} from '../../../../rules_client/lib'; import { retryIfBulkOperationConflicts, buildKueryNodeFilter, - getAndValidateCommonBulkOptions, -} from '../common'; -import { BulkOptions, BulkOperationError, RulesClientContext } from '../types'; +} from '../../../../rules_client/common'; +import type { RulesClientContext } from '../../../../rules_client/types'; +import type { + BulkOperationError, + BulkDeleteRulesResult, + BulkDeleteRulesRequestBody, +} from './types'; +import { validateCommonBulkOptions } from './validation'; +import type { RuleAttributes } from '../../../../data/rule/types'; +import { bulkDeleteRulesSo } from '../../../../data/rule'; +import { transformRuleAttributesToRuleDomain, transformRuleDomainToRule } from '../../transforms'; +import { ruleDomainSchema } from '../../schemas'; +import type { RuleParams, RuleDomain } from '../../types'; +import type { RawRule, SanitizedRule } from '../../../../types'; + +export const bulkDeleteRules = async ( + context: RulesClientContext, + options: BulkDeleteRulesRequestBody +): Promise> => { + try { + validateCommonBulkOptions(options); + } catch (error) { + throw Boom.badRequest(`Error validating bulk delete data - ${error.message}`); + } -export const bulkDeleteRules = async (context: RulesClientContext, options: BulkOptions) => { - const { ids, filter } = getAndValidateCommonBulkOptions(options); + const { ids, filter } = options; const kueryNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : buildKueryNodeFilter(filter); const authorizationFilter = await getAuthorizationFilter(context, { action: 'DELETE' }); @@ -71,20 +90,35 @@ export const bulkDeleteRules = async (context: RulesClientContext, options: Bulk ]); const deletedRules = rules.map(({ id, attributes, references }) => { - return getAlertFromRaw( - context, + // TODO (http-versioning): alertTypeId should never be null, but we need to + // fix the type cast from SavedObjectsBulkUpdateObject to SavedObjectsBulkUpdateObject + // when we are doing the bulk create and this should fix itself + const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId!); + const ruleDomain = transformRuleAttributesToRuleDomain(attributes as RuleAttributes, { id, - attributes.alertTypeId as string, - attributes as RawRule, + logger: context.logger, + ruleType, references, - false - ); + omitGeneratedValues: false, + }); + + try { + ruleDomainSchema.validate(ruleDomain); + } catch (e) { + context.logger.warn(`Error validating bulk edited rule domain object for id: ${id}, ${e}`); + } + return ruleDomain; }); + // // TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed + const deletedPublicRules = deletedRules.map((rule: RuleDomain) => { + return transformRuleDomainToRule(rule); + }) as Array>; + if (result.status === 'fulfilled') { - return { errors, total, rules: deletedRules, taskIdsFailedToBeDeleted: result.value }; + return { errors, total, rules: deletedPublicRules, taskIdsFailedToBeDeleted: result.value }; } else { - return { errors, total, rules: deletedRules, taskIdsFailedToBeDeleted: [] }; + return { errors, total, rules: deletedPublicRules, taskIdsFailedToBeDeleted: [] }; } }; @@ -98,15 +132,17 @@ const bulkDeleteWithOCC = async ( type: 'rules', }, () => - context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser({ - filter, - type: 'alert', - perPage: 100, - ...(context.namespace ? { namespaces: [context.namespace] } : undefined), - }) + context.encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser( + { + filter, + type: 'alert', + perPage: 100, + ...(context.namespace ? { namespaces: [context.namespace] } : undefined), + } + ) ); - const rulesToDelete: Array> = []; + const rulesToDelete: Array> = []; const apiKeyToRuleIdMapping: Record = {}; const taskIdToRuleIdMapping: Record = {}; const ruleNameToRuleIdMapping: Record = {}; @@ -142,7 +178,11 @@ const bulkDeleteWithOCC = async ( const result = await withSpan( { name: 'unsecuredSavedObjectsClient.bulkDelete', type: 'rules' }, - () => context.unsecuredSavedObjectsClient.bulkDelete(rulesToDelete) + () => + bulkDeleteRulesSo({ + savedObjectsClient: context.unsecuredSavedObjectsClient, + ids: rulesToDelete.map(({ id }) => id), + }) ); const deletedRuleIds: string[] = []; @@ -173,6 +213,7 @@ const bulkDeleteWithOCC = async ( const rules = rulesToDelete.filter((rule) => deletedRuleIds.includes(rule.id)); // migrate legacy actions only for SIEM rules + // TODO (http-versioning) Remove RawRuleAction and RawRule casts await pMap( rules, async (rule) => { diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/index.ts new file mode 100644 index 0000000000000..ac9048c366b6b --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/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 { bulkDeleteRules } from './bulk_delete_rules'; +export type { BulkDeleteRulesResult, BulkDeleteRulesRequestBody } from './types'; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/schemas/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/schemas/index.ts new file mode 100644 index 0000000000000..7cf5da6665087 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/schemas/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. + */ + +import { schema } from '@kbn/config-schema'; + +export const bulkDeleteRulesRequestBodySchema = schema.object({ + filter: schema.maybe(schema.string()), + ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })), +}); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/types/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/types/index.ts new file mode 100644 index 0000000000000..d7075638a5027 --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/types/index.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 type { TypeOf } from '@kbn/config-schema'; +import { bulkDeleteRulesRequestBodySchema } from '../schemas'; +import type { SanitizedRule } from '../../../../../types'; +import type { RuleParams } from '../../../types'; + +export interface BulkOperationError { + message: string; + status?: number; + rule: { + id: string; + name: string; + }; +} + +export type BulkDeleteRulesRequestBody = TypeOf; + +export interface BulkDeleteRulesResult { + rules: Array>; + errors: BulkOperationError[]; + total: number; + taskIdsFailedToBeDeleted: string[]; +} diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/validation/index.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/validation/index.ts new file mode 100644 index 0000000000000..35893977bb06c --- /dev/null +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/validation/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 Boom from '@hapi/boom'; +import { BulkDeleteRulesRequestBody } from '../types'; + +export const validateCommonBulkOptions = (options: BulkDeleteRulesRequestBody) => { + const filter = options.filter; + const ids = options.ids; + + if (!ids && !filter) { + throw Boom.badRequest( + "Either 'ids' or 'filter' property in method's arguments should be provided" + ); + } + + if (ids?.length === 0) { + throw Boom.badRequest("'ids' property should not be an empty array"); + } + + if (ids && filter) { + throw Boom.badRequest( + "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments" + ); + } +}; diff --git a/x-pack/plugins/alerting/server/data/rule/index.ts b/x-pack/plugins/alerting/server/data/rule/index.ts index 6256ac9b2278a..08306a281ac52 100644 --- a/x-pack/plugins/alerting/server/data/rule/index.ts +++ b/x-pack/plugins/alerting/server/data/rule/index.ts @@ -15,3 +15,5 @@ export { findRulesSo } from './methods/find_rules_so'; export type { FindRulesSoParams } from './methods/find_rules_so'; export { bulkCreateRulesSo } from './methods/bulk_create_rule_so'; export type { BulkCreateRulesSoParams } from './methods/bulk_create_rule_so'; +export { bulkDeleteRulesSo } from './methods/bulk_delete_rules_so'; +export type { BulkDeleteRulesSoParams } from './methods/bulk_delete_rules_so'; diff --git a/x-pack/plugins/alerting/server/data/rule/methods/bulk_delete_rules_so.ts b/x-pack/plugins/alerting/server/data/rule/methods/bulk_delete_rules_so.ts new file mode 100644 index 0000000000000..7f3568f76cf98 --- /dev/null +++ b/x-pack/plugins/alerting/server/data/rule/methods/bulk_delete_rules_so.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 { + SavedObjectsClientContract, + SavedObjectsBulkDeleteOptions, + SavedObjectsBulkDeleteResponse, +} from '@kbn/core/server'; + +export interface BulkDeleteRulesSoParams { + savedObjectsClient: SavedObjectsClientContract; + ids: string[]; + savedObjectsBulkDeleteOptions?: SavedObjectsBulkDeleteOptions; +} + +export const bulkDeleteRulesSo = ( + params: BulkDeleteRulesSoParams +): Promise => { + const { savedObjectsClient, ids, savedObjectsBulkDeleteOptions } = params; + + return savedObjectsClient.bulkDelete( + ids.map((id) => ({ id, type: 'alert' })), + savedObjectsBulkDeleteOptions + ); +}; diff --git a/x-pack/plugins/alerting/server/routes/bulk_delete_rules.ts b/x-pack/plugins/alerting/server/routes/bulk_delete_rules.ts deleted file mode 100644 index 1eca663de21a8..0000000000000 --- a/x-pack/plugins/alerting/server/routes/bulk_delete_rules.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { IRouter } from '@kbn/core/server'; -import { verifyAccessAndContext, handleDisabledApiKeysError } from './lib'; -import { ILicenseState, RuleTypeDisabledError } from '../lib'; -import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; - -export const bulkDeleteRulesRoute = ({ - router, - licenseState, -}: { - router: IRouter; - licenseState: ILicenseState; -}) => { - router.patch( - { - path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_delete`, - validate: { - body: schema.object({ - filter: schema.maybe(schema.string()), - ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })), - }), - }, - }, - handleDisabledApiKeysError( - router.handleLegacyErrors( - verifyAccessAndContext(licenseState, async (context, req, res) => { - const rulesClient = (await context.alerting).getRulesClient(); - const { filter, ids } = req.body; - - try { - const result = await rulesClient.bulkDeleteRules({ filter, ids }); - return res.ok({ body: result }); - } catch (e) { - if (e instanceof RuleTypeDisabledError) { - return e.sendResponse(res); - } - throw e; - } - }) - ) - ) - ); -}; diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index fe82abe56c4af..a93803ed6d585 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -40,7 +40,7 @@ import { bulkEditInternalRulesRoute } from './rule/apis/bulk_edit/bulk_edit_rule import { snoozeRuleRoute } from './snooze_rule'; import { unsnoozeRuleRoute } from './unsnooze_rule'; import { runSoonRoute } from './run_soon'; -import { bulkDeleteRulesRoute } from './bulk_delete_rules'; +import { bulkDeleteRulesRoute } from './rule/apis/bulk_delete/bulk_delete_rules_route'; import { bulkEnableRulesRoute } from './bulk_enable_rules'; import { bulkDisableRulesRoute } from './bulk_disable_rules'; import { cloneRuleRoute } from './clone_rule'; diff --git a/x-pack/plugins/alerting/server/routes/bulk_delete_rules.test.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.test.ts similarity index 87% rename from x-pack/plugins/alerting/server/routes/bulk_delete_rules.test.ts rename to x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.test.ts index c15405be85253..21099f6ba6086 100644 --- a/x-pack/plugins/alerting/server/routes/bulk_delete_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.test.ts @@ -7,16 +7,16 @@ import { httpServiceMock } from '@kbn/core/server/mocks'; -import { bulkDeleteRulesRoute } from './bulk_delete_rules'; -import { licenseStateMock } from '../lib/license_state.mock'; -import { mockHandlerArguments } from './_mock_handler_arguments'; -import { rulesClientMock } from '../rules_client.mock'; -import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { bulkDeleteRulesRoute } from './bulk_delete_rules_route'; +import { licenseStateMock } from '../../../../lib/license_state.mock'; +import { mockHandlerArguments } from '../../../_mock_handler_arguments'; +import { rulesClientMock } from '../../../../rules_client.mock'; +import { RuleTypeDisabledError } from '../../../../lib/errors/rule_type_disabled'; +import { verifyApiAccess } from '../../../../lib/license_api_access'; const rulesClient = rulesClientMock.create(); -jest.mock('../lib/license_api_access', () => ({ +jest.mock('../../../../lib/license_api_access', () => ({ verifyApiAccess: jest.fn(), })); diff --git a/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.ts b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.ts new file mode 100644 index 0000000000000..24dff47131978 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/rule/apis/bulk_delete/bulk_delete_rules_route.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 { IRouter } from '@kbn/core/server'; +import { verifyAccessAndContext, handleDisabledApiKeysError } from '../../../lib'; +import { ILicenseState, RuleTypeDisabledError } from '../../../../lib'; +import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../../../types'; +import { + bulkDeleteRulesRequestBodySchemaV1, + BulkDeleteRulesRequestBodyV1, + BulkDeleteRulesResponseV1, +} from '../../../../../common/routes/rule/apis/bulk_delete'; +import type { RuleParamsV1 } from '../../../../../common/routes/rule/response'; +import { transformRuleToRuleResponseV1 } from '../../transforms'; +import { Rule } from '../../../../application/rule/types'; + +export const bulkDeleteRulesRoute = ({ + router, + licenseState, +}: { + router: IRouter; + licenseState: ILicenseState; +}) => { + router.patch( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_delete`, + validate: { + body: bulkDeleteRulesRequestBodySchemaV1, + }, + }, + handleDisabledApiKeysError( + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async (context, req, res) => { + const rulesClient = (await context.alerting).getRulesClient(); + + const body: BulkDeleteRulesRequestBodyV1 = req.body; + const { filter, ids } = body; + + try { + const bulkDeleteResults = await rulesClient.bulkDeleteRules({ + filter, + ids, + }); + const resultBody: BulkDeleteRulesResponseV1 = { + body: { + ...bulkDeleteResults, + rules: bulkDeleteResults.rules.map((rule) => { + // TODO (http-versioning): Remove this cast, this enables us to move forward + // without fixing all of other solution types + return transformRuleToRuleResponseV1(rule as Rule); + }), + }, + }; + return res.ok(resultBody); + } catch (e) { + if (e instanceof RuleTypeDisabledError) { + return e.sendResponse(res); + } + throw e; + } + }) + ) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts index 428f43a0dcfa6..7a652c5230f47 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/retry_if_bulk_operation_conflicts.ts @@ -13,13 +13,13 @@ import { withSpan } from '@kbn/apm-utils'; import { convertRuleIdsToKueryNode } from '../../lib'; import { BulkOperationError } from '../types'; import { waitBeforeNextRetry, RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry'; -import { RawRule } from '../../types'; +import { RuleAttributes } from '../../data/rule/types'; const MAX_RULES_IDS_IN_RETRY = 1000; interface BulkOperationResult { errors: BulkOperationError[]; - rules: Array>; + rules: Array>; accListSpecificForBulkOperation: string[][]; } @@ -63,7 +63,7 @@ const handler = async ({ filter: KueryNode | null; accListSpecificForBulkOperation?: string[][]; accErrors?: BulkOperationError[]; - accRules?: Array>; + accRules?: Array>; retries?: number; }): Promise => { try { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts index 5ec0ae99ca99c..30b8b1eb01bba 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_disable.ts @@ -28,6 +28,7 @@ import { } from '../lib'; import { BulkOptions, BulkOperationError, RulesClientContext } from '../types'; import { tryToRemoveTasks } from '../common'; +import { RuleAttributes } from '../../data/rule/types'; export const bulkDisableRules = async (context: RulesClientContext, options: BulkOptions) => { const { ids, filter } = getAndValidateCommonBulkOptions(options); @@ -218,7 +219,8 @@ const bulkDisableRulesWithOCC = async ( return { errors, - rules: disabledRules, + // TODO: delete the casting when we do versioning of bulk disable api + rules: disabledRules as Array>, accListSpecificForBulkOperation: [taskIdsToDisable, taskIdsToDelete], }; }; diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts index 0e3f0eb042ec5..fda778e6b11af 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts @@ -30,6 +30,7 @@ import { } from '../lib'; import { RulesClientContext, BulkOperationError, BulkOptions } from '../types'; import { validateScheduleLimit } from '../../application/rule/methods/get_schedule_frequency'; +import { RuleAttributes } from '../../data/rule/types'; const getShouldScheduleTask = async ( context: RulesClientContext, @@ -284,7 +285,12 @@ const bulkEnableRulesWithOCC = async ( }); } }); - return { errors, rules, accListSpecificForBulkOperation: [taskIdsToEnable] }; + return { + errors, + // TODO: delete the casting when we do versioning of bulk disable api + rules: rules as Array>, + accListSpecificForBulkOperation: [taskIdsToEnable], + }; }; export const tryToEnableTasks = async ({ diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index f2274648d3981..4c1138f23cb39 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -8,7 +8,6 @@ import { SanitizedRule, RuleTypeParams } from '../types'; import { parseDuration } from '../../common/parse_duration'; import { RulesClientContext, BulkOptions, MuteOptions } from './types'; - import { clone, CloneArguments } from './methods/clone'; import { createRule, CreateRuleParams } from '../application/rule/methods/create'; import { get, GetParams } from './methods/get'; @@ -37,7 +36,10 @@ import { AggregateParams } from '../application/rule/methods/aggregate/types'; import { aggregateRules } from '../application/rule/methods/aggregate'; import { deleteRule } from './methods/delete'; import { update, UpdateOptions } from './methods/update'; -import { bulkDeleteRules } from './methods/bulk_delete'; +import { + bulkDeleteRules, + BulkDeleteRulesRequestBody, +} from '../application/rule/methods/bulk_delete'; import { bulkEditRules, BulkEditOptions, @@ -139,7 +141,8 @@ export class RulesClient { public getActionErrorLogWithAuth = (params: GetActionErrorLogByIdParams) => getActionErrorLogWithAuth(this.context, params); - public bulkDeleteRules = (options: BulkOptions) => bulkDeleteRules(this.context, options); + public bulkDeleteRules = (options: BulkDeleteRulesRequestBody) => + bulkDeleteRules(this.context, options); public bulkEdit = (options: BulkEditOptions) => bulkEditRules(this.context, options); public bulkEnableRules = (options: BulkOptions) => bulkEnableRules(this.context, options); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/test_helpers.ts b/x-pack/plugins/alerting/server/rules_client/tests/test_helpers.ts index fd4e534838940..ac8f482e88c5b 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/test_helpers.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/test_helpers.ts @@ -42,6 +42,24 @@ export const defaultRule = { version: '1', }; +export const defaultRuleForBulkDelete = { + id: 'id1', + type: 'alert', + attributes: { + name: 'fakeName', + consumer: 'fakeConsumer', + alertTypeId: 'fakeType', + schedule: { interval: '5m' }, + actions: [] as unknown, + executionStatus: { + lastExecutionDate: new Date('2019-02-12T21:01:22.000Z'), + status: 'pending', + }, + }, + references: [], + version: '1', +}; + export const siemRule1 = { ...defaultRule, attributes: { @@ -51,6 +69,15 @@ export const siemRule1 = { id: 'siem-id1', }; +export const siemRuleForBulkDelete1 = { + ...defaultRuleForBulkDelete, + attributes: { + ...defaultRuleForBulkDelete.attributes, + consumer: AlertConsumers.SIEM, + }, + id: 'siem-id1', +}; + export const siemRule2 = { ...siemRule1, id: 'siem-id2', @@ -77,6 +104,51 @@ export const enabledRule2 = { }, }; +export const enabledRule3 = { + ...defaultRule, + id: 'id3', + attributes: { + ...defaultRule.attributes, + enabled: true, + scheduledTaskId: 'id3', + apiKey: Buffer.from('789:ghi').toString('base64'), + apiKeyCreatedByUser: true, + }, +}; + +export const enabledRuleForBulkDelete1 = { + ...defaultRuleForBulkDelete, + attributes: { + ...defaultRuleForBulkDelete.attributes, + enabled: true, + scheduledTaskId: 'id1', + apiKey: Buffer.from('123:abc').toString('base64'), + }, +}; + +export const enabledRuleForBulkDelete2 = { + ...defaultRuleForBulkDelete, + id: 'id2', + attributes: { + ...defaultRuleForBulkDelete.attributes, + enabled: true, + scheduledTaskId: 'id2', + apiKey: Buffer.from('321:abc').toString('base64'), + }, +}; + +export const enabledRuleForBulkDelete3 = { + ...defaultRuleForBulkDelete, + id: 'id3', + attributes: { + ...defaultRuleForBulkDelete.attributes, + enabled: true, + scheduledTaskId: 'id3', + apiKey: Buffer.from('789:ghi').toString('base64'), + apiKeyCreatedByUser: true, + }, +}; + export const disabledRule1 = { ...defaultRule, attributes: { @@ -166,6 +238,67 @@ export const returnedRule2 = { snoozeSchedule: [], }; +export const returnedRuleForBulkDelete1 = { + actions: [], + alertTypeId: 'fakeType', + consumer: 'fakeConsumer', + enabled: true, + id: 'id1', + name: 'fakeName', + executionStatus: { + lastExecutionDate: new Date('2019-02-12T21:01:22.000Z'), + status: 'pending', + }, + createdAt: new Date('2019-02-12T21:01:22.479Z'), + updatedAt: new Date('2019-02-12T21:01:22.479Z'), + schedule: { + interval: '5m', + }, + scheduledTaskId: 'id1', + snoozeSchedule: [], +}; + +export const returnedRuleForBulkDelete2 = { + actions: [], + alertTypeId: 'fakeType', + consumer: 'fakeConsumer', + enabled: true, + id: 'id2', + name: 'fakeName', + executionStatus: { + lastExecutionDate: new Date('2019-02-12T21:01:22.000Z'), + status: 'pending', + }, + createdAt: new Date('2019-02-12T21:01:22.479Z'), + updatedAt: new Date('2019-02-12T21:01:22.479Z'), + schedule: { + interval: '5m', + }, + scheduledTaskId: 'id2', + snoozeSchedule: [], +}; + +export const returnedRuleForBulkDelete3 = { + actions: [], + alertTypeId: 'fakeType', + apiKeyCreatedByUser: true, + consumer: 'fakeConsumer', + enabled: true, + id: 'id3', + name: 'fakeName', + executionStatus: { + lastExecutionDate: new Date('2019-02-12T21:01:22.000Z'), + status: 'pending', + }, + createdAt: new Date('2019-02-12T21:01:22.479Z'), + updatedAt: new Date('2019-02-12T21:01:22.479Z'), + schedule: { + interval: '5m', + }, + scheduledTaskId: 'id3', + snoozeSchedule: [], +}; + export const returnedDisabledRule1 = { ...returnedRule1, enabled: false, diff --git a/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/stats.tsx b/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/stats.tsx index 3e8d42d5a2a3c..a0caf4b3002ac 100644 --- a/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/stats.tsx +++ b/x-pack/plugins/apm/public/components/app/mobile/service_overview/stats/stats.tsx @@ -110,10 +110,12 @@ export function MobileStats({ defaultMessage: 'Crash rate', }), icon: getIcon('bug'), - value: data?.currentPeriod?.crashRate?.value ?? NOT_AVAILABLE_LABEL, + value: data?.currentPeriod?.crashRate?.value ?? NaN, valueFormatter: (value: number) => - valueFormatter(Number((value * 100).toPrecision(2)), '%'), - trend: data?.currentPeriod?.crashRate?.timeseries, + Number.isNaN(value) + ? NOT_AVAILABLE_LABEL + : valueFormatter(Number((value * 100).toPrecision(2)), '%'), + trend: data?.currentPeriod?.crashRate?.timeseries ?? [], extra: getComparisonValueFormatter(data?.previousPeriod.crashRate?.value), trendShape: MetricTrendShape.Area, }, @@ -137,8 +139,9 @@ export function MobileStats({ defaultMessage: 'Sessions', }), icon: getIcon('timeslider'), - value: data?.currentPeriod?.sessions?.value ?? NOT_AVAILABLE_LABEL, - valueFormatter: (value: number) => valueFormatter(value), + value: data?.currentPeriod?.sessions?.value ?? NaN, + valueFormatter: (value: number) => + Number.isNaN(value) ? NOT_AVAILABLE_LABEL : valueFormatter(value), trend: data?.currentPeriod?.sessions?.timeseries, extra: getComparisonValueFormatter(data?.previousPeriod.sessions?.value), trendShape: MetricTrendShape.Area, @@ -149,10 +152,11 @@ export function MobileStats({ defaultMessage: 'HTTP requests', }), icon: getIcon('kubernetesPod'), - value: data?.currentPeriod?.requests?.value ?? NOT_AVAILABLE_LABEL, extra: getComparisonValueFormatter(data?.previousPeriod.requests?.value), - valueFormatter: (value: number) => valueFormatter(value), - trend: data?.currentPeriod?.requests?.timeseries, + value: data?.currentPeriod?.requests?.value ?? NaN, + valueFormatter: (value: number) => + Number.isNaN(value) ? NOT_AVAILABLE_LABEL : valueFormatter(value), + trend: data?.currentPeriod?.requests?.timeseries ?? [], trendShape: MetricTrendShape.Area, }, ]; diff --git a/x-pack/plugins/apm/public/components/app/storage_explorer/summary_stats.tsx b/x-pack/plugins/apm/public/components/app/storage_explorer/summary_stats.tsx index 57a348d4839d4..4ed48894c247a 100644 --- a/x-pack/plugins/apm/public/components/app/storage_explorer/summary_stats.tsx +++ b/x-pack/plugins/apm/public/components/app/storage_explorer/summary_stats.tsx @@ -205,7 +205,7 @@ function SummaryMetric({ loading: boolean; hasData: boolean; }) { - const xlFontSize = useEuiFontSize('xl', { measurement: 'px' }); + const xlFontSize = useEuiFontSize('xl', { unit: 'px' }); const { euiTheme } = useEuiTheme(); return ( diff --git a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts index 803a69bd44e21..df066a57418fc 100644 --- a/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/transaction_action_menu/sections.test.ts @@ -25,7 +25,7 @@ const apmRouter = { } as ApmRouter; const infraLocators = infraLocatorsMock; -const observabilityLogExplorerLocators = observabilityLogExplorerLocatorsMock; +const { allDatasetsLocator } = observabilityLogExplorerLocatorsMock; const expectInfraLocatorsToBeCalled = () => { expect(infraLocators.nodeLogsLocator.getRedirectUrl).toBeCalledTimes(3); @@ -65,7 +65,7 @@ describe('Transaction action menu', () => { location, apmRouter, infraLocators, - observabilityLogExplorerLocators, + allDatasetsLocator, infraLinksAvailable: false, rangeFrom: 'now-24h', rangeTo: 'now', @@ -131,7 +131,7 @@ describe('Transaction action menu', () => { location, apmRouter, infraLocators, - observabilityLogExplorerLocators, + allDatasetsLocator, infraLinksAvailable: true, rangeFrom: 'now-24h', rangeTo: 'now', @@ -216,7 +216,7 @@ describe('Transaction action menu', () => { location, apmRouter, infraLocators, - observabilityLogExplorerLocators, + allDatasetsLocator, infraLinksAvailable: true, rangeFrom: 'now-24h', rangeTo: 'now', diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 941e8568a45b7..e135161129c7c 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -132,6 +132,11 @@ export const infraLocatorsMock: InfraLocators = { nodeLogsLocator: sharePluginMock.createLocator(), }; +export const observabilityLogExplorerLocatorsMock = { + allDatasetsLocator: sharePluginMock.createLocator(), + singleDatasetLocator: sharePluginMock.createLocator(), +}; + const mockCorePlugins = { embeddable: {}, inspector: {}, diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/route.ts b/x-pack/plugins/apm/server/routes/assistant_functions/route.ts index 436f514e423ff..0d86bff4f7406 100644 --- a/x-pack/plugins/apm/server/routes/assistant_functions/route.ts +++ b/x-pack/plugins/apm/server/routes/assistant_functions/route.ts @@ -191,7 +191,7 @@ const getApmErrorDocRoute = createApmServerRoute({ }, }); -interface ApmServicesListItem { +export interface ApmServicesListItem { 'service.name': string; 'agent.name'?: string; 'transaction.type'?: string; diff --git a/x-pack/plugins/apm/server/routes/traces/queries.test.ts b/x-pack/plugins/apm/server/routes/traces/queries.test.ts index 03783b1f77874..18367b6745d84 100644 --- a/x-pack/plugins/apm/server/routes/traces/queries.test.ts +++ b/x-pack/plugins/apm/server/routes/traces/queries.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { loggerMock } from '@kbn/logging-mocks'; import { getTraceItems } from './get_trace_items'; import { SearchParamsMock, @@ -26,6 +27,7 @@ describe('trace queries', () => { apmEventClient: mockApmEventClient, start: 0, end: 50000, + logger: loggerMock.create(), }) ); diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts index 905b08ed49f60..34e24033db230 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/error_raw.ts @@ -18,7 +18,7 @@ import { TimestampUs } from './fields/timestamp_us'; import { Url } from './fields/url'; import { User } from './fields/user'; -interface Processor { +export interface Processor { name: 'error'; event: 'error'; } diff --git a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_api/get_csp_rule_template.ts b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_api/get_csp_rule_template.ts index 7b6345e749d71..350909e540d4d 100644 --- a/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_api/get_csp_rule_template.ts +++ b/x-pack/plugins/cloud_security_posture/common/schemas/csp_rule_template_api/get_csp_rule_template.ts @@ -57,7 +57,7 @@ export const findCspRuleTemplateRequest = schema.object({ schema.literal('metadata.benchmark.rule_number'), ], { - defaultValue: 'metadata.name', + defaultValue: 'metadata.benchmark.rule_number', } ), @@ -79,4 +79,9 @@ export const findCspRuleTemplateRequest = schema.object({ * package_policy_id */ packagePolicyId: schema.maybe(schema.string()), + + /** + * rule section + */ + section: schema.maybe(schema.string()), }); diff --git a/x-pack/plugins/cloud_security_posture/common/types.ts b/x-pack/plugins/cloud_security_posture/common/types.ts index f2b6c00399915..036826ba9e6de 100644 --- a/x-pack/plugins/cloud_security_posture/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/common/types.ts @@ -122,6 +122,7 @@ export interface Benchmark { export type BenchmarkId = CspRuleTemplateMetadata['benchmark']['id']; export type BenchmarkName = CspRuleTemplateMetadata['benchmark']['name']; +export type RuleSection = CspRuleTemplateMetadata['section']; // Fleet Integration types export type PostureInput = typeof SUPPORTED_CLOUDBEAT_INPUTS[number]; diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.test.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.test.ts index 7e7d25a5b28bd..6ce2add754f20 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.test.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.test.ts @@ -6,11 +6,7 @@ */ import { createPackagePolicyMock } from '@kbn/fleet-plugin/common/mocks'; -import { - getBenchmarkFromPackagePolicy, - getBenchmarkTypeFilter, - cleanupCredentials, -} from './helpers'; +import { getBenchmarkFromPackagePolicy, getBenchmarkFilter, cleanupCredentials } from './helpers'; describe('test helper methods', () => { it('get default integration type from inputs with multiple enabled types', () => { @@ -60,11 +56,20 @@ describe('test helper methods', () => { const typeK8s = getBenchmarkFromPackagePolicy(mockPackagePolicy.inputs); expect(typeK8s).toMatch('cis_k8s'); }); + it('get benchmark type filter based on a benchmark id', () => { - const typeFilter = getBenchmarkTypeFilter('cis_eks'); + const typeFilter = getBenchmarkFilter('cis_eks'); expect(typeFilter).toMatch('csp-rule-template.attributes.metadata.benchmark.id: "cis_eks"'); }); + it('should return a string with the correct filter when given a benchmark type and section', () => { + const typeAndSectionFilter = getBenchmarkFilter('cis_k8s', 'API Server'); + + expect(typeAndSectionFilter).toMatch( + 'csp-rule-template.attributes.metadata.benchmark.id: "cis_k8s" AND csp-rule-template.attributes.metadata.section: "API Server"' + ); + }); + describe('cleanupCredentials', () => { it('cleans unused aws credential methods, except role_arn when using assume_role', () => { const mockPackagePolicy = createPackagePolicyMock(); diff --git a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts index 9cf31c944d27a..1cf006589bd7a 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/helpers.ts @@ -27,6 +27,7 @@ import type { BaseCspSetupStatus, AwsCredentialsType, GcpCredentialsType, + RuleSection, } from '../types'; /** @@ -46,8 +47,12 @@ export const extractErrorMessage = (e: unknown, defaultMessage = 'Unknown Error' return defaultMessage; // TODO: i18n }; -export const getBenchmarkTypeFilter = (type: BenchmarkId): string => - `${CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.id: "${type}"`; +export const getBenchmarkFilter = (type: BenchmarkId, section?: RuleSection): string => + `${CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE}.attributes.metadata.benchmark.id: "${type}"${ + section + ? ` AND ${CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE}.attributes.metadata.section: "${section}"` + : '' + }`; export const isEnabledBenchmarkInputType = (input: PackagePolicyInput | NewPackagePolicyInput) => input.enabled; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx index 74ce3ba55ef85..81472e29b6ee7 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_container.tsx @@ -58,6 +58,7 @@ export const RulesContainer = () => { const [selectedRuleId, setSelectedRuleId] = useState(null); const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY); const [rulesQuery, setRulesQuery] = useState({ + section: undefined, search: '', page: 0, perPage: pageSize || 10, @@ -65,6 +66,7 @@ export const RulesContainer = () => { const { data, status, error } = useFindCspRuleTemplates( { + section: rulesQuery.section, search: rulesQuery.search, page: 1, perPage: MAX_ITEMS_PER_PAGE, @@ -77,12 +79,31 @@ export const RulesContainer = () => { [data, error, status, rulesQuery] ); + // We need to make this call again without the filters. this way the section list is always full + const allRules = useFindCspRuleTemplates( + { + page: 1, + perPage: MAX_ITEMS_PER_PAGE, + }, + params.packagePolicyId + ); + + const sectionList = useMemo( + () => allRules.data?.items.map((rule) => rule.metadata.section), + [allRules.data] + ); + const cleanedSectionList = [...new Set(sectionList)]; + return (
+ setRulesQuery((currentQuery) => ({ ...currentQuery, section: value })) + } + sectionSelectOptions={cleanedSectionList} search={(value) => setRulesQuery((currentQuery) => ({ ...currentQuery, search: value }))} - searchValue={rulesQuery.search} + searchValue={rulesQuery.search || ''} totalRulesCount={rulesPageData.all_rules.length} pageSize={rulesPageData.rules_page.length} isSearching={status === 'loading'} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx index 557239e5cb613..167589024529b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table.tsx @@ -58,12 +58,6 @@ export const RulesTable = ({ style: { background: row.metadata.id === selectedRuleId ? euiTheme.colors.highlight : undefined, }, - onClick: (e: MouseEvent) => { - const tag = (e.target as HTMLDivElement).tagName; - // Ignore checkbox and switch toggle columns - if (tag === 'BUTTON' || tag === 'INPUT') return; - setSelectedRuleId(row.metadata.id); - }, }); return ( @@ -86,6 +80,13 @@ type GetColumnProps = Pick; const getColumns = ({ setSelectedRuleId, }: GetColumnProps): Array> => [ + { + field: 'metadata.benchmark.rule_number', + name: i18n.translate('xpack.csp.rules.rulesTable.ruleNumberColumnLabel', { + defaultMessage: 'Rule Number', + }), + width: '10%', + }, { field: 'metadata.name', name: i18n.translate('xpack.csp.rules.rulesTable.nameColumnLabel', { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx index 0109915ff68c6..18e31bdb366db 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/rules_table_header.tsx @@ -5,13 +5,24 @@ * 2.0. */ import React, { useState } from 'react'; -import { EuiFieldSearch, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import { + EuiComboBox, + EuiFieldSearch, + EuiFlexItem, + EuiText, + EuiSpacer, + EuiFlexGroup, + type EuiComboBoxOptionOption, +} from '@elastic/eui'; import useDebounce from 'react-use/lib/useDebounce'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; interface RulesTableToolbarProps { - search(value: string): void; + search: (value: string) => void; + onSectionChange: (value: string | undefined) => void; + sectionSelectOptions: string[]; totalRulesCount: number; searchValue: string; isSearching: boolean; @@ -29,15 +40,47 @@ export const RulesTableHeader = ({ isSearching, totalRulesCount, pageSize, -}: RulesTableToolbarProps) => ( - -); + onSectionChange, + sectionSelectOptions, +}: RulesTableToolbarProps) => { + const [selected, setSelected] = useState([]); + + const sectionOptions = sectionSelectOptions.map((option) => ({ + label: option, + })); + + return ( + + + + + + { + setSelected(option); + onSectionChange(option.length ? option[0].label : undefined); + }} + /> + + + ); +}; const SEARCH_DEBOUNCE_MS = 300; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts b/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts index e0870d1e64b01..ac150e05bf95e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/rules/use_csp_rules.ts @@ -14,20 +14,20 @@ import { FIND_CSP_RULE_TEMPLATE_ROUTE_PATH, } from '../../../common/constants'; -export type RulesQuery = Required>; +export type RulesQuery = Pick; export type RulesQueryResult = ReturnType; export const useFindCspRuleTemplates = ( - { search, page, perPage }: RulesQuery, + { search, page, perPage, section }: RulesQuery, packagePolicyId: string ) => { const { http } = useKibana().services; return useQuery( - [CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE, { search, page, perPage, packagePolicyId }], + [CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE, { section, search, page, perPage, packagePolicyId }], () => { return http.get(FIND_CSP_RULE_TEMPLATE_ROUTE_PATH, { - query: { packagePolicyId, page, perPage, search }, + query: { packagePolicyId, page, perPage, search, section }, version: FIND_CSP_RULE_TEMPLATE_API_CURRENT_VERSION, }); } diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index a62e4a1c4ee09..4c78265c1c15e 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -18,7 +18,7 @@ import { benchmarksQueryParamsSchema } from '../../../common/schemas/benchmark'; import type { Benchmark } from '../../../common/types'; import { getBenchmarkFromPackagePolicy, - getBenchmarkTypeFilter, + getBenchmarkFilter, isNonNullable, } from '../../../common/utils/helpers'; import { CspRouter } from '../../types'; @@ -38,7 +38,7 @@ export const getRulesCountForPolicy = async ( ): Promise => { const rules = await soClient.find({ type: CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE, - filter: getBenchmarkTypeFilter(benchmarkId), + filter: getBenchmarkFilter(benchmarkId), perPage: 0, }); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_template.ts b/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_template.ts index fb6e0e7c53ab0..0b249e9f3656e 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_template.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_template.ts @@ -8,13 +8,12 @@ import { NewPackagePolicy } from '@kbn/fleet-plugin/common'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { transformError } from '@kbn/securitysolution-es-utils'; +import semverCompare from 'semver/functions/compare'; +import semverValid from 'semver/functions/valid'; import { GetCspRuleTemplateRequest, GetCspRuleTemplateResponse } from '../../../common/types'; import { CspRuleTemplate } from '../../../common/schemas'; import { findCspRuleTemplateRequest } from '../../../common/schemas/csp_rule_template_api/get_csp_rule_template'; -import { - getBenchmarkFromPackagePolicy, - getBenchmarkTypeFilter, -} from '../../../common/utils/helpers'; +import { getBenchmarkFromPackagePolicy, getBenchmarkFilter } from '../../../common/utils/helpers'; import { CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE, @@ -23,6 +22,22 @@ import { import { CspRouter } from '../../types'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../benchmarks/benchmarks'; +export const getSortedCspRulesTemplates = (cspRulesTemplates: CspRuleTemplate[]) => { + return cspRulesTemplates.slice().sort((a, b) => { + const ruleNumberA = a?.metadata?.benchmark?.rule_number; + const ruleNumberB = b?.metadata?.benchmark?.rule_number; + + const versionA = semverValid(ruleNumberA); + const versionB = semverValid(ruleNumberB); + + if (versionA !== null && versionB !== null) { + return semverCompare(versionA, versionB); + } else { + return String(ruleNumberA).localeCompare(String(ruleNumberB)); + } + }); +}; + const getBenchmarkIdFromPackagePolicyId = async ( soClient: SavedObjectsClientContract, packagePolicyId: string @@ -57,15 +72,18 @@ const findCspRuleTemplateHandler = async ( perPage: options.perPage, sortField: options.sortField, fields: options?.fields, - filter: getBenchmarkTypeFilter(benchmarkId), + filter: getBenchmarkFilter(benchmarkId, options.section), }); const cspRulesTemplates = cspRulesTemplatesSo.saved_objects.map( (cspRuleTemplate) => cspRuleTemplate.attributes ); + // Semantic version sorting using semver for valid versions and custom comparison for invalid versions + const sortedCspRulesTemplates = getSortedCspRulesTemplates(cspRulesTemplates); + return { - items: cspRulesTemplates, + items: sortedCspRulesTemplates, total: cspRulesTemplatesSo.total, page: options.page, perPage: options.perPage, diff --git a/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_templates.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_templates.test.ts new file mode 100644 index 0000000000000..f894d031c4bc0 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/csp_rule_template/get_csp_rule_templates.test.ts @@ -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 { getSortedCspRulesTemplates } from './get_csp_rule_template'; +import { CspRuleTemplate } from '../../../common/schemas'; + +describe('getSortedCspRulesTemplates', () => { + it('sorts by metadata.benchmark.rule_number, invalid semantic version still should still get sorted and empty values should be sorted last', () => { + const cspRulesTemplates = [ + { metadata: { benchmark: { rule_number: '1.0.0' } } }, + { metadata: { benchmark: { rule_number: '2.0.0' } } }, + { metadata: { benchmark: { rule_number: '1.1.0' } } }, + { metadata: { benchmark: { rule_number: '1.0.1' } } }, + { metadata: { benchmark: { rule_number: 'invalid' } } }, + { metadata: { benchmark: { rule_number: '3.0' } } }, + { metadata: { benchmark: {} } }, + ] as CspRuleTemplate[]; + + const sortedCspRulesTemplates = getSortedCspRulesTemplates(cspRulesTemplates); + + expect(sortedCspRulesTemplates).toEqual([ + { metadata: { benchmark: { rule_number: '1.0.0' } } }, + { metadata: { benchmark: { rule_number: '1.0.1' } } }, + { metadata: { benchmark: { rule_number: '1.1.0' } } }, + { metadata: { benchmark: { rule_number: '2.0.0' } } }, + { metadata: { benchmark: { rule_number: '3.0' } } }, + { metadata: { benchmark: { rule_number: 'invalid' } } }, + { metadata: { benchmark: {} } }, + ]); + }); + + it('edge case - returns empty array if input is empty', () => { + const cspRulesTemplates: CspRuleTemplate[] = []; + + const sortedCspRulesTemplates = getSortedCspRulesTemplates(cspRulesTemplates); + + expect(sortedCspRulesTemplates).toEqual([]); + }); + + it('edge case - returns sorted array even if input only has one element', () => { + const cspRulesTemplates = [ + { metadata: { benchmark: { rule_number: '1.0.0' } } }, + ] as CspRuleTemplate[]; + + const sortedCspRulesTemplates = getSortedCspRulesTemplates(cspRulesTemplates); + + expect(sortedCspRulesTemplates).toEqual([ + { metadata: { benchmark: { rule_number: '1.0.0' } } }, + ]); + }); + + it('returns sorted array even with undefined or null properties', () => { + const cspRulesTemplates = [ + { metadata: { benchmark: { rule_number: '1.0.0' } } }, + { metadata: { benchmark: { rule_number: undefined } } }, + { metadata: { benchmark: { rule_number: '2.0.0' } } }, + { metadata: { benchmark: { rule_number: null } } }, + ] as CspRuleTemplate[]; + + const sortedCspRulesTemplates = getSortedCspRulesTemplates(cspRulesTemplates); + + expect(sortedCspRulesTemplates).toEqual([ + { metadata: { benchmark: { rule_number: '1.0.0' } } }, + { metadata: { benchmark: { rule_number: '2.0.0' } } }, + { metadata: { benchmark: { rule_number: null } } }, + { metadata: { benchmark: { rule_number: undefined } } }, + ]); + }); + + it('returns sorted array with invalid semantic versions', () => { + const cspRulesTemplates = [ + { metadata: { benchmark: { rule_number: '1.0.0' } } }, + { metadata: { benchmark: { rule_number: '2.0' } } }, + { metadata: { benchmark: { rule_number: '3.0.0' } } }, + ] as CspRuleTemplate[]; + + const sortedCspRulesTemplates = getSortedCspRulesTemplates(cspRulesTemplates); + + expect(sortedCspRulesTemplates).toEqual([ + { metadata: { benchmark: { rule_number: '1.0.0' } } }, + { metadata: { benchmark: { rule_number: '2.0' } } }, + { metadata: { benchmark: { rule_number: '3.0.0' } } }, + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx index 694639b1d19c6..b2932c9547e27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx @@ -10,6 +10,7 @@ import React, { useEffect, useState, ChangeEvent } from 'react'; import { useActions, useValues } from 'kea'; import { + EuiCallOut, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, @@ -74,7 +75,7 @@ export const SearchIndexDocuments: React.FC = () => { const { makeRequest: getDocuments } = useActions(documentLogic); const { makeRequest: getMappings } = useActions(mappingLogic); - const { data, status } = useValues(documentLogic); + const { data, status, error } = useValues(documentLogic); const { data: mappingData, status: mappingStatus } = useValues(mappingLogic); const docs = data?.results?.hits.hits ?? []; @@ -84,6 +85,8 @@ export const SearchIndexDocuments: React.FC = () => { const shouldShowAccessControlSwitcher = hasDocumentLevelSecurityFeature && productFeatures.hasDocumentLevelSecurityEnabled; + const isAccessControlIndexNotFound = + shouldShowAccessControlSwitcher && error?.body?.statusCode === 404; useEffect(() => { getDocuments({ @@ -141,11 +144,29 @@ export const SearchIndexDocuments: React.FC = () => { - {docs.length === 0 && + {isAccessControlIndexNotFound && ( + +

+ {i18n.translate('xpack.enterpriseSearch.content.searchIndex.documents.noIndex', { + defaultMessage: + "An Access Control Index won't be created until you enable document-level security and run your first access control sync.", + })} +

+
+ )} + {!isAccessControlIndexNotFound && + docs.length === 0 && i18n.translate('xpack.enterpriseSearch.content.searchIndex.documents.noMappings', { defaultMessage: 'No documents found for index', })} - {docs.length > 0 && ( + {!isAccessControlIndexNotFound && docs.length > 0 && ( { ? indexName : stripSearchPrefix(indexName, CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX); const { makeRequest: makeMappingRequest } = useActions(mappingsWithPropsApiLogic(indexToShow)); - const { data: mappingData } = useValues(mappingsWithPropsApiLogic(indexToShow)); + const { data: mappingData, error } = useValues(mappingsWithPropsApiLogic(indexToShow)); const shouldShowAccessControlSwitch = hasDocumentLevelSecurityFeature && productFeatures.hasDocumentLevelSecurityEnabled; + const isAccessControlIndexNotFound = + shouldShowAccessControlSwitch && error?.body?.statusCode === 404; + useEffect(() => { makeMappingRequest({ indexName: indexToShow }); }, [indexToShow, indexName]); @@ -77,9 +81,27 @@ export const SearchIndexIndexMappings: React.FC = () => {
)} - - {JSON.stringify(mappingData, null, 2)} - + {isAccessControlIndexNotFound ? ( + +

+ {i18n.translate('xpack.enterpriseSearch.content.searchIndex.mappings.noIndex', { + defaultMessage: + "An Access Control Index won't be created until you enable document-level security and run your first access control sync.", + })} +

+
+ ) : ( + + {JSON.stringify(mappingData, null, 2)} + + )}
diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 334ef577c5fdf..dfa970fb4b43c 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -572,6 +572,24 @@ "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "in": "query", + "name": "ignoreMappingUpdateErrors", + "schema": { + "type": "boolean", + "default": false + }, + "description": "avoid erroring out on unexpected mapping update errors" + }, + { + "in": "query", + "name": "skipDataStreamRollover", + "schema": { + "type": "boolean", + "default": false + }, + "description": "skip data stream rollover during index template mapping or settings update" } ], "requestBody": { @@ -810,6 +828,24 @@ "name": "pkgkey", "in": "path", "required": true + }, + { + "in": "query", + "name": "ignoreMappingUpdateErrors", + "schema": { + "type": "boolean", + "default": false + }, + "description": "avoid erroring out on unexpected mapping update errors" + }, + { + "in": "query", + "name": "skipDataStreamRollover", + "schema": { + "type": "boolean", + "default": false + }, + "description": "skip data stream rollover during index template mapping or settings update" } ], "requestBody": { @@ -1090,6 +1126,24 @@ "parameters": [ { "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "in": "query", + "name": "ignoreMappingUpdateErrors", + "schema": { + "type": "boolean", + "default": false + }, + "description": "avoid erroring out on unexpected mapping update errors" + }, + { + "in": "query", + "name": "skipDataStreamRollover", + "schema": { + "type": "boolean", + "default": false + }, + "description": "skip data stream rollover during index template mapping or settings update" } ], "requestBody": { diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 6ba43b73ba643..a996c3403810d 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -368,6 +368,20 @@ paths: description: '' parameters: - $ref: '#/components/parameters/kbn_xsrf' + - in: query + name: ignoreMappingUpdateErrors + schema: + type: boolean + default: false + description: avoid erroring out on unexpected mapping update errors + - in: query + name: skipDataStreamRollover + schema: + type: boolean + default: false + description: >- + skip data stream rollover during index template mapping or settings + update requestBody: content: application/zip: @@ -516,6 +530,20 @@ paths: name: pkgkey in: path required: true + - in: query + name: ignoreMappingUpdateErrors + schema: + type: boolean + default: false + description: avoid erroring out on unexpected mapping update errors + - in: query + name: skipDataStreamRollover + schema: + type: boolean + default: false + description: >- + skip data stream rollover during index template mapping or settings + update requestBody: content: application/json: @@ -689,6 +717,20 @@ paths: description: '' parameters: - $ref: '#/components/parameters/kbn_xsrf' + - in: query + name: ignoreMappingUpdateErrors + schema: + type: boolean + default: false + description: avoid erroring out on unexpected mapping update errors + - in: query + name: skipDataStreamRollover + schema: + type: boolean + default: false + description: >- + skip data stream rollover during index template mapping or settings + update requestBody: content: application/json: diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml index 7434fd2d324a5..f0dbc48d7dc50 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml @@ -83,6 +83,18 @@ post: description: '' parameters: - $ref: ../components/headers/kbn_xsrf.yaml + - in: query + name: ignoreMappingUpdateErrors + schema: + type: boolean + default: false + description: avoid erroring out on unexpected mapping update errors + - in: query + name: skipDataStreamRollover + schema: + type: boolean + default: false + description: skip data stream rollover during index template mapping or settings update requestBody: content: application/zip: diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml index bc97c46bd4cd2..3d788b9f24e22 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkg_name}@{pkg_version}.yaml @@ -111,6 +111,18 @@ post: description: '' parameters: - $ref: ../components/headers/kbn_xsrf.yaml + - in: query + name: ignoreMappingUpdateErrors + schema: + type: boolean + default: false + description: avoid erroring out on unexpected mapping update errors + - in: query + name: skipDataStreamRollover + schema: + type: boolean + default: false + description: skip data stream rollover during index template mapping or settings update requestBody: content: application/json: diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}_deprecated.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}_deprecated.yaml index 035809782bc07..7ea8b18d413dc 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}_deprecated.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages@{pkgkey}_deprecated.yaml @@ -84,6 +84,18 @@ post: name: pkgkey in: path required: true + - in: query + name: ignoreMappingUpdateErrors + schema: + type: boolean + default: false + description: avoid erroring out on unexpected mapping update errors + - in: query + name: skipDataStreamRollover + schema: + type: boolean + default: false + description: skip data stream rollover during index template mapping or settings update requestBody: content: application/json: diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index ffc0742706063..5781fa623bb9b 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -392,6 +392,8 @@ export const installPackageFromRegistryHandler: FleetRequestHandler< ignoreConstraints: request.body?.ignore_constraints, prerelease: request.query?.prerelease, authorizationHeader, + ignoreMappingUpdateErrors: request.query?.ignoreMappingUpdateErrors, + skipDataStreamRollover: request.query?.skipDataStreamRollover, }); if (!res.error) { @@ -509,7 +511,7 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler< export const installPackageByUploadHandler: FleetRequestHandler< undefined, - undefined, + TypeOf, TypeOf > = async (context, request, response) => { const coreContext = await context.core; @@ -531,6 +533,8 @@ export const installPackageByUploadHandler: FleetRequestHandler< spaceId, contentType, authorizationHeader, + ignoreMappingUpdateErrors: request.query?.ignoreMappingUpdateErrors, + skipDataStreamRollover: request.query?.skipDataStreamRollover, }); if (!res.error) { const body: InstallPackageResponse = { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts index f684ad4b40a8c..e5c323de62dfd 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.test.ts @@ -12,6 +12,8 @@ import { safeLoad } from 'js-yaml'; import { loggerMock } from '@kbn/logging-mocks'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { errors } from '@elastic/elasticsearch'; + import { createAppContextStartContractMock } from '../../../../mocks'; import { appContextService } from '../../..'; import type { RegistryDataStream } from '../../../../types'; @@ -1261,5 +1263,134 @@ describe('EPM template', () => { }, }); }); + it('should rollover on expected error', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.indices.getDataStream.mockResponse({ + data_streams: [{ name: 'test.prefix1-default' }], + } as any); + esClient.indices.simulateTemplate.mockImplementation(() => { + throw new errors.ResponseError({ + statusCode: 400, + body: { + error: { + type: 'illegal_argument_exception', + }, + }, + } as any); + }); + const logger = loggerMock.create(); + await updateCurrentWriteIndices(esClient, logger, [ + { + templateName: 'test', + indexTemplate: { + index_patterns: ['test.*-*'], + template: { + settings: { index: {} }, + mappings: { properties: {} }, + }, + } as any, + }, + ]); + + expect(esClient.indices.rollover).toHaveBeenCalled(); + }); + it('should skip rollover on expected error when flag is on', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.indices.getDataStream.mockResponse({ + data_streams: [{ name: 'test.prefix1-default' }], + } as any); + esClient.indices.simulateTemplate.mockImplementation(() => { + throw new errors.ResponseError({ + statusCode: 400, + body: { + error: { + type: 'illegal_argument_exception', + }, + }, + } as any); + }); + const logger = loggerMock.create(); + await updateCurrentWriteIndices( + esClient, + logger, + [ + { + templateName: 'test', + indexTemplate: { + index_patterns: ['test.*-*'], + template: { + settings: { index: {} }, + mappings: { properties: {} }, + }, + } as any, + }, + ], + { + skipDataStreamRollover: true, + } + ); + + expect(esClient.indices.rollover).not.toHaveBeenCalled(); + }); + it('should not rollover on unexpected error', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.indices.getDataStream.mockResponse({ + data_streams: [{ name: 'test.prefix1-default' }], + } as any); + esClient.indices.simulateTemplate.mockImplementation(() => { + throw new Error(); + }); + const logger = loggerMock.create(); + try { + await updateCurrentWriteIndices(esClient, logger, [ + { + templateName: 'test', + indexTemplate: { + index_patterns: ['test.*-*'], + template: { + settings: { index: {} }, + mappings: { properties: {} }, + }, + } as any, + }, + ]); + fail('expected updateCurrentWriteIndices to throw error'); + } catch (err) { + // noop + } + + expect(esClient.indices.rollover).not.toHaveBeenCalled(); + }); + it('should not throw on unexpected error when flag is on', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.indices.getDataStream.mockResponse({ + data_streams: [{ name: 'test.prefix1-default' }], + } as any); + esClient.indices.simulateTemplate.mockImplementation(() => { + throw new Error(); + }); + const logger = loggerMock.create(); + await updateCurrentWriteIndices( + esClient, + logger, + [ + { + templateName: 'test', + indexTemplate: { + index_patterns: ['test.*-*'], + template: { + settings: { index: {} }, + mappings: { properties: {} }, + }, + } as any, + }, + ], + { + ignoreMappingUpdateErrors: true, + } + ); + + expect(esClient.indices.rollover).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts index 5682749d7e381..e55cad3f80a9c 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/template.ts @@ -12,6 +12,7 @@ import type { } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import pMap from 'p-map'; +import { isResponseError } from '@kbn/es-errors'; import type { Field, Fields } from '../../fields/field'; import type { @@ -662,7 +663,11 @@ function getBaseTemplate({ export const updateCurrentWriteIndices = async ( esClient: ElasticsearchClient, logger: Logger, - templates: IndexTemplateEntry[] + templates: IndexTemplateEntry[], + options?: { + ignoreMappingUpdateErrors?: boolean; + skipDataStreamRollover?: boolean; + } ): Promise => { if (!templates.length) return; @@ -677,7 +682,7 @@ export const updateCurrentWriteIndices = async ( return true; }); if (!allUpdatablesIndices.length) return; - return updateAllDataStreams(allUpdatablesIndices, esClient, logger); + return updateAllDataStreams(allUpdatablesIndices, esClient, logger, options); }; function isCurrentDataStream(item: CurrentDataStream[] | undefined): item is CurrentDataStream[] { @@ -729,7 +734,11 @@ const rolloverDataStream = (dataStreamName: string, esClient: ElasticsearchClien const updateAllDataStreams = async ( indexNameWithTemplates: CurrentDataStream[], esClient: ElasticsearchClient, - logger: Logger + logger: Logger, + options?: { + ignoreMappingUpdateErrors?: boolean; + skipDataStreamRollover?: boolean; + } ): Promise => { await pMap( indexNameWithTemplates, @@ -738,6 +747,7 @@ const updateAllDataStreams = async ( esClient, logger, dataStreamName: templateEntry.dataStreamName, + options, }); }, { @@ -751,10 +761,15 @@ const updateExistingDataStream = async ({ dataStreamName, esClient, logger, + options, }: { dataStreamName: string; esClient: ElasticsearchClient; logger: Logger; + options?: { + ignoreMappingUpdateErrors?: boolean; + skipDataStreamRollover?: boolean; + }; }) => { const existingDs = await esClient.indices.get({ index: dataStreamName, @@ -804,18 +819,45 @@ const updateExistingDataStream = async ({ // if update fails, rollover data stream and bail out } catch (err) { - logger.info(`Mappings update for ${dataStreamName} failed due to ${err}`); - logger.info(`Triggering a rollover for ${dataStreamName}`); - await rolloverDataStream(dataStreamName, esClient); - return; + if ( + isResponseError(err) && + err.statusCode === 400 && + err.body?.error?.type === 'illegal_argument_exception' + ) { + logger.info(`Mappings update for ${dataStreamName} failed due to ${err}`); + if (options?.skipDataStreamRollover === true) { + logger.info( + `Skipping rollover for ${dataStreamName} as "skipDataStreamRollover" is enabled` + ); + return; + } else { + logger.info(`Triggering a rollover for ${dataStreamName}`); + await rolloverDataStream(dataStreamName, esClient); + return; + } + } + logger.error(`Mappings update for ${dataStreamName} failed due to unexpected error: ${err}`); + if (options?.ignoreMappingUpdateErrors === true) { + logger.info(`Ignore mapping update errors as "ignoreMappingUpdateErrors" is enabled`); + return; + } else { + throw err; + } } // Trigger a rollover if the index mode or source type has changed if (currentIndexMode !== settings?.index?.mode || currentSourceType !== mappings?._source?.mode) { - logger.info( - `Index mode or source type has changed for ${dataStreamName}, triggering a rollover` - ); - await rolloverDataStream(dataStreamName, esClient); + if (options?.skipDataStreamRollover === true) { + logger.info( + `Index mode or source type has changed for ${dataStreamName}, skipping rollover as "skipDataStreamRollover" is enabled` + ); + return; + } else { + logger.info( + `Index mode or source type has changed for ${dataStreamName}, triggering a rollover` + ); + await rolloverDataStream(dataStreamName, esClient); + } } if (lifecycle?.data_retention) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 3bfc74bf68968..105331f54d2cf 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -77,6 +77,8 @@ export async function _installPackage({ force, verificationResult, authorizationHeader, + ignoreMappingUpdateErrors, + skipDataStreamRollover, }: { savedObjectsClient: SavedObjectsClientContract; savedObjectsImporter: Pick; @@ -93,6 +95,8 @@ export async function _installPackage({ force?: boolean; verificationResult?: PackageVerificationResult; authorizationHeader?: HTTPAuthorizationHeader | null; + ignoreMappingUpdateErrors?: boolean; + skipDataStreamRollover?: boolean; }): Promise { const { name: pkgName, version: pkgVersion, title: pkgTitle } = packageInfo; @@ -250,7 +254,10 @@ export async function _installPackage({ // update current backing indices of each data stream await withPackageSpan('Update write indices', () => - updateCurrentWriteIndices(esClient, logger, indexTemplates) + updateCurrentWriteIndices(esClient, logger, indexTemplates, { + ignoreMappingUpdateErrors, + skipDataStreamRollover, + }) ); ({ esReferences } = await withPackageSpan('Install transforms', () => diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index da31b26d275e9..78708c7716502 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -300,6 +300,8 @@ interface InstallRegistryPackageParams { ignoreConstraints?: boolean; prerelease?: boolean; authorizationHeader?: HTTPAuthorizationHeader | null; + ignoreMappingUpdateErrors?: boolean; + skipDataStreamRollover?: boolean; } export interface CustomPackageDatasetConfiguration { @@ -324,6 +326,8 @@ interface InstallUploadedArchiveParams { spaceId: string; version?: string; authorizationHeader?: HTTPAuthorizationHeader | null; + ignoreMappingUpdateErrors?: boolean; + skipDataStreamRollover?: boolean; } function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent { @@ -356,6 +360,8 @@ async function installPackageFromRegistry({ ignoreConstraints = false, neverIgnoreVerificationError = false, prerelease = false, + ignoreMappingUpdateErrors = false, + skipDataStreamRollover = false, }: InstallRegistryPackageParams): Promise { const logger = appContextService.getLogger(); // TODO: change epm API to /packageName/version so we don't need to do this @@ -429,6 +435,8 @@ async function installPackageFromRegistry({ paths, verificationResult, authorizationHeader, + ignoreMappingUpdateErrors, + skipDataStreamRollover, }); } catch (e) { sendEvent({ @@ -464,6 +472,8 @@ async function installPackageCommon(options: { verificationResult?: PackageVerificationResult; telemetryEvent?: PackageUpdateEvent; authorizationHeader?: HTTPAuthorizationHeader | null; + ignoreMappingUpdateErrors?: boolean; + skipDataStreamRollover?: boolean; }): Promise { const { pkgName, @@ -479,6 +489,8 @@ async function installPackageCommon(options: { paths, verificationResult, authorizationHeader, + ignoreMappingUpdateErrors, + skipDataStreamRollover, } = options; let { telemetryEvent } = options; const logger = appContextService.getLogger(); @@ -568,6 +580,8 @@ async function installPackageCommon(options: { installSource, authorizationHeader, force, + ignoreMappingUpdateErrors, + skipDataStreamRollover, }) .then(async (assets) => { await removeOldAssets({ @@ -622,6 +636,8 @@ async function installPackageByUpload({ spaceId, version, authorizationHeader, + ignoreMappingUpdateErrors, + skipDataStreamRollover, }: InstallUploadedArchiveParams): Promise { // if an error happens during getInstallType, report that we don't know let installType: InstallType = 'unknown'; @@ -670,6 +686,8 @@ async function installPackageByUpload({ packageInfo, paths, authorizationHeader, + ignoreMappingUpdateErrors, + skipDataStreamRollover, }); } catch (e) { return { @@ -703,8 +721,16 @@ export async function installPackage(args: InstallPackageParams): Promise Registry.pkgToPkgKey(pkg) === pkgkey @@ -723,6 +749,8 @@ export async function installPackage(args: InstallPackageParams): Promise ) : hasIntegration ? ( { @@ -240,7 +241,12 @@ export async function cloneJob(jobId) { return; } - if (cloneableJob !== undefined && originalJob?.custom_settings?.created_by !== undefined) { + const createdBy = originalJob?.custom_settings?.created_by; + if ( + cloneableJob !== undefined && + createdBy !== undefined && + createdBy !== CREATED_BY_LABEL.ADVANCED + ) { // if the job is from a wizards, i.e. contains a created_by property // use tempJobCloningObjects to temporarily store the job mlJobService.tempJobCloningObjects.createdBy = originalJob?.custom_settings?.created_by; 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 face450f58cd3..150b57402ee5e 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 @@ -11,14 +11,14 @@ import type { Field, Aggregation, SplitField } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { JobCreator } from './job_creator'; -import { +import type { Job, Datafeed, Detector, CustomRule, } from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; -import { JOB_TYPE } from '../../../../../../common/constants/new_job'; +import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; import { isValidJson } from '../../../../../../common/util/validation_utils'; @@ -41,6 +41,7 @@ export class AdvancedJobCreator extends JobCreator { constructor(indexPattern: DataView, savedSearch: SavedSearch | null, query: object) { super(indexPattern, savedSearch, query); + this.createdBy = CREATED_BY_LABEL.ADVANCED; this._queryString = JSON.stringify(this._datafeed_config.query); @@ -182,6 +183,7 @@ export class AdvancedJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); + this.createdBy = CREATED_BY_LABEL.ADVANCED; const detectors = getRichDetectors(job, datafeed, this.additionalFields, true); // keep track of the custom rules for each detector diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts index 596eb56230dc0..59e89070b38dd 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/geo_job_creator.ts @@ -9,7 +9,11 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, SplitField, AggFieldPair } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { JobCreator } from './job_creator'; -import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; +import type { + Job, + Datafeed, + Detector, +} from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index 794057ea209e8..39ad966b595e7 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -9,7 +9,11 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, SplitField, AggFieldPair } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { JobCreator } from './job_creator'; -import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; +import type { + Job, + Datafeed, + Detector, +} from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index 09f10b0a2f1aa..08de5d30d11eb 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -9,7 +9,11 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { Field, Aggregation, SplitField, AggFieldPair } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { JobCreator } from './job_creator'; -import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; +import type { + Job, + Datafeed, + Detector, +} from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts index f586ec332773d..cc9601fc85634 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/rare_job_creator.ts @@ -14,7 +14,11 @@ import { } from '@kbn/ml-anomaly-utils'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { JobCreator } from './job_creator'; -import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; +import type { + Job, + Datafeed, + Detector, +} from '../../../../../../common/types/anomaly_detection_jobs'; import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors, isSparseDataJob } from './util/general'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index b0a1a1136bc6b..07a949b6a34a3 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -17,7 +17,7 @@ import { import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JobCreator } from './job_creator'; -import { +import type { Job, Datafeed, Detector, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 471ba1e30608e..3f288d8f6191f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -63,6 +63,7 @@ async function getWizardUrlFromCloningJob(createdBy: string | undefined, dataVie case CREATED_BY_LABEL.GEO: page = JOB_TYPE.GEO; break; + case CREATED_BY_LABEL.ADVANCED: default: page = JOB_TYPE.ADVANCED; break; diff --git a/x-pack/plugins/ml/public/locator/use_ml_href.ts b/x-pack/plugins/ml/public/locator/use_ml_href.ts index 1ed7321f3fd2f..04e6ce94db1ff 100644 --- a/x-pack/plugins/ml/public/locator/use_ml_href.ts +++ b/x-pack/plugins/ml/public/locator/use_ml_href.ts @@ -6,7 +6,7 @@ */ import { DependencyList } from 'react'; -import { MlPluginStart } from '..'; +import { MlPluginSetup } from '..'; import { MlLocatorParams } from '../../common/types/locator'; /** @@ -14,7 +14,7 @@ import { MlLocatorParams } from '../../common/types/locator'; * TODO remove basePath parameter */ export const useMlHref = ( - ml: MlPluginStart | undefined, + ml: MlPluginSetup | undefined, basePath: string | undefined, params: MlLocatorParams, dependencies?: DependencyList diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 46dfd7a50d3ec..2d251ac71660e 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -14,11 +14,8 @@ import { Router, Routes, Route } from '@kbn/shared-ux-router'; import { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '@kbn/core/public'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; -import { - KibanaContextProvider, - KibanaThemeProvider, - RedirectAppLinks, -} from '@kbn/kibana-react-plugin/public'; +import { KibanaContextProvider, RedirectAppLinks } from '@kbn/kibana-react-plugin/public'; +import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public'; @@ -90,7 +87,7 @@ export const renderApp = ({ ReactDOM.render( - + { - describe('when the common resources are not installed yet', () => { - it('installs the common resources', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - mockClusterClient.indices.getIndexTemplate.mockResponseOnce({ index_templates: [] }); - const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); + it('installs the common resources', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + mockClusterClient.indices.getIndexTemplate.mockResponseOnce({ index_templates: [] }); + const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); - await installer.ensureCommonResourcesInstalled(); + await installer.ensureCommonResourcesInstalled(); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ name: SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME }) - ); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ name: SLO_COMPONENT_TEMPLATE_SETTINGS_NAME }) - ); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( - 3, - expect.objectContaining({ name: SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME }) - ); - expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( - 4, - expect.objectContaining({ name: SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME }) - ); - expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(2); - expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ name: SLO_INDEX_TEMPLATE_NAME }) - ); - expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ name: SLO_SUMMARY_INDEX_TEMPLATE_NAME }) - ); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenCalledTimes(4); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ name: SLO_COMPONENT_TEMPLATE_MAPPINGS_NAME }) + ); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ name: SLO_COMPONENT_TEMPLATE_SETTINGS_NAME }) + ); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ name: SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME }) + ); + expect(mockClusterClient.cluster.putComponentTemplate).toHaveBeenNthCalledWith( + 4, + expect.objectContaining({ name: SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME }) + ); + expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(2); + expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ name: SLO_INDEX_TEMPLATE_NAME }) + ); + expect(mockClusterClient.indices.putIndexTemplate).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ name: SLO_SUMMARY_INDEX_TEMPLATE_NAME }) + ); - expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledTimes(2); - expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ id: SLO_INGEST_PIPELINE_NAME }) - ); - expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ id: SLO_SUMMARY_INGEST_PIPELINE_NAME }) - ); - }); - }); - - describe('when the common resources are already installed', () => { - it('skips the installation', async () => { - const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); - mockClusterClient.indices.getIndexTemplate.mockResponseOnce({ - index_templates: [ - { - name: SLO_INDEX_TEMPLATE_NAME, - index_template: { - index_patterns: [], - composed_of: [], - _meta: { version: SLO_RESOURCES_VERSION }, - }, - }, - ], - }); - mockClusterClient.indices.getIndexTemplate.mockResponseOnce({ - index_templates: [ - { - name: SLO_SUMMARY_INDEX_TEMPLATE_NAME, - index_template: { - index_patterns: [], - composed_of: [], - _meta: { version: SLO_RESOURCES_VERSION }, - }, - }, - ], - }); - mockClusterClient.ingest.getPipeline.mockResponseOnce({ - // @ts-ignore _meta not typed properly - [SLO_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } }, - }); - mockClusterClient.ingest.getPipeline.mockResponseOnce({ - // @ts-ignore _meta not typed properly - [SLO_SUMMARY_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } }, - }); - const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create()); - - await installer.ensureCommonResourcesInstalled(); - - expect(mockClusterClient.cluster.putComponentTemplate).not.toHaveBeenCalled(); - expect(mockClusterClient.indices.putIndexTemplate).not.toHaveBeenCalled(); - expect(mockClusterClient.ingest.putPipeline).not.toHaveBeenCalled(); - }); + expect(mockClusterClient.ingest.putPipeline).toHaveBeenCalledTimes(2); + expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ id: SLO_INGEST_PIPELINE_NAME }) + ); + expect(mockClusterClient.ingest.putPipeline).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ id: SLO_SUMMARY_INGEST_PIPELINE_NAME }) + ); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/resource_installer.ts b/x-pack/plugins/observability/server/services/slo/resource_installer.ts index 742b2a8e7202e..4fc77ea2399c7 100644 --- a/x-pack/plugins/observability/server/services/slo/resource_installer.ts +++ b/x-pack/plugins/observability/server/services/slo/resource_installer.ts @@ -23,7 +23,6 @@ import { SLO_INDEX_TEMPLATE_PATTERN, SLO_INGEST_PIPELINE_INDEX_NAME_PREFIX, SLO_INGEST_PIPELINE_NAME, - SLO_RESOURCES_VERSION, SLO_SUMMARY_COMPONENT_TEMPLATE_MAPPINGS_NAME, SLO_SUMMARY_COMPONENT_TEMPLATE_SETTINGS_NAME, SLO_SUMMARY_DESTINATION_INDEX_NAME, @@ -46,13 +45,6 @@ export class DefaultResourceInstaller implements ResourceInstaller { constructor(private esClient: ElasticsearchClient, private logger: Logger) {} public async ensureCommonResourcesInstalled(): Promise { - const alreadyInstalled = await this.areResourcesAlreadyInstalled(); - - if (alreadyInstalled) { - this.logger.info('SLO resources already installed - skipping'); - return; - } - try { this.logger.info('Installing SLO shared resources'); await Promise.all([ @@ -105,77 +97,6 @@ export class DefaultResourceInstaller implements ResourceInstaller { } } - private async areResourcesAlreadyInstalled(): Promise { - let indexTemplateExists = false; - try { - const { index_templates: indexTemplates } = await this.execute(() => - this.esClient.indices.getIndexTemplate({ - name: SLO_INDEX_TEMPLATE_NAME, - }) - ); - - const sloIndexTemplate = indexTemplates.find( - (template) => template.name === SLO_INDEX_TEMPLATE_NAME - ); - indexTemplateExists = - !!sloIndexTemplate && - sloIndexTemplate.index_template._meta?.version === SLO_RESOURCES_VERSION; - } catch (err) { - return false; - } - - let summaryIndexTemplateExists = false; - try { - const { index_templates: indexTemplates } = await this.execute(() => - this.esClient.indices.getIndexTemplate({ - name: SLO_SUMMARY_INDEX_TEMPLATE_NAME, - }) - ); - const sloSummaryIndexTemplate = indexTemplates.find( - (template) => template.name === SLO_SUMMARY_INDEX_TEMPLATE_NAME - ); - summaryIndexTemplateExists = - !!sloSummaryIndexTemplate && - sloSummaryIndexTemplate.index_template._meta?.version === SLO_RESOURCES_VERSION; - } catch (err) { - return false; - } - - let ingestPipelineExists = false; - try { - const pipeline = await this.execute(() => - this.esClient.ingest.getPipeline({ id: SLO_INGEST_PIPELINE_NAME }) - ); - - ingestPipelineExists = - // @ts-ignore _meta is not defined on the type - pipeline && pipeline[SLO_INGEST_PIPELINE_NAME]._meta.version === SLO_RESOURCES_VERSION; - } catch (err) { - return false; - } - - let summaryIngestPipelineExists = false; - try { - const pipeline = await this.execute(() => - this.esClient.ingest.getPipeline({ id: SLO_SUMMARY_INGEST_PIPELINE_NAME }) - ); - - summaryIngestPipelineExists = - pipeline && - // @ts-ignore _meta is not defined on the type - pipeline[SLO_SUMMARY_INGEST_PIPELINE_NAME]._meta.version === SLO_RESOURCES_VERSION; - } catch (err) { - return false; - } - - return ( - indexTemplateExists && - summaryIndexTemplateExists && - ingestPipelineExists && - summaryIngestPipelineExists - ); - } - private async createOrUpdateComponentTemplate(template: ClusterPutComponentTemplateRequest) { this.logger.info(`Installing SLO component template [${template.name}]`); return this.execute(() => this.esClient.cluster.putComponentTemplate(template)); diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 53a2fd3815170..74a54afe8b53b 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -86,7 +86,8 @@ "@kbn/osquery-plugin", "@kbn/aiops-plugin", "@kbn/content-management-plugin", - "@kbn/deeplinks-observability" + "@kbn/deeplinks-observability", + "@kbn/react-kibana-context-theme" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/observability_log_explorer/common/index.ts b/x-pack/plugins/observability_log_explorer/common/index.ts index 40e3f1373687d..f7639f742e67e 100644 --- a/x-pack/plugins/observability_log_explorer/common/index.ts +++ b/x-pack/plugins/observability_log_explorer/common/index.ts @@ -5,4 +5,8 @@ * 2.0. */ -export { SingleDatasetLocatorDefinition, AllDatasetsLocatorDefinition } from './locators'; +export { + type ObservabilityLogExplorerLocators, + SingleDatasetLocatorDefinition, + AllDatasetsLocatorDefinition, +} from './locators'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx index 7b0c35dd45a1f..e811b33e3b572 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_details_view.tsx @@ -28,11 +28,11 @@ const EnrichmentSectionHeader: React.FC<{ type?: ENRICHMENT_TYPES }> = ({ type } -
+

{type === ENRICHMENT_TYPES.IndicatorMatchRule ? i18n.INDICATOR_ENRICHMENT_TITLE : i18n.INVESTIGATION_ENRICHMENT_TITLE} -

+
diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx index 2bccfbc8ac34b..44a1a23abf774 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx @@ -231,12 +231,12 @@ export const HostDetails: React.FC = ({ hostName, timestamp, s return ( <> -

+

-

+
= ({ hostName, timestamp, s data-test-subj={HOST_DETAILS_TEST_ID} > -
+

-

+
= ({ hostName, timestamp, s -
+

-

+
diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/related_cases.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/related_cases.tsx index 76a97e6e71053..681d542eac3a1 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/related_cases.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/related_cases.tsx @@ -10,6 +10,7 @@ import type { EuiBasicTableColumn } from '@elastic/eui'; import { EuiInMemoryTable, EuiSkeletonText } from '@elastic/eui'; import type { RelatedCase } from '@kbn/cases-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; import { CellTooltipWrapper } from '../../shared/components/cell_tooltip_wrapper'; import { CaseDetailsLink } from '../../../common/components/links'; import { @@ -65,7 +66,19 @@ export const RelatedCases: React.VFC = ({ eventId }) => { const { loading, error, data, dataCount } = useFetchRelatedCases({ eventId }); if (loading) { - return ; + return ( + + ); } if (error) { diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx index 12ad057284cd6..8d395f4a1d3dd 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx @@ -232,12 +232,12 @@ export const UserDetails: React.FC = ({ userName, timestamp, s return ( <> -

+

-

+
= ({ userName, timestamp, s data-test-subj={USER_DETAILS_TEST_ID} > -
+

-

+
= ({ userName, timestamp, s -
+

-

+
diff --git a/x-pack/plugins/security_solution/public/flyout/left/tabs/insights_tab.tsx b/x-pack/plugins/security_solution/public/flyout/left/tabs/insights_tab.tsx index 6f35b6b26f3c8..3e8c1f22de622 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/tabs/insights_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/tabs/insights_tab.tsx @@ -113,7 +113,7 @@ export const InsightsTab: React.FC = memo(() => { color="primary" name="coarsness" legend={i18n.translate( - 'xpack.securitySolution.flyout.left.insights.buttonGroupButtonLabel', + 'xpack.securitySolution.flyout.left.insights.buttonGroupLegendLabel', { defaultMessage: 'Insights options', } diff --git a/x-pack/plugins/security_solution/public/flyout/left/tabs/visualize_tab.tsx b/x-pack/plugins/security_solution/public/flyout/left/tabs/visualize_tab.tsx index 923a2e9aa3ed6..632bcb856a257 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/tabs/visualize_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/tabs/visualize_tab.tsx @@ -91,7 +91,7 @@ export const VisualizeTab: FC = memo(() => { color="primary" name="coarsness" legend={i18n.translate( - 'xpack.securitySolution.flyout.left.visualize.buttonGroupButtonLabel', + 'xpack.securitySolution.flyout.left.visualize.buttonGroupLegendLabel', { defaultMessage: 'Visualize options', } diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/description.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/description.tsx index ac8ec64a0ee34..4b0169eeacbc2 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/description.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/description.tsx @@ -11,6 +11,7 @@ import React, { useMemo, useCallback } from 'react'; import { isEmpty } from 'lodash'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; import { useRightPanelContext } from '../context'; import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; import { @@ -65,6 +66,12 @@ export const Description: FC = () => { onClick={openRulePreview} iconSide="right" data-test-subj={RULE_SUMMARY_BUTTON_TEST_ID} + aria-label={i18n.translate( + 'xpack.securitySolution.flyout.right.about.description.ruleSummaryButtonAriaLabel', + { + defaultMessage: 'Show rule summary', + } + )} > { onClick={collapseDetails} iconType="arrowEnd" data-test-subj={COLLAPSE_DETAILS_BUTTON_TEST_ID} + aria-label={i18n.translate( + 'xpack.securitySolution.flyout.right.header.collapseDetailButtonAriaLabel', + { + defaultMessage: 'Collapse details', + } + )} > { onClick={expandDetails} iconType="arrowStart" data-test-subj={EXPAND_DETAILS_BUTTON_TEST_ID} + aria-label={i18n.translate( + 'xpack.securitySolution.flyout.right.header.expandDetailButtonAriaLabel', + { + defaultMessage: 'Expand details', + } + )} > = memo(({ flyoutIsExpandable }) )} -

+

{isAlert && !isEmpty(ruleName) ? ( ruleName ) : ( @@ -95,7 +95,7 @@ export const HeaderTitle: VFC = memo(({ flyoutIsExpandable }) defaultMessage="Event details" /> )} -

+
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_row.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_row.tsx index 8a981d037667f..9ffc267c3ccae 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_row.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_row.tsx @@ -9,6 +9,7 @@ import type { ReactElement, VFC } from 'react'; import React from 'react'; import { css } from '@emotion/react'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiHealth, EuiSkeletonText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedCount } from '../../../common/components/formatted_number'; export interface InsightsSummaryRowProps { @@ -64,7 +65,13 @@ export const InsightsSummaryRow: VFC = ({ lines={1} size="m" isLoading={loading} - contentAriaLabel="Loading" + contentAriaLabel={i18n.translate( + 'xpack.securitySolution.flyout.right.insights.insightSummaryLoadingAriaLabel', + { + defaultMessage: 'Loading insights for {value}', + values: { value }, + } + )} data-test-subj={loadingDataTestSubj} /> ); @@ -83,7 +90,12 @@ export const InsightsSummaryRow: VFC = ({ { onClick={goToInvestigationsTab} iconType="documentation" data-test-subj={INVESTIGATION_GUIDE_BUTTON_TEST_ID} + aria-label={i18n.translate( + 'xpack.securitySolution.flyout.right.investigation.investigationGuide.investigationGuideButtonAriaLabel', + { + defaultMessage: 'Show investigation guide', + } + )} > { onClick={openRulePreview} iconSide="right" data-test-subj={REASON_DETAILS_PREVIEW_BUTTON_TEST_ID} + aria-label={i18n.translate( + 'xpack.securitySolution.flyout.right.about.reason.alertReasonButtonAriaLabel', + { + defaultMessage: 'Show full reason', + } + )} > { onClick={goToResponseTab} iconType="documentation" data-test-subj={RESPONSE_BUTTON_TEST_ID} + aria-label={i18n.translate( + 'xpack.securitySolution.flyout.right.response.responseButtonAriaLabel', + { + defaultMessage: 'Response', + } + )} > { -
+

-

+
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/severity.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/severity.tsx index 81896bf770636..a7cfddde6c903 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/severity.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/severity.tsx @@ -46,12 +46,12 @@ export const DocumentSeverity: FC = memo(() => { -
+

-

+
diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/share_button.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/share_button.tsx index e9637e61205da..4c5e5a4507c38 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/share_button.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/share_button.tsx @@ -9,6 +9,7 @@ import { copyToClipboard, EuiButtonEmpty, EuiCopy } from '@elastic/eui'; import type { FC } from 'react'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url'; import { SHARE_BUTTON_TEST_ID } from './test_ids'; @@ -40,6 +41,12 @@ export const ShareButton: FC = ({ alertUrl }) => { }} iconType="share" data-test-subj={SHARE_BUTTON_TEST_ID} + aria-label={i18n.translate( + 'xpack.securitySolution.flyout.right.header.shareButtonAriaLabel', + { + defaultMessage: 'Share Alert', + } + )} > { return ( - <> + @@ -29,7 +40,7 @@ export const OverviewTab: FC = memo(() => { - + ); }); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx index 4f06ce9a45eb4..3e273d7d126f3 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx +++ b/x-pack/plugins/security_solution/public/flyout/shared/components/expandable_panel.tsx @@ -22,6 +22,7 @@ import { } from '@elastic/eui'; import type { IconType } from '@elastic/eui'; import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; export interface ExpandablePanelPanelProps { header: { @@ -99,7 +100,12 @@ export const ExpandablePanel: React.FC = ({ () => ( { +describe('Disabled experimental features on: ', { tags: '@ess' }, () => { describe('Policy list', () => { describe('Renders policy list without protection updates feature flag', () => { let indexedPolicy: IndexedFleetEndpointPolicyResponse; diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/policy/policy_list.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/policy/policy_list.cy.ts index b3e5e0477c5be..0a88b58359649 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/policy/policy_list.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/policy/policy_list.cy.ts @@ -16,6 +16,7 @@ import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../task describe( 'Policy List', { + tags: '@ess', env: { ftrConfig: { enableExperimental: ['protectionUpdatesEnabled'] } }, }, () => { diff --git a/x-pack/plugins/task_manager/server/task_validator.test.ts b/x-pack/plugins/task_manager/server/task_validator.test.ts index 52822adf6f49f..08c18591e468e 100644 --- a/x-pack/plugins/task_manager/server/task_validator.test.ts +++ b/x-pack/plugins/task_manager/server/task_validator.test.ts @@ -64,9 +64,7 @@ describe('TaskValidator', () => { expect(result).toEqual(task); }); - // TODO: Remove skip once all task types have defined their state schema. - // https://github.com/elastic/kibana/issues/159347 - it.skip(`should fail to validate the state schema when the task type doesn't have stateSchemaByVersion defined`, () => { + it(`should fail to validate the state schema when the task type doesn't have stateSchemaByVersion defined`, () => { const definitions = new TaskTypeDictionary(mockLogger()); definitions.registerTaskDefinitions({ foo: fooTaskDefinition, @@ -322,9 +320,7 @@ describe('TaskValidator', () => { expect(result).toEqual(task); }); - // TODO: Remove skip once all task types have defined their state schema. - // https://github.com/elastic/kibana/issues/159347 - it.skip(`should fail to validate the state schema when the task type doesn't have stateSchemaByVersion defined`, () => { + it(`should fail to validate the state schema when the task type doesn't have stateSchemaByVersion defined`, () => { const definitions = new TaskTypeDictionary(mockLogger()); definitions.registerTaskDefinitions({ foo: fooTaskDefinition, diff --git a/x-pack/plugins/task_manager/server/task_validator.ts b/x-pack/plugins/task_manager/server/task_validator.ts index 61d9a903dd5b4..900af04cd1207 100644 --- a/x-pack/plugins/task_manager/server/task_validator.ts +++ b/x-pack/plugins/task_manager/server/task_validator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { max, memoize } from 'lodash'; +import { max, memoize, isEmpty } from 'lodash'; import type { Logger } from '@kbn/core/server'; import type { ObjectType } from '@kbn/config-schema'; import { TaskTypeDictionary } from './task_type_dictionary'; @@ -64,14 +64,13 @@ export class TaskValidator { const taskTypeDef = this.definitions.get(task.taskType); const latestStateSchema = this.cachedGetLatestStateSchema(taskTypeDef); - // TODO: Remove once all task types have defined their state schema. - // https://github.com/elastic/kibana/issues/159347 - // Otherwise, failures on read / write would occur. (don't forget to unskip test) - if (!latestStateSchema) { + let state = task.state; + + // Skip validating tasks that don't use state + if (!latestStateSchema && isEmpty(state)) { return task; } - let state = task.state; try { state = this.getValidatedStateSchema( this.migrateTaskState(task.state, task.stateVersion, taskTypeDef, latestStateSchema), @@ -111,10 +110,8 @@ export class TaskValidator { const taskTypeDef = this.definitions.get(task.taskType); const latestStateSchema = this.cachedGetLatestStateSchema(taskTypeDef); - // TODO: Remove once all task types have defined their state schema. - // https://github.com/elastic/kibana/issues/159347 - // Otherwise, failures on read / write would occur. (don't forget to unskip test) - if (!latestStateSchema) { + // Skip validating tasks that don't use state + if (!latestStateSchema && isEmpty(task.state)) { return task; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0e167e1ce60d3..0b350d50056b6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -1208,7 +1208,6 @@ "dashboard.panel.clonedToast": "Panneau cloné", "dashboard.panel.clonePanel": "Cloner le panneau", "dashboard.panel.copyToDashboard.cancel": "Annuler", - "dashboard.panel.copyToDashboard.description": "Choisissez le tableau de bord de destination.", "dashboard.panel.copyToDashboard.existingDashboardOptionLabel": "Tableau de bord existant", "dashboard.panel.copyToDashboard.goToDashboard": "Copier et accéder au tableau de bord", "dashboard.panel.copyToDashboard.newDashboardOptionLabel": "Nouveau tableau de bord", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f0e47690da767..c6948a329c948 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1222,7 +1222,6 @@ "dashboard.panel.clonedToast": "クローンパネル", "dashboard.panel.clonePanel": "パネルのクローン", "dashboard.panel.copyToDashboard.cancel": "キャンセル", - "dashboard.panel.copyToDashboard.description": "対象ダッシュボードを選択します。", "dashboard.panel.copyToDashboard.existingDashboardOptionLabel": "既存のダッシュボード", "dashboard.panel.copyToDashboard.goToDashboard": "コピーしてダッシュボードを開く", "dashboard.panel.copyToDashboard.newDashboardOptionLabel": "新規ダッシュボード", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 17beacb3184e2..491e2870c636d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1222,7 +1222,6 @@ "dashboard.panel.clonedToast": "克隆的面板", "dashboard.panel.clonePanel": "克隆面板", "dashboard.panel.copyToDashboard.cancel": "取消", - "dashboard.panel.copyToDashboard.description": "选择目标仪表板。", "dashboard.panel.copyToDashboard.existingDashboardOptionLabel": "现有仪表板", "dashboard.panel.copyToDashboard.goToDashboard": "复制并前往仪表板", "dashboard.panel.copyToDashboard.newDashboardOptionLabel": "新仪表板", diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts index ef50420bb6561..4d2e9781688c9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts @@ -17,33 +17,32 @@ import { const getDefaultRules = (response: any) => ({ id: response.body.rules[0].id, - apiKey: response.body.rules[0].apiKey, - apiKeyCreatedByUser: false, - notifyWhen: 'onThrottleInterval', + api_key_created_by_user: false, + notify_when: 'onThrottleInterval', enabled: true, name: 'abc', tags: ['foo'], consumer: 'alertsFixture', throttle: '1m', - alertTypeId: 'test.noop', - apiKeyOwner: response.body.rules[0].apiKeyOwner, - createdBy: 'elastic', - updatedBy: response.body.rules[0].updatedBy, - muteAll: false, - mutedInstanceIds: [], + rule_type_id: 'test.noop', + api_key_owner: response.body.rules[0].api_key_owner, + created_by: 'elastic', + updated_by: response.body.rules[0].updated_by, + mute_all: false, + muted_alert_ids: [], schedule: { interval: '1m' }, actions: [], params: {}, running: false, - snoozeSchedule: [], - updatedAt: response.body.rules[0].updatedAt, - createdAt: response.body.rules[0].createdAt, - scheduledTaskId: response.body.rules[0].scheduledTaskId, - executionStatus: response.body.rules[0].executionStatus, + snooze_schedule: [], + updated_at: response.body.rules[0].updated_at, + created_at: response.body.rules[0].created_at, + scheduled_task_id: response.body.rules[0].scheduled_task_id, + execution_status: response.body.rules[0].execution_status, monitoring: response.body.rules[0].monitoring, revision: 0, - ...(response.body.rules[0].nextRun ? { nextRun: response.body.rules[0].nextRun } : {}), - ...(response.body.rules[0].lastRun ? { lastRun: response.body.rules[0].lastRun } : {}), + ...(response.body.rules[0].next_run ? { next_run: response.body.rules[0].next_run } : {}), + ...(response.body.rules[0].last_run ? { last_run: response.body.rules[0].last_run } : {}), }); const getThreeRules = (response: any) => { @@ -51,33 +50,32 @@ const getThreeRules = (response: any) => { for (let i = 0; i < 3; i++) { rules.push({ id: response.body.rules[i].id, - apiKey: response.body.rules[i].apiKey, - apiKeyCreatedByUser: false, - notifyWhen: 'onThrottleInterval', + api_key_created_by_user: false, + notify_when: 'onThrottleInterval', enabled: true, name: 'abc', tags: ['multiple-rules-delete'], consumer: 'alertsFixture', throttle: '1m', - alertTypeId: 'test.noop', - apiKeyOwner: response.body.rules[i].apiKeyOwner, - createdBy: 'elastic', - updatedBy: response.body.rules[i].updatedBy, - muteAll: false, - mutedInstanceIds: [], + rule_type_id: 'test.noop', + api_key_owner: response.body.rules[i].api_key_owner, + created_by: 'elastic', + updated_by: response.body.rules[i].updated_by, + mute_all: false, + muted_alert_ids: [], schedule: { interval: '1m' }, actions: [], params: {}, running: false, - snoozeSchedule: [], - updatedAt: response.body.rules[i].updatedAt, - createdAt: response.body.rules[i].createdAt, - scheduledTaskId: response.body.rules[i].scheduledTaskId, - executionStatus: response.body.rules[i].executionStatus, + snooze_schedule: [], + updated_at: response.body.rules[i].updated_at, + created_at: response.body.rules[i].created_at, + scheduled_task_id: response.body.rules[i].scheduled_task_id, + execution_status: response.body.rules[i].execution_status, monitoring: response.body.rules[i].monitoring, revision: 0, - ...(response.body.rules[i].nextRun ? { nextRun: response.body.rules[i].nextRun } : {}), - ...(response.body.rules[i].lastRun ? { lastRun: response.body.rules[i].lastRun } : {}), + ...(response.body.rules[i].next_run ? { next_run: response.body.rules[i].next_run } : {}), + ...(response.body.rules[i].last_run ? { last_run: response.body.rules[i].last_run } : {}), }); } return rules; @@ -230,7 +228,7 @@ export default ({ getService }: FtrProviderContext) => { rules: [ { ...getDefaultRules(response), - alertTypeId: 'test.restricted-noop', + rule_type_id: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }, ], @@ -298,7 +296,7 @@ export default ({ getService }: FtrProviderContext) => { break; case 'superuser at space1': expect(response.body).to.eql({ - rules: [{ ...getDefaultRules(response), alertTypeId: 'test.restricted-noop' }], + rules: [{ ...getDefaultRules(response), rule_type_id: 'test.restricted-noop' }], errors: [], total: 1, taskIdsFailedToBeDeleted: [], @@ -604,7 +602,7 @@ export default ({ getService }: FtrProviderContext) => { expect(response.statusCode).to.eql(400); expect(response.body.message).to.eql( - "Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments" + "Error validating bulk delete data - Both 'filter' and 'ids' are supplied. Define either 'ids' or 'filter' properties in method's arguments" ); objectRemover.add(space.id, createdRule1.id, 'rule', 'alerting'); await getScheduledTask(createdRule1.scheduled_task_id); @@ -641,7 +639,8 @@ export default ({ getService }: FtrProviderContext) => { expect(response.body).to.eql({ error: 'Bad Request', - message: "Either 'ids' or 'filter' property in method's arguments should be provided", + message: + "Error validating bulk delete data - Either 'ids' or 'filter' property in method's arguments should be provided", statusCode: 400, }); }); @@ -669,7 +668,8 @@ export default ({ getService }: FtrProviderContext) => { expect(response.body).to.eql({ error: 'Bad Request', - message: "Either 'ids' or 'filter' property in method's arguments should be provided", + message: + "Error validating bulk delete data - Either 'ids' or 'filter' property in method's arguments should be provided", statusCode: 400, }); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_hidden_datastreams.ts b/x-pack/test/fleet_api_integration/apis/epm/install_hidden_datastreams.ts index 93b30775170dc..2fe976352944a 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_hidden_datastreams.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_hidden_datastreams.ts @@ -143,5 +143,74 @@ export default function (providerContext: FtrProviderContext) { // datastream did not roll over expect(indicesAfter).equal(indicesBefore); }); + + it('should not rollover datastream when failed to update mappings and skipDataStreamRollover is true', async function () { + await supertest + .post(`/api/fleet/epm/packages/apm/8.7.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + + await es.index({ + index: 'metrics-apm.service_summary.10m-default', + document: { + '@timestamp': '2023-05-30T07:50:00.000Z', + agent: { + name: 'go', + }, + data_stream: { + dataset: 'apm.service_summary.10m', + namespace: 'default', + type: 'metrics', + }, + ecs: { + version: '8.6.0-dev', + }, + event: { + agent_id_status: 'missing', + ingested: '2023-05-30T07:57:12Z', + }, + metricset: { + interval: '10m', + name: 'service_summary', + }, + observer: { + hostname: '047e282994fb', + type: 'apm-server', + version: '8.7.0', + }, + processor: { + event: 'metric', + name: 'metric', + }, + service: { + language: { + name: 'go', + }, + name: '___main_elastic_cloud_87_ilm_fix', + }, + }, + }); + + let ds = await es.indices.get({ + index: 'metrics-apm.service_summary*', + expand_wildcards: ['open', 'hidden'], + }); + const indicesBefore = Object.keys(ds).length; + + await supertest + .post(`/api/fleet/epm/packages/apm/8.8.0?skipDataStreamRollover=true`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + + ds = await es.indices.get({ + index: 'metrics-apm.service_summary*', + expand_wildcards: ['open', 'hidden'], + }); + const indicesAfter = Object.keys(ds).length; + // datastream did not roll over + expect(indicesAfter).equal(indicesBefore); + }); }); } diff --git a/yarn.lock b/yarn.lock index 7e1992e29ade1..6a4f80236417c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16172,7 +16172,7 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== -execa@4.1.0, execa@^4.0.2: +execa@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==