From bf0920d324155d4f66f98725b2ce4b5285d84886 Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Tue, 2 May 2023 15:18:50 +0200 Subject: [PATCH 01/24] Fix alert details page name (#156370) Fixes #156163 ## Summary This PR fixes the alert details page name: --- .../alert_details/components/page_title.test.tsx | 10 +++------- .../alert_details/components/page_title.tsx | 16 +++++++--------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx index d988be160c48a..125ea4885a564 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.test.tsx @@ -28,9 +28,7 @@ describe('Page Title', () => { it('should display Log threshold title', () => { const { getByTestId } = renderComp(defaultProps); - expect(getByTestId('page-title-container').children.item(0)?.textContent).toEqual( - 'Log threshold breached' - ); + expect(getByTestId('page-title-container').textContent).toContain('Log threshold breached'); }); it('should display Anomaly title', () => { @@ -46,9 +44,7 @@ describe('Page Title', () => { const { getByTestId } = renderComp(props); - expect(getByTestId('page-title-container').children.item(0)?.textContent).toEqual( - 'Anomaly detected' - ); + expect(getByTestId('page-title-container').textContent).toContain('Anomaly detected'); }); it('should display Inventory title', () => { @@ -64,7 +60,7 @@ describe('Page Title', () => { const { getByTestId } = renderComp(props); - expect(getByTestId('page-title-container').children.item(0)?.textContent).toEqual( + expect(getByTestId('page-title-container').textContent).toContain( 'Inventory threshold breached' ); }); diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx index 201e19a5c94a6..a2e47ac4ccf16 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/page_title.tsx @@ -35,15 +35,13 @@ export interface PageTitleProps { } export function pageTitleContent(ruleCategory: string) { - return ( - - ); + return i18n.translate('xpack.observability.pages.alertDetails.pageTitle.title', { + defaultMessage: + '{ruleCategory} {ruleCategory, select, Anomaly {detected} Inventory {threshold breached} other {breached}}', + values: { + ruleCategory, + }, + }); } export function PageTitle({ alert }: PageTitleProps) { From 74c981462d67c1cfe6540603ef6c6987da4aa8c4 Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Tue, 2 May 2023 15:30:55 +0200 Subject: [PATCH 02/24] [Infrastructure UI] Adopt new saved views API (#155827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 Summary Part of https://github.com/elastic/kibana/issues/152617 Closes https://github.com/elastic/kibana/issues/106650 Closes https://github.com/elastic/kibana/issues/154725 This PR replace the existing client React logic for handling saved views on the `infra` plugin with a new state management implementation that interacts with the newly created API. It brings the following changes: - Implement `useInventoryViews` and `useMetricsExplorerViews` custom hooks. - Adopt `@tanstack/react-query` for the above hooks implementation as it was already used across the plugin and simplifies the server state management. Extract the provider for the react query cache. - Update server services to fix an issue while updating views, which was preventing the unset of properties from the view. - Update Saved Views components to integrate the new hooks. - The `Load views` option has been removed accordingly to the decision made with the UX team, since it wasn't adding any value that wasn't already present in the `Manage views` option. Even if we are duplicating similar logic to handle the Inventory and Metrics Explorer views, we decided to keep them separated to easily control their behaviour and avoid coupled logic that can be painful to split in future. ## 🐞 Bug fixes This implementation also fixed some existing bugs in production: - As reported in [this comment](https://github.com/elastic/kibana/pull/155174#pullrequestreview-1399982744), when the current view is deleted, the selector doesn't fallback on another view and keeps showing the same view title. It has been fixed and the selected view fallbacks to the default view. - When refreshing the page after a view was selected, the view was not correctly recovered and shown. The implemented changes fix this behaviour. - The "include time" option for creating/updating a saved view was not working and never removed the time property if disabled. - Minor visual adjustments such as action button type and alignment. ## 👨‍💻 Review hints The best way to verify all the interactions and loadings work correctly as a user expects, running the branch locally with an oblt cluster is recommended. In both the inventory and metrics explorer pages, the user should be able to: - Access and manage the saved views, select and load a view, delete a view, and set a view as default. - Save a new view. - Update the currently used view, except for the static **Default view**. - Show an error when trying to create/update a view with a name already held by another view. - Restore the view with the following priority order - Use from the URL the stored view id to restore the view - Use the default view id stored in the source configuration as a user preference - Use the static **Default view** ## 👣 Following steps - [ ] https://github.com/elastic/kibana/issues/155117 --------- Co-authored-by: Marco Antonio Ghiani Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: jennypavlova --- .../saved_views/manage_views_flyout.tsx | 130 ++++--- .../saved_views/toolbar_control.tsx | 215 ++++------- .../components/saved_views/upsert_modal.tsx | 34 +- .../saved_views/view_list_modal.tsx | 103 ------ .../containers/react_query_provider.tsx | 51 +++ .../infra/public/hooks/use_inventory_views.ts | 262 ++++++++++++++ .../hooks/use_metrics_explorer_views.ts | 263 ++++++++++++++ .../public/hooks/use_saved_views_notifier.ts | 52 +++ .../infra/public/pages/metrics/index.tsx | 33 +- .../inventory_view/components/layout.tsx | 336 +++++++++--------- .../inventory_view/components/layout_view.tsx | 6 +- .../inventory_view/components/saved_views.tsx | 36 +- .../hooks/use_waffle_view_state.ts | 38 +- .../pages/metrics/inventory_view/index.tsx | 56 ++- .../components/saved_views.tsx | 50 +++ .../hooks/use_metric_explorer_state.ts | 17 +- .../pages/metrics/metrics_explorer/index.tsx | 26 +- .../inventory_views/inventory_views_client.ts | 8 +- .../public/services/inventory_views/types.ts | 10 +- .../metrics_explorer_views_client.ts | 6 +- .../inventory_views/create_inventory_view.ts | 6 +- .../create_metrics_explorer_view.ts | 10 +- .../inventory_views_client.mock.ts | 1 - .../inventory_views_client.test.ts | 48 +-- .../inventory_views/inventory_views_client.ts | 31 +- .../server/services/inventory_views/types.ts | 6 +- .../metrics_explorer_views_client.mock.ts | 1 - .../metrics_explorer_views_client.test.ts | 48 +-- .../metrics_explorer_views_client.ts | 34 +- .../services/metrics_explorer_views/types.ts | 6 +- .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../test/functional/apps/infra/home_page.ts | 55 ++- .../functional/apps/infra/metrics_explorer.ts | 74 ++-- .../page_objects/infra_saved_views.ts | 76 ++-- 36 files changed, 1306 insertions(+), 834 deletions(-) delete mode 100644 x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx create mode 100644 x-pack/plugins/infra/public/containers/react_query_provider.tsx create mode 100644 x-pack/plugins/infra/public/hooks/use_inventory_views.ts create mode 100644 x-pack/plugins/infra/public/hooks/use_metrics_explorer_views.ts create mode 100644 x-pack/plugins/infra/public/hooks/use_saved_views_notifier.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/saved_views.tsx diff --git a/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx b/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx index 33e5b71c56a3f..e503bdebafa03 100644 --- a/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useMemo } from 'react'; import useToggle from 'react-use/lib/useToggle'; import { @@ -22,16 +22,23 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { SavedView } from '../../containers/saved_view/saved_view'; +import { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiButtonIcon } from '@elastic/eui'; +import { MetricsExplorerView } from '../../../common/metrics_explorer_views'; +import type { InventoryView } from '../../../common/inventory_views'; +import { UseInventoryViewsResult } from '../../hooks/use_inventory_views'; +import { UseMetricsExplorerViewsResult } from '../../hooks/use_metrics_explorer_views'; -interface Props { - views: Array>; +type View = InventoryView | MetricsExplorerView; +type UseViewResult = UseInventoryViewsResult | UseMetricsExplorerViewsResult; + +export interface ManageViewsFlyoutProps { + views: UseViewResult['views']; loading: boolean; - sourceIsLoading: boolean; onClose(): void; - onMakeDefaultView(id: string): void; - setView(viewState: ViewState): void; - onDeleteView(id: string): void; + onMakeDefaultView: UseViewResult['setDefaultViewById']; + onSwitchView: UseViewResult['switchViewById']; + onDeleteView: UseViewResult['deleteViewById']; } interface DeleteConfimationProps { @@ -39,55 +46,27 @@ interface DeleteConfimationProps { onConfirm(): void; } -const DeleteConfimation = ({ isDisabled, onConfirm }: DeleteConfimationProps) => { - const [isConfirmVisible, toggleVisibility] = useToggle(false); - - return isConfirmVisible ? ( - - - - - - - - - ) : ( - - ); +const searchConfig = { + box: { incremental: true }, }; -export function SavedViewManageViewsFlyout({ +export function ManageViewsFlyout({ onClose, - views, - setView, + views = [], + onSwitchView, onMakeDefaultView, onDeleteView, loading, - sourceIsLoading, -}: Props) { - const [inProgressView, setInProgressView] = useState(null); +}: ManageViewsFlyoutProps) { + // Add name as top level property to allow in memory search + const namedViews = useMemo(() => views.map(addOwnName), [views]); - const renderName = (name: string, item: SavedView) => ( + const renderName = (name: string, item: View) => ( { - setView(item); + onSwitchView(item.id); onClose(); }} > @@ -95,11 +74,11 @@ export function SavedViewManageViewsFlyout({ ); - const renderDeleteAction = (item: SavedView) => { + const renderDeleteAction = (item: View) => { return ( { onDeleteView(item.id); }} @@ -107,22 +86,21 @@ export function SavedViewManageViewsFlyout({ ); }; - const renderMakeDefaultAction = (item: SavedView) => { + const renderMakeDefaultAction = (item: View) => { return ( - { - setInProgressView(item.id); onMakeDefaultView(item.id); }} /> ); }; - const columns = [ + const columns: Array> = [ { field: 'name', name: i18n.translate('xpack.infra.openView.columnNames.name', { defaultMessage: 'Name' }), @@ -139,7 +117,7 @@ export function SavedViewManageViewsFlyout({ render: renderMakeDefaultAction, }, { - available: (item: SavedView) => item.id !== '0', + available: (item) => !item.attributes.isStatic, render: renderDeleteAction, }, ], @@ -161,10 +139,10 @@ export function SavedViewManageViewsFlyout({ @@ -178,3 +156,41 @@ export function SavedViewManageViewsFlyout({ ); } + +const DeleteConfimation = ({ isDisabled, onConfirm }: DeleteConfimationProps) => { + const [isConfirmVisible, toggleVisibility] = useToggle(false); + + return isConfirmVisible ? ( + + + + + + + + + ) : ( + + ); +}; + +/** + * Helpers + */ +const addOwnName = (view: View) => ({ ...view, name: view.attributes.name }); diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 1610b1b63fd82..b52d83cac60c6 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -5,132 +5,105 @@ * 2.0. */ -import React, { useCallback, useState, useEffect } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiPopover, EuiListGroup, EuiListGroupItem } from '@elastic/eui'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; import { FormattedMessage } from '@kbn/i18n-react'; -import { SavedViewManageViewsFlyout } from './manage_views_flyout'; -import { useSavedViewContext } from '../../containers/saved_view/saved_view'; -import { SavedViewListModal } from './view_list_modal'; +import { NonEmptyString } from '@kbn/io-ts-utils'; +import { ManageViewsFlyout } from './manage_views_flyout'; import { useBoolean } from '../../hooks/use_boolean'; import { UpsertViewModal } from './upsert_modal'; - -interface Props { - viewState: ViewState; +import { UseInventoryViewsResult } from '../../hooks/use_inventory_views'; +import { UseMetricsExplorerViewsResult } from '../../hooks/use_metrics_explorer_views'; + +type UseViewProps = + | 'currentView' + | 'views' + | 'isFetchingViews' + | 'isFetchingCurrentView' + | 'isCreatingView' + | 'isUpdatingView'; + +type UseViewResult = UseInventoryViewsResult | UseMetricsExplorerViewsResult; +type InventoryViewsResult = Pick; +type MetricsExplorerViewsResult = Pick; + +interface Props extends InventoryViewsResult, MetricsExplorerViewsResult { + viewState: ViewState & { time?: number }; + onCreateView: UseViewResult['createView']; + onDeleteView: UseViewResult['deleteViewById']; + onUpdateView: UseViewResult['updateViewById']; + onLoadViews: UseViewResult['fetchViews']; + onSetDefaultView: UseViewResult['setDefaultViewById']; + onSwitchView: UseViewResult['switchViewById']; } export function SavedViewsToolbarControls(props: Props) { - const kibana = useKibana(); const { - views, - saveView, - loading, - updateView, - deletedId, - deleteView, - makeDefault, - sourceIsLoading, - find, - errorOnFind, - errorOnCreate, - createdView, - updatedView, currentView, - setCurrentView, - } = useSavedViewContext(); + views, + isFetchingViews, + isFetchingCurrentView, + isCreatingView, + isUpdatingView, + onCreateView, + onDeleteView, + onUpdateView, + onLoadViews, + onSetDefaultView, + onSwitchView, + viewState, + } = props; const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false); const [isManageFlyoutOpen, { on: openManageFlyout, off: closeManageFlyout }] = useBoolean(false); - const [isUpdateModalOpen, { on: openUpdateModal, off: closeUpdateModal }] = useBoolean(false); - const [isLoadModalOpen, { on: openLoadModal, off: closeLoadModal }] = useBoolean(false); const [isCreateModalOpen, { on: openCreateModal, off: closeCreateModal }] = useBoolean(false); + const [isUpdateModalOpen, { on: openUpdateModal, off: closeUpdateModal }] = useBoolean(false); - const [isInvalid, setIsInvalid] = useState(false); + const togglePopoverAndLoad = () => { + if (!isPopoverOpen) { + onLoadViews(); + } + togglePopover(); + }; const goToManageViews = () => { closePopover(); - find(); openManageFlyout(); }; - const goToLoadView = () => { - closePopover(); - find(); - openLoadModal(); - }; - const goToCreateView = () => { closePopover(); - setIsInvalid(false); openCreateModal(); }; const goToUpdateView = () => { closePopover(); - setIsInvalid(false); openUpdateModal(); }; - const save = useCallback( - (name: string, hasTime: boolean = false) => { - const currentState = { - ...props.viewState, - ...(!hasTime ? { time: undefined } : {}), - }; - saveView({ ...currentState, name }); - }, - [props.viewState, saveView] - ); - - const update = useCallback( - (name: string, hasTime: boolean = false) => { - const currentState = { - ...props.viewState, - ...(!hasTime ? { time: undefined } : {}), - }; - updateView(currentView.id, { ...currentState, name }); - }, - [props.viewState, updateView, currentView] - ); + const handleCreateView = (name: NonEmptyString, shouldIncludeTime: boolean = false) => { + const attributes = { ...viewState, name }; - useEffect(() => { - if (errorOnCreate) { - setIsInvalid(true); + if (!shouldIncludeTime) { + delete attributes.time; } - }, [errorOnCreate]); - useEffect(() => { - if (updatedView !== undefined) { - setCurrentView(updatedView); - // INFO: Close the modal after the view is created. - closeUpdateModal(); - } - }, [updatedView, setCurrentView, closeUpdateModal]); + onCreateView(attributes).then(closeCreateModal); + }; - useEffect(() => { - if (createdView !== undefined) { - // INFO: Close the modal after the view is created. - setCurrentView(createdView); - closeCreateModal(); - } - }, [createdView, setCurrentView, closeCreateModal]); + const handleUpdateView = (name: NonEmptyString, shouldIncludeTime: boolean = false) => { + if (!currentView) return; - useEffect(() => { - if (deletedId !== undefined) { - // INFO: Refresh view list after an item is deleted - find(); - } - }, [deletedId, find]); + const attributes = { ...viewState, name }; - useEffect(() => { - if (errorOnCreate) { - kibana.notifications.toasts.warning(getErrorToast('create', errorOnCreate)!); - } else if (errorOnFind) { - kibana.notifications.toasts.warning(getErrorToast('find', errorOnFind)!); + if (!shouldIncludeTime) { + delete attributes.time; } - }, [errorOnCreate, errorOnFind, kibana]); + + onUpdateView({ id: currentView.id, attributes }).then(closeUpdateModal); + }; return ( <> @@ -138,14 +111,15 @@ export function SavedViewsToolbarControls(props: Props) { data-test-subj="savedViews-popover" button={ {currentView - ? currentView.name + ? currentView.attributes.name : i18n.translate('xpack.infra.savedView.unknownView', { defaultMessage: 'No view selected', })} @@ -168,19 +142,11 @@ export function SavedViewsToolbarControls(props: Props) { data-test-subj="savedViews-updateView" iconType="refresh" onClick={goToUpdateView} - isDisabled={!currentView || currentView.id === '0'} + isDisabled={!currentView || currentView.attributes.isStatic} label={i18n.translate('xpack.infra.savedView.updateView', { defaultMessage: 'Update view', })} /> - (props: Props) { {isCreateModalOpen && ( (props: Props) { )} {isUpdateModalOpen && ( (props: Props) { } /> )} - {isLoadModalOpen && ( - - currentView={currentView} - views={views} - onClose={closeLoadModal} - setView={setCurrentView} - /> - )} {isManageFlyoutOpen && ( - - sourceIsLoading={sourceIsLoading} - loading={loading} + )} ); } - -const getErrorToast = (type: 'create' | 'find', msg?: string) => { - if (type === 'create') { - return { - toastLifeTimeMs: 3000, - title: - msg || - i18n.translate('xpack.infra.savedView.errorOnCreate.title', { - defaultMessage: `An error occured saving view.`, - }), - }; - } else if (type === 'find') { - return { - toastLifeTimeMs: 3000, - title: - msg || - i18n.translate('xpack.infra.savedView.findError.title', { - defaultMessage: `An error occurred while loading views.`, - }), - }; - } -}; diff --git a/x-pack/plugins/infra/public/components/saved_views/upsert_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/upsert_modal.tsx index fa2fc3777ca90..08476357cec01 100644 --- a/x-pack/plugins/infra/public/components/saved_views/upsert_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/upsert_modal.tsx @@ -19,29 +19,36 @@ import { EuiFieldText, EuiSpacer, EuiSwitch, + EuiSwitchEvent, EuiText, } from '@elastic/eui'; -import { EuiSwitchEvent } from '@elastic/eui'; +import { NonEmptyString } from '@kbn/io-ts-utils'; interface Props { - isInvalid: boolean; onClose(): void; - onSave(name: string, shouldIncludeTime: boolean): void; + onSave(name: NonEmptyString, shouldIncludeTime: boolean): void; + isSaving: boolean; initialName?: string; initialIncludeTime?: boolean; title: React.ReactNode; } +const nameLabel = i18n.translate('xpack.infra.waffle.savedViews.viewNamePlaceholder', { + defaultMessage: 'Name', +}); + export const UpsertViewModal = ({ onClose, onSave, - isInvalid, + isSaving, initialName = '', initialIncludeTime = false, title, }: Props) => { const [viewName, setViewName] = useState(initialName); - const [includeTime, setIncludeTime] = useState(initialIncludeTime); + const [shouldIncludeTime, setIncludeTime] = useState(initialIncludeTime); + + const trimmedName = viewName.trim() as NonEmptyString; const handleNameChange: React.ChangeEventHandler = (e) => { setViewName(e.target.value); @@ -52,7 +59,7 @@ export const UpsertViewModal = ({ }; const saveView = () => { - onSave(viewName, includeTime); + onSave(trimmedName, shouldIncludeTime); }; return ( @@ -62,16 +69,11 @@ export const UpsertViewModal = ({ } - checked={includeTime} + checked={shouldIncludeTime} onChange={handleTimeCheckChange} /> @@ -101,9 +103,9 @@ export const UpsertViewModal = ({ /> diff --git a/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx deleted file mode 100644 index 43ca2776b284d..0000000000000 --- a/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useState, useMemo } from 'react'; - -import { EuiButtonEmpty, EuiModalFooter, EuiButton, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiModal, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody } from '@elastic/eui'; -import { EuiSelectable } from '@elastic/eui'; -import { EuiSelectableOption } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { SavedView } from '../../containers/saved_view/saved_view'; - -interface Props { - views: Array>; - onClose(): void; - setView(viewState: ViewState): void; - currentView?: ViewState; -} - -export function SavedViewListModal({ - onClose, - views, - setView, - currentView, -}: Props) { - const [options, setOptions] = useState(null); - - const onChange = useCallback((opts: EuiSelectableOption[]) => { - setOptions(opts); - }, []); - - const loadView = useCallback(() => { - if (!options) { - onClose(); - return; - } - - const selected = options.find((o) => o.checked); - if (!selected) { - onClose(); - return; - } - setView(views.find((v) => v.id === selected.key)!); - onClose(); - }, [options, views, setView, onClose]); - - const defaultOptions = useMemo(() => { - return views.map((v) => ({ - label: v.name, - key: v.id, - checked: currentView?.id === v.id ? 'on' : undefined, - })); - }, [views, currentView]); - - return ( - - - - - - - - - {(list, search) => ( - <> - {search} - - {list} - - )} - - - - - - - - - - - - ); -} diff --git a/x-pack/plugins/infra/public/containers/react_query_provider.tsx b/x-pack/plugins/infra/public/containers/react_query_provider.tsx new file mode 100644 index 0000000000000..050575dcc054a --- /dev/null +++ b/x-pack/plugins/infra/public/containers/react_query_provider.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { QueryClient, QueryClientConfig, QueryClientProvider } from '@tanstack/react-query'; +import merge from 'lodash/merge'; +import { EuiButtonIcon } from '@elastic/eui'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +const DEFAULT_CONFIG = { + defaultOptions: { + queries: { keepPreviousData: true, refetchOnWindowFocus: false }, + }, +}; + +interface ProviderProps { + children: React.ReactNode; + config?: QueryClientConfig; +} + +export function ReactQueryProvider({ children, config = {} }: ProviderProps) { + const [queryClient] = useState(() => new QueryClient(merge(DEFAULT_CONFIG, config))); + + return ( + + + {children} + + ); +} + +function HideableReactQueryDevTools() { + const [isHidden, setIsHidden] = useState(false); + + return !isHidden ? ( +
+ setIsHidden(!isHidden)} + aria-label="Disable React Query Dev Tools" + /> + +
+ ) : null; +} diff --git a/x-pack/plugins/infra/public/hooks/use_inventory_views.ts b/x-pack/plugins/infra/public/hooks/use_inventory_views.ts new file mode 100644 index 0000000000000..a75e8beaf3f65 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_inventory_views.ts @@ -0,0 +1,262 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; + +import { + QueryObserverBaseResult, + UseMutateAsyncFunction, + UseMutateFunction, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; +import { useUiTracker } from '@kbn/observability-plugin/public'; + +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; +import { MetricsSourceConfigurationResponse } from '../../common/metrics_sources'; +import { + CreateInventoryViewAttributesRequestPayload, + UpdateInventoryViewAttributesRequestPayload, +} from '../../common/http_api/latest'; +import type { InventoryView } from '../../common/inventory_views'; +import { useKibanaContextForPlugin } from './use_kibana'; +import { useUrlState } from '../utils/use_url_state'; +import { useSavedViewsNotifier } from './use_saved_views_notifier'; +import { useSourceContext } from '../containers/metrics_source'; + +interface UpdateViewParams { + id: string; + attributes: UpdateInventoryViewAttributesRequestPayload; +} + +export interface UseInventoryViewsResult { + views?: InventoryView[]; + currentView?: InventoryView | null; + createView: UseMutateAsyncFunction< + InventoryView, + ServerError, + CreateInventoryViewAttributesRequestPayload + >; + deleteViewById: UseMutateFunction; + fetchViews: QueryObserverBaseResult['refetch']; + updateViewById: UseMutateAsyncFunction; + switchViewById: (id: InventoryViewId) => void; + setDefaultViewById: UseMutateFunction< + MetricsSourceConfigurationResponse, + ServerError, + string, + MutationContext + >; + isCreatingView: boolean; + isFetchingCurrentView: boolean; + isFetchingViews: boolean; + isUpdatingView: boolean; +} + +type ServerError = IHttpFetchError; + +interface MutationContext { + id?: string; + previousViews?: InventoryView[]; +} + +const queryKeys = { + find: ['inventory-views-find'] as const, + get: ['inventory-views-get'] as const, + getById: (id: string) => ['inventory-views-get', id] as const, +}; + +export const useInventoryViews = (): UseInventoryViewsResult => { + const { inventoryViews } = useKibanaContextForPlugin().services; + const trackMetric = useUiTracker({ app: 'infra_metrics' }); + + const queryClient = useQueryClient(); + const { source, updateSourceConfiguration } = useSourceContext(); + + const defaultViewId = source?.configuration.inventoryDefaultView ?? '0'; + + const [currentViewId, switchViewById] = useUrlState({ + defaultState: defaultViewId, + decodeUrlState, + encodeUrlState, + urlStateKey: 'inventoryViewId', + writeDefaultState: true, + }); + + const notify = useSavedViewsNotifier(); + + const { + data: views, + refetch: fetchViews, + isFetching: isFetchingViews, + } = useQuery({ + queryKey: queryKeys.find, + queryFn: () => inventoryViews.client.findInventoryViews(), + enabled: false, // We will manually fetch the list when necessary + placeholderData: [], // Use a default empty array instead of undefined + onError: (error: ServerError) => notify.getViewFailure(error.body?.message ?? error.message), + onSuccess: (data) => { + const prefix = data.length >= 1000 ? 'over' : 'under'; + trackMetric({ metric: `${prefix}_1000_saved_objects_for_inventory_view` }); + }, + }); + + const { data: currentView, isFetching: isFetchingCurrentView } = useQuery({ + queryKey: queryKeys.getById(currentViewId), + queryFn: ({ queryKey: [, id] }) => inventoryViews.client.getInventoryView(id), + onError: (error: ServerError) => { + notify.getViewFailure(error.body?.message ?? error.message); + switchViewById(defaultViewId); + }, + placeholderData: null, + }); + + const { mutate: setDefaultViewById } = useMutation< + MetricsSourceConfigurationResponse, + ServerError, + string, + MutationContext + >({ + mutationFn: (id) => updateSourceConfiguration({ inventoryDefaultView: id }), + /** + * To provide a quick feedback, we perform an optimistic update on the list + * when updating the default view. + * 1. Cancel any outgoing refetches (so they don't overwrite our optimistic update) + * 2. Snapshot the previous views list + * 3. Optimistically update the list with new default view and store in cache + * 4. Return a context object with the snapshotted views + */ + onMutate: async (id) => { + await queryClient.cancelQueries({ queryKey: queryKeys.find }); // 1 + const previousViews = queryClient.getQueryData(queryKeys.find); // 2 + const updatedViews = getListWithUpdatedDefault(id, previousViews); // 3 + queryClient.setQueryData(queryKeys.find, updatedViews); + return { previousViews }; // 4 + }, + // If the mutation fails but doesn't retrigger the error, use the context returned from onMutate to roll back + onSuccess: (data, _id, context) => { + if (!data && context?.previousViews) { + return queryClient.setQueryData(queryKeys.find, context.previousViews); + } + return queryClient.invalidateQueries({ queryKey: queryKeys.get }); + }, + }); + + const { mutateAsync: createView, isLoading: isCreatingView } = useMutation< + InventoryView, + ServerError, + CreateInventoryViewAttributesRequestPayload + >({ + mutationFn: (attributes) => inventoryViews.client.createInventoryView(attributes), + onError: (error) => { + notify.upsertViewFailure(error.body?.message ?? error.message); + }, + onSuccess: (createdView) => { + queryClient.setQueryData(queryKeys.getById(createdView.id), createdView); // Store in cache created view + switchViewById(createdView.id); // Update current view and url state + }, + }); + + const { mutateAsync: updateViewById, isLoading: isUpdatingView } = useMutation< + InventoryView, + ServerError, + UpdateViewParams + >({ + mutationFn: ({ id, attributes }) => inventoryViews.client.updateInventoryView(id, attributes), + onError: (error) => { + notify.upsertViewFailure(error.body?.message ?? error.message); + }, + onSuccess: (updatedView) => { + queryClient.setQueryData(queryKeys.getById(updatedView.id), updatedView); // Store in cache updated view + }, + }); + + const { mutate: deleteViewById } = useMutation({ + mutationFn: (id: string) => inventoryViews.client.deleteInventoryView(id), + /** + * To provide a quick feedback, we perform an optimistic update on the list + * when deleting a view. + * 1. Cancel any outgoing refetches (so they don't overwrite our optimistic update) + * 2. Snapshot the previous views list + * 3. Optimistically update the list removing the view and store in cache + * 4. Return a context object with the snapshotted views + */ + onMutate: async (id) => { + await queryClient.cancelQueries({ queryKey: queryKeys.find }); // 1 + + const previousViews = queryClient.getQueryData(queryKeys.find); // 2 + + const updatedViews = getListWithoutDeletedView(id, previousViews); // 3 + queryClient.setQueryData(queryKeys.find, updatedViews); + + return { previousViews }; // 4 + }, + // If the mutation fails, use the context returned from onMutate to roll back + onError: (error, _id, context) => { + notify.deleteViewFailure(error.body?.message ?? error.message); + if (context?.previousViews) { + queryClient.setQueryData(queryKeys.find, context.previousViews); + } + }, + onSuccess: (_data, id) => { + // If the deleted view was the current one, switch to the default view + if (currentView?.id === id) { + switchViewById(defaultViewId); + } + }, + onSettled: () => { + fetchViews(); // Invalidate views list cache and refetch views + }, + }); + + return { + // Values + views, + currentView, + // Actions about updating view + createView, + deleteViewById, + fetchViews, + updateViewById, + switchViewById, + setDefaultViewById, + // Status flags + isCreatingView, + isFetchingCurrentView, + isFetchingViews, + isUpdatingView, + }; +}; + +const inventoryViewIdRT = rt.string; +type InventoryViewId = rt.TypeOf; + +const encodeUrlState = inventoryViewIdRT.encode; +const decodeUrlState = (value: unknown) => { + const state = pipe(inventoryViewIdRT.decode(value), fold(constant(undefined), identity)); + return state; +}; + +/** + * Helpers + */ +const getListWithUpdatedDefault = (id: string, views: InventoryView[] = []) => { + return views.map((view) => ({ + ...view, + attributes: { + ...view.attributes, + isDefault: view.id === id, + }, + })); +}; + +const getListWithoutDeletedView = (id: string, views: InventoryView[] = []) => { + return views.filter((view) => view.id !== id); +}; diff --git a/x-pack/plugins/infra/public/hooks/use_metrics_explorer_views.ts b/x-pack/plugins/infra/public/hooks/use_metrics_explorer_views.ts new file mode 100644 index 0000000000000..34a3666b5c0e0 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_metrics_explorer_views.ts @@ -0,0 +1,263 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; + +import { + QueryObserverBaseResult, + UseMutateAsyncFunction, + UseMutateFunction, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; +import { useUiTracker } from '@kbn/observability-plugin/public'; + +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser'; +import { MetricsSourceConfigurationResponse } from '../../common/metrics_sources'; +import { + CreateMetricsExplorerViewAttributesRequestPayload, + UpdateMetricsExplorerViewAttributesRequestPayload, +} from '../../common/http_api/latest'; +import { MetricsExplorerView } from '../../common/metrics_explorer_views'; +import { useKibanaContextForPlugin } from './use_kibana'; +import { useUrlState } from '../utils/use_url_state'; +import { useSavedViewsNotifier } from './use_saved_views_notifier'; +import { useSourceContext } from '../containers/metrics_source'; + +interface UpdateViewParams { + id: string; + attributes: UpdateMetricsExplorerViewAttributesRequestPayload; +} + +export interface UseMetricsExplorerViewsResult { + views?: MetricsExplorerView[]; + currentView?: MetricsExplorerView | null; + createView: UseMutateAsyncFunction< + MetricsExplorerView, + ServerError, + CreateMetricsExplorerViewAttributesRequestPayload + >; + deleteViewById: UseMutateFunction; + fetchViews: QueryObserverBaseResult['refetch']; + updateViewById: UseMutateAsyncFunction; + switchViewById: (id: MetricsExplorerViewId) => void; + setDefaultViewById: UseMutateFunction< + MetricsSourceConfigurationResponse, + ServerError, + string, + MutationContext + >; + isCreatingView: boolean; + isFetchingCurrentView: boolean; + isFetchingViews: boolean; + isUpdatingView: boolean; +} + +type ServerError = IHttpFetchError; + +interface MutationContext { + id?: string; + previousViews?: MetricsExplorerView[]; +} + +const queryKeys = { + find: ['metrics-explorer-views-find'] as const, + get: ['metrics-explorer-views-get'] as const, + getById: (id: string) => ['metrics-explorer-views-get', id] as const, +}; + +export const useMetricsExplorerViews = (): UseMetricsExplorerViewsResult => { + const { metricsExplorerViews } = useKibanaContextForPlugin().services; + const trackMetric = useUiTracker({ app: 'infra_metrics' }); + + const queryClient = useQueryClient(); + const { source, updateSourceConfiguration } = useSourceContext(); + + const defaultViewId = source?.configuration.metricsExplorerDefaultView ?? '0'; + + const [currentViewId, switchViewById] = useUrlState({ + defaultState: defaultViewId, + decodeUrlState, + encodeUrlState, + urlStateKey: 'metricsExplorerViewId', + writeDefaultState: true, + }); + + const notify = useSavedViewsNotifier(); + + const { + data: views, + refetch: fetchViews, + isFetching: isFetchingViews, + } = useQuery({ + queryKey: queryKeys.find, + queryFn: () => metricsExplorerViews.client.findMetricsExplorerViews(), + enabled: false, // We will manually fetch the list when necessary + placeholderData: [], // Use a default empty array instead of undefined + onError: (error: ServerError) => notify.getViewFailure(error.body?.message ?? error.message), + onSuccess: (data) => { + const prefix = data.length >= 1000 ? 'over' : 'under'; + trackMetric({ metric: `${prefix}_1000_saved_objects_for_metrics_explorer_view` }); + }, + }); + + const { data: currentView, isFetching: isFetchingCurrentView } = useQuery({ + queryKey: queryKeys.getById(currentViewId), + queryFn: ({ queryKey: [, id] }) => metricsExplorerViews.client.getMetricsExplorerView(id), + onError: (error: ServerError) => { + notify.getViewFailure(error.body?.message ?? error.message); + switchViewById(defaultViewId); + }, + placeholderData: null, + }); + + const { mutate: setDefaultViewById } = useMutation< + MetricsSourceConfigurationResponse, + ServerError, + string, + MutationContext + >({ + mutationFn: (id) => updateSourceConfiguration({ metricsExplorerDefaultView: id }), + /** + * To provide a quick feedback, we perform an optimistic update on the list + * when updating the default view. + * 1. Cancel any outgoing refetches (so they don't overwrite our optimistic update) + * 2. Snapshot the previous views list + * 3. Optimistically update the list with new default view and store in cache + * 4. Return a context object with the snapshotted views + */ + onMutate: async (id) => { + await queryClient.cancelQueries({ queryKey: queryKeys.find }); // 1 + const previousViews = queryClient.getQueryData(queryKeys.find); // 2 + const updatedViews = getListWithUpdatedDefault(id, previousViews); // 3 + queryClient.setQueryData(queryKeys.find, updatedViews); + return { previousViews }; // 4 + }, + // If the mutation fails but doesn't retrigger the error, use the context returned from onMutate to roll back + onSuccess: (data, _id, context) => { + if (!data && context?.previousViews) { + return queryClient.setQueryData(queryKeys.find, context.previousViews); + } + return queryClient.invalidateQueries({ queryKey: queryKeys.get }); + }, + }); + + const { mutateAsync: createView, isLoading: isCreatingView } = useMutation< + MetricsExplorerView, + ServerError, + CreateMetricsExplorerViewAttributesRequestPayload + >({ + mutationFn: (attributes) => metricsExplorerViews.client.createMetricsExplorerView(attributes), + onError: (error) => { + notify.upsertViewFailure(error.body?.message ?? error.message); + }, + onSuccess: (createdView) => { + queryClient.setQueryData(queryKeys.getById(createdView.id), createdView); // Store in cache created view + switchViewById(createdView.id); // Update current view and url state + }, + }); + + const { mutateAsync: updateViewById, isLoading: isUpdatingView } = useMutation< + MetricsExplorerView, + ServerError, + UpdateViewParams + >({ + mutationFn: ({ id, attributes }) => + metricsExplorerViews.client.updateMetricsExplorerView(id, attributes), + onError: (error) => { + notify.upsertViewFailure(error.body?.message ?? error.message); + }, + onSuccess: (updatedView) => { + queryClient.setQueryData(queryKeys.getById(updatedView.id), updatedView); // Store in cache updated view + }, + }); + + const { mutate: deleteViewById } = useMutation({ + mutationFn: (id: string) => metricsExplorerViews.client.deleteMetricsExplorerView(id), + /** + * To provide a quick feedback, we perform an optimistic update on the list + * when deleting a view. + * 1. Cancel any outgoing refetches (so they don't overwrite our optimistic update) + * 2. Snapshot the previous views list + * 3. Optimistically update the list removing the view and store in cache + * 4. Return a context object with the snapshotted views + */ + onMutate: async (id) => { + await queryClient.cancelQueries({ queryKey: queryKeys.find }); // 1 + + const previousViews = queryClient.getQueryData(queryKeys.find); // 2 + + const updatedViews = getListWithoutDeletedView(id, previousViews); // 3 + queryClient.setQueryData(queryKeys.find, updatedViews); + + return { previousViews }; // 4 + }, + // If the mutation fails, use the context returned from onMutate to roll back + onError: (error, _id, context) => { + notify.deleteViewFailure(error.body?.message ?? error.message); + if (context?.previousViews) { + queryClient.setQueryData(queryKeys.find, context.previousViews); + } + }, + onSuccess: (_data, id) => { + // If the deleted view was the current one, switch to the default view + if (currentView?.id === id) { + switchViewById(defaultViewId); + } + }, + onSettled: () => { + fetchViews(); // Invalidate views list cache and refetch views + }, + }); + + return { + // Values + views, + currentView, + // Actions about updating view + createView, + deleteViewById, + fetchViews, + updateViewById, + switchViewById, + setDefaultViewById, + // Status flags + isCreatingView, + isFetchingCurrentView, + isFetchingViews, + isUpdatingView, + }; +}; + +const metricsExplorerViewIdRT = rt.string; +type MetricsExplorerViewId = rt.TypeOf; + +const encodeUrlState = metricsExplorerViewIdRT.encode; +const decodeUrlState = (value: unknown) => { + const state = pipe(metricsExplorerViewIdRT.decode(value), fold(constant(undefined), identity)); + return state; +}; + +/** + * Helpers + */ +const getListWithUpdatedDefault = (id: string, views: MetricsExplorerView[] = []) => { + return views.map((view) => ({ + ...view, + attributes: { + ...view.attributes, + isDefault: view.id === id, + }, + })); +}; + +const getListWithoutDeletedView = (id: string, views: MetricsExplorerView[] = []) => { + return views.filter((view) => view.id !== id); +}; diff --git a/x-pack/plugins/infra/public/hooks/use_saved_views_notifier.ts b/x-pack/plugins/infra/public/hooks/use_saved_views_notifier.ts new file mode 100644 index 0000000000000..b7b8822e3ec14 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_saved_views_notifier.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useKibanaContextForPlugin } from './use_kibana'; + +export const useSavedViewsNotifier = () => { + const { notifications } = useKibanaContextForPlugin(); + + const deleteViewFailure = (message?: string) => { + notifications.toasts.danger({ + toastLifeTimeMs: 3000, + title: + message || + i18n.translate('xpack.infra.savedView.errorOnDelete.title', { + defaultMessage: `An error occured deleting the view.`, + }), + }); + }; + + const getViewFailure = (message?: string) => { + notifications.toasts.danger({ + toastLifeTimeMs: 3000, + title: + message || + i18n.translate('xpack.infra.savedView.findError.title', { + defaultMessage: `An error occurred while loading views.`, + }), + }); + }; + + const upsertViewFailure = (message?: string) => { + notifications.toasts.danger({ + toastLifeTimeMs: 3000, + title: + message || + i18n.translate('xpack.infra.savedView.errorOnCreate.title', { + defaultMessage: `An error occured saving view.`, + }), + }); + }; + + return { + deleteViewFailure, + getViewFailure, + upsertViewFailure, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 37f2a8bbbf01e..df66dc5eb88ba 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -10,8 +10,6 @@ import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { RouteComponentProps, Switch } from 'react-router-dom'; import { Route } from '@kbn/shared-ux-router'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { EuiErrorBoundary, EuiHeaderLinks, EuiHeaderLink } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; @@ -20,11 +18,7 @@ import { useLinkProps } from '@kbn/observability-plugin/public'; import { MetricsSourceConfigurationProperties } from '../../../common/metrics_sources'; import { HelpCenterContent } from '../../components/help_center_content'; import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; -import { - MetricsExplorerOptionsContainer, - useMetricsExplorerOptionsContainerContext, - DEFAULT_METRICS_EXPLORER_VIEW_STATE, -} from './metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricsExplorerOptionsContainer } from './metrics_explorer/hooks/use_metrics_explorer_options'; import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; import { MetricsExplorerPage } from './metrics_explorer'; import { SnapshotPage } from './inventory_view'; @@ -36,13 +30,13 @@ import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; import { MetricsAlertDropdown } from '../../alerting/common/components/metrics_alert_dropdown'; -import { SavedViewProvider } from '../../containers/saved_view/saved_view'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout'; import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; import { CreateDerivedIndexPattern, useSourceContext } from '../../containers/metrics_source'; import { NotFoundPage } from '../404'; +import { ReactQueryProvider } from '../../containers/react_query_provider'; const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', { defaultMessage: 'Add data', @@ -51,7 +45,6 @@ const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLab export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; const { setHeaderActionMenu, theme$ } = useContext(HeaderActionMenuContext); - const queryClient = new QueryClient(); const settingsTabTitle = i18n.translate('xpack.infra.metrics.settingsTabTitle', { defaultMessage: 'Settings', @@ -74,8 +67,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { - - + { } /> - + @@ -136,19 +128,12 @@ const PageContent = (props: { createDerivedIndexPattern: CreateDerivedIndexPattern; }) => { const { createDerivedIndexPattern, configuration } = props; - const { options } = useMetricsExplorerOptionsContainerContext(); return ( - - - + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 373a6a563fee9..9827c866b1424 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -36,7 +36,6 @@ import { LegendControls } from './waffle/legend_controls'; import { TryItButton } from '../../../../components/try_it_button'; interface Props { - shouldLoadDefault: boolean; currentView: SavedView | null; reload: () => Promise; interval: string; @@ -52,186 +51,173 @@ interface LegendControlOptions { const HOSTS_LINK_LOCAL_STORAGE_KEY = 'inventoryUI:hostsLinkClicked'; -export const Layout = React.memo( - ({ shouldLoadDefault, currentView, reload, interval, nodes, loading }: Props) => { - const [showLoading, setShowLoading] = useState(true); - const { - metric, - groupBy, - sort, - nodeType, - changeView, - view, - autoBounds, - boundsOverride, - legend, - changeBoundsOverride, - changeAutoBounds, - changeLegend, - } = useWaffleOptionsContext(); - const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); - const { applyFilterQuery } = useWaffleFiltersContext(); - const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette; - const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps; - const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors; - - const [hostsLinkClicked, setHostsLinkClicked] = useLocalStorage( - HOSTS_LINK_LOCAL_STORAGE_KEY, - false - ); - const hostsLinkClickedRef = useRef(hostsLinkClicked); - - const options = { - formatter: InfraFormatterType.percent, - formatTemplate: '{{value}}', - legend: createLegend(legendPalette, legendSteps, legendReverseColors), - metric, - sort, - groupBy, - }; - - useInterval( - () => { - if (!loading) { - jumpToTime(Date.now()); - } - }, - isAutoReloading ? 5000 : null - ); - - const dataBounds = calculateBoundsFromNodes(nodes); - const bounds = autoBounds ? dataBounds : boundsOverride; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); - const { onViewChange } = useWaffleViewState(); - - useEffect(() => { - if (currentView) { - onViewChange(currentView); +export const Layout = React.memo(({ currentView, reload, interval, nodes, loading }: Props) => { + const [showLoading, setShowLoading] = useState(true); + const { + metric, + groupBy, + sort, + nodeType, + changeView, + view, + autoBounds, + boundsOverride, + legend, + changeBoundsOverride, + changeAutoBounds, + changeLegend, + } = useWaffleOptionsContext(); + const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); + const { applyFilterQuery } = useWaffleFiltersContext(); + const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette; + const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps; + const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors; + + const [hostsLinkClicked, setHostsLinkClicked] = useLocalStorage( + HOSTS_LINK_LOCAL_STORAGE_KEY, + false + ); + const hostsLinkClickedRef = useRef(hostsLinkClicked); + + const options = { + formatter: InfraFormatterType.percent, + formatTemplate: '{{value}}', + legend: createLegend(legendPalette, legendSteps, legendReverseColors), + metric, + sort, + groupBy, + }; + + useInterval( + () => { + if (!loading) { + jumpToTime(Date.now()); } - }, [currentView, onViewChange]); - - useEffect(() => { - // load snapshot data after default view loaded, unless we're not loading a view - if (currentView != null || !shouldLoadDefault) { - reload(); - } - - /** - * INFO: why disable exhaustive-deps - * We need to wait on the currentView not to be null because it is loaded async and could change the view state. - * We don't actually need to watch the value of currentView though, since the view state will be synched up by the - * changing params in the reload method so we should only "watch" the reload method. - * - * TODO: Should refactor this in the future to make it more clear where all the view state is coming - * from and it's precedence [query params, localStorage, defaultView, out of the box view] - */ - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [reload, shouldLoadDefault]); - - useEffect(() => { - setShowLoading(true); - }, [options.metric, nodeType]); - - useEffect(() => { - const hasNodes = nodes && nodes.length; - // Don't show loading screen when we're auto-reloading - setShowLoading(!hasNodes); - }, [nodes]); - - const handleLegendControlChange = useCallback( - (opts: LegendControlOptions) => { - changeBoundsOverride(opts.bounds); - changeAutoBounds(opts.auto); - changeLegend(opts.legend); - }, - [changeBoundsOverride, changeAutoBounds, changeLegend] - ); - - return ( - <> - - - - - - - {view === 'map' && ( - - - - )} + }, + isAutoReloading ? 5000 : null + ); + + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); + const { onViewChange } = useWaffleViewState(); + + useEffect(() => { + if (currentView) { + onViewChange(currentView); + } + }, [currentView, onViewChange]); + + useEffect(() => { + // load snapshot data after default view loaded, unless we're not loading a view + if (currentView != null) { + reload(); + } + }, [currentView, reload]); + + useEffect(() => { + setShowLoading(true); + }, [options.metric, nodeType]); + + useEffect(() => { + const hasNodes = nodes && nodes.length; + // Don't show loading screen when we're auto-reloading + setShowLoading(!hasNodes); + }, [nodes]); + + const handleLegendControlChange = useCallback( + (opts: LegendControlOptions) => { + changeBoundsOverride(opts.bounds); + changeAutoBounds(opts.auto); + changeLegend(opts.legend); + }, + [changeBoundsOverride, changeAutoBounds, changeLegend] + ); + + return ( + <> + + + + + + + {view === 'map' && ( - + - + )} + + + - - - {!hostsLinkClickedRef.current && nodeType === 'host' && ( - { - setHostsLinkClicked(true); - }} + + + + {!hostsLinkClickedRef.current && nodeType === 'host' && ( + { + setHostsLinkClicked(true); + }} + /> + )} + + + + {({ bounds: { height = 0 } }) => ( + )} - - - - {({ bounds: { height = 0 } }) => ( - - )} - - - - - - - ); - } -); + + + + + + + ); +}); const TopActionContainer = euiStyled(EuiFlexItem)` padding: ${(props) => `${props.theme.eui.euiSizeM} 0`}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout_view.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout_view.tsx index af9c9ab5e2b30..f2ea500a98fd1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout_view.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout_view.tsx @@ -6,8 +6,8 @@ */ import React from 'react'; +import { useInventoryViews } from '../../../../hooks/use_inventory_views'; import { SnapshotNode } from '../../../../../common/http_api'; -import { useSavedViewContext } from '../../../../containers/saved_view/saved_view'; import { Layout } from './layout'; interface Props { @@ -18,6 +18,6 @@ interface Props { } export const LayoutView = (props: Props) => { - const { shouldLoadDefault, currentView } = useSavedViewContext(); - return ; + const { currentView } = useInventoryViews(); + return ; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx index 484c0a38f4d06..4547b0dbb0147 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx @@ -6,10 +6,42 @@ */ import React from 'react'; +import { useInventoryViews } from '../../../../hooks/use_inventory_views'; import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; -import { useWaffleViewState } from '../hooks/use_waffle_view_state'; +import { useWaffleViewState, WaffleViewState } from '../hooks/use_waffle_view_state'; export const SavedViews = () => { const { viewState } = useWaffleViewState(); - return ; + const { + currentView, + views, + isFetchingViews, + isFetchingCurrentView, + isCreatingView, + isUpdatingView, + createView, + deleteViewById, + fetchViews, + updateViewById, + switchViewById, + setDefaultViewById, + } = useInventoryViews(); + + return ( + + currentView={currentView} + views={views} + isFetchingViews={isFetchingViews} + isFetchingCurrentView={isFetchingCurrentView} + isCreatingView={isCreatingView} + isUpdatingView={isUpdatingView} + onCreateView={createView} + onDeleteView={deleteViewById} + onUpdateView={updateViewById} + onLoadViews={fetchViews} + onSetDefaultView={setDefaultViewById} + onSwitchView={switchViewById} + viewState={viewState} + /> + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts index 02a2144f1282e..6e685a6cc105f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts @@ -65,30 +65,32 @@ export const useWaffleViewState = () => { }; const onViewChange = useCallback( - (newState: WaffleViewState) => { + (newState) => { + const attributes = newState.attributes as WaffleViewState; + setWaffleOptionsState({ - sort: newState.sort, - metric: newState.metric, - groupBy: newState.groupBy, - nodeType: newState.nodeType, - view: newState.view, - customOptions: newState.customOptions, - customMetrics: newState.customMetrics, - boundsOverride: newState.boundsOverride, - autoBounds: newState.autoBounds, - accountId: newState.accountId, - region: newState.region, - legend: newState.legend, - timelineOpen: newState.timelineOpen, + sort: attributes.sort, + metric: attributes.metric, + groupBy: attributes.groupBy, + nodeType: attributes.nodeType, + view: attributes.view, + customOptions: attributes.customOptions, + customMetrics: attributes.customMetrics, + boundsOverride: attributes.boundsOverride, + autoBounds: attributes.autoBounds, + accountId: attributes.accountId, + region: attributes.region, + legend: attributes.legend, + timelineOpen: attributes.timelineOpen, }); - if (newState.time) { + if (attributes.time) { setWaffleTimeState({ - currentTime: newState.time, - isAutoReloading: newState.autoReload, + currentTime: attributes.time, + isAutoReloading: attributes.autoReload, }); } - setWaffleFiltersState(newState.filterQuery); + setWaffleFiltersState(attributes.filterQuery); }, [setWaffleOptionsState, setWaffleTimeState, setWaffleFiltersState] ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 922f0cdfaaaa8..6c97172a30f82 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -16,9 +16,6 @@ import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useSourceContext } from '../../../containers/metrics_source'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import { LayoutView } from './components/layout_view'; -import { SavedViewProvider } from '../../../containers/saved_view/saved_view'; -import { DEFAULT_WAFFLE_VIEW_STATE } from './hooks/use_waffle_view_state'; -import { useWaffleOptionsContext } from './hooks/use_waffle_options'; import { MetricsPageTemplate } from '../page_template'; import { inventoryTitle } from '../../../translations'; import { SavedViews } from './components/saved_views'; @@ -32,7 +29,6 @@ export const SnapshotPage = () => { useTrackPageview({ app: 'infra_metrics', path: 'inventory' }); useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 }); - const { source: optionsSource } = useWaffleOptionsContext(); useMetricsBreadcrumbs([ { @@ -60,36 +56,30 @@ export const SnapshotPage = () => { return (
- , ], + }} + pageSectionProps={{ + contentProps: { + css: css` + ${fullHeightContentStyles}; + padding-bottom: 0; + `, + }, + }} > - , ], - }} - pageSectionProps={{ - contentProps: { - css: css` - ${fullHeightContentStyles}; - padding-bottom: 0; - `, - }, - }} - > - ( - <> - - - - )} - /> - - + ( + <> + + + + )} + /> +
); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/saved_views.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/saved_views.tsx new file mode 100644 index 0000000000000..2d329f121f008 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/saved_views.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useMetricsExplorerViews } from '../../../../hooks/use_metrics_explorer_views'; +import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { MetricExplorerViewState } from '../hooks/use_metric_explorer_state'; + +interface Props { + viewState: MetricExplorerViewState; +} + +export const SavedViews = ({ viewState }: Props) => { + const { + currentView, + views, + isFetchingViews, + isFetchingCurrentView, + isCreatingView, + isUpdatingView, + createView, + deleteViewById, + fetchViews, + updateViewById, + switchViewById, + setDefaultViewById, + } = useMetricsExplorerViews(); + + return ( + + currentView={currentView} + views={views} + isFetchingViews={isFetchingViews} + isFetchingCurrentView={isFetchingCurrentView} + isCreatingView={isCreatingView} + isUpdatingView={isUpdatingView} + onCreateView={createView} + onDeleteView={deleteViewById} + onUpdateView={updateViewById} + onLoadViews={fetchViews} + onSetDefaultView={setDefaultViewById} + onSwitchView={switchViewById} + viewState={viewState} + /> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index a9a4611094174..bc098e441eb29 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -8,6 +8,7 @@ import DateMath from '@kbn/datemath'; import { useCallback, useEffect } from 'react'; import { DataViewBase } from '@kbn/es-query'; +import { MetricsExplorerView } from '../../../../../common/metrics_explorer_views'; import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerMetric, @@ -124,19 +125,19 @@ export const useMetricsExplorerState = ( ); const onViewStateChange = useCallback( - (vs: MetricExplorerViewState) => { - if (vs.chartOptions) { - setChartOptions(vs.chartOptions); + (view: MetricsExplorerView) => { + if (view.attributes.chartOptions) { + setChartOptions(view.attributes.chartOptions as MetricsExplorerChartOptions); } - if (vs.currentTimerange) { + if (view.attributes.currentTimerange) { // if this is the "Default View" view, don't update the time range to the view's time range, // this way it will use the global Kibana time or the default time already set - if (vs.id !== '0') { - setTimeRange(vs.currentTimerange); + if (!view.attributes.isStatic) { + setTimeRange(view.attributes.currentTimerange as MetricsExplorerTimeOptions); } } - if (vs.options) { - setOptions(vs.options); + if (view.attributes.options) { + setOptions(view.attributes.options as MetricsExplorerOptions); } }, [setChartOptions, setOptions, setTimeRange] diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index d62d6ea1e82b1..07d5e210c46a1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -9,6 +9,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { useMetricsExplorerViews } from '../../../hooks/use_metrics_explorer_views'; import { MetricsSourceConfigurationProperties } from '../../../../common/metrics_sources'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import { NoData } from '../../../components/empty_states'; @@ -16,11 +17,10 @@ import { MetricsExplorerCharts } from './components/charts'; import { MetricsExplorerToolbar } from './components/toolbar'; import { useMetricsExplorerState } from './hooks/use_metric_explorer_state'; import { useSourceContext } from '../../../containers/metrics_source'; -import { useSavedViewContext } from '../../../containers/saved_view/saved_view'; import { MetricsPageTemplate } from '../page_template'; import { metricsExplorerTitle } from '../../../translations'; -import { SavedViewsToolbarControls } from '../../../components/saved_views/toolbar_control'; import { DerivedIndexPattern } from '../../../containers/metrics_source'; +import { SavedViews } from './components/saved_views'; interface MetricsExplorerPageProps { source: MetricsSourceConfigurationProperties; derivedIndexPattern: DerivedIndexPattern; @@ -45,7 +45,7 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl onViewStateChange, refresh, } = useMetricsExplorerState(source, derivedIndexPattern, enabled); - const { currentView, shouldLoadDefault } = useSavedViewContext(); + const { currentView } = useMetricsExplorerViews(); useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' }); useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); @@ -58,11 +58,11 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl }, [currentView, onViewStateChange]); useEffect(() => { - if (currentView != null || !shouldLoadDefault) { + if (currentView != null) { // load metrics explorer data after default view loaded, unless we're not isLoading a view setEnabled(true); } - }, [currentView, shouldLoadDefault]); + }, [currentView]); useMetricsBreadcrumbs([ { @@ -70,21 +70,19 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl }, ]); + const viewState = { + options, + chartOptions, + currentTimerange: timeRange, + }; + return ( , - ], + rightSideItems: [], }} > { - throw new UpsertInventoryViewError(`Failed to create new inventory view : ${error}`); + throw new UpsertInventoryViewError( + `Failed to create new inventory view: ${error.body?.message ?? error.message}` + ); }); const { data } = decodeOrThrow( @@ -92,7 +94,9 @@ export class InventoryViewsClient implements IInventoryViewsClient { }) .catch((error) => { throw new UpsertInventoryViewError( - `Failed to update inventory view "${inventoryViewId}": ${error}` + `Failed to update inventory view "${inventoryViewId}": ${ + error.body?.message ?? error.message + }` ); }); diff --git a/x-pack/plugins/infra/public/services/inventory_views/types.ts b/x-pack/plugins/infra/public/services/inventory_views/types.ts index 2a690c4cc6c2e..573c144e9c441 100644 --- a/x-pack/plugins/infra/public/services/inventory_views/types.ts +++ b/x-pack/plugins/infra/public/services/inventory_views/types.ts @@ -6,7 +6,11 @@ */ import { HttpStart } from '@kbn/core/public'; -import { InventoryView, InventoryViewAttributes } from '../../../common/inventory_views'; +import { + CreateInventoryViewAttributesRequestPayload, + UpdateInventoryViewAttributesRequestPayload, +} from '../../../common/http_api/latest'; +import type { InventoryView } from '../../../common/inventory_views'; export type InventoryViewsServiceSetup = void; @@ -22,11 +26,11 @@ export interface IInventoryViewsClient { findInventoryViews(): Promise; getInventoryView(inventoryViewId: string): Promise; createInventoryView( - inventoryViewAttributes: Partial + inventoryViewAttributes: CreateInventoryViewAttributesRequestPayload ): Promise; updateInventoryView( inventoryViewId: string, - inventoryViewAttributes: Partial + inventoryViewAttributes: UpdateInventoryViewAttributesRequestPayload ): Promise; deleteInventoryView(inventoryViewId: string): Promise; } diff --git a/x-pack/plugins/infra/public/services/metrics_explorer_views/metrics_explorer_views_client.ts b/x-pack/plugins/infra/public/services/metrics_explorer_views/metrics_explorer_views_client.ts index d37d2fc81b31d..788a8789abe73 100644 --- a/x-pack/plugins/infra/public/services/metrics_explorer_views/metrics_explorer_views_client.ts +++ b/x-pack/plugins/infra/public/services/metrics_explorer_views/metrics_explorer_views_client.ts @@ -73,7 +73,7 @@ export class MetricsExplorerViewsClient implements IMetricsExplorerViewsClient { }) .catch((error) => { throw new UpsertMetricsExplorerViewError( - `Failed to create new metrics explorer view: ${error}` + `Failed to create new metrics explorer view: ${error.body?.message ?? error.message}` ); }); @@ -102,7 +102,9 @@ export class MetricsExplorerViewsClient implements IMetricsExplorerViewsClient { }) .catch((error) => { throw new UpsertMetricsExplorerViewError( - `Failed to update metrics explorer view "${metricsExplorerViewId}": ${error}` + `Failed to update metrics explorer view "${metricsExplorerViewId}": ${ + error.body?.message ?? error.message + }` ); }); diff --git a/x-pack/plugins/infra/server/routes/inventory_views/create_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/create_inventory_view.ts index 90bb47d8a2d76..d2df0a0f96f65 100644 --- a/x-pack/plugins/infra/server/routes/inventory_views/create_inventory_view.ts +++ b/x-pack/plugins/infra/server/routes/inventory_views/create_inventory_view.ts @@ -9,6 +9,7 @@ import { isBoom } from '@hapi/boom'; import { createValidationFunction } from '../../../common/runtime_types'; import { createInventoryViewRequestPayloadRT, + inventoryViewRequestQueryRT, inventoryViewResponsePayloadRT, INVENTORY_VIEW_URL, } from '../../../common/http_api/latest'; @@ -24,15 +25,16 @@ export const initCreateInventoryViewRoute = ({ path: INVENTORY_VIEW_URL, validate: { body: createValidationFunction(createInventoryViewRequestPayloadRT), + query: createValidationFunction(inventoryViewRequestQueryRT), }, }, async (_requestContext, request, response) => { - const { body } = request; + const { body, query } = request; const [, , { inventoryViews }] = await getStartServices(); const inventoryViewsClient = inventoryViews.getScopedClient(request); try { - const inventoryView = await inventoryViewsClient.create(body.attributes); + const inventoryView = await inventoryViewsClient.update(null, body.attributes, query); return response.custom({ statusCode: 201, diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer_views/create_metrics_explorer_view.ts b/x-pack/plugins/infra/server/routes/metrics_explorer_views/create_metrics_explorer_view.ts index 948dd757e7e01..d02ed1208eb11 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer_views/create_metrics_explorer_view.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer_views/create_metrics_explorer_view.ts @@ -9,6 +9,7 @@ import { isBoom } from '@hapi/boom'; import { createValidationFunction } from '../../../common/runtime_types'; import { createMetricsExplorerViewRequestPayloadRT, + metricsExplorerViewRequestQueryRT, metricsExplorerViewResponsePayloadRT, METRICS_EXPLORER_VIEW_URL, } from '../../../common/http_api/latest'; @@ -24,15 +25,20 @@ export const initCreateMetricsExplorerViewRoute = ({ path: METRICS_EXPLORER_VIEW_URL, validate: { body: createValidationFunction(createMetricsExplorerViewRequestPayloadRT), + query: createValidationFunction(metricsExplorerViewRequestQueryRT), }, }, async (_requestContext, request, response) => { - const { body } = request; + const { body, query } = request; const [, , { metricsExplorerViews }] = await getStartServices(); const metricsExplorerViewsClient = metricsExplorerViews.getScopedClient(request); try { - const metricsExplorerView = await metricsExplorerViewsClient.create(body.attributes); + const metricsExplorerView = await metricsExplorerViewsClient.update( + null, + body.attributes, + query + ); return response.custom({ statusCode: 201, diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts index 9d832f8502104..5b21a4e43d267 100644 --- a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts @@ -11,6 +11,5 @@ export const createInventoryViewsClientMock = (): jest.Mocked { const mockFindInventoryList = (savedObjectsClient: jest.Mocked) => { @@ -115,45 +112,6 @@ describe('InventoryViewsClient class', () => { expect(inventoryView).toEqual(inventoryViewMock); }); - describe('.create', () => { - it('generate a new inventory view', async () => { - const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); - - const inventoryViewMock = createInventoryViewMock('new_id', { - name: 'New view', - isStatic: false, - } as InventoryViewAttributes); - - mockFindInventoryList(savedObjectsClient); - - savedObjectsClient.create.mockResolvedValue({ - ...inventoryViewMock, - type: inventoryViewSavedObjectName, - references: [], - }); - - const inventoryView = await inventoryViewsClient.create({ - name: 'New view', - } as CreateInventoryViewAttributesRequestPayload); - - expect(savedObjectsClient.create).toHaveBeenCalled(); - expect(inventoryView).toEqual(inventoryViewMock); - }); - - it('throws an error when a conflicting name is given', async () => { - const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); - - mockFindInventoryList(savedObjectsClient); - - await expect( - async () => - await inventoryViewsClient.create({ - name: 'Custom', - } as CreateInventoryViewAttributesRequestPayload) - ).rejects.toThrow('A view with that name already exists.'); - }); - }); - describe('.update', () => { it('update an existing inventory view by id', async () => { const { inventoryViewsClient, infraSources, savedObjectsClient } = @@ -171,7 +129,7 @@ describe('InventoryViewsClient class', () => { infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); - savedObjectsClient.update.mockResolvedValue({ + savedObjectsClient.create.mockResolvedValue({ ...inventoryViewMock, type: inventoryViewSavedObjectName, references: [], @@ -185,7 +143,7 @@ describe('InventoryViewsClient class', () => { {} ); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalled(); expect(inventoryView).toEqual(inventoryViewMock); }); diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts index c32da344354b6..5efce009da410 100644 --- a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts @@ -5,11 +5,12 @@ * 2.0. */ -import type { +import { Logger, SavedObject, SavedObjectsClientContract, SavedObjectsUpdateResponse, + SavedObjectsUtils, } from '@kbn/core/server'; import Boom from '@hapi/boom'; import { @@ -50,6 +51,7 @@ export class InventoryViewsClient implements IInventoryViewsClient { const defaultView = InventoryViewsClient.createStaticView( sourceConfiguration.configuration.inventoryDefaultView ); + const views = inventoryViewSavedObject.saved_objects.map((savedObject) => this.mapSavedObjectToInventoryView( savedObject, @@ -95,37 +97,26 @@ export class InventoryViewsClient implements IInventoryViewsClient { ); } - public async create( - attributes: CreateInventoryViewAttributesRequestPayload - ): Promise { - this.logger.debug(`Trying to create inventory view ...`); - - // Validate there is not a view with the same name - await this.assertNameConflict(attributes.name); - - const inventoryViewSavedObject = await this.savedObjectsClient.create( - inventoryViewSavedObjectName, - attributes - ); - - return this.mapSavedObjectToInventoryView(inventoryViewSavedObject); - } - public async update( - inventoryViewId: string, + inventoryViewId: string | null, attributes: CreateInventoryViewAttributesRequestPayload, query: InventoryViewRequestQuery ): Promise { this.logger.debug(`Trying to update inventory view with id "${inventoryViewId}"...`); + const viewId = inventoryViewId ?? SavedObjectsUtils.generateId(); + // Validate there is not a view with the same name - await this.assertNameConflict(attributes.name, [inventoryViewId]); + await this.assertNameConflict(attributes.name, [viewId]); const sourceId = query.sourceId ?? InventoryViewsClient.DEFAULT_SOURCE_ID; const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([ this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId), - this.savedObjectsClient.update(inventoryViewSavedObjectName, inventoryViewId, attributes), + this.savedObjectsClient.create(inventoryViewSavedObjectName, attributes, { + id: viewId, + overwrite: true, + }), ]); return this.mapSavedObjectToInventoryView( diff --git a/x-pack/plugins/infra/server/services/inventory_views/types.ts b/x-pack/plugins/infra/server/services/inventory_views/types.ts index 3e023b77af6c2..2537203d1754c 100644 --- a/x-pack/plugins/infra/server/services/inventory_views/types.ts +++ b/x-pack/plugins/infra/server/services/inventory_views/types.ts @@ -11,7 +11,6 @@ import type { SavedObjectsServiceStart, } from '@kbn/core/server'; import type { - CreateInventoryViewAttributesRequestPayload, InventoryViewRequestQuery, UpdateInventoryViewAttributesRequestPayload, } from '../../../common/http_api/latest'; @@ -34,11 +33,8 @@ export interface IInventoryViewsClient { delete(inventoryViewId: string): Promise<{}>; find(query: InventoryViewRequestQuery): Promise; get(inventoryViewId: string, query: InventoryViewRequestQuery): Promise; - create( - inventoryViewAttributes: CreateInventoryViewAttributesRequestPayload - ): Promise; update( - inventoryViewId: string, + inventoryViewId: string | null, inventoryViewAttributes: UpdateInventoryViewAttributesRequestPayload, query: InventoryViewRequestQuery ): Promise; diff --git a/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.mock.ts b/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.mock.ts index 82a8cba3f6427..a19b8847adee1 100644 --- a/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.mock.ts +++ b/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.mock.ts @@ -12,6 +12,5 @@ export const createMetricsExplorerViewsClientMock = delete: jest.fn(), find: jest.fn(), get: jest.fn(), - create: jest.fn(), update: jest.fn(), }); diff --git a/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.test.ts b/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.test.ts index c903e9af360f8..791b7366f086c 100644 --- a/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.test.ts +++ b/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.test.ts @@ -15,10 +15,7 @@ import { createInfraSourcesMock } from '../../lib/sources/mocks'; import { metricsExplorerViewSavedObjectName } from '../../saved_objects/metrics_explorer_view'; import { MetricsExplorerViewsClient } from './metrics_explorer_views_client'; import { createMetricsExplorerViewMock } from '../../../common/metrics_explorer_views/metrics_explorer_view.mock'; -import { - CreateMetricsExplorerViewAttributesRequestPayload, - UpdateMetricsExplorerViewAttributesRequestPayload, -} from '../../../common/http_api/latest'; +import { UpdateMetricsExplorerViewAttributesRequestPayload } from '../../../common/http_api/latest'; describe('MetricsExplorerViewsClient class', () => { const mockFindMetricsExplorerList = ( @@ -118,45 +115,6 @@ describe('MetricsExplorerViewsClient class', () => { expect(metricsExplorerView).toEqual(metricsExplorerViewMock); }); - describe('.create', () => { - it('generate a new metrics explorer view', async () => { - const { metricsExplorerViewsClient, savedObjectsClient } = createMetricsExplorerViewsClient(); - - const metricsExplorerViewMock = createMetricsExplorerViewMock('new_id', { - name: 'New view', - isStatic: false, - } as MetricsExplorerViewAttributes); - - mockFindMetricsExplorerList(savedObjectsClient); - - savedObjectsClient.create.mockResolvedValue({ - ...metricsExplorerViewMock, - type: metricsExplorerViewSavedObjectName, - references: [], - }); - - const metricsExplorerView = await metricsExplorerViewsClient.create({ - name: 'New view', - } as CreateMetricsExplorerViewAttributesRequestPayload); - - expect(savedObjectsClient.create).toHaveBeenCalled(); - expect(metricsExplorerView).toEqual(metricsExplorerViewMock); - }); - - it('throws an error when a conflicting name is given', async () => { - const { metricsExplorerViewsClient, savedObjectsClient } = createMetricsExplorerViewsClient(); - - mockFindMetricsExplorerList(savedObjectsClient); - - await expect( - async () => - await metricsExplorerViewsClient.create({ - name: 'Custom', - } as CreateMetricsExplorerViewAttributesRequestPayload) - ).rejects.toThrow('A view with that name already exists.'); - }); - }); - describe('.update', () => { it('update an existing metrics explorer view by id', async () => { const { metricsExplorerViewsClient, infraSources, savedObjectsClient } = @@ -174,7 +132,7 @@ describe('MetricsExplorerViewsClient class', () => { infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); - savedObjectsClient.update.mockResolvedValue({ + savedObjectsClient.create.mockResolvedValue({ ...metricsExplorerViewMock, type: metricsExplorerViewSavedObjectName, references: [], @@ -188,7 +146,7 @@ describe('MetricsExplorerViewsClient class', () => { {} ); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalled(); expect(metricsExplorerView).toEqual(metricsExplorerViewMock); }); diff --git a/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.ts b/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.ts index 1ba34456d88a8..e2dd15940bb19 100644 --- a/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.ts +++ b/x-pack/plugins/infra/server/services/metrics_explorer_views/metrics_explorer_views_client.ts @@ -5,11 +5,12 @@ * 2.0. */ -import type { +import { Logger, SavedObject, SavedObjectsClientContract, SavedObjectsUpdateResponse, + SavedObjectsUtils, } from '@kbn/core/server'; import Boom from '@hapi/boom'; import { @@ -98,24 +99,8 @@ export class MetricsExplorerViewsClient implements IMetricsExplorerViewsClient { ); } - public async create( - attributes: CreateMetricsExplorerViewAttributesRequestPayload - ): Promise { - this.logger.debug(`Trying to create metrics explorer view ...`); - - // Validate there is not a view with the same name - await this.assertNameConflict(attributes.name); - - const metricsExplorerViewSavedObject = await this.savedObjectsClient.create( - metricsExplorerViewSavedObjectName, - attributes - ); - - return this.mapSavedObjectToMetricsExplorerView(metricsExplorerViewSavedObject); - } - public async update( - metricsExplorerViewId: string, + metricsExplorerViewId: string | null, attributes: CreateMetricsExplorerViewAttributesRequestPayload, query: MetricsExplorerViewRequestQuery ): Promise { @@ -123,18 +108,19 @@ export class MetricsExplorerViewsClient implements IMetricsExplorerViewsClient { `Trying to update metrics explorer view with id "${metricsExplorerViewId}"...` ); + const viewId = metricsExplorerViewId ?? SavedObjectsUtils.generateId(); + // Validate there is not a view with the same name - await this.assertNameConflict(attributes.name, [metricsExplorerViewId]); + await this.assertNameConflict(attributes.name, [viewId]); const sourceId = query.sourceId ?? MetricsExplorerViewsClient.DEFAULT_SOURCE_ID; const [sourceConfiguration, metricsExplorerViewSavedObject] = await Promise.all([ this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId), - this.savedObjectsClient.update( - metricsExplorerViewSavedObjectName, - metricsExplorerViewId, - attributes - ), + this.savedObjectsClient.create(metricsExplorerViewSavedObjectName, attributes, { + id: viewId, + overwrite: true, + }), ]); return this.mapSavedObjectToMetricsExplorerView( diff --git a/x-pack/plugins/infra/server/services/metrics_explorer_views/types.ts b/x-pack/plugins/infra/server/services/metrics_explorer_views/types.ts index 0e64aaa83d27e..851cdf3ad77f0 100644 --- a/x-pack/plugins/infra/server/services/metrics_explorer_views/types.ts +++ b/x-pack/plugins/infra/server/services/metrics_explorer_views/types.ts @@ -11,7 +11,6 @@ import type { SavedObjectsServiceStart, } from '@kbn/core/server'; import type { - CreateMetricsExplorerViewAttributesRequestPayload, MetricsExplorerViewRequestQuery, UpdateMetricsExplorerViewAttributesRequestPayload, } from '../../../common/http_api/latest'; @@ -37,11 +36,8 @@ export interface IMetricsExplorerViewsClient { metricsExplorerViewId: string, query: MetricsExplorerViewRequestQuery ): Promise; - create( - metricsExplorerViewAttributes: CreateMetricsExplorerViewAttributesRequestPayload - ): Promise; update( - metricsExplorerViewId: string, + metricsExplorerViewId: string | null, metricsExplorerViewAttributes: UpdateMetricsExplorerViewAttributesRequestPayload, query: MetricsExplorerViewRequestQuery ): Promise; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 5c3e05fe9671e..a82ea33392517 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -17877,7 +17877,6 @@ "xpack.infra.openView.columnNames.actions": "Actions", "xpack.infra.openView.columnNames.name": "Nom", "xpack.infra.openView.flyoutHeader": "Gérer les vues enregistrées", - "xpack.infra.openView.loadButton": "Charger la vue", "xpack.infra.registerFeatures.infraOpsDescription": "Explorez les indicateurs et logs d'infrastructure pour les serveurs, conteneurs et services courants.", "xpack.infra.registerFeatures.infraOpsTitle": "Indicateurs", "xpack.infra.registerFeatures.logsDescription": "Diffusez les logs en temps réel ou faites défiler les vues d'historique comme sur une console.", @@ -17887,10 +17886,8 @@ "xpack.infra.savedView.errorOnCreate.duplicateViewName": "Une vue portant ce nom existe déjà.", "xpack.infra.savedView.errorOnCreate.title": "Une erreur s'est produite lors de l'enregistrement de la vue.", "xpack.infra.savedView.findError.title": "Une erreur s'est produite lors du chargement des vues.", - "xpack.infra.savedView.loadView": "Charger la vue", "xpack.infra.savedView.manageViews": "Gérer les vues", "xpack.infra.savedView.saveNewView": "Enregistrer la nouvelle vue", - "xpack.infra.savedView.searchPlaceholder": "Rechercher les vues enregistrées", "xpack.infra.savedView.unknownView": "Aucune vue sélectionnée", "xpack.infra.savedView.updateView": "Mettre à jour la vue", "xpack.infra.showHistory": "Afficher l'historique", @@ -18003,7 +18000,6 @@ "xpack.infra.waffle.region": "Tous", "xpack.infra.waffle.regionLabel": "Région", "xpack.infra.waffle.savedView.createHeader": "Enregistrer la vue", - "xpack.infra.waffle.savedView.selectViewHeader": "Sélectionner une vue à charger", "xpack.infra.waffle.savedView.updateHeader": "Mettre à jour la vue", "xpack.infra.waffle.savedViews.cancel": "annuler", "xpack.infra.waffle.savedViews.cancelButton": "Annuler", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0ed53a0ef38fc..3c793b60772bc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17876,7 +17876,6 @@ "xpack.infra.openView.columnNames.actions": "アクション", "xpack.infra.openView.columnNames.name": "名前", "xpack.infra.openView.flyoutHeader": "保存されたビューの管理", - "xpack.infra.openView.loadButton": "ビューの読み込み", "xpack.infra.registerFeatures.infraOpsDescription": "共通のサーバー、コンテナー、サービスのインフラストラクチャメトリックとログを閲覧します。", "xpack.infra.registerFeatures.infraOpsTitle": "メトリック", "xpack.infra.registerFeatures.logsDescription": "ログをリアルタイムでストリーするか、コンソール式の UI で履歴ビューをスクロールします。", @@ -17886,10 +17885,8 @@ "xpack.infra.savedView.errorOnCreate.duplicateViewName": "その名前のビューはすでに存在します。", "xpack.infra.savedView.errorOnCreate.title": "ビューの保存中にエラーが発生しました。", "xpack.infra.savedView.findError.title": "ビューの読み込み中にエラーが発生しました。", - "xpack.infra.savedView.loadView": "ビューの読み込み", "xpack.infra.savedView.manageViews": "ビューの管理", "xpack.infra.savedView.saveNewView": "新しいビューの保存", - "xpack.infra.savedView.searchPlaceholder": "保存されたビューの検索", "xpack.infra.savedView.unknownView": "ビューが選択されていません", "xpack.infra.savedView.updateView": "ビューの更新", "xpack.infra.showHistory": "履歴を表示", @@ -18002,7 +17999,6 @@ "xpack.infra.waffle.region": "すべて", "xpack.infra.waffle.regionLabel": "地域", "xpack.infra.waffle.savedView.createHeader": "ビューを保存", - "xpack.infra.waffle.savedView.selectViewHeader": "読み込むビューを選択", "xpack.infra.waffle.savedView.updateHeader": "ビューの更新", "xpack.infra.waffle.savedViews.cancel": "キャンセル", "xpack.infra.waffle.savedViews.cancelButton": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d88f43949ac7c..d20ad056909de 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17878,7 +17878,6 @@ "xpack.infra.openView.columnNames.actions": "操作", "xpack.infra.openView.columnNames.name": "名称", "xpack.infra.openView.flyoutHeader": "管理已保存视图", - "xpack.infra.openView.loadButton": "加载视图", "xpack.infra.registerFeatures.infraOpsDescription": "浏览常用服务器、容器和服务的基础设施指标和日志。", "xpack.infra.registerFeatures.infraOpsTitle": "指标", "xpack.infra.registerFeatures.logsDescription": "实时流式传输日志或在类似控制台的工具中滚动浏览历史视图。", @@ -17888,10 +17887,8 @@ "xpack.infra.savedView.errorOnCreate.duplicateViewName": "具有该名称的视图已存在。", "xpack.infra.savedView.errorOnCreate.title": "保存视图时出错。", "xpack.infra.savedView.findError.title": "加载视图时出错。", - "xpack.infra.savedView.loadView": "加载视图", "xpack.infra.savedView.manageViews": "管理视图", "xpack.infra.savedView.saveNewView": "保存新视图", - "xpack.infra.savedView.searchPlaceholder": "搜索已保存视图", "xpack.infra.savedView.unknownView": "未选择视图", "xpack.infra.savedView.updateView": "更新视图", "xpack.infra.showHistory": "显示历史记录", @@ -18004,7 +18001,6 @@ "xpack.infra.waffle.region": "全部", "xpack.infra.waffle.regionLabel": "地区", "xpack.infra.waffle.savedView.createHeader": "保存视图", - "xpack.infra.waffle.savedView.selectViewHeader": "选择要加载的视图", "xpack.infra.waffle.savedView.updateHeader": "更新视图", "xpack.infra.waffle.savedViews.cancel": "取消", "xpack.infra.waffle.savedViews.cancelButton": "取消", diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 54435f11a33e1..13acd911cfbea 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -19,6 +19,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); const pageObjects = getPageObjects(['common', 'header', 'infraHome', 'infraSavedViews']); const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); describe('Home page', function () { this.tags('includeFirefox'); @@ -220,36 +221,54 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.infraHome.ensurePopoverClosed(); }); }); - // Failing: See https://github.com/elastic/kibana/issues/106650 - describe.skip('Saved Views', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - it('should have save and load controls', async () => { + + describe('Saved Views', () => { + before(async () => { + esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await pageObjects.common.navigateToApp('infraOps'); await pageObjects.infraHome.waitForLoading(); - await pageObjects.infraHome.goToTime(DATE_WITH_DATA); - await pageObjects.infraSavedViews.getSavedViewsButton(); + }); + + after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); + + it('should render a button with the view name', async () => { await pageObjects.infraSavedViews.ensureViewIsLoaded('Default view'); }); - it('should open popover', async () => { + it('should open/close the views popover menu on button click', async () => { await pageObjects.infraSavedViews.clickSavedViewsButton(); + testSubjects.existOrFail('savedViews-popover'); await pageObjects.infraSavedViews.closeSavedViewsPopover(); }); - it('should create new saved view and load it', async () => { - await pageObjects.infraSavedViews.clickSavedViewsButton(); - await pageObjects.infraSavedViews.clickSaveNewViewButton(); - await pageObjects.infraSavedViews.getCreateSavedViewModal(); - await pageObjects.infraSavedViews.createNewSavedView('view1'); + it('should create a new saved view and load it', async () => { + await pageObjects.infraSavedViews.createView('view1'); await pageObjects.infraSavedViews.ensureViewIsLoaded('view1'); }); - it('should new views should be listed in the load views list', async () => { - await pageObjects.infraSavedViews.clickSavedViewsButton(); - await pageObjects.infraSavedViews.clickLoadViewButton(); - await pageObjects.infraSavedViews.ensureViewIsLoadable('view1'); - await pageObjects.infraSavedViews.closeSavedViewsLoadModal(); + it('should laod a clicked view from the manage views section', async () => { + await pageObjects.infraSavedViews.ensureViewIsLoaded('view1'); + const views = await pageObjects.infraSavedViews.getManageViewsEntries(); + await views[0].click(); + await pageObjects.infraSavedViews.ensureViewIsLoaded('Default view'); + }); + + it('should update the current saved view and load it', async () => { + let views = await pageObjects.infraSavedViews.getManageViewsEntries(); + expect(views.length).to.equal(2); + await pageObjects.infraSavedViews.pressEsc(); + + await pageObjects.infraSavedViews.createView('view2'); + await pageObjects.infraSavedViews.ensureViewIsLoaded('view2'); + views = await pageObjects.infraSavedViews.getManageViewsEntries(); + expect(views.length).to.equal(3); + await pageObjects.infraSavedViews.pressEsc(); + + await pageObjects.infraSavedViews.updateView('view3'); + await pageObjects.infraSavedViews.ensureViewIsLoaded('view3'); + views = await pageObjects.infraSavedViews.getManageViewsEntries(); + expect(views.length).to.equal(3); + await pageObjects.infraSavedViews.pressEsc(); }); }); }); diff --git a/x-pack/test/functional/apps/infra/metrics_explorer.ts b/x-pack/test/functional/apps/infra/metrics_explorer.ts index 4d6859a4e99e7..650040b95f420 100644 --- a/x-pack/test/functional/apps/infra/metrics_explorer.ts +++ b/x-pack/test/functional/apps/infra/metrics_explorer.ts @@ -24,6 +24,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'timePicker', 'infraSavedViews', ]); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); describe('Metrics Explorer', function () { this.tags('includeFirefox'); @@ -78,8 +80,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should display multple charts', async () => { - const charts = await pageObjects.infraMetricsExplorer.getCharts(); - expect(charts.length).to.equal(6); + await retry.try(async () => { + const charts = await pageObjects.infraMetricsExplorer.getCharts(); + expect(charts.length).to.equal(6); + }); }); it('should render as area chart by default', async () => { @@ -97,35 +101,51 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); describe('Saved Views', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); + before(async () => { + esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + await pageObjects.infraHome.goToMetricExplorer(); + }); + after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - describe('save functionality', () => { - it('should have saved views component', async () => { - await pageObjects.common.navigateToApp('infraOps'); - await pageObjects.infraHome.goToMetricExplorer(); - await pageObjects.infraSavedViews.getSavedViewsButton(); - await pageObjects.infraSavedViews.ensureViewIsLoaded('Default view'); - }); - it('should open popover', async () => { - await pageObjects.infraSavedViews.clickSavedViewsButton(); - await pageObjects.infraSavedViews.closeSavedViewsPopover(); - }); + it('should render a button with the view name', async () => { + await pageObjects.infraSavedViews.ensureViewIsLoaded('Default view'); + }); - it('should create new saved view and load it', async () => { - await pageObjects.infraSavedViews.clickSavedViewsButton(); - await pageObjects.infraSavedViews.clickSaveNewViewButton(); - await pageObjects.infraSavedViews.getCreateSavedViewModal(); - await pageObjects.infraSavedViews.createNewSavedView('view1'); - await pageObjects.infraSavedViews.ensureViewIsLoaded('view1'); - }); + it('should open/close the views popover menu on button click', async () => { + await pageObjects.infraSavedViews.clickSavedViewsButton(); + testSubjects.existOrFail('savedViews-popover'); + await pageObjects.infraSavedViews.closeSavedViewsPopover(); + }); - it('should new views should be listed in the load views list', async () => { - await pageObjects.infraSavedViews.clickSavedViewsButton(); - await pageObjects.infraSavedViews.clickLoadViewButton(); - await pageObjects.infraSavedViews.ensureViewIsLoadable('view1'); - await pageObjects.infraSavedViews.closeSavedViewsLoadModal(); - }); + it('should create a new saved view and load it', async () => { + await pageObjects.infraSavedViews.createView('view1'); + await pageObjects.infraSavedViews.ensureViewIsLoaded('view1'); + }); + + it('should laod a clicked view from the manage views section', async () => { + await pageObjects.infraSavedViews.ensureViewIsLoaded('view1'); + const views = await pageObjects.infraSavedViews.getManageViewsEntries(); + await views[0].click(); + await pageObjects.infraSavedViews.ensureViewIsLoaded('Default view'); + }); + + it('should update the current saved view and load it', async () => { + let views = await pageObjects.infraSavedViews.getManageViewsEntries(); + expect(views.length).to.equal(2); + await pageObjects.infraSavedViews.pressEsc(); + + await pageObjects.infraSavedViews.createView('view2'); + await pageObjects.infraSavedViews.ensureViewIsLoaded('view2'); + views = await pageObjects.infraSavedViews.getManageViewsEntries(); + expect(views.length).to.equal(3); + await pageObjects.infraSavedViews.pressEsc(); + + await pageObjects.infraSavedViews.updateView('view3'); + await pageObjects.infraSavedViews.ensureViewIsLoaded('view3'); + views = await pageObjects.infraSavedViews.getManageViewsEntries(); + expect(views.length).to.equal(3); + await pageObjects.infraSavedViews.pressEsc(); }); }); }); diff --git a/x-pack/test/functional/page_objects/infra_saved_views.ts b/x-pack/test/functional/page_objects/infra_saved_views.ts index 839b10fef1c68..2097be3d083a8 100644 --- a/x-pack/test/functional/page_objects/infra_saved_views.ts +++ b/x-pack/test/functional/page_objects/infra_saved_views.ts @@ -15,57 +15,67 @@ export function InfraSavedViewsProvider({ getService }: FtrProviderContext) { const browser = getService('browser'); return { - async getSavedViewsButton() { - return await testSubjects.find('savedViews-openPopover'); + getSavedViewsButton() { + return testSubjects.find('savedViews-openPopover'); }, - async clickSavedViewsButton() { - return await testSubjects.click('savedViews-openPopover'); + clickSavedViewsButton() { + return testSubjects.click('savedViews-openPopover'); }, - async getSavedViewsPopoer() { - return await testSubjects.find('savedViews-popover'); + getSavedViewsPopover() { + return testSubjects.find('savedViews-popover'); + }, + + pressEsc() { + return browser.pressKeys([Key.ESCAPE]); }, async closeSavedViewsPopover() { await testSubjects.find('savedViews-popover'); - return await browser.pressKeys([Key.ESCAPE]); + return this.pressEsc(); + }, + + getLoadViewButton() { + return testSubjects.find('savedViews-loadView'); }, - async getLoadViewButton() { - return await testSubjects.find('savedViews-loadView'); + getManageViewsButton() { + return testSubjects.find('savedViews-manageViews'); }, - async clickLoadViewButton() { - return await testSubjects.click('savedViews-loadView'); + clickManageViewsButton() { + return testSubjects.click('savedViews-manageViews'); }, - async getManageViewsButton() { - return await testSubjects.find('savedViews-manageViews'); + getManageViewsFlyout() { + return testSubjects.find('loadViewsFlyout'); }, - async clickManageViewsButton() { - return await testSubjects.click('savedViews-manageViews'); + async getManageViewsEntries() { + await this.clickSavedViewsButton(); + await this.clickManageViewsButton(); + return testSubjects.findAll('infraRenderNameButton'); }, - async getUpdateViewButton() { - return await testSubjects.find('savedViews-updateView'); + getUpdateViewButton() { + return testSubjects.find('savedViews-updateView'); }, - async clickUpdateViewButton() { - return await testSubjects.click('savedViews-updateView'); + clickUpdateViewButton() { + return testSubjects.click('savedViews-updateView'); }, - async getSaveNewViewButton() { - return await testSubjects.find('savedViews-saveNewView'); + getSaveNewViewButton() { + return testSubjects.find('savedViews-saveNewView'); }, - async clickSaveNewViewButton() { - return await testSubjects.click('savedViews-saveNewView'); + clickSaveNewViewButton() { + return testSubjects.click('savedViews-saveNewView'); }, - async getCreateSavedViewModal() { - return await testSubjects.find('savedViews-upsertModal'); + getCreateSavedViewModal() { + return testSubjects.find('savedViews-upsertModal'); }, async createNewSavedView(name: string) { @@ -74,6 +84,18 @@ export function InfraSavedViewsProvider({ getService }: FtrProviderContext) { await testSubjects.missingOrFail('savedViews-upsertModal'); }, + async createView(name: string) { + await this.clickSavedViewsButton(); + await this.clickSaveNewViewButton(); + await this.createNewSavedView(name); + }, + + async updateView(name: string) { + await this.clickSavedViewsButton(); + await this.clickUpdateViewButton(); + await this.createNewSavedView(name); + }, + async ensureViewIsLoaded(name: string) { await retry.try(async () => { const subject = await testSubjects.find('savedViews-openPopover'); @@ -86,8 +108,8 @@ export function InfraSavedViewsProvider({ getService }: FtrProviderContext) { await subject.findByCssSelector(`li[title="${name}"]`); }, - async closeSavedViewsLoadModal() { - return await testSubjects.click('cancelSavedViewModal'); + closeSavedViewsLoadModal() { + return testSubjects.click('cancelSavedViewModal'); }, }; } From dbedd53b481439a0c24c99fc11249caa50e78272 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Tue, 2 May 2023 09:42:32 -0400 Subject: [PATCH 03/24] feat(slo): delete associated rules when deleting an SLO (#156307) --- x-pack/plugins/observability/server/plugin.ts | 25 ++++++++++++------- .../server/routes/register_routes.ts | 4 ++- .../observability/server/routes/slo/route.ts | 11 ++++++-- .../server/services/slo/delete_slo.test.ts | 11 ++++++-- .../server/services/slo/delete_slo.ts | 15 ++++++++++- 5 files changed, 51 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 9fdf113cc64af..4e44593dc7767 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -14,7 +14,7 @@ import { Logger, } from '@kbn/core/server'; import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; -import { PluginSetupContract } from '@kbn/alerting-plugin/server'; +import { PluginSetupContract, PluginStartContract } from '@kbn/alerting-plugin/server'; import { Dataset, RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server'; import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server'; import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils'; @@ -59,6 +59,10 @@ interface PluginSetup { usageCollection?: UsageCollectionSetup; } +interface PluginStart { + alerting: PluginStartContract; +} + export class ObservabilityPlugin implements Plugin { private logger: Logger; @@ -67,7 +71,7 @@ export class ObservabilityPlugin implements Plugin { this.logger = initContext.logger.get(); } - public setup(core: CoreSetup, plugins: PluginSetup) { + public setup(core: CoreSetup, plugins: PluginSetup) { const casesCapabilities = createCasesUICapabilities(); const casesApiTags = getCasesApiTags(observabilityFeatureId); @@ -237,13 +241,16 @@ export class ObservabilityPlugin implements Plugin { registerRuleTypes(plugins.alerting, this.logger, ruleDataClient, core.http.basePath); registerSloUsageCollector(plugins.usageCollection); - registerRoutes({ - core, - dependencies: { - ruleDataService, - }, - logger: this.logger, - repository: getObservabilityServerRouteRepository(), + core.getStartServices().then(([coreStart, pluginStart]) => { + registerRoutes({ + core, + dependencies: { + ruleDataService, + getRulesClientWithRequest: pluginStart.alerting.getRulesClientWithRequest, + }, + logger: this.logger, + repository: getObservabilityServerRouteRepository(), + }); }); /** diff --git a/x-pack/plugins/observability/server/routes/register_routes.ts b/x-pack/plugins/observability/server/routes/register_routes.ts index de46f624addb7..f79027bd4d6bb 100644 --- a/x-pack/plugins/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability/server/routes/register_routes.ts @@ -10,10 +10,11 @@ import { parseEndpoint, routeValidationObject, } from '@kbn/server-route-repository'; -import { CoreSetup, Logger, RouteRegistrar } from '@kbn/core/server'; +import { CoreSetup, KibanaRequest, Logger, RouteRegistrar } from '@kbn/core/server'; import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server'; +import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; import { ObservabilityRequestHandlerContext } from '../types'; import { AbstractObservabilityServerRouteRepository } from './types'; @@ -28,6 +29,7 @@ interface RegisterRoutes { export interface RegisterRoutesDependencies { ruleDataService: RuleDataPluginService; + getRulesClientWithRequest: (request: KibanaRequest) => RulesClientApi; } export function registerRoutes({ repository, core, logger, dependencies }: RegisterRoutes) { diff --git a/x-pack/plugins/observability/server/routes/slo/route.ts b/x-pack/plugins/observability/server/routes/slo/route.ts index 1aec94e3dde1f..0d0a964408773 100644 --- a/x-pack/plugins/observability/server/routes/slo/route.ts +++ b/x-pack/plugins/observability/server/routes/slo/route.ts @@ -106,18 +106,25 @@ const deleteSLORoute = createObservabilityServerRoute({ tags: ['access:slo_write'], }, params: deleteSLOParamsSchema, - handler: async ({ context, params, logger }) => { + handler: async ({ + request, + context, + params, + logger, + dependencies: { getRulesClientWithRequest }, + }) => { if (!isLicenseAtLeastPlatinum(context)) { throw badRequest('Platinum license or higher is needed to make use of this feature.'); } const esClient = (await context.core).elasticsearch.client.asCurrentUser; const soClient = (await context.core).savedObjects.client; + const rulesClient = getRulesClientWithRequest(request); const repository = new KibanaSavedObjectsSLORepository(soClient); const transformManager = new DefaultTransformManager(transformGenerators, esClient, logger); - const deleteSLO = new DeleteSLO(repository, transformManager, esClient); + const deleteSLO = new DeleteSLO(repository, transformManager, esClient, rulesClient); await deleteSLO.execute(params.path.id); }, diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts b/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts index a979265ce1ef9..e1e76fa56400d 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; +import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock'; import { ElasticsearchClient } from '@kbn/core/server'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; import { getSLOTransformId, SLO_INDEX_TEMPLATE_NAME } from '../../assets/constants'; @@ -18,17 +20,19 @@ describe('DeleteSLO', () => { let mockRepository: jest.Mocked; let mockTransformManager: jest.Mocked; let mockEsClient: jest.Mocked; + let mockRulesClient: jest.Mocked; let deleteSLO: DeleteSLO; beforeEach(() => { mockRepository = createSLORepositoryMock(); mockTransformManager = createTransformManagerMock(); mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); - deleteSLO = new DeleteSLO(mockRepository, mockTransformManager, mockEsClient); + mockRulesClient = rulesClientMock.create(); + deleteSLO = new DeleteSLO(mockRepository, mockTransformManager, mockEsClient, mockRulesClient); }); describe('happy path', () => { - it('removes the transform, the roll up data and the SLO from the repository', async () => { + it('removes the transform, the roll up data, the associated rules and the SLO from the repository', async () => { const slo = createSLO({ indicator: createAPMTransactionErrorRateIndicator() }); mockRepository.findById.mockResolvedValueOnce(slo); @@ -51,6 +55,9 @@ describe('DeleteSLO', () => { }, }) ); + expect(mockRulesClient.bulkDeleteRules).toHaveBeenCalledWith({ + filter: `alert.attributes.params.sloId:${slo.id}`, + }); expect(mockRepository.deleteById).toHaveBeenCalledWith(slo.id); }); }); diff --git a/x-pack/plugins/observability/server/services/slo/delete_slo.ts b/x-pack/plugins/observability/server/services/slo/delete_slo.ts index 8ec8d2060f730..dc812b1bd4f31 100644 --- a/x-pack/plugins/observability/server/services/slo/delete_slo.ts +++ b/x-pack/plugins/observability/server/services/slo/delete_slo.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { RulesClientApi } from '@kbn/alerting-plugin/server/types'; import { ElasticsearchClient } from '@kbn/core/server'; import { getSLOTransformId, SLO_INDEX_TEMPLATE_NAME } from '../../assets/constants'; @@ -15,7 +16,8 @@ export class DeleteSLO { constructor( private repository: SLORepository, private transformManager: TransformManager, - private esClient: ElasticsearchClient + private esClient: ElasticsearchClient, + private rulesClient: RulesClientApi ) {} public async execute(sloId: string): Promise { @@ -26,6 +28,7 @@ export class DeleteSLO { await this.transformManager.uninstall(sloTransformId); await this.deleteRollupData(slo.id); + await this.deleteAssociatedRules(slo.id); await this.repository.deleteById(slo.id); } @@ -40,4 +43,14 @@ export class DeleteSLO { }, }); } + + private async deleteAssociatedRules(sloId: string): Promise { + try { + await this.rulesClient.bulkDeleteRules({ + filter: `alert.attributes.params.sloId:${sloId}`, + }); + } catch (err) { + // no-op: bulkDeleteRules throws if no rules are found. + } + } } From 5e1055c2955ce4c8b1e278dff154cacfdc647731 Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Tue, 2 May 2023 06:43:06 -0700 Subject: [PATCH 04/24] [ResponseOps] [Event Log] Remove event log HTTP APIs if no longer used (#155913) Resolves https://github.com/elastic/kibana/issues/90486 ## Summary Removes the event log HTTP apis since they are not used, and adds them to the functional tests ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/event_log/server/plugin.ts | 26 ---- .../server/routes/_mock_handler_arguments.ts | 73 ---------- .../event_log/server/routes/find.test.ts | 126 ----------------- .../plugins/event_log/server/routes/find.ts | 58 -------- .../server/routes/find_by_ids.test.ts | 130 ------------------ .../event_log/server/routes/find_by_ids.ts | 65 --------- .../plugins/event_log/server/routes/index.ts | 8 -- x-pack/plugins/event_log/server/types.ts | 21 +-- .../common/lib/get_event_log.ts | 2 +- .../common/plugins/alerts/kibana.jsonc | 3 +- .../common/plugins/alerts/server/plugin.ts | 2 + .../common/plugins/alerts/server/routes.ts | 34 +++++ .../plugins/event_log/server/init_routes.ts | 82 +++++++++++ .../plugins/event_log/server/plugin.ts | 6 +- .../plugins/event_log/tsconfig.json | 3 +- .../event_log/public_api_integration.ts | 4 +- .../event_log/service_api_integration.ts | 2 +- x-pack/test/rule_registry/common/config.ts | 6 +- 18 files changed, 137 insertions(+), 514 deletions(-) delete mode 100644 x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts delete mode 100644 x-pack/plugins/event_log/server/routes/find.test.ts delete mode 100644 x-pack/plugins/event_log/server/routes/find.ts delete mode 100644 x-pack/plugins/event_log/server/routes/find_by_ids.test.ts delete mode 100644 x-pack/plugins/event_log/server/routes/find_by_ids.ts delete mode 100644 x-pack/plugins/event_log/server/routes/index.ts diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index d5bf14a1259c1..9c96e2ae8073a 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -12,23 +12,19 @@ import { Plugin as CorePlugin, PluginInitializerContext, IClusterClient, - IContextProvider, } from '@kbn/core/server'; import { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import type { - EventLogRequestHandlerContext, IEventLogConfig, IEventLogService, IEventLogger, IEventLogClientService, } from './types'; -import { findRoute } from './routes'; import { EventLogService } from './event_log_service'; import { createEsContext, EsContext } from './es'; import { EventLogClientService } from './event_log_start_service'; import { SavedObjectProviderRegistry } from './saved_object_provider_registry'; -import { findByIdsRoute } from './routes/find_by_ids'; export type PluginClusterClient = Pick; @@ -88,17 +84,6 @@ export class Plugin implements CorePlugin( - 'eventLog', - this.createRouteHandlerContext() - ); - - // Routes - const router = core.http.createRouter(); - // Register routes - findRoute(router, this.systemLogger); - findByIdsRoute(router, this.systemLogger); - return this.eventLogService; } @@ -161,15 +146,4 @@ export class Plugin implements CorePlugin => { - return async (context, request) => { - return { - getEventLogClient: () => this.eventLogClientService!.getClient(request), - }; - }; - }; } diff --git a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts deleted file mode 100644 index d9c78aea08b8e..0000000000000 --- a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts +++ /dev/null @@ -1,73 +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 { identity, merge } from 'lodash'; -import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from '@kbn/core/server'; -import type { MethodKeysOf } from '@kbn/utility-types'; - -import { httpServerMock } from '@kbn/core/server/mocks'; -import { IEventLogClient } from '../types'; - -export function mockHandlerArguments( - eventLogClient: IEventLogClient, - request: unknown, - response?: Array> -): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { - return [ - { - eventLog: { - getEventLogClient() { - return eventLogClient; - }, - }, - } as unknown as RequestHandlerContext, - request as KibanaRequest, - mockResponseFactory(response), - ]; -} - -export const mockResponseFactory = (resToMock: Array> = []) => { - const factory: jest.Mocked = httpServerMock.createResponseFactory(); - resToMock.forEach((key: string) => { - if (key in factory) { - Object.defineProperty(factory, key, { - value: jest.fn(identity), - }); - } - }); - return factory as unknown as KibanaResponseFactory; -}; - -export function fakeEvent(overrides = {}) { - return merge( - { - event: { - provider: 'actions', - action: 'execute', - start: '2020-03-30T14:55:47.054Z', - end: '2020-03-30T14:55:47.055Z', - duration: '1000000', - }, - kibana: { - saved_objects: [ - { - namespace: 'default', - type: 'action', - id: '968f1b82-0414-4a10-becc-56b6473e4a29', - }, - ], - server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', - }, - message: 'action executed: .server-log:968f1b82-0414-4a10-becc-56b6473e4a29: logger', - '@timestamp': '2020-03-30T14:55:47.055Z', - ecs: { - version: '1.3.1', - }, - }, - overrides - ); -} diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts deleted file mode 100644 index ee7985d34d249..0000000000000 --- a/x-pack/plugins/event_log/server/routes/find.test.ts +++ /dev/null @@ -1,126 +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 { findRoute } from './find'; -import { httpServiceMock } from '@kbn/core/server/mocks'; -import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments'; -import { eventLogClientMock } from '../event_log_client.mock'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; - -const eventLogClient = eventLogClientMock.create(); -const systemLogger = loggingSystemMock.createLogger(); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('find', () => { - it('finds events with proper parameters', async () => { - const router = httpServiceMock.createRouter(); - - findRoute(router, systemLogger); - - const [config, handler] = router.get.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/internal/event_log/{type}/{id}/_find"`); - - const events = [fakeEvent(), fakeEvent()]; - const result = { - page: 0, - per_page: 10, - total: events.length, - data: events, - }; - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(result); - - const [context, req, res] = mockHandlerArguments( - eventLogClient, - { - params: { id: '1', type: 'action' }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - - const [type, ids] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; - expect(type).toEqual(`action`); - expect(ids).toEqual(['1']); - - expect(res.ok).toHaveBeenCalledWith({ - body: result, - }); - }); - - it('supports optional pagination parameters', async () => { - const router = httpServiceMock.createRouter(); - - findRoute(router, systemLogger); - - const [, handler] = router.get.mock.calls[0]; - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce({ - page: 0, - per_page: 10, - total: 0, - data: [], - }); - - const [context, req, res] = mockHandlerArguments( - eventLogClient, - { - params: { id: '1', type: 'action' }, - query: { page: 3, per_page: 10 }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - - const [type, ids, options] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; - expect(type).toEqual(`action`); - expect(ids).toEqual(['1']); - expect(options).toMatchObject({}); - - expect(res.ok).toHaveBeenCalledWith({ - body: { - page: 0, - per_page: 10, - total: 0, - data: [], - }, - }); - }); - - it('logs a warning when the query throws an error', async () => { - const router = httpServiceMock.createRouter(); - - findRoute(router, systemLogger); - - const [, handler] = router.get.mock.calls[0]; - eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('oof!')); - - const [context, req, res] = mockHandlerArguments( - eventLogClient, - { - params: { id: '1', type: 'action' }, - query: { page: 3, per_page: 10 }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(systemLogger.debug).toHaveBeenCalledTimes(1); - expect(systemLogger.debug).toHaveBeenCalledWith( - 'error calling eventLog findEventsBySavedObjectIds(action, [1], {"page":3,"per_page":10}): oof!' - ); - }); -}); diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts deleted file mode 100644 index 4c9ab70bbe3a5..0000000000000 --- a/x-pack/plugins/event_log/server/routes/find.ts +++ /dev/null @@ -1,58 +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, TypeOf } from '@kbn/config-schema'; -import type { - KibanaRequest, - IKibanaResponse, - KibanaResponseFactory, - Logger, -} from '@kbn/core/server'; -import type { EventLogRouter, EventLogRequestHandlerContext } from '../types'; -import { BASE_EVENT_LOG_API_PATH } from '../../common'; -import { queryOptionsSchema, FindOptionsType } from '../event_log_client'; - -const paramSchema = schema.object({ - type: schema.string(), - id: schema.string(), -}); - -export const findRoute = (router: EventLogRouter, systemLogger: Logger) => { - router.get( - { - path: `${BASE_EVENT_LOG_API_PATH}/{type}/{id}/_find`, - validate: { - params: paramSchema, - query: queryOptionsSchema, - }, - }, - router.handleLegacyErrors(async function ( - context: EventLogRequestHandlerContext, - req: KibanaRequest, FindOptionsType, unknown>, - res: KibanaResponseFactory - ): Promise { - if (!context.eventLog) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for eventLog' }); - } - const eventLogClient = (await context.eventLog).getEventLogClient(); - const { - params: { id, type }, - query, - } = req; - - try { - return res.ok({ - body: await eventLogClient.findEventsBySavedObjectIds(type, [id], query), - }); - } catch (err) { - const call = `findEventsBySavedObjectIds(${type}, [${id}], ${JSON.stringify(query)})`; - systemLogger.debug(`error calling eventLog ${call}: ${err.message}`); - return res.notFound(); - } - }) - ); -}; diff --git a/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts b/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts deleted file mode 100644 index 81172f2119410..0000000000000 --- a/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts +++ /dev/null @@ -1,130 +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 { httpServiceMock } from '@kbn/core/server/mocks'; -import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments'; -import { eventLogClientMock } from '../event_log_client.mock'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; -import { findByIdsRoute } from './find_by_ids'; - -const eventLogClient = eventLogClientMock.create(); -const systemLogger = loggingSystemMock.createLogger(); - -beforeEach(() => { - jest.resetAllMocks(); -}); - -describe('find_by_ids', () => { - it('finds events with proper parameters', async () => { - const router = httpServiceMock.createRouter(); - - findByIdsRoute(router, systemLogger); - - const [config, handler] = router.post.mock.calls[0]; - - expect(config.path).toMatchInlineSnapshot(`"/internal/event_log/{type}/_find"`); - - const events = [fakeEvent(), fakeEvent()]; - const result = { - page: 0, - per_page: 10, - total: events.length, - data: events, - }; - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(result); - - const [context, req, res] = mockHandlerArguments( - eventLogClient, - { - params: { type: 'action' }, - body: { ids: ['1'], legacyIds: ['2'] }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - - const [type, ids, , legacyIds] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; - expect(type).toEqual(`action`); - expect(ids).toEqual(['1']); - expect(legacyIds).toEqual(['2']); - - expect(res.ok).toHaveBeenCalledWith({ - body: result, - }); - }); - - it('supports optional pagination parameters', async () => { - const router = httpServiceMock.createRouter(); - - findByIdsRoute(router, systemLogger); - - const [, handler] = router.post.mock.calls[0]; - eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce({ - page: 0, - per_page: 10, - total: 0, - data: [], - }); - - const [context, req, res] = mockHandlerArguments( - eventLogClient, - { - params: { type: 'action' }, - body: { ids: ['1'] }, - query: { page: 3, per_page: 10 }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - - const [type, id, options] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; - expect(type).toEqual(`action`); - expect(id).toEqual(['1']); - expect(options).toMatchObject({}); - - expect(res.ok).toHaveBeenCalledWith({ - body: { - page: 0, - per_page: 10, - total: 0, - data: [], - }, - }); - }); - - it('logs a warning when the query throws an error', async () => { - const router = httpServiceMock.createRouter(); - - findByIdsRoute(router, systemLogger); - - const [, handler] = router.post.mock.calls[0]; - eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('oof!')); - - const [context, req, res] = mockHandlerArguments( - eventLogClient, - { - params: { type: 'action' }, - body: { ids: ['1'] }, - query: { page: 3, per_page: 10 }, - }, - ['ok'] - ); - - await handler(context, req, res); - - expect(systemLogger.debug).toHaveBeenCalledTimes(1); - expect(systemLogger.debug).toHaveBeenCalledWith( - 'error calling eventLog findEventsBySavedObjectIds(action, [1], {"page":3,"per_page":10}): oof!' - ); - }); -}); diff --git a/x-pack/plugins/event_log/server/routes/find_by_ids.ts b/x-pack/plugins/event_log/server/routes/find_by_ids.ts deleted file mode 100644 index a33a03c8242c4..0000000000000 --- a/x-pack/plugins/event_log/server/routes/find_by_ids.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema, TypeOf } from '@kbn/config-schema'; -import type { - KibanaRequest, - IKibanaResponse, - KibanaResponseFactory, - Logger, -} from '@kbn/core/server'; -import type { EventLogRouter, EventLogRequestHandlerContext } from '../types'; - -import { BASE_EVENT_LOG_API_PATH } from '../../common'; -import { queryOptionsSchema, FindOptionsType } from '../event_log_client'; - -const paramSchema = schema.object({ - type: schema.string(), -}); - -const bodySchema = schema.object({ - ids: schema.arrayOf(schema.string(), { defaultValue: [] }), - legacyIds: schema.arrayOf(schema.string(), { defaultValue: [] }), -}); - -export const findByIdsRoute = (router: EventLogRouter, systemLogger: Logger) => { - router.post( - { - path: `${BASE_EVENT_LOG_API_PATH}/{type}/_find`, - validate: { - params: paramSchema, - query: queryOptionsSchema, - body: bodySchema, - }, - }, - router.handleLegacyErrors(async function ( - context: EventLogRequestHandlerContext, - req: KibanaRequest, FindOptionsType, TypeOf>, - res: KibanaResponseFactory - ): Promise { - if (!context.eventLog) { - return res.badRequest({ body: 'RouteHandlerContext is not registered for eventLog' }); - } - const eventLogClient = (await context.eventLog).getEventLogClient(); - const { - params: { type }, - body: { ids, legacyIds }, - query, - } = req; - - try { - return res.ok({ - body: await eventLogClient.findEventsBySavedObjectIds(type, ids, query, legacyIds), - }); - } catch (err) { - const call = `findEventsBySavedObjectIds(${type}, [${ids}], ${JSON.stringify(query)})`; - systemLogger.debug(`error calling eventLog ${call}: ${err.message}`); - return res.notFound(); - } - }) - ); -}; diff --git a/x-pack/plugins/event_log/server/routes/index.ts b/x-pack/plugins/event_log/server/routes/index.ts deleted file mode 100644 index be9b64342a974..0000000000000 --- a/x-pack/plugins/event_log/server/routes/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { findRoute } from './find'; diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 6969ade7c3a8e..7287da0c0fd6e 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -6,7 +6,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import type { IRouter, KibanaRequest, CustomRequestHandlerContext } from '@kbn/core/server'; +import type { KibanaRequest } from '@kbn/core/server'; import { KueryNode } from '@kbn/es-query'; export type { IEvent, IValidatedEvent } from '../generated/schemas'; @@ -84,22 +84,3 @@ export interface IEventLogger { startTiming(event: IEvent, startTime?: Date): void; stopTiming(event: IEvent): void; } - -/** - * @internal - */ -export interface EventLogApiRequestHandlerContext { - getEventLogClient(): IEventLogClient; -} - -/** - * @internal - */ -export type EventLogRequestHandlerContext = CustomRequestHandlerContext<{ - eventLog: EventLogApiRequestHandlerContext; -}>; - -/** - * @internal - */ -export type EventLogRouter = IRouter; diff --git a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts index 912e153f9a8c0..0ea5a2601f75a 100644 --- a/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts +++ b/x-pack/test/alerting_api_integration/common/lib/get_event_log.ts @@ -39,7 +39,7 @@ export async function getEventLog(params: GetEventLogParams): Promise { diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/routes.ts index 135d3c02b9f8f..28e0abd955b2c 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/routes.ts @@ -24,6 +24,7 @@ import { TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; import { SECURITY_EXTENSION_ID, SPACES_EXTENSION_ID } from '@kbn/core-saved-objects-server'; +import { queryOptionsSchema } from '@kbn/event-log-plugin/server/event_log_client'; import { FixtureStartDeps } from './plugin'; import { retryIfConflicts } from './lib/retry_if_conflicts'; @@ -449,4 +450,37 @@ export function defineRoutes( } } ); + + router.get( + { + path: '/_test/event_log/{type}/{id}/_find', + validate: { + params: schema.object({ + type: schema.string(), + id: schema.string(), + }), + query: queryOptionsSchema, + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + const [, { eventLog }] = await core.getStartServices(); + const eventLogClient = eventLog.getClient(req); + const { + params: { id, type }, + query, + } = req; + + try { + return res.ok({ + body: await eventLogClient.findEventsBySavedObjectIds(type, [id], query), + }); + } catch (err) { + return res.notFound(); + } + } + ); } diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts index f4cbc4ad31ffc..f08a58848311e 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts @@ -6,6 +6,7 @@ */ import { + CoreSetup, RequestHandlerContext, KibanaRequest, KibanaResponseFactory, @@ -16,6 +17,9 @@ import { } from '@kbn/core/server'; import { IEventLogService, IEventLogger } from '@kbn/event-log-plugin/server'; import { IValidatedEvent } from '@kbn/event-log-plugin/server/types'; +import { schema } from '@kbn/config-schema'; +import { queryOptionsSchema } from '@kbn/event-log-plugin/server/event_log_client'; +import { EventLogFixtureStartDeps } from './plugin'; export const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger: Logger) => { router.post( @@ -205,3 +209,81 @@ export const isEventLogServiceLoggingEntriesRoute = ( } ); }; + +export const getEventLogRoute = (router: IRouter, core: CoreSetup) => { + router.get( + { + path: '/_test/event_log/{type}/{id}/_find', + validate: { + params: schema.object({ + type: schema.string(), + id: schema.string(), + }), + query: queryOptionsSchema, + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + const [, { eventLog }] = await core.getStartServices(); + const eventLogClient = eventLog.getClient(req); + const { + params: { id, type }, + query, + } = req; + + try { + return res.ok({ + body: await eventLogClient.findEventsBySavedObjectIds(type, [id], query), + }); + } catch (err) { + return res.notFound(); + } + } + ); +}; + +export const getEventLogByIdsRoute = ( + router: IRouter, + core: CoreSetup +) => { + router.post( + { + path: '/_test/event_log/{type}/_find', + validate: { + params: schema.object({ + type: schema.string(), + }), + query: queryOptionsSchema, + body: schema.object({ + ids: schema.arrayOf(schema.string(), { defaultValue: [] }), + legacyIds: schema.arrayOf(schema.string(), { defaultValue: [] }), + }), + }, + }, + router.handleLegacyErrors(async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise { + const [, { eventLog }] = await core.getStartServices(); + const eventLogClient = eventLog.getClient(req); + + const { + params: { type }, + body: { ids, legacyIds }, + query, + } = req; + + try { + return res.ok({ + body: await eventLogClient.findEventsBySavedObjectIds(type, ids, query, legacyIds), + }); + } catch (err) { + return res.notFound(); + } + }) + ); +}; diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts index 87bb7de7f2f41..cfb78197d3aef 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts @@ -15,6 +15,8 @@ import { isIndexingEntriesRoute, isEventLogServiceLoggingEntriesRoute, isEventLogServiceEnabledRoute, + getEventLogRoute, + getEventLogByIdsRoute, } from './init_routes'; // this plugin's dependendencies @@ -34,7 +36,7 @@ export class EventLogFixturePlugin this.logger = initializerContext.logger.get('plugins', 'eventLogFixture'); } - public setup(core: CoreSetup, { eventLog }: EventLogFixtureSetupDeps) { + public setup(core: CoreSetup, { eventLog }: EventLogFixtureSetupDeps) { const router = core.http.createRouter(); eventLog.registerProviderActions('event_log_fixture', ['test']); @@ -60,6 +62,8 @@ export class EventLogFixturePlugin isIndexingEntriesRoute(router, eventLog, this.logger); isEventLogServiceLoggingEntriesRoute(router, eventLog, this.logger); isEventLogServiceEnabledRoute(router, eventLog, this.logger); + getEventLogRoute(router, core); + getEventLogByIdsRoute(router, core); } public start() {} diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/tsconfig.json b/x-pack/test/plugin_api_integration/plugins/event_log/tsconfig.json index f1fc9d8927258..1276cb52b0109 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/tsconfig.json +++ b/x-pack/test/plugin_api_integration/plugins/event_log/tsconfig.json @@ -12,6 +12,7 @@ ], "kbn_references": [ "@kbn/core", - "@kbn/event-log-plugin" + "@kbn/event-log-plugin", + "@kbn/config-schema" ] } diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index 04c3907cde588..ed7d31efe1c10 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -240,7 +240,7 @@ export default function ({ getService }: FtrProviderContext) { query: Record = {} ) { const urlPrefix = urlPrefixFromNamespace(namespace); - const url = `${urlPrefix}/internal/event_log/event_log_test/${id}/_find${ + const url = `${urlPrefix}/_test/event_log/event_log_test/${id}/_find${ isEmpty(query) ? '' : `?${Object.entries(query) @@ -261,7 +261,7 @@ export default function ({ getService }: FtrProviderContext) { legacyIds: string[] = [] ) { const urlPrefix = urlPrefixFromNamespace(namespace); - const url = `${urlPrefix}/internal/event_log/event_log_test/_find${ + const url = `${urlPrefix}/_test/event_log/event_log_test/_find${ isEmpty(query) ? '' : `?${Object.entries(query) diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index dc244a51bb183..c5d95ba845302 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -260,7 +260,7 @@ export default function ({ getService }: FtrProviderContext) { async function fetchEvents(savedObjectType: string, savedObjectId: string) { log.debug(`Fetching events of Saved Object ${savedObjectId}`); return await supertest - .get(`/internal/event_log/${savedObjectType}/${savedObjectId}/_find`) + .get(`/_test/event_log/${savedObjectType}/${savedObjectId}/_find`) .set('kbn-xsrf', 'foo') .expect(200); } diff --git a/x-pack/test/rule_registry/common/config.ts b/x-pack/test/rule_registry/common/config.ts index 01d71b6ab8290..703e71a8613b3 100644 --- a/x-pack/test/rule_registry/common/config.ts +++ b/x-pack/test/rule_registry/common/config.ts @@ -5,8 +5,9 @@ * 2.0. */ +import path from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { FtrConfigProviderContext } from '@kbn/test'; +import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test'; import { getAllExternalServiceSimulatorPaths } from '@kbn/actions-simulators-plugin/server/plugin'; import { services } from './services'; @@ -83,6 +84,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...disabledPlugins .filter((k) => k !== 'security') .map((key) => `--xpack.${key}.enabled=false`), + ...findTestPluginPaths([ + path.resolve(__dirname, '../../alerting_api_integration/common/plugins'), + ]), '--xpack.ruleRegistry.write.enabled=true', `--server.xsrf.allowlist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl From aeded80d8625833a3dee83bb723732631b32c6a9 Mon Sep 17 00:00:00 2001 From: Jatin Kathuria Date: Tue, 2 May 2023 15:47:12 +0200 Subject: [PATCH 05/24] [Security Solution] [Fix] Alert Page Controls do not take Alert Table "Additional Filters" into account. (#155861) ## Summary This PR handles : [Security Solution] New alert filters are not taking into consideration alert table filters #155173 and #156252 Currently, Alert Page Controls do not take Alert Table Checkboxes ( Building block + Threat indicator alerts only ) into account. This PR enables the effect of Alert Table Checkboxes on Alert Page controls | Before | After | |--|--| |